欢迎光临
我们一直在努力

主从延迟会造成什么问题?订单系统\\“读走从、判断走主\\“的一致性策略

主从延迟会造成什么问题?订单系统"读走从、判断走主"的一致性策略

承接上一篇《MySQL 读写分离完整链路》:那篇讲怎么把读写分离搭起来,这篇只讲搭建之后必然要还的那笔债——主从延迟。文中策略与判定全部来自笔者的微服务订单系统 order-system-cloud 的落地实践;属于备选方案、未落地的部分会显式标注。

一、业务背景:一个"删不掉的购物车"

先说一个真实场景,它比任何原理图都更能说明问题:

用户点"删除购物车商品"
→ 请求打到主库,DELETE 成功,接口返回 200
→ 用户秒刷购物车列表(这个查询走了从库)
→ 刚才删掉的那一项,又冒出来了

用户看到的现象是"删除失败",但数据库里数据是对的、接口也返回成功了。如果没有意识到主从延迟,这个 bug 会非常难查——因为你在任何一台库上单独查,都是对的。

同样的形态还有很多:

用户完成支付 → 主库把订单改成 PAID → 用户秒进订单详情(走从库)→ 仍显示"待支付"
用户下单成功 → 主库写入订单 → 用户秒进"我的订单"(走从库)→ 列表里没有这一单

这三个场景有一个共同点:“刚刚写过的数据,马上就要读”。这就是主从延迟唯一会咬人的地方,也是本文全部策略的出发点。

二、主从延迟的本质:异步复制

主从复制靠三个线程接力:

主库 Dump 线程 ──推 binlog──▶ 从库 IO 线程 ──▶ relay log ──▶ 从库 SQL 线程 逐条重放

关键在于:主库提交事务时,并不等待从库重放完成。主库只要把 binlog 写出去就返回了,从库什么时候追平是它自己的事。这中间的时间差,就是主从延迟(replication lag)。

所以延迟不是一个"故障",而是异步复制的固有属性。它不是"会不会发生"的问题,而是"这一瞬间有多长"的问题——正常情况是毫秒级,但只要有放大因素(下一节会讲),它会突然拉长到秒级甚至分钟级。

这条认知很重要:如果你把延迟当成故障来处理,你会去"修"它;但它是设计的一部分,你只能在业务上决定哪些操作不能容忍它。

下面是主从复制的完整链路与延迟产生的位置:

#mermaid-svg-lx7q4neH7JAwAL1W{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-lx7q4neH7JAwAL1W .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-lx7q4neH7JAwAL1W .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-lx7q4neH7JAwAL1W .error-icon{fill:#552222;}#mermaid-svg-lx7q4neH7JAwAL1W .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-lx7q4neH7JAwAL1W .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-lx7q4neH7JAwAL1W .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-lx7q4neH7JAwAL1W .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-lx7q4neH7JAwAL1W .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-lx7q4neH7JAwAL1W .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-lx7q4neH7JAwAL1W .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-lx7q4neH7JAwAL1W .marker{fill:#333333;stroke:#333333;}#mermaid-svg-lx7q4neH7JAwAL1W .marker.cross{stroke:#333333;}#mermaid-svg-lx7q4neH7JAwAL1W svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-lx7q4neH7JAwAL1W p{margin:0;}#mermaid-svg-lx7q4neH7JAwAL1W .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-lx7q4neH7JAwAL1W .cluster-label text{fill:#333;}#mermaid-svg-lx7q4neH7JAwAL1W .cluster-label span{color:#333;}#mermaid-svg-lx7q4neH7JAwAL1W .cluster-label span p{background-color:transparent;}#mermaid-svg-lx7q4neH7JAwAL1W .label text,#mermaid-svg-lx7q4neH7JAwAL1W span{fill:#333;color:#333;}#mermaid-svg-lx7q4neH7JAwAL1W .node rect,#mermaid-svg-lx7q4neH7JAwAL1W .node circle,#mermaid-svg-lx7q4neH7JAwAL1W .node ellipse,#mermaid-svg-lx7q4neH7JAwAL1W .node polygon,#mermaid-svg-lx7q4neH7JAwAL1W .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-lx7q4neH7JAwAL1W .rough-node .label text,#mermaid-svg-lx7q4neH7JAwAL1W .node .label text,#mermaid-svg-lx7q4neH7JAwAL1W .image-shape .label,#mermaid-svg-lx7q4neH7JAwAL1W .icon-shape .label{text-anchor:middle;}#mermaid-svg-lx7q4neH7JAwAL1W .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-lx7q4neH7JAwAL1W .rough-node .label,#mermaid-svg-lx7q4neH7JAwAL1W .node .label,#mermaid-svg-lx7q4neH7JAwAL1W .image-shape .label,#mermaid-svg-lx7q4neH7JAwAL1W .icon-shape .label{text-align:center;}#mermaid-svg-lx7q4neH7JAwAL1W .node.clickable{cursor:pointer;}#mermaid-svg-lx7q4neH7JAwAL1W .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-lx7q4neH7JAwAL1W .arrowheadPath{fill:#333333;}#mermaid-svg-lx7q4neH7JAwAL1W .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-lx7q4neH7JAwAL1W .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-lx7q4neH7JAwAL1W .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-lx7q4neH7JAwAL1W .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-lx7q4neH7JAwAL1W .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-lx7q4neH7JAwAL1W .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-lx7q4neH7JAwAL1W .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-lx7q4neH7JAwAL1W .cluster text{fill:#333;}#mermaid-svg-lx7q4neH7JAwAL1W .cluster span{color:#333;}#mermaid-svg-lx7q4neH7JAwAL1W div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-lx7q4neH7JAwAL1W .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-lx7q4neH7JAwAL1W rect.text{fill:none;stroke-width:0;}#mermaid-svg-lx7q4neH7JAwAL1W .icon-shape,#mermaid-svg-lx7q4neH7JAwAL1W .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-lx7q4neH7JAwAL1W .icon-shape p,#mermaid-svg-lx7q4neH7JAwAL1W .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-lx7q4neH7JAwAL1W .icon-shape .label rect,#mermaid-svg-lx7q4neH7JAwAL1W .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-lx7q4neH7JAwAL1W .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-lx7q4neH7JAwAL1W .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-lx7q4neH7JAwAL1W :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

从库

主库

推送 binlog

写入

逐条重放

网络传输

主库不等待从库,直接返回

放大因素

业务写入

事务提交

Dump 线程

binlog

IO 线程

relay log

SQL 线程

从库数据

延迟窗口 = 主库提交到从库重放完成的时间差

毫秒级 → 秒级甚至分钟级

三、延迟会造成的四类线上问题

把延迟的后果归类,比零散记场景更有用:

类型表现危险程度
读己之写失效 用户刚提交的修改,自己刷新看不到 高——用户直接感知,像 bug
判断依据过期 先读状态再决定动作,读到旧状态 最高——会导致业务逻辑错误,不只是显示问题
幂等校验穿透 靠"查一下有没有"来防重,查的是从库 高——可能重复扣减、重复下单
监控/统计延迟 报表数字对不上、对账有偏差 中——可容忍,但要知情

其中第二类"判断依据过期"是唯一会真正写坏数据的,也是本项目花最大力气防的一类。

举个例子:支付接口的 validateOrder 要先查订单状态,确认它是"待支付"才允许支付。如果这个查询走了从库,而主从延迟刚好存在,用户重复点击支付时——从库可能还认为订单是 WAIT_PAY,于是第二次支付被放行。这不是显示错误,是资损。

四类问题的危险程度可以这样分层:

#mermaid-svg-o4zncjerEiIB71ik{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-o4zncjerEiIB71ik .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-o4zncjerEiIB71ik .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-o4zncjerEiIB71ik .error-icon{fill:#552222;}#mermaid-svg-o4zncjerEiIB71ik .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-o4zncjerEiIB71ik .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-o4zncjerEiIB71ik .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-o4zncjerEiIB71ik .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-o4zncjerEiIB71ik .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-o4zncjerEiIB71ik .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-o4zncjerEiIB71ik .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-o4zncjerEiIB71ik .marker{fill:#333333;stroke:#333333;}#mermaid-svg-o4zncjerEiIB71ik .marker.cross{stroke:#333333;}#mermaid-svg-o4zncjerEiIB71ik svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-o4zncjerEiIB71ik p{margin:0;}#mermaid-svg-o4zncjerEiIB71ik .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-o4zncjerEiIB71ik .cluster-label text{fill:#333;}#mermaid-svg-o4zncjerEiIB71ik .cluster-label span{color:#333;}#mermaid-svg-o4zncjerEiIB71ik .cluster-label span p{background-color:transparent;}#mermaid-svg-o4zncjerEiIB71ik .label text,#mermaid-svg-o4zncjerEiIB71ik span{fill:#333;color:#333;}#mermaid-svg-o4zncjerEiIB71ik .node rect,#mermaid-svg-o4zncjerEiIB71ik .node circle,#mermaid-svg-o4zncjerEiIB71ik .node ellipse,#mermaid-svg-o4zncjerEiIB71ik .node polygon,#mermaid-svg-o4zncjerEiIB71ik .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-o4zncjerEiIB71ik .rough-node .label text,#mermaid-svg-o4zncjerEiIB71ik .node .label text,#mermaid-svg-o4zncjerEiIB71ik .image-shape .label,#mermaid-svg-o4zncjerEiIB71ik .icon-shape .label{text-anchor:middle;}#mermaid-svg-o4zncjerEiIB71ik .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-o4zncjerEiIB71ik .rough-node .label,#mermaid-svg-o4zncjerEiIB71ik .node .label,#mermaid-svg-o4zncjerEiIB71ik .image-shape .label,#mermaid-svg-o4zncjerEiIB71ik .icon-shape .label{text-align:center;}#mermaid-svg-o4zncjerEiIB71ik .node.clickable{cursor:pointer;}#mermaid-svg-o4zncjerEiIB71ik .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-o4zncjerEiIB71ik .arrowheadPath{fill:#333333;}#mermaid-svg-o4zncjerEiIB71ik .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-o4zncjerEiIB71ik .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-o4zncjerEiIB71ik .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-o4zncjerEiIB71ik .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-o4zncjerEiIB71ik .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-o4zncjerEiIB71ik .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-o4zncjerEiIB71ik .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-o4zncjerEiIB71ik .cluster text{fill:#333;}#mermaid-svg-o4zncjerEiIB71ik .cluster span{color:#333;}#mermaid-svg-o4zncjerEiIB71ik div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-o4zncjerEiIB71ik .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-o4zncjerEiIB71ik rect.text{fill:none;stroke-width:0;}#mermaid-svg-o4zncjerEiIB71ik .icon-shape,#mermaid-svg-o4zncjerEiIB71ik .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-o4zncjerEiIB71ik .icon-shape p,#mermaid-svg-o4zncjerEiIB71ik .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-o4zncjerEiIB71ik .icon-shape .label rect,#mermaid-svg-o4zncjerEiIB71ik .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-o4zncjerEiIB71ik .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-o4zncjerEiIB71ik .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-o4zncjerEiIB71ik :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

主从延迟

四类线上问题

读己之写失效

判断依据过期

幂等校验穿透

监控/统计延迟

用户直接感知,像 bug

危险程度:高

先读状态再决定动作,读到旧状态

危险程度:最高

唯一会真正写坏数据

可能重复扣减、重复下单

危险程度:高

报表数字对不上、对账有偏差

危险程度:中,可容忍但要知情

四、项目的取舍:读走从、写走主、判断走主

针对上面四类问题,本项目的一致性策略压缩成一句话:

读走从、写走主、判断走主。

落到 @DS 注解上,规则只有一条:纯读、且容忍秒级延迟的方法才加 @DS("slave");写方法、以及一切"先读后写"的方法一律不加注解(默认走 primary = master)。

走从库 @DS("slave")走主库(默认,不加注解)
OrderService.detail() —— 订单详情展示 OrderService.create() / pay() / cancel()
OrderService.myOrders() —— 我的订单列表 CartService.add() / updateQuantity() / remove()
CartService.myCart() —— 购物车列表展示 两个定时调度器(条件抢占 WAIT→SENDING)
MessageService.myMessages() —— 消息列表 MQ 消费者(插入 / 更新)

判断规则可以写成一句可执行的检查:这个方法读出来的数据,会不会被用来做后续的判断或写入?会,就走主库。

代码形态:

@Service
public class OrderServiceImpl implements OrderService {

/** 纯展示:读出来直接返回给前端,不做任何判断 → 容忍延迟,走从库 */
@DS("slave")
@Override
public OrderDetailVO detail(Long orderId) { ... }

/** 先读后写:读状态是为了决定能不能改 → 必须走主库 */
@Override
@Transactional(rollbackFor = Exception.class)
public void pay(Long orderId, Long userId) {
Order order = validateOrder(orderId, userId); // ← 这一步读到的状态将决定后续写入
if (!OrderStatus.WAIT_PAY.name().equals(order.getStatus())) {
throw new BizException("订单状态不允许支付");
}
// … 更新为 PAID
}
}

pay() 不加 @DS("slave") 不是"忘了加",而是刻意的:它的读是"读来改"的,一旦读到延迟的旧数据,逻辑就错了。

万一某个读接口确实需要强一致怎么办

那就显式声明,不要靠默认值靠运气:

@DS("master") // 显式强制读主库:我知道这里不能容忍延迟
@Override
public OrderVO getForUpdate(Long orderId) { ... }

关键原则是"显式":容忍延迟走从库,是默认选择;不容忍延迟走主库,必须写出来。让每一处"我要强一致"都在代码里看得见。

项目的核心决策逻辑可以用下面这张图表达:

渲染错误: Mermaid 渲染失败: Parse error on line 4:
…> D["加 @DS(\\"slave\\")走从库"] B –>
———————–^
Expecting 'SQE', 'DOUBLECIRCLEEND', 'PE', '-)', 'STADIUMEND', 'SUBROUTINEEND', 'PIPE', 'CYLINDEREND', 'DIAMOND_STOP', 'TAGEND', 'TRAPEND', 'INVTRAPEND', 'UNICODE_TEXT', 'TEXT', 'TAGSTART', got 'STR'

五、延迟从哪来:五个放大因素

正常情况延迟是毫秒级,会让它突然拉长的通常是这几种:

  • 从库 SQL 线程单线程重放:MySQL 5.6 之前从库只有一个 SQL 线程串行重放,主库并发写入的压力会全堆在这里;
  • 大事务:一次更新几十万行,从库要一行一行重放,主库早提交完了;
  • DDL 操作:加字段、加索引在主库执行完,从库还要重放一遍;
  • 从库自身压力大:从库既要重放 binlog 又要扛读流量,读 QPS 一高,重放就慢——这是读写分离方案里最讽刺的一种延迟来源;
  • binlog 格式的影响:本项目用 ROW 格式(记录每行前后镜像,最安全),代价是一次批量更新产生的 binlog 量远大于 STATEMENT,从库要搬运和重放的数据也更多。
  • 第 4 条值得单独强调:读流量本身会加剧延迟,而延迟又会让人更想读主库,主库压力上升——这是一个正反馈,容量规划时不能只看平均值。

    五个放大因素中最值得警惕的是第 4 条的正反馈循环:

    #mermaid-svg-C9UICgrnRGLz7Jc8{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-C9UICgrnRGLz7Jc8 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-C9UICgrnRGLz7Jc8 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-C9UICgrnRGLz7Jc8 .error-icon{fill:#552222;}#mermaid-svg-C9UICgrnRGLz7Jc8 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-C9UICgrnRGLz7Jc8 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-C9UICgrnRGLz7Jc8 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-C9UICgrnRGLz7Jc8 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-C9UICgrnRGLz7Jc8 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-C9UICgrnRGLz7Jc8 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-C9UICgrnRGLz7Jc8 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-C9UICgrnRGLz7Jc8 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-C9UICgrnRGLz7Jc8 .marker.cross{stroke:#333333;}#mermaid-svg-C9UICgrnRGLz7Jc8 svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-C9UICgrnRGLz7Jc8 p{margin:0;}#mermaid-svg-C9UICgrnRGLz7Jc8 .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-C9UICgrnRGLz7Jc8 .cluster-label text{fill:#333;}#mermaid-svg-C9UICgrnRGLz7Jc8 .cluster-label span{color:#333;}#mermaid-svg-C9UICgrnRGLz7Jc8 .cluster-label span p{background-color:transparent;}#mermaid-svg-C9UICgrnRGLz7Jc8 .label text,#mermaid-svg-C9UICgrnRGLz7Jc8 span{fill:#333;color:#333;}#mermaid-svg-C9UICgrnRGLz7Jc8 .node rect,#mermaid-svg-C9UICgrnRGLz7Jc8 .node circle,#mermaid-svg-C9UICgrnRGLz7Jc8 .node ellipse,#mermaid-svg-C9UICgrnRGLz7Jc8 .node polygon,#mermaid-svg-C9UICgrnRGLz7Jc8 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-C9UICgrnRGLz7Jc8 .rough-node .label text,#mermaid-svg-C9UICgrnRGLz7Jc8 .node .label text,#mermaid-svg-C9UICgrnRGLz7Jc8 .image-shape .label,#mermaid-svg-C9UICgrnRGLz7Jc8 .icon-shape .label{text-anchor:middle;}#mermaid-svg-C9UICgrnRGLz7Jc8 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-C9UICgrnRGLz7Jc8 .rough-node .label,#mermaid-svg-C9UICgrnRGLz7Jc8 .node .label,#mermaid-svg-C9UICgrnRGLz7Jc8 .image-shape .label,#mermaid-svg-C9UICgrnRGLz7Jc8 .icon-shape .label{text-align:center;}#mermaid-svg-C9UICgrnRGLz7Jc8 .node.clickable{cursor:pointer;}#mermaid-svg-C9UICgrnRGLz7Jc8 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-C9UICgrnRGLz7Jc8 .arrowheadPath{fill:#333333;}#mermaid-svg-C9UICgrnRGLz7Jc8 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-C9UICgrnRGLz7Jc8 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-C9UICgrnRGLz7Jc8 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-C9UICgrnRGLz7Jc8 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-C9UICgrnRGLz7Jc8 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-C9UICgrnRGLz7Jc8 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-C9UICgrnRGLz7Jc8 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-C9UICgrnRGLz7Jc8 .cluster text{fill:#333;}#mermaid-svg-C9UICgrnRGLz7Jc8 .cluster span{color:#333;}#mermaid-svg-C9UICgrnRGLz7Jc8 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-C9UICgrnRGLz7Jc8 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-C9UICgrnRGLz7Jc8 rect.text{fill:none;stroke-width:0;}#mermaid-svg-C9UICgrnRGLz7Jc8 .icon-shape,#mermaid-svg-C9UICgrnRGLz7Jc8 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-C9UICgrnRGLz7Jc8 .icon-shape p,#mermaid-svg-C9UICgrnRGLz7Jc8 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-C9UICgrnRGLz7Jc8 .icon-shape .label rect,#mermaid-svg-C9UICgrnRGLz7Jc8 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-C9UICgrnRGLz7Jc8 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-C9UICgrnRGLz7Jc8 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-C9UICgrnRGLz7Jc8 :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

    从库读 QPS 升高

    从库既要重放 binlog又要扛读流量

    重放变慢,延迟拉长

    用户读到旧数据

    更想读主库

    主库压力上升

    主库写入变慢

    六、三档解法与项目的选择

    业界应对主从延迟有三档手段,从简单到硬核:

    档位手段代价本项目
    第一档 写后必须读准的,强制读主库(@DS("master")) 几乎没有,只是少了一个读分流点 ✅ 已落地
    第二档 半同步复制(semi-sync):主库提交前等从库确认收到 binlog 牺牲写性能换强一致,延迟敏感型写入变慢 ⬜ 未落地(README 列为备选策略)
    第三档 从库并行复制(MySQL 5.7 MTS):从库 SQL 线程并行重放 需要配置,且并行度受事务冲突限制 ⬜ 未落地(README 列为备选策略)

    本项目的选择是只用第一档,并且明确不承诺"零延迟"。 理由是:本项目的读方法全部是"列表展示类",容忍秒级延迟;凡是"读来改"的都留在主库了。也就是说——延迟仍然存在,但它不再落在任何会写坏数据的路径上。

    这是一个很重要的表达方式:不是"我们消除了主从延迟",而是"我们把延迟隔离在了不会造成错误的地方"。

    三档解法的取舍一目了然:

    渲染错误: Mermaid 渲染失败: Parse error on line 3:
    …加 @DS(\\"master\\")"] A –> A1
    ———————–^
    Expecting 'SQE', 'DOUBLECIRCLEEND', 'PE', '-)', 'STADIUMEND', 'SUBROUTINEEND', 'PIPE', 'CYLINDEREND', 'DIAMOND_STOP', 'TAGEND', 'TRAPEND', 'INVTRAPEND', 'UNICODE_TEXT', 'TEXT', 'TAGSTART', got 'STR'

    七、边界与并发:能承诺什么,不能承诺什么

    写这类方案时必须把边界说清楚,否则就是给自己埋雷:

    能承诺的:

    • 从库上的查询最终会与主库一致(异步复制保证最终一致);
    • 任何"读后判断/读后修改"的操作都在主库上执行,不会因延迟产生错误判断(本项目已按此规则划分数据源)。

    不能承诺的:

    • 从库的读不保证读到最新数据——这是设计取舍,不是缺陷;
    • 极端情况下(大事务、DDL、从库过载)延迟可能达到秒级以上,展示类接口会出现短暂的数据偏旧;
    • @DS("slave") 的切换依赖 AOP 代理 + ThreadLocal,同类内部 this.xxx() 自调用不走代理、@Async/线程池场景 ThreadLocal 不跨线程传递——这两种情况下注解会静默失效(退化为走默认的 master,不会报错)。

    最后一条是并发边界里最容易踩的:注解失效的表现不是报错,而是"看起来正常但走了主库"。所以核查数据源路由时,不能只看注解写没写,还要看调用路径是否真的过了代理。

    八、面试与简历亮点

  • 能一句话说清策略:“读走从、写走主、判断走主”——把读写分离的一致性边界讲成了可执行的规则,而不是"我们有读写分离";
  • 知道延迟不是故障而是属性:因此方案的目标是"隔离"而不是"消灭",这个认知层次比背八股高;
  • 能指出真正危险的那类问题:读己之写失效只是体验问题,"判断依据过期"才会写坏数据——把风险按后果分级,是工程判断力的体现;
  • 显式优先于默认:容忍延迟靠默认、需要强一致必须显式写 @DS("master"),让例外在代码里可见;
  • 知道自己的注解会失效:自调用、异步场景下 @DS 静默失效且不报错。
  • 九、场景题

    Q1:用户反馈"删了购物车商品又自己回来了",但你查数据库确认数据是正确的。怎么解释、怎么修?

    解释:删除走主库、列表查询走从库,用户"秒刷"时从库还没重放到这次删除,于是读到了删除前的快照。数据是正确的,用户看到的是延迟窗口内的旧快照。

    排查要点:先确认两台库分别查这条记录,主库已删、从库还在 → 立刻能定性为延迟而不是删除失败(这一步能省掉大量无效排查)。

    修法(按性价比排序):

  • 该场景强制读主库(@DS("master")),或删除成功后由接口直接返回删除结果、前端本地移除该行,不做二次查询;
  • 更通用的做法:写操作返回后,对同一用户短时间内的读请求强制走主库("写后粘主库"窗口);
  • 半同步复制 / 从库并行复制,按下延迟本身(本项目未落地,属备选)。
  • 关键提醒:不要用"加个 sleep 等一下"来解——那是把不确定性换成随机性,线上延迟一波动就会复发。

    Q2:同事给 pay() 加上了 @DS("slave") 想"减轻主库压力",说"这个方法就是查一下订单,纯读"。你怎么判断这个改动能不能上?

    判断标准一句话:这个读的结果,会不会被后面用来做判断或写入?

    pay() 的 validateOrder 是典型的"读来改"——它读订单状态是为了决定能不能支付,并且紧接着就要写。走从库意味着:延迟窗口内,一笔已支付的订单可能被从库读成 WAIT_PAY,于是重复支付被放行。这是资损,不是体验问题。

    所以答案是不能上。正确做法是:要减主库压力,应该从它周围的纯展示读(订单详情、我的订单、购物车列表、消息列表)入手,而不是从带判断的读入手。如果确实要优化 pay(),方向是缩短它的主库事务、减少锁持有时间,而不是把它的读挪到从库。


    一句话总结:主从延迟是异步复制的固有属性,不可能被"消灭",只能被隔离。订单系统的做法是把延迟全部赶到"读出来只用于展示"的路径上,而把"读出来要用于判断或修改"的操作留在主库——读走从、写走主、判断走主。

    推荐标签:MySQL、主从延迟、读写分离、数据一致性、微服务

    赞(0)
    未经允许不得转载:171主机测评 » 主从延迟会造成什么问题?订单系统\\“读走从、判断走主\\“的一致性策略
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址