ci4读写分离必须通过.env定义多组连接(如default/slave),在模型基类重写db()方法按操作类型自动切换,不可依赖database.php配置或sql解析,测试需mock连接对象验证来源。

CI4 读写分离不能靠 app/Config/Database.php
CodeIgniter 4 完全忽略 app/Config/Database.php 中的数据库配置,所有连接参数必须从 .env 文件加载。试图在 Database.php 里定义主库/从库数组、或重写 DBConnection 类来实现读写分离,只会白忙一场——框架压根不会读它。
真正可行的路径只有一条:利用 CI4 的「多组数据库配置」能力,在 .env 中声明多个连接组(如 default 和 slave),再通过运行时逻辑决定用哪一组。这不是“开箱即用”的功能,但它是唯一不侵入核心、不破坏升级兼容性的做法。
-
database.default.hostname设为主库地址(如mysql-master) -
database.slave.hostname设为从库地址(如mysql-slave),其他字段(username、password、database)照常填写 - 确保两个连接组的
DBDriver、DBPort、charset等基础参数一致,否则运行时可能因连接属性冲突报错
手动切换读写连接需覆盖 BaseModel 的 db() 方法
CI4 没有内置 SQL 类型自动路由机制(不像 Laravel 的 read/write connection resolver)。要让 $this->find() 走从库、$this->insert() 走主库,必须在模型层拦截连接选择逻辑。
推荐做法是创建一个基类 App\Models\ReadWriteModel,继承 \CodeIgniter\Model,并重写 db() 方法:
protected function db(): BaseConnection
{
$method = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]['function'] ?? '';
if (in_array($method, ['find', 'findAll', 'select', 'countAll', 'like'])) {
return \Config\Database::connect('slave');
}
return \Config\Database::connect('default');
}
注意点:
- 不要依赖
$_SERVER['REQUEST_METHOD']或全局 HTTP 判断读写——CLI 命令、队列任务、API Webhook 都会失效 - 避免在
db()里做复杂 SQL 解析(比如正则匹配INSERT),CI4 的 query builder 可能生成带子查询的SELECT,误判风险高 - 该方案对原生
$db =& \Config\Database::connect()调用无效,仅影响继承此基类的模型
phpunit 测试读写分离必须伪造连接而非真实连库
自动化测试里直接连真实 MySQL 主从环境,会导致测试不稳定(网络抖动、从库延迟、权限隔离难)、执行慢、CI 流水线失败排查成本高。CI4 的 TestResponse 和 DatabaseTestCase 也不支持动态切换连接组。
正确做法是:用 Mockery 或 PHP 内置 createMock() 替换 \CodeIgniter\Database\BaseConnection 实例,并在测试中显式注入:
$mockDb = $this->createMock(BaseConnection::class);
$mockDb->method('query')->willReturn((object)['result' => []]);
$model = new MyModel();
$reflection = new \ReflectionClass($model);
$property = $reflection->getProperty('db');
$property->setAccessible(true);
$property->setValue($model, $mockDb);
这样你就能断言:
- 调用
$model->find(1)时,是否触发了slave连接的 mock 实例 - 调用
$model->insert($data)时,是否触发了default连接的 mock 实例 - 关键不是查数据对不对,而是「连接对象来源是否符合预期」
从库延迟导致测试结果不可靠是最大隐藏坑
即使读写分离逻辑本身完全正确,MySQL 从库的复制延迟会让 insert() 后立刻 find() 查不到刚写入的数据——这在本地开发环境(主从同机)可能不明显,但在 Docker Compose 或云数据库场景下极易复现。
这不是代码 bug,而是架构约束。应对方式只有两个:
- 业务上接受「最终一致性」:读接口明确标注「可能延迟 X 秒」,前端加 loading 或轮询兜底
- 强一致性场景强制走主库读:提供
$model->findOnMaster(1)这类显式方法,绕过自动路由
测试里最容易忽略这点:用 sleep(1) 等从库同步,看似通过,实则掩盖了设计缺陷。真要验证延迟影响,得在测试中模拟 500ms 复制 lag,而不是祈祷它快。











