ci4读写分离必须手动实现,无内置自动路由机制;需在.env中定义read/write两组连接,代码中通过config\database::connect('read'/'write')显式获取连接,并在服务类中封装连接逻辑以确保事务一致性与复用性。

CI4 读写分离必须手动实现,没有内置开关
CodeIgniter 4 官方不提供类似 Laravel 的 read_write_connection 自动路由机制。所谓“读写分离”,本质是你自己控制哪次查询走哪个连接——框架只负责帮你建多个连接,剩下的调度逻辑全得手写。
常见错误是以为只要在 .env 里配两套数据库参数(比如 database.read.* 和 database.write.*)就能自动分流。实际上 CI4 的 Database 配置只认 database.default.*,其他前缀的配置项会被完全忽略,除非你主动调用。
- 必须在
.env中显式定义两组连接,例如:database.read.hostname = 192.168.1.10<br>database.read.username = app_reader<br>database.read.password = r3ad0nly<br>database.read.database = myapp_db<br><br>database.write.hostname = 192.168.1.20<br>database.write.username = app_writer<br>database.write.password = wR1t3r!<br>database.write.database = myapp_db
- CI4 不会自动加载这些配置,你需要在代码中用
\Config\Database::connect('read')或\Config\Database::connect('write')显式获取连接实例 - 连接名(如
'read')必须和.env中的前缀一致,且不能含点号或下划线以外的符号
如何在模型或服务类中安全切换读/写连接
直接在控制器里每次 connect() 是反模式:易出错、难复用、无法统一处理事务或重试逻辑。推荐把连接选择逻辑封装进服务类或自定义模型基类。
一个典型做法是让服务类构造时接受连接名,并缓存连接实例:
class UserService
{
protected $db;
public function __construct(string $connection = 'write')
{
$this->db = \Config\Database::connect($connection);
}
public function getUser(int $id): array
{
// 强制走 read 连接
return $this->db->table('users')->where('id', $id)->get()->getRowArray();
}
public function createUser(array $data): bool
{
// 默认走 write 连接
return (bool) $this->db->table('users')->insert($data);
}
}
- 注意:不要在构造函数里硬编码
'read'或'write',而是通过参数注入,方便单元测试 mock - 如果某操作既读又写(比如先查再更新),必须确保两个操作使用同一个连接实例,否则跨连接事务不生效
- 避免在循环中反复调用
\Config\Database::connect()—— 每次都会新建连接,可能触发 MySQL 的max_connections限制
事务只能在 write 连接上执行,且不能跨连接
CI4 的 $db->transStart() 只对当前连接有效。如果你在 read 连接上调用事务方法,它不会报错,但也不会起作用——MySQL 会静默忽略 START TRANSACTION,后续的 INSERT/UPDATE 仍会执行,只是不被事务包裹。
典型翻车场景:
- 用
UserService实例传入'read',然后调用createUser()—— 插入成功,但没事务保护 - 先用
read连接查数据,再用write连接改数据,中间加了transStart()—— 事务只包住写操作,查操作不在事务内,存在幻读风险 - 试图在一次请求中同时 commit 两个连接的事务 —— CI4 不支持,MySQL 也不支持分布式事务(除非你上 XA 或 ProxySQL)
正确姿势:所有涉及写操作 + 事务的流程,必须从一开始就绑定 'write' 连接,并把读操作也拉到这个连接上(哪怕性能稍差,也要保证一致性)。
连接池与长连接容易被忽略的细节
CI4 默认使用 PHP 的 mysqli 或 PDO 原生连接,不带连接池。这意味着每次 connect() 都是一次 TCP 握手 + 认证,高并发下延迟明显。
真正影响线上表现的几个点:
- Docker 环境下,
database.read.hostname写localhost会导致连接失败——容器内localhost指向自身,不是宿主机或另一容器;必须写实际服务名,如mysql-read - MySQL 服务端设置了
wait_timeout(默认 28800 秒),PHP 连接空闲超时后会被服务端断开,下次使用时报MySQL server has gone away;建议在.env中加上database.default.port = 3306并确认database.default.charset = utf8mb4,避免隐式重连失败 - CI4 的
ConnectionInterface不提供ping()方法,你得自己用$db->connID->ping()(mysqli)或$db->connID->getAttribute(PDO::ATTR_CONNECTION_STATUS)(PDO)做健康检查
最常被跳过的一步:上线前没验证主从延迟。即使连接配对了,如果从库延迟 5 秒,用户刚注册就去读个人页,可能读不到新记录——这得靠业务层加缓存兜底,不是框架能解决的。











