CI4读写分离不能靠Database.php配置,必须在.env中定义多个连接组(如default和slave),通过Config\Database::connect('group')显式获取连接,框架不自动路由读写请求,需开发者手动区分并确保只读账号权限正确。

CI4 读写分离不能靠 Database.php 配置
CodeIgniter 4 的数据库连接完全由 .env 文件驱动,app/Config/Database.php 是只读的占位文件,修改它不会生效。想实现读写分离,必须在 .env 中定义多个连接组(如 default 和 slave),再通过代码显式选择连接对象——CI4 没有内置的“自动读写分离”开关。
常见错误是把 CI3 的多库写法照搬过来,在 Database.php 里加 $db['slave'],结果调用时始终连的是 default,因为框架压根不读这个数组。
-
.env中需至少定义两套配置,命名区分明确,例如:database.default.hostname、database.slave.hostname - 所有字段名必须带
database.[group].前缀,漏掉任意一个点或拼错 group 名(如写成slvae)都会静默 fallback 到 default - 如果 slave 库用的是只读账号,务必确认该账号对目标表有
SELECT权限,否则->get()会抛出SQLSTATE[HY000]: General error: 1290 The MySQL server is running with the --read-only option类似错误
手动切换连接:用 \Config\Database::connect() 拿指定组
CI4 不提供类似 CI3 的 $this->load->database('slave', TRUE) 方法。你需要直接调用静态工厂方法获取连接实例,并赋值给变量使用。控制器或服务类中不能依赖 $this->db 全局引用,因为它始终绑定 default 组。
示例:在控制器中分别读主库写、从库读
// 主库写
$master = \Config\Database::connect('default');
$master->table('users')->insert(['name' => 'Alice']);
// 从库读(注意:必须传字符串名,不是数组)
$slave = \Config\Database::connect('slave');
$users = $slave->table('users')->get()->getResult();
- 每次调用
\Config\Database::connect('xxx')都会复用已有连接(PDO 持久化机制生效),无需担心频繁创建开销 - 不要写成
\Config\Database::connect(['group' => 'slave'])—— 这个方法只接受字符串参数,传数组会触发TypeError - 若
slave组配置缺失或连不上,connect()返回null,后续调用->table()会报Fatal error: Uncaught Error: Call to a member function table() on null,建议加空值判断
模型层适配读写分离的两种安全做法
在 Model 中硬编码 \Config\Database::connect('slave') 看似简单,但会导致测试困难、逻辑耦合、无法动态降级(比如 slave 挂了自动切回 default)。推荐以下两种更可控的方式:
- 构造函数注入连接对象:把连接实例作为依赖传入 Model,由上层(如 Service 或 Controller)决定用哪个库,Model 只负责执行 SQL
- 封装一个
DBPool工具类:内部维护default和slave实例,提供write()/read()方法,自动处理故障转移(例如 slave 连不上时 fallback 到 default)
避免在 Model 的 protected $DBGroup = 'slave' —— 这个属性只在 CI4 的 BaseModel 初始化时被读取一次,且仅用于构造默认连接,无法支持运行时切换;强行设它会导致所有 CRUD 都走 slave,写操作直接失败。
ORM 查询构建器不自动识别读写意图
CI4 的 Query Builder(如 $builder->select()->from()->where())本身没有“这是读请求”的元信息,它只是拼 SQL 字符串。即使你用 \Config\Database::connect('slave') 获取了从库连接,调用 ->update() 或 ->delete() 依然会发到 slave,而不会报错或重定向——MySQL 层会拒绝执行并返回错误。
这意味着读写分离的语义必须由开发者自己保证,框架不做拦截或路由。容易踩的坑包括:
- 在
UserService::getUserById()里用了 slave 连接,但同一 Service 的updateLastLogin()忘记切回 default,导致更新失败 - 使用
resource()路由自动生成的控制器方法(如Posts::update())默认走$this->db(即 default),但如果业务要求所有读都走 slave,就必须手动覆盖每个方法里的连接对象 - 事务只能跨单库执行:
$master->transStart()+$master->transComplete()有效,但混用$master和$slave会破坏一致性,CI4 不支持分布式事务
最稳妥的做法,是在应用层明确划分「读服务」和「写服务」,让每个服务只持有一个连接对象,从源头杜绝误用。


















