ci4读写分离不能靠.env自动生效,必须手动定义default和slave连接组并显式调用;$dbgroup仅对query builder生效,rawquery和事务内需主动指定连接,且主从字符集须一致。

CI4 读写分离不能靠 .env 配置自动生效
CodeIgniter 4 的 .env 文件只支持单数据库连接,原生不提供主从配置项。试图在 database.default.hostname 后加逗号写多个地址,或新增 database.slave.* 字段,完全无效——CI4 的 Database 类压根不解析这些键名。
常见错误现象:Database connection failed 且日志里查不到连接尝试痕迹;或者所有查询都走同一个库(通常是 default),毫无分流效果。
- 必须通过自定义 Database Group 实现,即手动定义多个连接配置(如
default和slave),再由业务代码显式选择 - 不能依赖 CI3 风格的“SQL 自动识别 SELECT/INSERT”逻辑,CI4 的 Query Builder 不拦截语句类型做路由
- 若强行改写
\Config\Database::connect()或重载 DB 类,需确保不破坏Database工厂方法的返回类型(必须是ConnectionInterface实例)
如何正确定义主从连接组
在 app/Config/Database.php 中添加两个独立连接配置,而不是修改 .env:
public $default = [
'DSN' => '',
'hostname' => '192.168.1.10', // 主库
'username' => 'root',
'password' => 'pass',
'database' => 'myapp',
'DBDriver' => 'MySQLi',
];
public $slave = [
'DSN' => '',
'hostname' => '192.168.1.11', // 从库
'username' => 'readonly',
'password' => 'ro-pass',
'database' => 'myapp',
'DBDriver' => 'MySQLi',
];
注意:这两个配置必须同名(如都叫 $default 和 $slave)且位于同一 Config 类中;.env 仅影响 $default 的值,$slave 的参数必须硬编码或通过环境变量间接注入(如用 $_ENV['SLAVE_HOST'])。
- 不要给
$slave设置'DBDebug' => true,从库通常无写权限,开启调试可能触发权限错误中断查询 - 两个连接的
'charset'和'DBCollat'必须一致,否则跨库 JOIN 或字符比较会出乱码 - 从库连接建议加
'port' => 3307显式指定,避免与主库端口冲突或 DNS 缓存导致连错实例
模型层调用时必须显式指定连接
CI4 没有“透明读写分离”机制,$this->db 默认永远指向 $default。读从库必须主动切换:
// 在 Model 中
protected $DBGroup = 'slave'; // 仅对 Query Builder 生效
// 或运行时切换
$slaveDB = \Config\Database::connect('slave');
$result = $slaveDB->table('users')->select('id, name')->get()->getResult();
容易踩的坑:$DBGroup 只作用于当前 Model 实例的 Query Builder 操作,不影响 rawQuery()、事务或手写 SQL;事务内绝不能混用主从连接,否则会报 Commands out of sync 错误。
- 使用
connect('slave')时,务必检查返回值是否为有效连接对象,从库不可达时它不会抛异常,而是返回空连接 - 不要在控制器里反复
connect('slave'),连接对象可复用;但也不要全局 static 存储,PHP-FPM 每次请求都是新进程 - 涉及写后立即读的场景(如注册后跳转显示用户信息),必须用主库查,否则可能读到过期数据
审查代码时重点盯这三处
读写分离失效往往不是配置问题,而是业务代码绕过了分流逻辑。审查时直接 grep 这些关键词:
-
->table(+->get(组合:确认前面是否有$this->db->groupStart()或其他干扰 Query Builder 的调用,它们可能导致$DBGroup失效 -
rawQuery(:这类查询默认走$default,即使 Model 设了$DBGroup = 'slave'也无效,必须显式传入connect('slave')实例 -
startTransaction(:事务块内所有操作强制走主库,若里面夹带connect('slave')查询,MySQL 会拒绝执行
最隐蔽的问题是缓存穿透:比如 Redis 缓存失效后回源查从库,但代码里用了 $this->db->query("SELECT ...") 而非 connect('slave')->query(...),结果还是打到主库扛压。











