CI4读写分离必须手动实现,框架原生不支持自动路由;需显式调用不同连接组,事务中须使用同一连接,模型中应封装连接工厂,Shield等组件默认只认defaultGroup,健康检查与故障转移也需自行实现。

CI4 读写分离必须手动实现,框架原生不支持
CodeIgniter 4 官方数据库层 CodeIgniter\Database 没有内置读写分离开关或自动路由逻辑。所谓“支持”,实际是指它提供了足够灵活的连接管理能力,让你能自己搭出读写分离结构——但所有路由判断、连接切换、事务约束都得自己写。
常见错误是以为配置多个数据库组(如 database.read 和 database.write)就能自动分流。CI4 的 Database.php 文件只用于定义连接组名和参数,$db = \Config\Database::connect('read') 这类调用必须显式触发,框架不会根据 SELECT 或 INSERT 自动选组。
- 所有读操作(
find()、get()、query("SELECT..."))需手动指定读库连接 - 所有写操作(
insert()、update()、delete()、query("INSERT..."))需手动指定写库连接 - 事务中必须全程使用同一连接实例,否则会报错
mysqli_sql_exception: Commands out of sync - 若用模型(Model),需重写
protected $DBGroup = 'write'并在方法内动态切换$this->db = \Config\Database::connect('read')
如何安全地在 Model 中做读写分组切换
直接在 Model 方法里反复调用 \Config\Database::connect() 很危险:连接对象被复用时可能残留上一次查询的状态(比如未清空的绑定参数、未重置的 fetch mode)。推荐封装一个轻量连接工厂,带连接池语义。
例如,在 app/Models/BaseModel.php 中:
class BaseModel extends \CodeIgniter\Model
{
protected $DBGroup = 'write';
protected function getReadDB(): \CodeIgniter\Database\MySQLi\Connection
{
return \Config\Database::connect('read');
}
protected function getWriteDB(): \CodeIgniter\Database\MySQLi\Connection
{
return \Config\Database::connect('write');
}
}
然后在具体 Model 中按需调用:
$this->getReadDB()->table('users')->where('status', 'active')->get()$this->getWriteDB()->table('logs')->insert(['action' => 'login'])- 绝不混用:不要在同一个方法里先用
getReadDB()查询,再用getWriteDB()插入并期望事务包裹两者
与 CI4 生态组件(如 Shield、IonAuth)集成时的坑
Shield(CI4 官方认证库)和第三方 Auth 库默认只认 $db 配置项,即 .env 里 database.defaultGroup 指向的连接组。它们不会感知你手动切换的 read 或 write 组。
这意味着:用户登录、密码校验、token 刷新等关键流程,全部走的是 defaultGroup 配置的连接——如果你把它设为 read,$auth->login() 会因无法写 session 或更新 last_login 时间而失败;如果设为 write,又白白浪费主库资源去查用户是否存在。
- 最稳妥做法:把
defaultGroup设为write,让 Shield 走主库;读密集型业务(如用户资料页)再单独切到read - 不要试图 patch Shield 的
AuthModel去动态换 DBGroup——它的很多方法(如attempt())内部嵌套多层查询,状态难保一致 - 若必须读库验证用户,需自行实现
CustomAuthenticator,并在其中明确指定\Config\Database::connect('read')实例
连接健康检查与故障转移不能靠 CI4 自动完成
当主库宕机,CI4 不会自动把写请求转到从库(也不可能这么做),也不会在读库不可用时 fallback 到主库。这些逻辑必须你自己加。
简单可行的做法是在连接工厂里加一层探测:
protected function getReadDB(): \CodeIgniter\Database\MySQLi\Connection
{
$db = \Config\Database::connect('read');
if (! $db->connID || ! $db->query('SELECT 1')->getResult()) {
// 降级到 write 组读取(仅限非关键场景)
return \Config\Database::connect('write');
}
return $db;
}
但要注意:
- 这种降级只适用于最终一致性要求不高的读场景(如首页 banner)
- 绝对不能用于强一致性读(如订单详情页,刚支付完立刻查状态)
- 生产环境建议配合外部健康检查服务(如 Consul 或自建心跳接口),而非每次请求都探活
真正复杂的故障转移和负载均衡,得交给中间件(如 ProxySQL)或云数据库服务(如 AWS RDS Read Replica + Route53 权重),CI4 层只负责连接透传。这点容易被高估——框架不是数据库网关。


















