codeigniter 4 原生不支持数据库读写分离,因模型默认使用 $dbgroup = 'default' 且 basemodel 懒加载 $this->db,手动调用 database::connect() 不会自动绑定至模型,导致 find()、insert() 等方法仍走配置中 default 组(常设为从库),引发事务失效、主从延迟读脏数据等问题;正确做法是继承 basemodel 并通过 protected $dbgroup 声明式指定连接组,配合构造函数或方法内动态切换,避免混用 dbgroup() 与 database::connect()。

CodeIgniter 4 原生不支持数据库读写分离,直接在 app/Config/Database.php 里配多个组、再手动切连接,很容易导致事务失效、主从延迟读取脏数据、或模型方法意外走错连接。
为什么 $db->table() 会走错连接?
CI4 的 BaseModel 默认使用 default 数据库组,即使你在构造函数里用 \Config\Database::connect('write') 拿到主库连接,只要没显式绑定到模型实例,后续所有 find()、insert() 都仍走配置里的 default 组——而这个组你很可能设成了从库。
- 模型未指定连接时,始终读取
Database::$defaultGroup对应的配置 -
$this->db在模型中是懒加载的,首次访问才初始化,且不继承构造时手动创建的连接 - 事务(
transStart())必须全程使用同一连接,跨连接调用会静默失败
如何让 UserModel 写主库、读从库?
不能靠“运行时切换配置”,得靠模型级连接绑定 + 方法级路由控制。标准做法是:继承 BaseModel,重写 __construct() 并按需指定连接组名,再在具体方法里明确区分读写路径。
- 在模型构造函数中不直接 new 连接,而是调用
DBGroup('write')或DBGroup('read') - 写操作统一走
protected $DBGroup = 'write';,并在insert()、update()前强制设置 - 读操作(如
find()、findAll())可覆盖为先查从库,但需加if ($this->db->getPlatform() === 'MySQL') { ... }避免 SQLite 环境报错
示例:
class UserModel extends BaseModel
{
protected $table = 'users';
protected $DBGroup = 'read'; // 默认走从库
public function __construct()
{
parent::__construct();
// 强制写操作用主库
if (in_array($this->request->getMethod(), ['post', 'put', 'delete'])) {
$this->DBGroup = 'write';
}
}
public function insert($data)
{
$this->DBGroup = 'write';
return parent::insert($data);
}
}
DBGroup() 和 \Config\Database::connect() 的区别
前者是模型层面的声明式路由,后者是过程式连接获取——混用会导致连接池混乱、事务丢失。
-
DBGroup('write')只影响当前模型后续所有查询,且会被BaseModel的内部逻辑识别并复用连接实例 -
\Config\Database::connect('write')每次都新建连接对象,不会被模型自动接管,$this->db仍指向默认组 - 若在控制器里手动
$db = Database::connect('write')后传给模型,必须同时设$model->db = $db,否则无效
主从延迟下怎么避免刚写就查不到?
没有银弹。CI4 不提供自动等待从库同步的机制,硬等会拖慢响应;不等又可能读到旧数据。关键取舍点在业务容忍度。
- 用户注册后立即跳转到个人页?必须用主库查,加
$this->db = \Config\Database::connect('write');临时切换 - 列表页分页查询?允许 1~2 秒延迟,坚持走从库,但要在前端加 loading 或缓存 fallback
- 高频更新+强一致性场景(如库存扣减),干脆不用从库,或改用消息队列异步更新缓存
最易被忽略的是:cache 配置里如果用了数据库驱动,它默认也走 default 组——这意味着缓存读写可能跨主从,引发数据不一致。务必单独配一个 cache 专用连接组。











