ci4读写分离必须通过.env配置多组连接并手动判断sql类型切换连接,database.php配置无效,框架不提供自动路由机制。

CI4 读写分离不能靠 database.php 配置
CodeIgniter 4 完全不读 app/Config/Database.php 里的数据库配置,所有连接参数必须写进项目根目录的 .env 文件。想在 Database.php 里加 $db['master'] 和 $db['slave'] 是无效的——框架压根不会解析它。
读写分离要落地,得靠两件事:一是用 .env 定义多组连接(比如 database.master.* 和 database.slave.*),二是自己写逻辑判断 SQL 类型并切换连接对象。CI4 没提供开箱即用的读写路由机制,不像 Laravel 的 read/write 键那样自动分流。
-
.env中必须显式定义至少两个连接组,例如:database.master.hostname = "192.168.1.10"database.master.username = "root"database.slave.hostname = "192.168.1.11"database.slave.username = "replica" - CI4 的
\Config\Database::connect('master')和\Config\Database::connect('slave')是合法调用,但不会自动识别SELECT就走 slave ——你得在模型或服务层手动判断 - 不要试图复用 CI3 的
$this->load->database('slave', TRUE)写法,CI4 的 Loader 不再暴露database()方法,$this->load也不支持传参加载指定组
怎么让 SELECT 自动走从库
没有魔法开关,只能在查询发起前做拦截。最稳妥的做法是在 BaseModel 或自定义 Service 类里封装一个 query() 方法,对 SQL 做前缀判断:
示例逻辑(非完整类,仅示意):
// 判断是否只读语句
$readOnly = preg_match('/^\s*(SELECT|SHOW|DESCRIBE|EXPLAIN)\b/i', $sql);
$db = $readOnly
? \Config\Database::connect('slave')
: \Config\Database::connect('master');
- 正则匹配要宽松,比如
SELECT DISTINCT、SELECT /*+ USE_INDEX */都得命中,建议用/^\s*(SELECT|SHOW|DESCRIBE|EXPLAIN)/i - 别依赖
mysqli_ping()或重连逻辑来“保活”从库连接——CI4 的connect()默认复用已有连接,只要没超时就直接返回,性能影响极小 - 如果用了 Query Builder,得在
get()、countAll()等方法之前把连接对象塞进去,不能等execute()才换,否则 Builder 已绑定默认连接
CI4 新特性让读写分离更可控
CI4 的 ConnectionInterface 和可注入的 BaseBuilder 让你能在不改核心的前提下替换查询执行链路。比起 CI3 必须重写 DB.php 或 Loader,现在可以:
- 在
app/Config/Services.php里注册一个自定义 DB 服务,返回包装后的连接实例,统一处理读写路由 - 利用
Events监听db:query事件,在 SQL 执行前修改连接对象(注意:事件回调里不能 throw 异常,否则中断整个请求生命周期) - 用
QueryFilter(需自行实现)拦截 Builder 的getCompiledSelect()结果,动态决定用哪个连接执行 - CI4.5+ 支持
connection()->useReadConnection()这类语义化方法,但仍是实验性 API,不建议在生产环境强依赖
容易被忽略的坑:.env 编码与 Docker 网络
很多读写分离失败根本不是逻辑问题,而是环境配置翻车:
-
.env文件必须是 UTF-8 无 BOM 编码,Windows 记事本另存为“UTF-8”默认带 BOM,会导致 CI4 解析失败,报ParseError或静默 500 ——用 VS Code 打开后右下角确认编码并点击转换 - Docker 场景下,
database.master.hostname写localhost是错的:容器内localhost指向自己,不是宿主机或另一容器;必须写实际服务名(如mysql-master)或宿主机网关 IP(如172.17.0.1) - 从库延迟高时,CI4 的
connect()不会自动 fallback,也不会抛出 “read-only mode” 错误;你得自己在查询后检查$result->getResult()是否为空,再决定是否重试主库
真正麻烦的不是配两个库,而是保证从库查询不因网络抖动、权限错配或时钟不同步而静默失败——这些细节比路由逻辑更耗调试时间。











