读写分离失效时应手动实现从库容灾:连接阶段捕获异常并让读操作动态指向主库,配合缓存开关限流防压垮,校验类读强制走主库,容灾逻辑须配置化可测可控。

读写分离失效时,主库连不上怎么办
当从库宕机或网络中断,CI4 默认仍会尝试连接从库执行 SELECT,结果是整个读请求超时甚至阻塞。这不是“自动降级”,而是直接失败——因为 CI4 原生不带读库容灾逻辑。
必须手动干预:在自定义 DB 类中捕获 ConnectionException,并 fallback 到主库执行读操作。关键点不是“重试”,而是“绕过从库连接初始化”。
- 不要在
DB_driver.php的query()里 catch 后再调用$this->connect('write')——这会导致事务上下文丢失、连接复用失效 - 应在连接建立阶段(
initConnection()或connect())判断:若从库配置存在但连接失败,跳过该连接组,将$this->read_db指向主库实例 - 避免全局切换:
$this->db仍应保持主库引用,只让读方法(如get()、select())内部动态选择连接对象
从库全部不可用时,如何防止主库被读流量压垮
单纯 fallback 到主库读,等于把读压力 100% 导回主库,QPS 翻倍可能触发慢查询、锁表甚至连接数打满。CI4 没有内置熔断或限流,得靠代码层控制。
推荐做法是在模型或服务层加轻量级开关:用 cache()->get('read_fallback_active') 缓存一个布尔值,初始为 FALSE;首次从库连接失败时设为 TRUE,并记录时间戳;后续读请求先检查该键,若已启用 fallback,则强制加 SQL_NO_CACHE(防主库缓存污染),且限制单次查询返回行数(如 limit(100))。
- 缓存键建议带环境前缀:
ci4_rw_fallback_{env},避免开发/生产环境互相干扰 - 不要依赖
session()存开关状态——会话可能跨请求失效,且无法共享到 CLI 或队列任务 - 主库 fallback 不应持续超过 5 分钟,超时后自动清空缓存键,重新尝试连接从库
主库写入失败后,如何安全回滚已发生的从库读操作
CI4 本身不支持跨库事务,所谓“读写分离下的事务一致性”本质是伪命题。一旦主库写失败,从库上刚查出来的数据(比如库存余量)已经过期,但你无法 rollback 那个 SELECT。
真正要做的,是切断“读结果用于写决策”的链路。例如用户下单时先查库存再扣减,这个流程必须改成:所有校验类读操作(库存、余额、状态)都走主库,哪怕慢一点;只允许非校验类读(如商品详情、历史订单列表)走从库。
- 在模型方法命名上区分:
getStockForDeduction()强制用主库,getProductDetail()可走从库 - 不要在构造函数里一次性加载
$this->master_db和$this->slave_db,而应在每个方法内按需加载,避免误用 - CI4 的
transStart()/transComplete()只对当前连接有效,跨库调用无效,文档没说但实测会静默忽略
配置文件里怎么写才能让容灾逻辑可测试、可灰度
硬编码 fallback 行为会让单元测试无法 mock 从库状态,也难以做灰度发布。正确方式是把容灾策略外移到配置项,而不是写死在 DB 类里。
在 app/Config/Database.php 中增加一个数组:
public array $read_failover = [
'enabled' => env('DB_READ_FAILOVER_ENABLED', false),
'mode' => env('DB_READ_FAILOVER_MODE', 'direct'), // 'direct' or 'queue'
'timeout' => (int) env('DB_READ_FAILOVER_TIMEOUT', 300), // seconds
];
然后在自定义 DB 类中读取该配置,决定是否启用 fallback、走直连还是进消息队列重试。这样测试时只需改 .env 即可模拟不同故障场景。
- CI4 的
env()函数默认只读取字符串,布尔值和整数需显式转换,否则env('DB_READ_FAILOVER_ENABLED')返回的是字符串"false",不是布尔false - 灰度时可用路由参数或请求头控制:
if ($this->request->getHeaderLine('X-Db-Failover') === 'on'),比改配置更灵活 - 别把容灾开关放在数据库里——故障时连不上库就取不到开关值,陷入死循环


















