必须显式设$pdo = null而非仅unset($pdo),因PDO析构仅在引用计数归零时触发,而静态变量、全局数组等可能持引用致释放延迟;需配合gc_collect_cycles()并验证MySQL端Sleep连接是否消失。

必须在每次请求处理结束时显式释放连接,否则在 FrankenPHP、Swoole、CLI 长驻进程等场景下,连接会持续堆积,最终触发 Too many connections 或 Connection refused 错误。
为什么 unset($pdo) 不够,必须设为 null?
PDO 对象的析构函数(__destruct)只在引用计数归零时才被调用。仅 unset($pdo) 并不能立即清空引用——尤其当对象被静态变量、全局数组、闭包或框架容器意外持有时,引用计数不会降为 0。
-
$pdo = null;是最直接有效的方式,它强制切断当前作用域对 PDO 实例的引用 - 若使用 MySQLi,
mysqli_close($conn)或$mysqli->close()才真正断开底层 socket;unset($mysqli)不触发关闭 - ORM 如 Laravel 的
DB::connection()返回的是复用连接,需配对调用DB::purge('default')或DB::disconnect()
哪些地方最容易漏掉连接释放?
不是所有“用了数据库”的地方都会自动清理,尤其在非标准请求生命周期中:
- 命令行脚本(如队列消费者)中,每处理一条消息后未重置
$pdo,导致连接越积越多 - 中间件或事件监听器里调用了
DB::transaction(),但没确保commit()或rollback()执行完毕就返回 - 把
$pdo赋值给static $instance或global $db,使连接跨请求残留 - 在
frankenphp_handle_request()回调外提前 return,跳过了后续释放逻辑
释放后要不要手动触发垃圾回收?
要,而且必须放在回调末尾。PHP 的 GC 默认不高频运行,长驻进程里大量临时对象(如 JSON 解析结果、大数组)会缓慢吃内存,最终 OOM。
立即学习“PHP免费学习笔记(深入)”;
- 入口处加
gc_enable();(虽默认开启,但 CLI 启动时可能被关) - 在
frankenphp_handle_request()或请求处理函数的最后,紧挨着$pdo = null;后调用gc_collect_cycles(); - 禁用
register_shutdown_function():它在 Worker 生命周期中不可靠,可能根本不执行
连接池场景下怎么确认真释放了?
不能只信代码逻辑,得看 MySQL 端是否真的断开了。最简单验证方式:
- 执行
show processlist;,观察Command列是否还有大量Sleep状态连接,且Time值持续增长 - 用
netstat -an | grep :3306 | grep ESTABLISHED | wc -l统计活跃连接数,对比 PHP 进程数 —— 若前者远大于后者,说明有连接没归还 - 在释放前加日志:
error_log("Closing PDO, pid=" . getmypid());,再配合strace -p $(pgrep -f 'frankenphp') -e trace=connect,close观察系统调用
真正难的不是写那行 $pdo = null;,而是确保它在每一个退出路径上都被执行到,包括异常分支、早期 return、信号中断——这些地方最容易被忽略,也最常出问题。



















