协程退出时mysql连接不会自动释放,必须显式归还或销毁;因hyperf连接池依赖开发者触发release()或析构,否则pdo句柄滞留、active计数不减,导致连接池耗尽和too many connections。

协程退出时 MySQL 连接不会自动释放,必须显式归还或销毁 —— 否则连接句柄滞留、连接池耗尽、Too many connections 是必然结果,不是偶发问题。
为什么 DB::getConnection() 拿到的连接不随协程退出自动归还
Hyperf 的连接池管理是“按需取出、用完归还”,而归还动作依赖开发者显式触发(如连接对象析构、或调用 release())。协程结束时:
• PDO 实例若未被 unset 或超出作用域,其底层 socket fd 仍被持有;
• Connection 对象未调用 close() 或未触发 __destruct()(因仍被引用);
• 连接池内部的 Active 计数不减,该连接就卡在当前协程上下文中,无法复用。
常见错误现象:SHOW PROCESSLIST 中大量 Sleep 状态连接、Threads_connected 持续逼近 max_connections、异步任务中首次查询快、后续变慢甚至超时。
兜底释放的三种实操方式(按推荐顺序)
核心原则:**任何可能提前退出的路径(异常、return、exit)都必须保证连接释放**。
- 用
try/finally包裹事务或长流程,最后调用$connection->release()(推荐) - 在类属性中持有
$connection时,务必在__destruct()中调用$this->connection->release()(注意:仅当该对象生命周期与协程一致时有效) - 避免在循环内反复调用
DB::getConnection();改用一次获取 + 复用,例如$connection = DB::connection(); $connection->table(...)->get();
示例:
public function handle()
{
$connection = DB::connection();
try {
$connection->transaction(function ($txn) {
$txn->table('users')->insert(['name' => 'foo']);
// 可能抛异常
});
} finally {
// 即使 transaction 内部 throw,这里也必执行
$connection->release();
}
}
pool.max_idle_time 和 wait_timeout 不是救命稻草
这两个参数常被误当作“自动兜底”:
-
pool.max_idle_time只对空闲连接生效,**事务中活跃连接不受影响**;事务挂起 10 分钟,它不会回收 -
wait_timeout是 MySQL 服务端参数,Hyperf 客户端不感知;即使设为 60 秒,只要事务没提交,连接就一直挂着 -
pool.heartbeat默认无效——它只在连接取出时做一次PING,不是后台探测;真正靠的是IdleConnectionChecker线程,但该线程只处理Idle状态连接
所以别指望靠调大 wait_timeout 来掩盖代码缺陷。查 INNODB_TRX 表发现长时间运行事务,99% 是应用层没兜底。
最容易被忽略的泄漏点:Eloquent + 静态变量 + Context
以下写法看似无害,实则埋雷:
- 在中间件或命令行任务中,把
DB::connection()存进Context::set('db_conn', $conn),但忘了Context::del('db_conn') - 定义静态属性
private static $connection;并复用,导致连接跨协程污染、无法释放 - 用
User::query()->with('posts')...时,Eloquent 内部会复用连接,但若你在关联闭包里手动 new Connection,就绕过了连接池管理
验证方法:在协程退出前调用 co::stats(),对比 coroutine_num 和 connection_num 是否同步下降;再用 memory_get_usage(true) 查看内存是否持续增长。
真正要盯住的不是协程启停,而是每次 DB::getConnection() 调用后,是否有一一对应的 release() 或自然析构。漏掉一次,就可能让整个连接池卡死。











