Laravel 读写分离配置与粘性写入
解读
在国内高并发电商、SaaS、内容社区等场景下,单库性能瓶颈明显,面试时考官希望候选人不仅能把“读写分离”四个字说出来,更能把 Laravel 底层如何识别读写语句、如何保持主从延迟场景下的数据一致性(粘性写入)讲清楚,并给出线上踩坑经验。问题背后考察点:
- 是否熟悉 Laravel 数据库层(DatabaseManager、Connection、Connector)的初始化流程;
- 是否理解 sticky 选项对事务、隐式写入、队列、定时任务的影响;
- 能否结合 MySQL 主从半同步、延迟监控、强制读主等国内运维方案做权衡。
知识点
- Laravel 读写分离配置入口:config/database.php 中 mysql 配置项增加 'read'、'write' 主机数组,支持多 host、port、username、password。
- 底层实现:Illuminate\Database\Connectors\ConnectionFactory 根据 PDO 语句类型(select 开头或是否开启事务)动态选择 read 或 write 连接;select 结果集缓存键默认带连接名,防止脏读。
- sticky 选项:当请求生命周期内出现写操作(insert/update/delete 或显式事务),Laravel 会把当前进程后续所有读请求强制指向 master,直到请求结束;在 FPM 常驻模式下,sticky 状态仅存在于当前请求,不会污染其他请求。
- 国内常见坑:
- 云数据库 RDS 只读实例延迟 300ms 以上,sticky=false 时刚注册的用户立即登录报“用户不存在”;
- 队列消费者常驻内存,sticky 状态残留,需 restart 队列或把队列连接独立成非 sticky 配置;
- 多库配置时,read 与 write 的 charset/collation 必须一致,否则会出现“Illegal mix of collations” 导致 SQL 报错;
- 使用 DB::statement() 执行非 select 语句时,底层不会自动识别为写,需要包在 DB::transaction() 里或强制使用 write 连接。
- 监控与兜底:阿里 RDS 读写分离代理支持一致性读 Hint(/FORCE_MASTER/),可在 Repository 层封装 selectMaster() 方法;腾讯云 TDSQL 提供只读延迟告警,Laravel 可配合 Horizon/Prometheus exporter 把延迟指标打到 Grafana。
答案
- 配置示例(config/database.php):
'mysql' => [
'driver' => 'mysql',
'read' => [
'host' => [env('DB_RO_HOST_1', '127.0.0.1'), env('DB_RO_HOST_2', '127.0.0.1')],
'port' => env('DB_RO_PORT', 3306),
],
'write' => [
'host' => env('DB_RW_HOST', '127.0.0.1'),
'port' => env('DB_RW_PORT', 3306),
],
'sticky' => true, // 开启粘性写入
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
// 其余 username/password/database 相同
],
- 使用层面无需改动代码,Eloquent 与 QueryBuilder 自动按语句类型路由;若必须强制读主,可:
DB::connection('mysql')->select(DB::raw("select * from users where id = ? /*FORCE_MASTER*/"), [$id]);
- 队列/命令行独立连接:在 config/queue.php 中把 queue 连接指定为 'mysql-write',该连接下 sticky=false,防止常驻进程污染。
- 上线 checklist:
- 在预发布环境压测 1:5 读写比,确认 sticky 开启后 QPS 下降不超过 3%;
- 打开 RDS 慢日志,确认无 “Waiting for table level lock” 堆积;
- 灰度发布时先切 10% 流量到读写分离配置,观察 Grafana 延迟曲线 < 200ms;
- 准备回滚脚本:一键把 read 主机数组改成 write 主机,重启 php-fpm 即可回滚。
拓展思考
- 分库分表后的读写分离:Laravel 本身只支持单库维度读写分离,若使用 ShardingSphere-Proxy 或自研中间件,需在 Laravel 侧关闭读写分离,由中间件做 SQL 解析与路由;此时 sticky 逻辑下沉到中间件,Laravel 只需保证连接池长连接复用。
- Serverless 场景:阿里云 PolarDB Serverless 只读节点自动弹性伸缩,Laravel 可结合 DB::beforeExecuting() 事件监听,把延迟 >500ms 的只读连接标记为 unavailable,动态剔除出 read 数组,实现“弹性读写分离”。
- 多租户 SaaS:按租户维度拆分读库,在运行时利用 Laravel 的 Connection::resolverFor() 动态替换 read 配置,实现“租户级读写分离”,同时保证 sticky 写入只对当前租户连接生效,避免跨租户污染。
- 单元测试:使用 Laravel 的 DatabaseTransactions 特性时,测试用例默认在事务里,底层强制走 write 连接;若测试代码手动 rollback 后再查询,需开启 sticky 否则可能读到从库旧数据,导致断言失败。