Hyperf中MySQL长连接断开后auto_reconnect=true无效,因协程环境下PDO不触发重连、连接未归还导致泄漏,真正可靠方案需连接池心跳+空闲回收+应用层兜底协同。

Hyperf 的 MySQL 长连接断开后,auto_reconnect=true 基本无效——它只在 PDO 层做一次尝试,且不处理协程上下文污染、连接未归还、事务中断等真实场景。真正可靠的重连,必须靠连接池心跳 + 空闲回收 + 应用层兜底三者协同。
为什么 options.ATTR_EMULATE_PREPARES 和 auto_reconnect 都不顶用
Hyperf 默认用 PDO_MySQL,而 PDO 的 ATTR_EMULATE_PREPARES 关闭后走原生预编译,但 auto_reconnect 在协程环境下根本不会触发:协程挂起时连接没被释放,取出来时已失效,PDO 不会自动重连;异常抛出后连接也没被归还,导致后续协程拿不到可用连接。
-
auto_reconnect=true只在执行query()前调用PDO::ping(),但 Hyperf 连接池取出连接后不自动 ping,除非你显式配了pool.heartbeat并启用空闲检测 - 即使 ping 成功,若连接在 MySQL 侧已被
wait_timeoutkill,下一次 query 仍报MySQL server has gone away - 事务中连接断开,PDO 不会回滚或重连,协程直接 crash,连接泄漏
pool.heartbeat 怎么配才真起作用
pool.heartbeat 不是开关,而是空闲连接健康检查的触发阈值。它只有和 max_idle_time、MySQL 服务端 wait_timeout 对齐才有效,否则要么白检测,要么漏检测。
- 先查 MySQL 实际值:
SHOW VARIABLES LIKE 'wait_timeout';(生产常见 300 或 600) -
max_idle_time必须严格小于wait_timeout,建议设为wait_timeout - 30 -
heartbeat推荐设为max_idle_time / 2(例如max_idle_time=270→heartbeat=135) -
min_connections别设 0,冷启延迟高,建议 ≥ 2
示例(MySQL wait_timeout=300):
'pool' => [
'min_connections' => 2,
'max_connections' => 50,
'wait_timeout' => 3.0,
'max_idle_time' => 270,
'heartbeat' => 135,
],
连接泄漏才是重连失效的主因
90% 的“断线重连不生效”,其实是连接没归还,新协程根本拿不到连接去触发重连逻辑。典型泄漏点集中在事务、pipeline、multi 和异常跳出。
-
$db->transaction()中未正常 commit/rollback,连接被锁死直到协程结束 -
$redis->pipeline()或$redis->multi()后没调exec(),或中间throw跳出 try 块 - 自定义封装里忘了
$connection->close()或没用defer归还 - 用
Context::set()存了 DB 实例但没清理,协程复用时拿到脏连接
验证方式:在关键位置加日志,检查 Context::has($db->getContextKey()) 是否始终为 true;或用 swoole_get_local_socket_count() 观察 ESTABLISHED 连接数是否长期贴近 max_connections。
应用层兜底:捕获 MySQL server has gone away 后重试要谨慎
不能无脑重试所有 SQL。非幂等操作(如 INSERT、UPDATE)重试可能引发重复写入;事务内重试更危险,因为部分语句可能已提交。
- 只对明确幂等的查询(
SELECT、SHOW)做 1 次重试 - 捕获错误码
2006(MySQL server has gone away)或2013(Lost connection)再触发 - 重试前必须确保旧连接已关闭(
$pdo = null或显式$connection->close()) - 避免在事务块内重试——应由上层业务控制重试边界
真正难处理的是“连接看似活着,但执行失败”的情况,比如 MySQL 主从切换后连接还在,但写请求发到了只读从库。这种得靠健康检查 + 读写分离路由层解决,不是单靠重连能覆盖的。



















