读写分离后,主从延迟 1 秒导致用户看不到刚提交订单,如何优化
解读
- 场景还原:国内电商大促或秒杀期间,下单 QPS 过万,主库写入后 1 s 才同步到从库;用户刷新“我的订单”列表走的是从库,结果刚下的订单查不到,体验等同于“订单丢失”。
- 面试官真正想听的,不是“把延迟降到 0”这种理想口号,而是:
- 能否第一时间给出“用户体感优先”的兜底方案;
- 能否把 1 s 延迟拆成“复制链路”+“读路由”两段,分别给出可落地的优化手段;
- 能否用量化指标(RT、TPS、SLA)证明方案有效;
- 是否具备灰度、回滚、压测验证的全链路质量意识。
- 面试陷阱:只答“强制走主库”会被追问“主库被打爆怎么办”;只答“半同步”会被追问“半同步退化为异步的边界 case 如何兜底”;必须给出分层策略。
知识点
- MySQL 并行复制、组提交、binlog 压缩、WRITESET 冲突检测
- 半同步复制 AFTER_SYNC、AFTER_COMMIT 的 SLA 差异与退化开关
- 读写路由中间件:ShardingSphere、MyCat、TDDL、Consul+自研 SDK
- 业务层“写后读窗口”:用户会话标记、订单号后缀 Hash、Redis 分布式锁+TTL
- 缓存最终一致性:订单写主库后写 Redis 并设置 3 s 兜底过期,读从库 miss 再回源主库
- 容量模型:主库因“强制读”额外增加的 QPS ≤10 % 时,RT 上涨 <20 %,可接受
- 性能验证:用 Gatling/JMeter 构造“下单-查询”闭环脚本,90th 延迟 <500 ms,成功率 99.9 %
- 灰度:按用户尾号 1 %→5 %→30 % 放量,实时对比错误日志“no order found”指标
答案
整体思路分三层:用户体感兜底、复制链路加速、读路由策略。每一层都给出“可回滚”的开关与压测验收标准。
-
用户体感兜底(毫秒级生效)
a. 会话粘性:同一用户在下单成功后 3 s 内,所有“读我的订单”请求强制打主库,通过网关层在 Cookie/JWT 里写入 ts_flag,TTL=3 s。
b. 结果缓存:订单写主库成功后,同步写 Redis(Key=order_{userId}_{orderId},Value=JSON,过期 5 s),读从库之前先查缓存, miss 再走从库。
验收:压测 2 k 并发“下单-立即查询”闭环,缓存命中 ≥85 %,主库额外读 QPS ≤8 %,90th 延迟 <400 ms。 -
复制链路加速(百毫秒级)
a. 开启 MySQL 5.7 并行复制 slave_parallel_type=LOGICAL_CLOCK,slave_parallel_workers=16,把 1 s 延迟压到 200 ms 以内。
b. 大促前夜关闭 binlog_checksum 压缩,网络包大小降低 18 %;调整 group_commit 延迟为 0(牺牲 1 % TPS 换 40 ms 复制延迟)。
c. 半同步复制 AFTER_SYNC,超时阈值 300 ms,退化为异步的告警阈值 1 %,一旦退化立即短信值班。
验收:用 pt-heartbeat 测量 lag,P99 ≤300 ms,主库 TPS 下跌 ≤3 %。 -
读路由策略(秒级兜底)
a. 中间件层增加“延迟阈值路由”:从库 lag>500 ms 时自动把读流量切换到 lag 最小的从库或主库;开关为“只读流量比例阀值”,默认 30 %。
b. 订单号 Hash 路由:订单号末 2 位 00-49 的读请求允许走从库,50-99 强制走主库,把热点用户打散。
c. 业务降级:当主库 CPU>70 % 时,关闭“强制读主”策略,降级为“提示用户订单可能存在 1-2 s 延迟”,通过前端轮询补偿。
验收:灰度 5 % 流量,错误日志“no order found”从 0.12 % 降到 0.01 %,主库 CPU 峰值上涨 6 %,可接受。
回滚方案:所有策略配置化写入 Consul,秒级下发;任何一级导致主库负载异常,1 分钟内可回滚到“全部异步复制+从库读”原始状态。
拓展思考
-
如果业务拓展到异地多活,上海主库与深圳从库 RT 40 ms,如何设计“分区写、全球读”方案?
提示:考虑 XA 事务的吞吐瓶颈,改用“订单分区表+全局二级索引+Binlog 回打”方式,放弃强一致读,改用“用户维度最终一致”+“客服后台补偿”。 -
当延迟降到 100 ms 后,用户仍抱怨“秒杀完看不到订单”,是否继续压缩延迟?
提示:此时瓶颈已不再是数据库,而是前端轮询间隔 500 ms,建议把“写后读”升级为 WebSocket 推送,延迟感知 <50 ms,成本更低。 -
压测如何模拟“从库延迟 1 s”而不是“直接网络延迟 1 s”?
提示:在 Docker 从库容器内使用 tc qdisc 只延迟 SQL Thread 的 ACK 包,保证 IO Thread 实时,真实模拟“复制延迟”而非“网络延迟”。