MySQL读写分离通过主从复制和路由策略实现,写操作走主库,读操作分发至从库以提升性能;需配置主从复制、选择应用层或中间件路由方式,并应对主从延迟与一致性问题。

CI4 的 Database 配置不支持原生读写分离
CI4 官方没有像 Yii2 或 ThinkPHP 那样内置 master/slave 键名或自动 SQL 类型路由。你直接在 app/Config/Database.php 里写两个连接组(比如 $db['master'] 和 $db['slave']),框架本身不会自动判断 SELECT 走从库、INSERT 走主库——它只会把你指定的组名当作独立数据库用,需要手动切换。
这意味着:如果你希望“对开发者透明”,就必须自己封装一层查询入口;如果只是临时跨库查数据,用多连接加载即可,无需走读写分离逻辑。
常见错误现象:
- 配置了
$db['slave']但模型里仍调用$this->db->select(),结果还是连的 default 主库 - 误以为设置
autoConnect => false就能延迟路由,其实只是禁用了自动初始化,不解决路由问题 - 在
.env中用database.default.hostname和database.slave.hostname并行配置,但 CI4 的环境变量解析器只认default组,其余组名不会被自动注入
手动实现读写分离的三种可行路径
CI4 没有强制方案,但有三类实操性较强的落地方式,按侵入性和维护成本排序:
-
轻量级:在 Service 层显式调用不同 DB 实例 —— 在业务逻辑中明确使用
$this->dbMaster->insert()和$this->dbSlave->get(),适合读写比例清晰、一致性要求高的场景(如订单创建后立即查详情) -
中间层:自定义
DBConnection子类 + 重写query()方法 —— 判断 SQL 开头是否为SELECT、SHOW、EXPLAIN等读操作,再动态选择连接;需注意事务内所有操作必须强制走主库,否则会报Commands out of sync -
代理式:用
Query Builder包装器统一拦截 —— 创建一个ReadWriteBuilder类,把$this->builder->select()等方法重定向到对应 DB 实例;缺点是无法覆盖原生$db->query('SELECT ...')调用,且join()、subquery()等复杂操作可能绕过判断
性能影响方面:中间层和代理式都会增加一次正则匹配或方法转发开销(约 0.02–0.05ms/次),但远低于网络 IO;真正瓶颈往往出在主从延迟未监控,导致从库返回过期数据。
$db['default'] 必须指向主库,且不能设为从库
CI4 启动时默认加载 $db['default'],很多核心组件(如 Session、Migration、Seeder)都隐式依赖它。如果你把 default 设成从库:
-
php spark migrate会失败,报错SQLSTATE[HY000]: General error: 1785 Statement violates GTID consistency(因从库禁止写) - Session 写入失败,用户反复登出,日志里出现
Unable to connect to the database(实际是写权限拒绝) - Seeder 执行
$this->db->table('users')->insert()时静默失败,无报错但数据没进库
正确做法是保留 $db['default'] 为主库连接,另配 $db['slave_1']、$db['slave_2'] 等只读组,通过代码显式加载。别试图“欺骗”框架让 default 变成只读——它不是设计来这么用的。
主从延迟导致的脏读比性能问题更隐蔽
CI4 不提供主从延迟检测机制,但生产环境中这是最常被忽略的一环。典型表现是:
- 用户注册(写主库)后跳转个人页(读从库),页面显示“用户不存在”
- 后台改完配置,前端缓存已刷新,但从库还没同步,导致功能异常持续数秒至数十秒
解决方案不是加 sleep(0.1),而是:
- 在关键读操作前,先执行
SELECT MASTER_POS_WAIT(...)等待从库追上(仅限 MySQL 原生复制) - 对强一致性场景,直接复用主库连接,例如
$this->dbMaster->table('orders')->where('id', $id)->get() - 用 Redis 缓存写操作的主键,读前先查缓存是否存在,存在则强制走主库
真正的难点不在怎么连两个库,而在于什么时候该放弃从库——这需要结合业务语义做判断,没法靠配置一劳永逸。



















