ci4默认不支持读写分离,需重写db工厂逻辑实现:通过sql类型判断(正则匹配select等)动态切换主从连接,事务内强制走主库,避免预处理与混合语句误判,连接复用依赖sharedinstance且不可跨请求。

CI4 默认不支持读写分离,必须自己扩展 DB 类
CodeIgniter 4 的 Database 类在初始化时只加载一个配置项($db = \Config\Database::connect()),底层不区分主从、也不识别 SQL 类型。所谓“透明读写分离”,本质是拦截查询执行前的 SQL 类型判断 + 动态切换连接实例。官方没提供钩子,所以必须重写 BaseConnection 或包装 DB 工厂逻辑。
常见错误是直接改 system/Database/DB.php —— 这会导致升级失败。正确做法是在 app/Database/DB.php(手动创建)中复写工厂方法,并让 \Config\Database::connect() 指向它。
- 不要启用
autoConnect,否则每次请求都连主库浪费资源 - 主库连接应标记为
write,从库连接标记为read,用sharedInstance控制复用 - SQL 判断不能只看关键词(如
SELECT),要跳过注释、字符串、大小写干扰,建议用正则/^\s*(SELECT|SHOW|EXPLAIN|DESCRIBE)/i
如何动态选择主库或从库连接
核心逻辑在连接获取阶段:不是“执行时才选”,而是“首次调用 db() 时根据当前上下文决定返回哪个连接实例”。CI4 的 db() 辅助函数默认走 Database::connect(),你可以把它替换成自定义工厂:
// app/Database/DB.php
function db($group = null, $getShared = true)
{
if ($group === 'read') {
return \Config\Database::connect('slave', $getShared);
}
if ($group === 'write') {
return \Config\Database::connect('master', $getShared);
}
// 默认行为:根据当前 SQL 类型决定
$backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
$caller = $backtrace[1]['function'] ?? '';
if (in_array($caller, ['query', 'simpleQuery', 'table'])) {
$sql = $_SERVER['CI4_LAST_SQL'] ?? '';
if (preg_match('/^\s*(SELECT|SHOW|EXPLAIN|DESCRIBE)/i', $sql)) {
return \Config\Database::connect('slave', $getShared);
}
}
return \Config\Database::connect('master', $getShared);
}
注意:$_SERVER['CI4_LAST_SQL'] 需配合 Query Builder 的 getCompiledSelect() 等方法提前捕获,不能依赖运行时未解析的原始字符串。
- 避免在模型里硬编码
db('slave')—— 违反“对开发者透明”原则 - 事务内所有操作必须强制走主库,哪怕语句是
SELECT,需监听startTransaction()状态 - 连接复用靠
$getShared参数控制,设为false会新建连接,慎用于长耗时任务
连接池管理只能靠应用层模拟,CI4 无原生支持
MySQL 本身没有连接池概念,PHP-FPM 模式下每个请求进程独立维护连接,所谓“连接池”其实是复用已建立的连接实例。CI4 的 BaseConnection 支持 sharedInstance,但默认只对同一配置组生效。要实现跨组复用(比如多个从库间轮询),得自己加一层路由逻辑:
- 把从库配置成数组(
'slaves' => [['hostname' => '192.168.1.10'], ['hostname' => '192.168.1.11']]) - 用
spl_object_id()或计数器做简单轮询,避免单点过载 - 连接健康检查不能依赖
ping()(太重),建议在connect()失败时标记该节点临时不可用,5 秒后自动恢复 - 不要试图复用连接跨请求生命周期 —— PHP-FPM 请求结束即销毁资源,强行保持只会泄漏内存
最容易被忽略的事务与预处理语句陷阱
读写分离下,SELECT ... FOR UPDATE 和 INSERT ... SELECT 这类混合语句会被误判为纯读操作,发到从库导致报错。CI4 的 Query Builder 不会自动识别这类语义,必须人工干预:
- 显式调用
db('write')->table(...)强制走主库,适用于已知需写锁的场景 - 预处理语句(
prepare())的 SQL 在 prepare 阶段就需确定连接,但 CI4 的PreparedQuery是懒加载,实际执行才 connect —— 所以 prepare 时传入的$db实例必须是主库 - 使用
DB::transStart()后,后续所有db()调用应自动绑定到事务关联的主库连接,否则 commit 会失败
真正麻烦的是那些封装在服务类里的数据库调用 —— 它们可能跨多层调用链,一旦某处漏了连接指定,故障就难以追踪。建议在关键服务构造时注入明确的 $dbWrite 和 $dbRead 实例,而不是依赖全局 db() 函数。











