Hyperf协程MySQL客户端不支持事务内动态切换连接池,因事务连接已绑定协程上下文,混用会导致连接泄漏、上下文污染及MySQL错误;正确做法是用独立协程隔离读写操作,并严格配对pipeline/transaction的exec/commit与finally清理。

Hyperf 的协程 MySQL 客户端本身不支持“协程嵌套调用连接池”这种操作——所谓“嵌套”,比如在 DB::transaction() 内部又手动调用 Db::connection('other_pool')->select(),或在一个协程里并发启动多个事务并交叉使用不同连接池,这类写法极易触发连接泄漏、上下文污染、MySQL server has gone away 或 WaitTimeoutException,不是框架限制,而是协程资源模型决定的硬约束。
为什么不能在事务里动态切换连接池
Hyperf 的 DB::transaction() 会将当前连接绑定到协程上下文(通过 Context::set()),后续所有该协程内的 DB:: 调用都复用这个连接。此时若再执行 Db::connection('read_pool_1')->select(),它会尝试从另一个池取连接,但:
- 事务连接已独占当前协程上下文,新连接无法自动注入事务生命周期
- 若 read_pool_1 连接被 pipeline() 或异常中断未归还,会导致该协程长期持有两个连接,而连接池只回收“归还”的连接
- MySQL 层面不支持跨连接的事务传播,报错如 SAVEPOINT does not exist 或静默回滚
多连接池并发查询的正确姿势
需要同时查主库和从库(比如写完立刻读从库做最终一致性校验),必须用显式连接 + 协程隔离,不能依赖隐式上下文:
- 用
go(function () { $write = Db::connection('db.write'); $write->insert(...); })启动写协程 - 另起一个独立协程做读:
go(function () { $read = Db::connection('read_pool_1'); $read->select(...); }) - 避免在同一个协程内混用
Db::connection()和全局DB::静态调用——二者上下文不互通,DB::select()永远走default池 - 若需结果聚合,用
Co\Channel或Co\WaitGroup同步,不要用sleep()或轮询
连接池配置必须按池隔离调优
每个连接池(db.write、read_pool_1)的 min_connections 和 max_connections 必须单独计算,不能共用一套值:
-
db.write池:按峰值 QPS × 平均写耗时 × 1.5 设max_connections,例如 80 QPS × 0.12s = 9.6 → 建议设为 16~24 -
read_pool_1池:若承担 3 倍于主库的读流量,且平均耗时更短(0.05s),则 240 QPS × 0.05s = 12 → 建议设为 30~40 -
min_connections不设 0,但可比写库低(如写库设 2,读库设 1),避免冷启动抖动 - 两池的
max_idle_time必须一致(建议 ≤30.0),否则 VIP 切换或主从延迟场景下,旧空闲连接可能卡住数分钟
最容易被忽略的泄漏点:pipeline/multi 未归还连接
哪怕你严格分池、显式调用,只要在某个协程里用了 $redis->pipeline() 或 $db->transaction() 却没走到 exec() 或 commit(),这个连接就永远卡在该协程上下文中——下次同协程调用 Db::connection('other_pool') 时,other_pool 的连接可能拿不到,因为协程资源没释放。验证方式很简单:
- 在入口加日志:
var_dump(Context::get($db->getContextKey('db.write'))); - 若返回
null,说明连接已归还;若返回ConnectionInterface实例,且后续没再调用close()或事务结束,就是泄漏 - 所有
pipeline()、multi()、transaction()必须包在try/catch/finally里,finally中确保exec()或rollback()



















