db类在workerman中不释放pdo连接和查询器实例,因tp按单请求设计而workerman常驻进程导致资源累积;需手动重置容器实例缓存、禁用pdo缓冲、改用游标分页及关闭调试模式。

Db类在Workerman里不释放PDO连接和查询器实例
Workerman进程常驻,Db类底层的PDO连接、QueryBuilder对象、事件监听器都会被复用并持续累积。TP默认不主动销毁这些资源,每次调用Db::name('user')->select()都新建一个完整查询器链路,旧的却还挂在内存里——不是“没释放”,是根本没设计释放路径。
常见现象:memory_get_peak_usage(true)在连续10次请求后从8MB涨到120MB,且不回落;get_included_files()显示重复加载了几十个模型/配置文件,说明自动加载缓存也在膨胀。
- Db类的
query()和execute()方法返回结果后,PDOStatement对象未显式closeCursor(),底层C层句柄仍占用 - 每个查询都触发
think\db\Query实例化,它持有了Connection、Builder、Parser等引用,形成闭包链 - 若用了
with()关联预载,嵌套模型又会递归创建新Query,指数级放大内存占用
静态属性和容器绑定在Worker生命周期中不断堆积
ThinkPHP的Db门面实际代理到think\Container里的单例,而Workerman不会像FPM那样每次请求重建容器。Container::getInstance()->bind('db', ...)只执行一次,但后续所有Db::调用都复用这个绑定,同时setInstances()缓存里不断塞入新的Connection、Query实例,却从不清理。
错误日志若出现in /path/to/think/Container.php on line 142,基本可确认是getInstances()返回的数组已超万条——这不是泄漏,是设计上就没打算清。
- 不要依赖
$container->forget('db'),它只删绑定定义,不碰已实例化的对象 - 必须配合
$container->setInstances([])才能清空实例缓存(但注意:清完后首次Db::调用会重建,需确保核心服务如app、config不在清除范围内) - 更稳妥的做法是在每次请求结束时手动
unset($query); unset($result);,尤其在foreach循环外层变量作用域内
游标分页失效 + 全量select()在长连接下直接压垮内存
Workerman里Db::name('logs')->paginate(20)看似分页,但TP6默认先执行COUNT(*)再查数据,两次全表扫描+结果集缓存,加上PDO缓冲模式(buffered query),整张表数据会在内存里存两份。如果表有50万行,光COUNT就可能吃掉60MB,再查20条反而成了次要问题。
更危险的是误用chunk():在Workerman中写Db::name('orders')->chunk(100, function ($list) { ... }),回调函数里的$list仍是一整个数组,且chunk内部没有释放前一批的引用,内存只增不减。
- 改用游标分页:
Db::name('orders')->where('id', '>', $lastId)->limit(100)->select(),避免OFFSET和COUNT - 禁用PDO缓冲:
Db::connect(['params' => [PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => false]]),让数据库流式返回 - 大结果集别进PHP数组:用
Db::cursor()(TP6.1+)或原生mysqli_use_result()配合fetch_row()逐行处理
配置和日志在常驻进程里变成内存黑洞
ThinkPHP启动时会require_once全部config/*.php,如果里面写了file_get_contents('huge.json')或new \GuzzleHttp\Client(),这些对象在Workerman里永不销毁。同样,app_debug = true开启时,SQL日志、模板编译缓存、异常堆栈全堆在内存里,一次报错就能卡住后续所有请求。
典型线索:错误行号指向think\Config或think\log\driver\File,且memory_get_usage(true)在App::run()之前就超过100MB。
- 关闭调试:
app_debug => false,上线环境绝不能开 - 配置文件里禁止任何IO操作,大配置抽成JSON/YAML由外部服务提供
- 日志驱动改用
think\log\driver\Socket或直接error_log(),避开File驱动的文件句柄和缓冲区累积
unset()了变量,但Container里缓存的实例、PDO连接池里的句柄、配置加载时产生的临时对象,全都不受PHP GC控制——它们卡在C扩展层,只能靠进程重启或显式重置容器来清。php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











