Db::connect() 在 Swoole 协程中复用错连接,因 ThinkPHP 默认单例模式导致多个协程共享同一 PDO 实例,造成事务、fetch 模式等状态污染;需通过 think-swoole 的 DatabasePool 按协程 ID 隔离连接,并禁用读写分离、启用真实预处理。

为什么 Db::connect() 在 Swoole 协程里会复用错连接
ThinkPHP 默认的数据库连接是“单例 + 连接复用”模式,Db::connect() 返回的其实是同一个 PDO 实例。Swoole 协程环境下,多个协程共享这个实例,一旦某个协程执行了 beginTransaction() 或修改了 setFetchMode(),其他协程就会被污染——这不是 bug,是设计使然。
必须让每个协程拥有独立、隔离的数据库连接,且不能靠手动 new PDO() 管理生命周期(容易漏关、内存泄漏)。
- 启用协程适配前,确认已安装
swoole扩展且版本 ≥ 4.8.0(低版本不支持 MySQLi/PDO 协程化) - 在
config/database.php中将'deploy' => 0(禁用读写分离),否则Connection类内部会绕过协程驱动 - 必须设置
'params' => [PDO::ATTR_EMULATE_PREPARES => false],否则预处理语句会退化为同步阻塞
如何让 think-swoole 正确接管数据库连接池
官方扩展 think-swoole 3.x 起内置了协程连接池支持,但它不会自动替换 Db 门面的行为——你得显式告诉它:“这个连接要进池子,且按协程 ID 隔离”。
关键动作是重写连接工厂,在 app/provider.php 或服务提供者中绑定:
立即学习“PHP免费学习笔记(深入)”;
use think\swoole\pool\ChannelPool;
use think\swoole\pool\DatabasePool;
<p>// 替换默认 Db 连接创建逻辑
Container::getInstance()->bind('db', function ($app) {
$config = $app->config->pull('database');
return new DatabasePool($config, new ChannelPool(10)); // 10 是池大小
});
-
DatabasePool会拦截所有connect()调用,按协程 ID(Swoole\Coroutine::getuid())分配连接 - 池大小建议设为
max_request * 2(如 Swoolemax_request=3000,池大小设 60–100),太大易 OOM,太小会频繁创建销毁 - 若使用 Redis 或其它中间件,它们的连接池也需类似处理,否则 DB 池生效但 Redis 仍阻塞
Db::transaction() 在协程里为什么只回滚当前协程
协程内调用 Db::transaction() 时,底层实际是在当前协程绑定的连接上执行 BEGIN/COMMIT/ROLLBACK。这和传统 FPM 下“整个请求生命周期”的事务语义一致,但容易误以为“跨协程事务能传播”——其实不能。
如果你在协程 A 启动事务,再通过 go() 启一个协程 B 并操作 DB,B 的 SQL 不属于 A 的事务上下文,即使 A rollback(),B 的变更依然提交成功。
- 事务必须严格限定在单个协程内完成,禁止跨协程传递
$connection或$pdo实例 - 不要在事务块里调用
go(function () { Db::table()->insert(); }),这是典型陷阱 - 需要跨协程协调?改用最终一致性 + 补偿事务,或借助
Swoole\Table做轻量状态同步
协程上下文丢失导致 Db::name() 查不到表名
ThinkPHP 的查询构造器(如 Db::name('user'))依赖 Connection 对象中的 $this->getConfig('database'),而这个配置在协程切换后可能因静态缓存或闭包绑定失效——尤其当你在中间件或事件监听器里提前调用了 Db::connect(),后续协程拿到的是旧配置副本。
根本解法是避免“提前初始化”,让连接真正懒加载到协程内:
- 删掉所有全局
Db::connect()调用,包括common.php或服务启动脚本里的预热代码 - 确保
config/database.php中'hostname'、'database'等字段不为空字符串(空值会导致连接时 fallback 到静态默认值) - 如果用了多数据库配置,
Db::connect('mysql2')必须传字符串名,不能传数组——数组会被当成新配置直接 new 实例,跳过连接池
协程不是魔法,它只是把“请求生命周期”压缩到了协程栈帧里;任何假设“全局状态可跨协程共享”的写法,都会在连接、事务、日志上下文这些地方露馅。



















