ci4中自定义类反复new会撑爆数据库连接数,因config\database::connect()每次调用都新建物理连接;须改用懒加载、静态缓存或依赖注入,并手动管理连接生命周期。

CI4里自定义类反复new Database实例会撑爆连接数
CodeIgniter 4 的 \Config\Database::connect() 每次调用都新建一个物理连接,不是复用。如果你在自定义服务类、中间件或循环逻辑里写 new MyService(),而这个类构造函数里又调了 \Config\Database::connect('default'),那每次实例化就多占一个连接——PHP-FPM 进程不退出,连接就一直挂着。
常见现象:压测时 SHOW PROCESSLIST 看到几十个同用户、同 Host 的 Sleep 连接,TIME 值持续增长;错误日志里反复出现 Too many connections 或 max_user_connections 超限。
- 别在构造函数里直接连库,改用 lazy-init:把连接逻辑挪到具体方法里,且只在首次调用时建立
- 用静态属性缓存连接实例:
private static $db;+if (self::$db === null) { self::$db = \Config\Database::connect('default'); } - 避免在 foreach 循环里 new 类实例——改成单例复用,或把连接传入构造函数(
new MyService($db)) - 确认你的自定义类没实现
__destruct()但漏掉$this->db->close();CI4 不会自动关连接,得手动收尾
为什么禁用 persistent 对 CI4 自定义类更关键
CI4 默认配置里没开持久连接,但你如果在 app/Config/Database.php 的某个连接组里写了 'persistent' => true,再被自定义类反复实例化,问题会指数级放大:每个新实例都试图复用已存在的持久连接,但 CI4 的 Connection 实例不共享会话状态,容易导致字符集错乱、SQL mode 不一致,最终触发类似 ERROR 1235 (42000) 这种看似无关的报错,掩盖了真实连接泄漏。
- 从库连接必须显式设
'persistent' => false(即使默认就是 false,也建议写明) - 主库若真要用持久连接,仅限 CLI 场景(如定时任务),Web 请求一律关闭
-
mysqli.default_socket_timeout和pdo_mysql.default_socket_timeout对持久连接无效——它们只影响新建连接的握手超时,不控制已建立连接的存活时间
如何验证自定义类是否在偷偷建连接
别靠猜,用 MySQL 原生命令盯住连接生命周期。在应用跑起来后,立刻执行:
一款AI音频处理工具,主要用于MiniMax统一媒体生成技能,用于TokenPlan工作流。当用户要求生成音频、语音、TTS、旁白、图片、插图、姿势等媒体内容时使用,适合需要提升相关任务效率的用户。
SELECT ID, USER, HOST, DB, COMMAND, TIME, STATE, INFO FROM INFORMATION_SCHEMA.PROCESSLIST WHERE USER = 'your_db_user' AND COMMAND != 'Sleep' ORDER BY TIME DESC;
然后触发一次你怀疑有问题的自定义类逻辑(比如访问某个 API),再立刻查一遍。重点看:
- 新增的非 Sleep 连接数量是否等于你 new 的类实例数
- 这些连接的
INFO字段是否为空(空说明没执行 SQL,只是挂着) - 同一请求周期内,是否有多个连接
TIME值相同或接近(说明是同一次请求创建的)
如果确认是类实例化引发,下一步就是砍掉构造函数里的连接逻辑,改用依赖注入或静态缓存。
连接复用不能靠框架自动,得自己管住入口
CI4 没有全局连接池,\Config\Database::connect() 返回的是独立 Connection 实例,框架不帮你做连接复用。所有“复用”都得由你控制调用时机和生命周期。
- 不要在 Model 里写
$this->db = \Config\Database::connect('slave');—— 这会让整个 Model 绑定死一个连接,后续 insert/update 也会走从库 - 推荐做法:在 Service 层统一获取连接,传给 DAO 或 Query Builder,用完即弃(变量作用域结束自然销毁)
- 如果用了容器绑定(如
Services::injectMock()),确保绑定的是连接实例,而不是每次都 new 出来的类 - 注意 Migration 和 Seeder 仍走
default组,别在它们里面混用自定义类的连接,否则事务会跨连接失效
最易被忽略的一点:CI4 的连接对象没有析构自动 close 机制,只要变量还活着,连接就挂着。哪怕你 return 了一个 builder,只要上层没 unset 或让它出作用域,连接就不会释放。










