微服务架构落地:读写分离架构 —— 主从延迟引发的“刚开单查不到”灾难与破局

大宗商品销采运储系统・六层架构连载(数据持久层)

上一篇我们完成了数据持久层总览,明确了从单机到海量数据架构演进的必经之路。

今天进入第一个硬核实战:读写分离。

在销采运储系统初期,一台 MySQL 单机抗下了所有。但随着业务量上涨,矛盾很快爆发:白天大宗商品集中开盘,大量用户同时刷新行情、查询库存、提交采购/销售单据。

数据库的 CPU 和磁盘 IO 被两件事抢占:高频的写(开单、状态流转) 和 海量的读(列表查询、报表统计)。

为了不让"读"把"写"拖死,我们引入了读写分离:主库抗写,从库抗读。但随之而来的,是一个极其经典且致命的线上灾难------主从延迟。

一句话定调:

读写分离解决的是数据库的"并发瓶颈",主从延迟解决的是业务体验的"可用底线"。只做分离不做延迟治理,就是耍流氓。


一、为什么大宗商品系统必须做读写分离?

如果不做读写分离,单机 MySQL 会遇到三个死结:

  1. 锁竞争加剧:大量的报表统计(如月度进销存汇总)带有复杂的联表查询,消耗大量 CPU 并占用 Buffer Pool,导致开单的写请求被阻塞,出现锁等待超时。

  2. IOPS 瓶颈:核心单据表每天新增上百万行,写操作极其频繁,磁盘 IO 很快达到上限。

  3. 连接数打满:前端多、报表查询慢,数据库连接池被耗尽,新的交易请求无法建立连接。

读写分离的标准架构:

应用服务

→ 读写分离中间件(ShardingSphere / MyCat / 应用层路由)

→ 写请求(INSERT / UPDATE / DELETE) → 主库(Master)

→ 读请求(SELECT) → 从库(Slave 1 / Slave 2)
业务拆分规则:

  • 核心交易链路(写):开单、扣减库存、修改状态,走主库。

  • 报表与列表(读):历史单据查询、报表导出,走从库。

  • 强一致读:刚提交完单据要跳转详情页,必须走主库。


二、致命灾难:刚开完单,列表却查不到

读写分离架构最容易踩的坑,就是主从延迟。

真实场景回放:

  1. 业务员 A 提交了一张 100 吨的采购单。

  2. 系统写主库成功,返回"开单成功"。

  3. 前端自动跳转到"采购单列表"。

  4. 列表接口走从库查询,由于主从复制有延迟(哪怕只有 200ms)。

  5. 从库还没收到刚才的写入数据,查询结果为空。

  6. 业务员 A 懵了:"单子呢?是不是没保存成功?"

  7. 业务员 A 再次点击"提交",导致重复开单。

在大宗商品场景,这种重复开单不仅会导致库存虚增,还会引发后续的财务结算错乱。业务人员不懂什么是主从延迟,他们只知道"系统出 Bug 了"。

主从延迟到底怎么产生的?

  1. 网络延迟:主库到从库的专线抖动。

  2. 大事务阻塞:主库执行了一个耗时 5 秒的批量更新,从库必须等这个事务全部执行完才能继续同步(MySQL 并行复制虽能缓解,但无法根除)。

  3. 从库压力过大:从库不仅要同步数据,还要抗住海量的报表查询,CPU 打满导致回放 binlog 慢。

  4. 主库写入并发高:大宗商品开盘瞬间,主库大量并发写入,从库单线程或多线程回放跟不上。


三、生产级主从延迟治理方案

只靠 MySQL 原生参数调优(如开启半同步复制)是不够的,必须在架构和代码层面进行治理。

方案 1:应用层强制走主库(核心业务跳转)

对于"写完立刻读"的场景(如提交订单后跳转详情页),强制将读请求路由到主库。

实现思路:使用 ThreadLocal 记录当前线程是否强制走主库,配合 AOP 或注解实现。

复制代码
// 1. 定义注解
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface MasterRoute {
}

// 2. AOP 切面
@Aspect
@Component
public class MasterRouteAspect {
    @Around("@annotation(masterRoute)")
    public Object routeToMaster(ProceedingJoinPoint pjp, MasterRoute masterRoute) throws Throwable {
        try {
            MasterRouteContext.setForceMaster(true);
            return pjp.proceed();
        } finally {
            MasterRouteContext.clear();
        }
    }
}

// 3. 在需要强一致读的接口上打注解
@MasterRoute
@GetMapping("/order/detail/{orderNo}")
public Result<OrderVO> getOrderDetail(@PathVariable String orderNo) {
    return Result.success(orderService.getByOrderNo(orderNo));
}

⚠️ 注意: 强制走主库只能用于小范围的"刚写完立刻读"场景,绝对不能滥用。大宗商品的高并发查询如果全部走主库,主库依然会被打崩。

方案 2:缓存双写兜底(工业级最佳实践)

写主库成功后,立刻将这条新数据写入 Redis,并设置较短的过期时间(如 30 秒)。

后续的列表查询或详情查询,优先查 Redis;查不到再走从库。

复制代码
@Transactional
public void createOrder(OrderDTO dto) {
    // 1. 落主库
    orderMapper.insert(dto);
    
    // 2. 写入 Redis,防主从延迟期间查不到
    String key = "order:detail:" + dto.getOrderNo();
    redisTemplate.opsForValue().set(key, dto, 30, TimeUnit.SECONDS);
    
    // 3. 缓存双删(防止并发导致的脏数据)
    // ...
}

方案 3:数据库层配置半同步复制(Semi-Sync)

MySQL 默认的异步复制,主库写完就返回,不管从库。半同步复制要求主库必须收到至少一个从库的 ACK,才向客户端返回成功。

主库开启半同步插件

INSTALL PLUGIN rpl_semi_sync_master SONAME 'semisync_master.so';

SET GLOBAL rpl_semi_sync_master_enabled = 1;

超时时间(毫秒),超时后退化为异步复制

SET GLOBAL rpl_semi_sync_master_timeout = 1000;
评价: 半同步复制能大幅降低延迟导致的"丢读"概率,但当从库全部宕机或网络极差时,会退化为异步复制,不能完全依赖。

方案 4:业务妥协设计(用户体验层)

对于非核心的列表展示,如果实在查不到,可以在前端做一层体验优化:

"单据同步中,请稍后刷新"。

或者在前端提交成功后,直接使用接口返回的数据渲染列表第一行,而不是立刻发一次全量列表查询。

方案 5:延迟监控与告警

必须监控 Seconds_Behind_Master。

SHOW SLAVE STATUS\G;

-- 重点看 Seconds_Behind_Master

在系统中接入 Prometheus + MySQL Exporter,如果延迟超过阈值(如 2 秒),立刻触发告警。


四、读写分离落地避坑指南

坑点 1:事务内的读请求走从库

现象 :在同一个事务里,先 update,再 select,结果查出了旧数据。

原因 :Spring 的 @Transactional 保证的是主库事务,如果 MyBatis 拦截器把事务内的 select 发给了从库,由于隔离性,从库读不到主库未提交的数据。

规范 :同一个事务内,所有 SQL 必须强制走主库。 可以在动态数据源路由逻辑中判断,如果当前处于事务中(TransactionSynchronizationManager.isActualTransactionActive()),一律走主库。

坑点 2:大报表查询压垮从库

现象 :财务导出百万级数据,从库 CPU 瞬间 100%,导致后续所有读请求超时。

规范:导出类、超重度的报表查询,必须带 Sentinel 限流,或者直接下沉到专用的报表库/数仓(离线 T+1 查询)。

坑点 3:主库宕机后的读操作

现象 :主库挂了,但业务还在继续发读请求,从库也被拖死。

规范:配置 Sentinel,对访问数据库的所有接口做熔断降级。主库挂掉时,写操作全部降级提示,读操作走缓存。


五、本篇小结

读写分离是数据库架构演进的第一步,它能解决"读"把"写"拖死的问题,但也会引入"主从延迟"这个新的挑战。

记住三句话:

  1. 写走主,读走从,写完立刻读必须走主。

  2. 半同步复制保底,缓存双写兜底,延迟监控告警。

  3. 重查询(报表)不仅走从库,还要上限制,防止从库被拖垮。

下篇预告:我们进入数据持久层最硬核的实战:分库分表(上),拆解切分策略、基因法路由设计,以及如何做到不锁表平滑扩容。

相关推荐
FII工业富联科技服务2 小时前
AI Factory 技术架构解析:数字孪生、自动化、机器人与 AI Agent 如何协同?
人工智能·架构·自动化
凤山老林2 小时前
构建多通道通知中心:模板化设计、异步削峰与可靠重试实践
spring boot·异步削峰·模板设计
花间相见2 小时前
【计算基础|MySQL08】索引(上):为什么是 B+ 树
数据结构·mysql·算法·架构
2501_933923252 小时前
SpringCloud组件:Nacos服务注册快速入门
后端·spring·spring cloud·nacos
microrain2 小时前
专业版守核心,企业版做增量:SagooIoT 的跨模块能力出口
物联网·架构·golang·sagooiot
学linux的QQ蛋2 小时前
IMX6 嵌入式多传感器系统整体架构梳理
linux·嵌入式硬件·架构
布莱克6052 小时前
QtWidgets 与 QML:两大 UI 开发框架的全面对比
开发语言·c++·qt·ui·架构
你看我名字3 小时前
LAMP 架构演练:从云主机准备到 Apache、PHP、MySQL 上线验证
运维·ci/cd·架构·kubernetes·jenkins