ci4事务必须走主库,否则因从库只读会报错;需显式调用usemaster()或connect('write'),框架不支持自动路由、hintmanager及注解路由,读写分离依赖配置层手动切换。

CI4 中事务必须走主库,否则会报错
CodeIgniter 4 默认不支持跨数据源事务,transStart() 和 transComplete() 只作用于当前激活的 $db 实例。如果你在读写分离配置下未显式切换到主库就开启事务,操作会被路由到从库 —— 而从库是只读的,执行 INSERT/UPDATE 时直接抛出 SQLSTATE[HY000]: General error: 1788 The target server is not writable 类似错误。
实操建议:
- 所有涉及写操作的事务块,必须先调用
$this->db->useMaster()(前提是已按 CI4 官方方式配置了主从连接组) - 不要依赖自动路由:即使你用了
Database::connect('group_name'),CI4 也不会在事务中自动切回主库 - 若使用服务类封装数据库逻辑,务必在构造函数或方法入口处明确指定连接目标,例如:
$this->masterDb = \Config\Database::connect('writable')
HintManager 类不存在,CI4 没有 Sharding-JDBC 那套机制
很多开发者搜索“CI4 读写分离”时会误抄 Java 生态的方案,比如用 HintManager.setMasterRouteOnly() 强制主库路由 —— 这在 CI4 里根本不存在。CI4 的读写分离能力完全依赖配置层和手动连接切换,框架本身不提供 SQL 解析或运行时路由规则。
常见错误现象:
- 写了
use CodeIgniter\Shield\Authentication\Actions\HintManager;,结果报Class 'HintManager' not found - 试图在 Model 中通过注解或方法名前缀(如
getFromMaster())触发自动路由,但 CI4 的Model类不识别这类约定
正确做法:
- 在控制器或服务类中,用
\Config\Database::connect('write')显式获取主库连接实例 - 把读操作封装成独立方法,统一使用
\Config\Database::connect('read'),避免混用 - 不要试图给
BaseModel加通用路由逻辑,CI4 的BaseModel不支持动态切换连接组
更新后立即读取,必须手动切主库 + 控制查询时机
典型场景:用户提交表单后跳转列表页,列表页默认走从库,但此时主从延迟可能导致查不到刚插入的数据。CI4 不会帮你判断“这条记录是否已同步”,它只管按配置路由。
可行方案:
- 写操作完成后,立刻用主库连接查一次关键字段(如新记录 ID 或状态),确认落库成功,再跳转 —— 不要省略这一步验证
- 对强一致性要求高的接口(如支付回调、订单创建),整个请求生命周期内只用主库连接,避免切换成本和延迟风险
- 如果业务允许短暂延迟,可在跳转 URL 中加时间戳或随机参数(如
?t=1722952440),配合 Nginx 缓存策略强制绕过 CDN 缓存,但无法解决从库延迟本身
注意:sleep(1) 或重试查询不是可靠解法 —— 主从延迟可能波动在毫秒到数秒之间,硬等待既伤性能又不可控。
配置文件里 group 名称必须一致,且 write/read 必须共用同一 driver
CI4 的读写分离不是靠两个独立数据库配置实现的,而是通过一个连接组(group)下的多个子配置来区分主从。如果 app/Config/Database.php 中 'write' 和 'read' 子项的 DBDriver 不一致(比如一个是 mysqli,一个是 pdo_mysql),或者没在 defaultGroup 中声明该 group,Database::connect('group_name') 就会 fallback 到默认连接,导致读写都打到同一个库。
关键配置要点:
-
'DSN' => ''必须留空,否则 CI4 会忽略 host/port 等字段 -
'hostname'、'username'、'password'等字段需分别填入主库和从库的实际值 - 从库配置中必须设置
'readOnly' => true,否则 CI4 不会在内部做只读保护 - 主库连接需额外配置
'failover' => ['read'](可选),但 CI4 不自动 failover,仅用于连接池管理
最容易被忽略的是:CI4 的连接组机制不校验主从节点是否真实可达,也不做健康检查 —— 如果从库宕机,connect('read') 仍会返回连接对象,直到执行第一条查询才暴露异常。











