Hyperf连接池默认不自动重连MySQL重启后的失效连接,因其仅在获取连接时做一次PING检查,不主动探测已借出连接状态;需显式配置heartbeat(如30.0)协同max_idle_time(如30.0),并捕获“gone away”错误调用Db::reconnect()及清理Context。

Hyperf 的数据库连接池本身不自动处理 MySQL 服务重启后的连接失效(gone away),必须手动加心跳检测 + 重连逻辑,否则会持续报错直到连接池被全部污染。
为什么默认连接池不重连 MySQL 重启后的连接
Hyperf 的 hyperf/database 连接池只在获取连接时做一次健康检查(如 ping),但不会主动探测已借出连接是否还活着。MySQL 服务重启后,连接池里所有旧连接都变成“僵尸连接”,后续查询直接抛 PDOException:`MySQL server has gone away`(错误码 2006)或 `Connection refused`。
- 连接池不会自动清理这些失效连接,除非触发
max_idle_time超时(默认 60 秒),但业务请求可能在几毫秒内就失败 -
wait_timeout是 MySQL 服务端参数,控制空闲连接存活时间;而连接池的max_idle_time是客户端控制,两者不联动 - 仅靠配置
heartbeat => -1或enable_pool => true完全无效——那只是启用池,不是加心跳
在 config/autoload/databases.php 中启用连接心跳
必须显式开启连接级心跳,让连接池定期对已借出连接执行 PING,发现失效立即销毁并重建:
- 在目标数据库连接(如
default)的pool配置下添加:'heartbeat' => 30.0(单位:秒),表示每 30 秒对每个空闲连接发一次PING -
heartbeat值不能为-1(禁用)或0(无效),建议设为30.0 ~ 60.0,比 MySQL 的wait_timeout小 10~20 秒更安全 - 确保同时配了
'max_idle_time' => 30.0,避免心跳包发到已被 MySQL 主动关闭的连接上引发异常 - 该配置只对已归还至池中的连接生效,正在使用的连接不会被心跳干扰
捕获 gone away 错误并触发重试的封装类
心跳只能清理空闲连接,对正在使用的连接断开(比如 MySQL 突然宕机)无能为力。此时需在业务层捕获并重试:
- 错误判断必须覆盖多种字符串:
server has gone away、2006、Connection refused、connection was forcibly closed - 重试前要调用
Db::reconnect()(Hyperf v3.0+)或手动清空协程上下文中的连接缓存:Context::set(Db::class, null) - 重试间隔建议
usleep(200000)(200ms),避免雪崩;最多重试 1 次,再失败就该报警而非继续扛 - 不要在事务中重试——事务上下文已损坏,重试只会放大问题;应在外层捕获后回滚再重试整个业务逻辑
示例关键逻辑片段:
if (str_contains($e->getMessage(), 'server has gone away') || str_contains($e->getMessage(), '2006')) {
$this->logger->warning('MySQL gone away, triggering reconnect');
Db::reconnect();
continue;
}
容易被忽略的协程上下文污染问题
MySQL 连接断开后,如果没正确处理异常,协程上下文里的 DB 实例可能残留损坏状态,导致后续 Redis 或 HTTP 请求也报 Connection reset by peer ——这不是 Redis 出问题,是上下文被 MySQL 异常污染了。
- 务必在
catch块里显式清理:Context::set(Db::class, null),强制下次获取新连接 - 避免在同一个协程里混用 DB 事务 + Redis pipeline,尤其当 DB 报错但没被捕获时,Redis 操作大概率跟着失败
- 用
swoole_get_local_socket_count()观察 ESTABLISHED 连接数是否稳定;若持续上涨且伴随大量WaitTimeoutException,基本可判定是上下文泄漏
真正的难点不在配置心跳,而在确保每次异常都走到清理路径——少一个 Context::set,就可能让整个协程后续所有 I/O 都不可控。


















