Laravel读写分离需在config/database.php中将read/write配置为独立数组,.env中须分别定义DB_WRITE_HOST和DB_READ_HOST,启用sticky可解决主从延迟导致的查询不一致问题。

mysql 连接配置好读写分离后,Laravel 会自动按操作类型分发查询——select 走 read,insert/update/delete 走 write。但实际落地时,光改配置远远不够,关键在环境隔离、连接复用和主从延迟应对。
config/database.php 里怎么填 read/write 数组
read 和 write 必须是独立数组,不能直接写成字符串或扁平列表。常见错误是把多个从库 IP 写成一个字符串,或者漏掉外层方括号:
✅ 正确(单从库):'read' => ['host' => '192.168.1.10']'write' => ['host' => '192.168.1.20']
❌ 错误(语法无效):'read' => '192.168.1.10''read' => ['192.168.1.10', '192.168.1.11'](缺少键名)
多从库支持数组嵌套,每个元素是一个完整连接配置:'read' => [ ['host' => '192.168.1.10', 'port' => '3307'], ['host' => '192.168.1.11', 'port' => '3307'] ]
Laravel 每次执行 select 时会随机选一个,不轮询也不加权。
.env 文件必须拆开写 DB_WRITE_HOST 和 DB_READ_HOST
Laravel 不识别DB_HOST 同时用于读写,read/write 子配置里的值不会自动 fallback 到 DB_HOST。必须显式声明:
在 .env 中补全:DB_WRITE_HOST=192.168.1.20DB_READ_HOST=192.168.1.10DB_DATABASE=appDB_USERNAME=app_userDB_PASSWORD=secret
如果共用账号密码,read 和 write 配置里就别重复写 username/password,统一由外层继承;但如果从库账号权限受限(比如只读),就得在 read 数组里单独指定 username 和 password。
sticky 配置不是“可有可无”,而是要明确开关
sticky 默认是 false,一旦开启,只要当前请求里执行过一次 save() 或 update(),后续所有 get()、first() 都强制走写库——哪怕你模型里写了 on('mysql::read') 也没用。
要不要开?看场景:
- 管理后台提交表单后立刻查列表:建议开,避免刚写入就查不到
- 用户中心高频刷新个人资料页:开,规避主从延迟导致“自己看不到刚改的昵称”
- 报表类页面(纯读、数据一致性要求低):关,让流量真正分散到从库
配置位置就在 mysql 主配置下:'sticky' => true,
注意它和 read/write 是同级字段,别塞进 read 里面。
Eloquent 查询有时不走 read,原因在这儿
select 类操作默认走 read,但以下情况会绕过:
- 使用 DB::table('users')->where(...)->update(...):这是 Query Builder 的写操作,走 write,没问题
- 用 User::where(...)->update(...):Eloquent 也会走 write
- 但 User::where(...)->lockForUpdate()->get():lockForUpdate 是写锁语义,Laravel 强制走 write 连接,哪怕你是 get()
- withTrashed() + restore() 这类软删除操作也走 write
最隐蔽的是事务:DB::transaction(function () { User::first(); });
只要进了事务,不管里面有没有写操作,整个事务内所有查询都走 write 连接——这是为了保证事务一致性,无法绕过。
真正难搞的不是配置本身,是主从延迟带来的业务逻辑错觉。比如用户注册后跳转到个人页,sticky=true 能解决,但要是注册成功后发短信通知另一个服务去查数据库,那个服务没开 sticky,就可能查到旧数据。这种跨请求的一致性,得靠业务层加缓存版本号或延迟补偿,Laravel 的配置管不到那儿。


















