CodeIgniter 4 不支持自动读写分离和字段脱敏,需通过定义多个数据库组(如 'write' 和 'read')并手动调用 db('group') 实现安全切换;脱敏必须在 Model 或 Service 层完成,避免 Controller 处理或响应体正则替换。

CodeIgniter 4 本身不内置读写分离或字段级脱敏能力,必须靠手动干预 + 第三方组件/中间件组合实现。直接改框架底层或硬编码路由极易出错,尤其在事务、连接复用、查询缓存等场景下容易丢数据或读到脏数据。
CI4 中如何安全切换主从数据源
CI4 的 $db 实例默认是单例,且 Database 类不支持运行时动态替换连接配置。强行在 Model 或 Controller 里 new 多个 DB 实例会导致连接泄漏、事务失效、Query Builder 状态错乱。
- 正确做法是基于
Config\Database定义多个组(如'write'和'read'),再通过自定义Database工厂类控制实例创建时机 - 所有写操作(
insert()、update()、delete()、transStart())必须显式调用db('write')获取连接;读操作(get()、getWhere())统一走db('read') - 切忌在同一个事务中混用两个数据源——CI4 的
transComplete()只作用于当前连接,无法跨库提交 - 注意
Query Builder缓存(cacheOn())会绑定到具体连接实例,主从切换后缓存不共享,需单独管理
为什么不能靠 CI4 的 group 配置自动路由
CI4 的 database.php 中的 groups 仅用于初始化多个独立连接池,不提供读写语义识别。框架不会根据 SQL 类型(SELECT vs INSERT)自动选组,也不会感知事务上下文。
-
$this->db在 Controller 初始化时就已固定为某一个组,除非你手动调用db('xxx') - Model 默认使用全局
$db实例,若未显式指定组,它永远只连第一个定义的组(通常是default) - 试图用
DBGroup或ConnectionInterface做透明路由,会破坏 CI4 的连接生命周期管理,导致PDO::beginTransaction()报错或静默失败
字段脱敏必须在应用层做,不能依赖数据库中间件
CI4 的 Result 对象返回的是原始数组或对象,没有钩子拦截查询结果做字段重写。像 TDE 或 DBG 这类网关级脱敏对 CI4 无效——它们处理的是 SQL 层,而 CI4 的 select() 返回前已经完成解析。
- 敏感字段(如
id_card、phone)应在 Model 的find()/get()后立即脱敏,推荐封装成maskPhone($str)等工具函数 - 避免在 Controller 层脱敏:容易漏掉 API 返回、JSON 输出、日志打印等路径;也违背 MVC 职责分离
- 不要用正则全局替换响应体——CI4 的
json()响应可能含非敏感字段(如时间戳、状态码),误脱敏会导致前端解析失败 - 若需动态策略(如不同角色看不同精度),脱敏逻辑要和权限校验耦合,放在 Service 层统一出口处理
最容易被忽略的三个坑
一是主从延迟导致刚写入的数据查不到:用户注册后立刻跳转个人页,db('read') 可能查不到新记录。解决办法不是加 sleep,而是对关键路径(如登录态、订单详情)强制走 db('write') 查询。
二是脱敏后丢失类型信息:比如把 int 型手机号转成 string 星号,前端 JS 做数值比较会出错。建议脱敏函数保留原始类型结构,仅替换值内容。
三是连接复用污染:CI4 的 Database 实例在 CLI 模式下可能被多次请求复用,若上一个请求用了 db('write'),下一个请求没重置就直接 db('read'),实际仍连主库——必须每次明确指定组名,不能依赖“上次用的是哪个”。

















