ci4 的 services::database() 不支持原生读写分离,因其仅返回单个连接实例且无sql解析能力,需手动配置 master/slave 多组连接并显式调用,事务内必须统一连接,跨库事务需外部协调器实现。

CI4 中 Services::database() 不支持原生读写分离
CodeIgniter 4 的 Services::database() 工厂方法返回的是单个数据库连接实例,它本身不识别主从拓扑,也不会根据 SQL 类型自动路由到不同节点。你调用一次 Services::database(),拿到的就是配置文件里 default 组定义的那个库——无论它是主库还是从库。
常见错误现象是:在控制器里写了两条查询,一条 INSERT、一条 SELECT,期望后者走从库,结果全打到同一个库上;或者手动切换连接后忘记还原,导致后续操作出错。
- 必须显式创建多个数据库组(如
master和slave),并在配置中分别定义 host、username 等参数 - 不能依赖 CI4 自动判断读/写——它没有 SQL 解析器,不会分析
SELECT或UPDATE关键字 - 事务内所有操作必须使用同一连接,否则会抛出
Transaction not started或数据不一致
手动路由时如何保证同一线程内读写一致性
在主从延迟明显或强一致性要求高的场景(比如用户刚提交订单,立即查订单列表),必须让后续读请求强制走主库。CI4 没有内置的“事务绑定主库”机制,得靠开发者控制连接生命周期。
典型做法是把主库连接存入 $this->db_master,并在需要实时读的地方显式使用它,而不是反复调用 Services::database()。
- 在基类控制器的
__construct()中初始化主库连接:$this->db_master = \Config\Database::connect('master') - 写操作后立刻要读,直接用
$this->db_master->table('orders')->where('id', $id)->get()->getRow() - 避免在模型中混用不同连接——模型默认用
default组,容易误走从库 - 不要在中间件里切换连接,除非你能确保整个请求生命周期内连接状态可预测
分布式事务在 CI4 中只能靠外部协调器实现
CI4 的数据库层完全不感知跨库事务。它的 $db->transStart() 只对当前连接生效,无法协调 master 和 slave 之间的提交或回滚。一旦涉及分库、分表或跨服务写入,就必须引入外部事务管理器。
比如用 Atomikos 或 Seata 做 2PC 协调,CI4 应用只作为参与者暴露 XA 接口;或者改用 Saga 模式,在业务层补偿失败步骤。
- ShardingSphere 支持 XA 事务,但 CI4 需配合 JDBC 驱动(PHP 不适用),所以实际不可行
- 基于消息队列的最终一致性更现实:写主库成功后发 MQ,下游服务消费并更新从库或其他系统
- CI4 自带的
transComplete()对多库无效,强行调用只会让主库提交、从库无感知,造成数据断裂
ShardingSphere + CI4 的真实集成边界在哪里
ShardingSphere 是 Java 生态的中间件,CI4 是 PHP 框架,两者不在同一进程。所谓“集成”,本质是 CI4 把 ShardingSphere 当作一个普通 MySQL 代理来用——即把 host 配成 ShardingSphere 的地址,端口设为它监听的 MySQL 协议端口(如 3307)。
这时 CI4 完全不知道背后是读写分离还是分库分表,所有路由逻辑由 ShardingSphere 完成。CI4 只需按单库方式写 SQL,但必须遵守它的约束:
- 不能用
SELECT * FROM db1.table1 JOIN db2.table2—— 跨库 JOIN 不被支持 - 分页查询要加
ORDER BY,否则合并结果可能乱序 - ShardingSphere 的
Hint强制主库路由需通过注释传递,例如:/*+ USE_MASTER() */ SELECT ... - CI4 的 Query Builder 生成的 SQL 若含反引号或特殊别名,可能触发 ShardingSphere 解析失败
真正容易被忽略的点是:ShardingSphere 的主从延迟容忍策略和 CI4 的超时设置不联动。比如 ShardingSphere 设置了 maxReplicationLag 为 500ms,但 CI4 的 DB['default']['DBDebug'] = true 下报错堆栈里根本看不出延迟问题,只会显示“连接超时”或“空结果”。











