PHP中Swoole协程连接数据库不能用原生mysqli/PDO,因其底层阻塞I/O无法被协程调度;必须用Swoole\Coroutine\MySQL或经mysqlnd+hook封装的PDO组件,并注意连接池、事务绑定协程等限制。

PHP 用 Swoole 协程连接数据库,不能直接用 mysqli 或 PDO——它们是同步阻塞的,协程里一调用就退化成同步模型,失去并发优势。
为什么原生 PDO / mysqli 在 Swoole 协程里会失效
因为这些扩展底层调用的是系统 socket 阻塞 I/O,Swoole 的协程调度器无法接管其阻塞点。哪怕你在 go 函数里跑 PDO::query(),整个协程仍会卡住,其他协程也得等它返回。
- 现象:压测时并发上不去,QPS 卡在个位数,
strace看到大量recvfrom阻塞 - 本质:协程「以为自己在让出 CPU」,但底层驱动不配合,实际线程挂起
- 兼容方案只有两个:
Swoole\Coroutine\MySQL(官方协程 MySQL 客户端)或基于mysqlnd+hook的协程化封装(如hyperf/database)
必须用 Swoole\Coroutine\MySQL 才算真协程
这是 Swoole 内置、完全由协程驱动的 MySQL 客户端,所有方法(connect、query、prepare)都自动挂起/恢复,不占线程。
- 连接必须显式调用
connect(),且需传完整 DSN 数组:['host' => '127.0.0.1', 'port' => 3306, 'user' => 'root', 'password' => '', 'database' => 'test'] - 不支持
SET NAMES utf8mb4这类语句单独执行,得写进connect的charset字段:'charset' => 'utf8mb4' - 查询结果是数组,不是
PDOStatement对象,没法用fetch()流式读取;大结果集注意内存,建议加limit - 示例片段:
go(function () {
$mysql = new \Swoole\Coroutine\MySQL();
$res = $mysql->connect([
'host' => '127.0.0.1',
'port' => 3306,
'user' => 'root',
'password' => '123456',
'database' => 'test',
'charset' => 'utf8mb4'
]);
if (!$res) {
throw new RuntimeException('MySQL connect failed: ' . $mysql->error);
}
$result = $mysql->query('SELECT id, name FROM users WHERE id > ?', [100]);
var_dump($result);
});
想继续用 PDO 语法?得靠 hook + mysqlnd
Hyperf、Swoft 等框架封装的 pdo 组件,其实是通过 Swoole 的 Hook 机制拦截 mysqlnd 的 socket 调用,再转成协程 I/O。但这有硬性前提:
立即学习“PHP免费学习笔记(深入)”;
- PHP 必须启用
mysqlnd(不是libmysql),运行php -i | grep mysqlnd确认存在 - 启动前必须调用
Swoole\Runtime::enableCoroutine(true),且推荐开启全部 hook:Swoole\Runtime::enableCoroutine(SWOOLE_HOOK_ALL) - 不是所有 PDO 方法都被 hook:比如
PDO::exec()可能漏掉,优先用query()和prepare()->execute() - 连接池需自行管理(
hyperf/pool),原生PDO实例不复用,频繁 new 会创建大量连接
常见报错和绕过方式
遇到 ERROR 2002 (HY000): Connection refused 或 SQLSTATE[HY000] [2002] No such file or directory,大概率是:
- 没开
enableCoroutine,或开了但没在go里执行 PDO 操作 - MySQL 服务监听的是
127.0.0.1,但代码连了localhost(触发 Unix socket,而 hook 不覆盖 socket 文件路径)→ 改成127.0.0.1 - 使用了
PDO::ATTR_PERSISTENT => true,持久连接与协程生命周期冲突 → 必须设为false - 连接超时未设,协程卡死:给 PDO DSN 加
;timeout=5,或在connect()时传['timeout' => 5]
协程数据库最易被忽略的点:事务不能跨协程。一个 go 里 begin,另一个 go 里 commit,必然失败——事务状态只绑定当前协程上下文。











