根本原因是Workerman常驻内存模型与PDO持久连接机制冲突:MySQL空闲断连后PDO仍持失效句柄,导致“MySQL server has gone away”或“There is no active transaction”;必须禁用PDO::ATTR_PERSISTENT,在onWorkerStart初始化独立PDO实例,每次查询前用SELECT 1检测并自动重连,且事务、prepare不得跨请求复用。

Workerman里用PDO长连接会报错,根本原因不是PDO本身不行,而是你把它当成了PHP-FPM那种“用完即焚”的模型来用——而Workerman进程常驻内存,连接空闲超时、状态残留、异常未清理,全都会累积成错误。
为什么PDO::ATTR_PERSISTENT => true在Workerman里是毒药
持久连接在PHP-FPM下靠进程重启自动回收,在Workerman里却会卡死:连接被MySQL主动断开(wait_timeout默认8小时,但网络抖动、防火墙策略可能几分钟就kill掉),PDO对象却还拿着失效句柄。下次$pdo->query()直接抛MySQL server has gone away,甚至更隐蔽的There is no active transaction——因为事务上下文随连接一起丢了。
- 禁用
PDO::ATTR_PERSISTENT,配置里必须显式设为false - 不要依赖
$pdo->getAttribute(PDO::ATTR_CONNECTION_STATUS)判断连通性,它经常返回假阳性 - 真正可靠的检测是执行一条轻量查询,比如
SELECT 1,捕获PDOException并检查$e->errorInfo[1]是否为2006或2013
onWorkerStart里初始化,但别只初始化一次就撒手不管
每个Worker进程必须拥有独立PDO实例,且必须在Worker::onWorkerStart中创建——这是底线。但光初始化不够,你还得应对连接中途失效。
- 把
$pdo存为Worker实例属性(如$this->pdo),而非全局静态变量 - 每次查询前加一层封装:先
try { $this->pdo->query('SELECT 1') } catch (...) { $this->reconnect() } -
reconnect()方法里要完整重建PDO实例,包括重设PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,否则后续异常不抛出 - 避免在
onClose或onWorkerStop里调$pdo = null或unset(),PHP进程退出时自动释放,手动干预反而可能触发warning
事务和prepare语句不能跨请求复用
Workerman单个Worker是同步阻塞模型,但多个请求并发进入同一个进程。如果你在某个onMessage里beginTransaction()却没commit()或rollback(),后续所有请求都会卡在START TRANSACTION上,直到超时或进程重启。
- 事务必须严格成对:每个
beginTransaction()后面,无论成功失败,都必须有commit()或rollback() - 绝不能把
$stmt = $pdo->prepare()的结果缓存到Worker属性里复用——prepare语句绑定的是当前连接上下文,连接重连后旧$stmt直接失效 - 如果要用prepare,每次查询都重新
prepare(),或者改用问号占位符+execute()组合,更安全 - 捕获
PDOException时,先检查是否因连接断开(错误码2006/2013),再决定是重连重试,还是直接rollback()保底
最常被忽略的点:连接复用逻辑写得再漂亮,如果系统级文件描述符(fd)没调够,$worker->maxConnections设再高也白搭——新连接连三次握手都进不来,更别说查库了。查/proc/<pid>/limits</pid>,设ulimit -n,再配$worker->maxConnections为fd上限的70%~80%,这事得在启动命令里硬编码,reload不生效,必须完整stop再start。

















