hyperf 3.1 中连接池耗尽与死锁根因是协程并发下资源争抢与事务顺序不一致;需通过 socket 连接数监控、withpipeline 封装、统一表访问顺序、启用 innodb_print_all_deadlocks 及索引优化来系统性修复。

Hyperf 3.1 中多个协程并发抢夺 Redis 或 MySQL 连接池资源时,若未规范管理生命周期或事务顺序不一致,极易触发连接耗尽、WaitTimeoutException 或数据库死锁,导致接口大面积超时甚至雪崩。
排查连接池是否已被耗尽
执行 swoole_get_local_socket_count() 查看当前 Worker 进程已建立的 socket 连接数;再对比 config/autoload/redis.php 或 database.php 中配置的 pool.max_connections 值。
若前者长期稳定在后者的 90% 以上,说明连接池处于饱和边缘,不是 Redis 慢,而是新协程等不到空闲连接。
临时验证:在复现请求中加入日志 Log::info('pool size: ' . $pool->getTotalCount() . '/' . $pool->getMaxConnections());,观察输出是否持续显示 【pool size: 50/50】。
修复 pipeline/multi 连接泄漏
方法一:用 withPipeline() 替代裸调用(Hyperf 3.1+ 内置安全封装)
直接写 $redis->pipeline()->set('k','v')->get('k')->exec(); 是危险的——一旦中间抛异常,exec() 不执行,连接就卡在协程上下文中不释放。
改用 $redis->withPipeline(fn ($pipe) => $pipe->set('k','v')->get('k'));,框架会在 finally 块自动调用 releaseContextConnection()。
方法二:手动加 try/finally(兼容旧版本)
必须包裹完整:try { $pipe = $redis->pipeline(); ... $pipe->exec(); } finally { if (isset($pipe)) $pipe->reset(); }。只写 try/catch 而不加 finally,【连接仍会泄漏】。
预防数据库事务死锁
第一步:启用 MySQL 死锁日志捕获
在 MySQL 配置文件中添加两行并重启服务:innodb_print_all_deadlocks = ON 和 log_error_verbosity = 3。否则你永远看不到真实死锁现场,只能靠猜。
第二步:统一 SQL 执行顺序
所有涉及多行更新的事务,强制按主键升序处理。例如转账场景,先查 SELECT * FROM account WHERE id IN (1001, 1002) ORDER BY id,再按此顺序执行 UPDATE ... WHERE id = 1001 → UPDATE ... WHERE id = 1002。避免 A 协程按 1001→1002、B 协程按 1002→1001 的交叉加锁。
第三步:显式设置事务超时
在 Hyperf 的 DB 配置中开启 'options' => [PDO::ATTR_TIMEOUT => 5],并在业务层用 DB::transaction(..., 5) 显式传入秒级超时。超时后自动 rollback,不卡住连接。
第四步:检查 where 条件是否命中索引
执行 EXPLAIN SELECT ... WHERE name = ?,若 type=ALL 或 key=NULL,说明走了全表扫描,InnoDB 会升级为表级锁——所有并发 UPDATE 全部阻塞,这不是死锁,但效果更差。











