MySQL 读写分离完整链路:主从复制、多数据源与 @DS 路由

MySQL 读写分离完整链路:主从复制、多数据源与 @DS 路由

本文基于笔者的微服务订单系统 order-system-cloud(SpringCloud Alibaba 微服务版)落地实践整理。文中所有配置、脚本、坑点均来自该项目真实落地内容;属于"技术储备未落地"的部分会显式标注。

一、业务背景:订单系统为什么必须考虑读写分离

订单系统的流量特征极其典型------读多写少:

  • 写操作是低频的:下单、支付、取消,一个用户一天可能就几次;
  • 读操作是高频的:订单详情、我的订单列表、购物车列表、消息中心,用户会反复刷新。

单体单库时代,所有这些请求都打在同一台 MySQL 上,抢同样的连接数和 CPU。读 QPS 一高,写请求也被拖慢------用户发现"下单变卡了",其实瓶颈根本不在下单逻辑,而在旁边那些无关的查询。

把读流量导到从库、主库专注写,是性价比最高的减压手段。但必须清楚它的本质:

读写分离本质是一笔交易------用「一致性 + 复杂度」换「读性能」,读多写少时才划算。

如果业务是写多读少,或者强一致要求极高(比如金融账务实时对账),这套方案反而会带来麻烦。

二、三层结构:缺一不可

很多人讲读写分离只讲 @DS 注解,其实那只是最上面一层开关。完整的链路是三层:

bash 复制代码
第3层  路由规则:这条 SQL 走主还是从?    @DS 注解
第2层  数据源管理:应用同时连主库+从库     Dynamic-Datasource
第1层  主从复制:主库数据自动同步到从库     MySQL binlog
  • 第 1 层是地基:数据得能同步过去,否则从库就是一堆过期数据;
  • 第 2 层是容器:应用得能同时持有主、从两个连接池;
  • 第 3 层是开关:决定具体某条 SQL 落到哪个库。

三层里最容易出问题的是第 1 层,因为它会"静默失效"(下文会讲)。第 3 层反而是最简单的,一个注解的事。

下面用一张图把三层结构串起来,方便理解整体链路:
#mermaid-svg-eC4ge4EREORaYMOm{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-eC4ge4EREORaYMOm .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-eC4ge4EREORaYMOm .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-eC4ge4EREORaYMOm .error-icon{fill:#552222;}#mermaid-svg-eC4ge4EREORaYMOm .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-eC4ge4EREORaYMOm .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-eC4ge4EREORaYMOm .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-eC4ge4EREORaYMOm .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-eC4ge4EREORaYMOm .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-eC4ge4EREORaYMOm .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-eC4ge4EREORaYMOm .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-eC4ge4EREORaYMOm .marker{fill:#333333;stroke:#333333;}#mermaid-svg-eC4ge4EREORaYMOm .marker.cross{stroke:#333333;}#mermaid-svg-eC4ge4EREORaYMOm svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-eC4ge4EREORaYMOm p{margin:0;}#mermaid-svg-eC4ge4EREORaYMOm .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-eC4ge4EREORaYMOm .cluster-label text{fill:#333;}#mermaid-svg-eC4ge4EREORaYMOm .cluster-label span{color:#333;}#mermaid-svg-eC4ge4EREORaYMOm .cluster-label span p{background-color:transparent;}#mermaid-svg-eC4ge4EREORaYMOm .label text,#mermaid-svg-eC4ge4EREORaYMOm span{fill:#333;color:#333;}#mermaid-svg-eC4ge4EREORaYMOm .node rect,#mermaid-svg-eC4ge4EREORaYMOm .node circle,#mermaid-svg-eC4ge4EREORaYMOm .node ellipse,#mermaid-svg-eC4ge4EREORaYMOm .node polygon,#mermaid-svg-eC4ge4EREORaYMOm .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-eC4ge4EREORaYMOm .rough-node .label text,#mermaid-svg-eC4ge4EREORaYMOm .node .label text,#mermaid-svg-eC4ge4EREORaYMOm .image-shape .label,#mermaid-svg-eC4ge4EREORaYMOm .icon-shape .label{text-anchor:middle;}#mermaid-svg-eC4ge4EREORaYMOm .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-eC4ge4EREORaYMOm .rough-node .label,#mermaid-svg-eC4ge4EREORaYMOm .node .label,#mermaid-svg-eC4ge4EREORaYMOm .image-shape .label,#mermaid-svg-eC4ge4EREORaYMOm .icon-shape .label{text-align:center;}#mermaid-svg-eC4ge4EREORaYMOm .node.clickable{cursor:pointer;}#mermaid-svg-eC4ge4EREORaYMOm .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-eC4ge4EREORaYMOm .arrowheadPath{fill:#333333;}#mermaid-svg-eC4ge4EREORaYMOm .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-eC4ge4EREORaYMOm .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-eC4ge4EREORaYMOm .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-eC4ge4EREORaYMOm .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-eC4ge4EREORaYMOm .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-eC4ge4EREORaYMOm .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-eC4ge4EREORaYMOm .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-eC4ge4EREORaYMOm .cluster text{fill:#333;}#mermaid-svg-eC4ge4EREORaYMOm .cluster span{color:#333;}#mermaid-svg-eC4ge4EREORaYMOm 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-eC4ge4EREORaYMOm .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-eC4ge4EREORaYMOm rect.text{fill:none;stroke-width:0;}#mermaid-svg-eC4ge4EREORaYMOm .icon-shape,#mermaid-svg-eC4ge4EREORaYMOm .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-eC4ge4EREORaYMOm .icon-shape p,#mermaid-svg-eC4ge4EREORaYMOm .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-eC4ge4EREORaYMOm .icon-shape .label rect,#mermaid-svg-eC4ge4EREORaYMOm .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-eC4ge4EREORaYMOm .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-eC4ge4EREORaYMOm .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-eC4ge4EREORaYMOm :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 第1层 MySQL 主从复制
第2层 数据源管理 Dynamic-Datasource
应用层 order-service
写操作 / 判断走主
纯读操作
binlog 异步复制
@DS 注解路由
master 连接池 :3306
slave 连接池 :3307
主库 master :3306
从库 slave :3307

三、第 1 层:MySQL 主从复制

3.1 docker-compose 里的主从配置

配置 主库 master 从库 slave
端口 3306 3307
server-id 1 2(必须不同)
关键参数 log-bin=mysql-bin(开 binlog) relay-log + read_only=1(只读)
binlog 格式 ROW(记录每行变更,最安全) ---

server-id 必须不同是硬性要求,两个实例 id 撞了复制直接起不来。binlog 用 ROW 格式是因为它记录的是每一行的前后镜像,不依赖 SQL 语义,对函数、触发器这类场景最安全(相对的是 STATEMENT 记录原始 SQL,遇到 NOW()、UUID() 这类非确定性函数就会主从不一致)。

3.2 复制原理:三个线程接力

bash 复制代码
主库 Dump 线程  ──推 binlog──▶  从库 IO 线程  ──▶  relay log  ──▶  从库 SQL 线程 逐条重放

主库的 Dump 线程把 binlog 推给从库;从库 IO 线程收下来写成 relay log;从库 SQL 线程逐条重放,最终数据追平。这条链路是异步的,这正是后面所有延迟问题的根源。

三个线程的接力过程用图表示更清晰:
从库 slave 从库 SQL 线程 relay log 从库 IO 线程 Dump 线程 主库 master 从库 slave 从库 SQL 线程 relay log 从库 IO 线程 Dump 线程 主库 master #mermaid-svg-uoCRxAh3hKD4Xxym{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-uoCRxAh3hKD4Xxym .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-uoCRxAh3hKD4Xxym .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-uoCRxAh3hKD4Xxym .error-icon{fill:#552222;}#mermaid-svg-uoCRxAh3hKD4Xxym .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-uoCRxAh3hKD4Xxym .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-uoCRxAh3hKD4Xxym .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-uoCRxAh3hKD4Xxym .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-uoCRxAh3hKD4Xxym .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-uoCRxAh3hKD4Xxym .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-uoCRxAh3hKD4Xxym .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-uoCRxAh3hKD4Xxym .marker{fill:#333333;stroke:#333333;}#mermaid-svg-uoCRxAh3hKD4Xxym .marker.cross{stroke:#333333;}#mermaid-svg-uoCRxAh3hKD4Xxym svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-uoCRxAh3hKD4Xxym p{margin:0;}#mermaid-svg-uoCRxAh3hKD4Xxym .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-uoCRxAh3hKD4Xxym text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-uoCRxAh3hKD4Xxym .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-uoCRxAh3hKD4Xxym .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-uoCRxAh3hKD4Xxym .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-uoCRxAh3hKD4Xxym .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-uoCRxAh3hKD4Xxym #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-uoCRxAh3hKD4Xxym .sequenceNumber{fill:white;}#mermaid-svg-uoCRxAh3hKD4Xxym #sequencenumber{fill:#333;}#mermaid-svg-uoCRxAh3hKD4Xxym #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-uoCRxAh3hKD4Xxym .messageText{fill:#333;stroke:none;}#mermaid-svg-uoCRxAh3hKD4Xxym .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-uoCRxAh3hKD4Xxym .labelText,#mermaid-svg-uoCRxAh3hKD4Xxym .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-uoCRxAh3hKD4Xxym .loopText,#mermaid-svg-uoCRxAh3hKD4Xxym .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-uoCRxAh3hKD4Xxym .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-uoCRxAh3hKD4Xxym .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-uoCRxAh3hKD4Xxym .noteText,#mermaid-svg-uoCRxAh3hKD4Xxym .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-uoCRxAh3hKD4Xxym .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-uoCRxAh3hKD4Xxym .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-uoCRxAh3hKD4Xxym .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-uoCRxAh3hKD4Xxym .actorPopupMenu{position:absolute;}#mermaid-svg-uoCRxAh3hKD4Xxym .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-uoCRxAh3hKD4Xxym .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-uoCRxAh3hKD4Xxym .actor-man circle,#mermaid-svg-uoCRxAh3hKD4Xxym line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-uoCRxAh3hKD4Xxym :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 整条链路是异步的,这是延迟问题的根源 记录 binlog推送 binlog写入 relay log读取并解析逐条重放 SQL

3.3 最大的坑:复制不会自动建立,而且不报错

这里必须重点强调:

docker-compose 只负责把两个 MySQL 起起来、各自配好静态参数,但"从库连主库"这一步(CHANGE MASTER TO)是运行时动作 ,不会自动完成------而且不执行也不会报错。

当时踩的坑就是:server-id、log-bin 都配了,从库看着一切正常,但主库写进去的数据从库查不到。原因是只配了静态参数,从没建立复制连接。它不会给你任何提示,这是最危险的地方。

解决方案是补一个幂等的 init-replication.sh,把建立复制的过程脚本化,可重复执行:

sql 复制代码
-- ① 主库:创建复制账号 + 记下 binlog 位置
CREATE USER 'repl'@'%' IDENTIFIED BY 'repl123';
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';
FLUSH PRIVILEGES;
SHOW MASTER STATUS;   -- 记下 File(如 mysql-bin.000003)和 Position

-- ② 从库:指向主库并启动复制
CHANGE MASTER TO
  MASTER_HOST='mysql-master', MASTER_PORT=3306,
  MASTER_USER='repl', MASTER_PASSWORD='repl123',
  MASTER_LOG_FILE='mysql-bin.000003',   -- 填 ① 记下的 File
  MASTER_LOG_POS=157;                   -- 填 ① 记下的 Position
START SLAVE;

-- ③ 验证:两个 Yes 即成功
SHOW SLAVE STATUS\G   -- Slave_IO_Running=Yes 且 Slave_SQL_Running=Yes

这三步是面试里能直接讲清楚的知识点:建复制账号拿到 binlog 位点 → 从库 CHANGE MASTER 指向主库 → START SLAVE 并用两个 Yes 验证。

而且脚本最后一定要做端到端验证 :主库写一条 → 从库查得到,才算真的通了。只看 SHOW SLAVE STATUS 两个 Yes 还不够,因为那只能证明线程在跑,不能证明数据真的落到了你要查的那张表。

另外还有一个环境级的坑:docker-compose up 报 Only one usage of each socket address。原因是本机安装的 MySQL80 服务开机自启,占着 3306,和 docker 的 mysql-master 端口映射冲突。解决办法是停掉本机服务并改成手动启动:

bash 复制代码
net stop MySQL80
sc.exe config MySQL80 start= demand
# PowerShell 里 sc 是 Set-Content 的别名,必须写 sc.exe
# 或用 Set-Service -Name MySQL80 -StartupType Manual

四、第 2 层:多数据源(Dynamic-Datasource)

第 2 层解决"应用怎么同时连两个库"。order-service 引入了 dynamic-datasource-spring-boot-starter(MyBatis-Plus 同作者 baomidou 团队出品),配置非常轻:

yaml 复制代码
spring:
  datasource:
    dynamic:
      primary: master        # 默认走主库(写操作绝不能丢)
      strict: false          # 找不到数据源时回退 primary
      datasource:
        master:              # 写库
          url: jdbc:mysql://localhost:3306/order_db
        slave:               # 读库
          url: jdbc:mysql://localhost:3307/order_db

两个配置项的含义值得说明:

  • primary: master ------ 默认走主库 。这是刻意选的保守默认值。因为"忘记加注解"的后果是某个读请求走了主库(性能稍差),而如果默认走从库,忘记加注解的后果可能是写操作打到只读从库直接报错,或者更糟,读到了旧数据。默认值要选"出错时后果更轻"的那一侧。
  • strict: false ------ 找不到指定数据源时回退到 primary,避免因为一个拼错的数据源名导致整个请求 500。

4.1 技术选型:为什么选轻的 Dynamic-Datasource,而不是 ShardingSphere

读写分离有好几套方案,选型看需求匹配度,不是越重越好:

维度 Dynamic-Datasource(本项目选了) ShardingSphere-JDBC(没选)
定位 纯读写分离 + 多数据源切换 分库分表 + 读写分离 + 治理全家桶
体量 一个 starter,配置 3 行 重,概念多(分片键 / 分片算法 / 路由)
适合 只需要读写分离 需要分库分表 / SQL 改写 / 数据治理
代价 几乎为零 学习成本高,大量功能用不上

本项目选轻的理由 :当前单表数据量还没到需要分库分表的量级,核心诉求只有"读走从、写走主"。一个 starter + @DS 注解就够了;引入 ShardingSphere 等于为用不上的分片功能买单,徒增复杂度。

演进思路是"先轻后重":将来单表数据量突破分片阈值时,再升级到 ShardingSphere-JDBC(它本身也支持读写分离),而不是一步到位。

面试话术:「选型看需求匹配。我当前只需要读写分离,Dynamic-Datasource 轻量够用;ShardingSphere 的重心在分库分表,功能我用不上,引入反而增加维护成本。等单表数据量上来了再平滑升级。」

五、第 3 层:@DS 路由与它的边界

5.1 路由决策:哪些走从库,哪些必须走主库

原则只有一句:@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 消费者(插入 / 更新)

代码长这样:

java 复制代码
@Service
public class OrderServiceImpl implements OrderService {

    // ✅ 纯读、列表展示类,容忍秒级延迟 → 走从库
    @DS("slave")
    @Override
    public OrderDetailVO detail(Long orderId) { ... }

    @DS("slave")
    @Override
    public PageResult<OrderVO> myOrders(Long userId, int page, int size) { ... }

    // ❌ 不加注解 → 默认走主库
    @Override
    @Transactional(rollbackFor = Exception.class)
    public OrderVO create(CreateOrderDTO dto) { ... }

    // ❌ 先读后写,绝不能走从库
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void pay(Long orderId, Long userId) {
        Order order = validateOrder(orderId, userId); // 先查状态
        // ... 再改状态
    }
}

5.2 @DS 背后发生了什么

@DS 注解本身只是个标记,真正干活的是 starter 里的 AOP 切面:

bash 复制代码
方法执行前 → 把数据源 key 塞进 ThreadLocal
执行 SQL 时 → AbstractRoutingDataSource 读 ThreadLocal 决定用哪个连接池
方法执行后 → 清除 ThreadLocal

注意这是 ThreadLocal,所以它的作用域是"当前线程的当前方法调用"。这也解释了两个常见失效场景(见第 6.4 节)。

@DS 注解的 AOP 切面工作流程如下:
#mermaid-svg-qKG4Oerd5z7mI731{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-qKG4Oerd5z7mI731 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-qKG4Oerd5z7mI731 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-qKG4Oerd5z7mI731 .error-icon{fill:#552222;}#mermaid-svg-qKG4Oerd5z7mI731 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-qKG4Oerd5z7mI731 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-qKG4Oerd5z7mI731 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-qKG4Oerd5z7mI731 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-qKG4Oerd5z7mI731 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-qKG4Oerd5z7mI731 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-qKG4Oerd5z7mI731 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-qKG4Oerd5z7mI731 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-qKG4Oerd5z7mI731 .marker.cross{stroke:#333333;}#mermaid-svg-qKG4Oerd5z7mI731 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-qKG4Oerd5z7mI731 p{margin:0;}#mermaid-svg-qKG4Oerd5z7mI731 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-qKG4Oerd5z7mI731 .cluster-label text{fill:#333;}#mermaid-svg-qKG4Oerd5z7mI731 .cluster-label span{color:#333;}#mermaid-svg-qKG4Oerd5z7mI731 .cluster-label span p{background-color:transparent;}#mermaid-svg-qKG4Oerd5z7mI731 .label text,#mermaid-svg-qKG4Oerd5z7mI731 span{fill:#333;color:#333;}#mermaid-svg-qKG4Oerd5z7mI731 .node rect,#mermaid-svg-qKG4Oerd5z7mI731 .node circle,#mermaid-svg-qKG4Oerd5z7mI731 .node ellipse,#mermaid-svg-qKG4Oerd5z7mI731 .node polygon,#mermaid-svg-qKG4Oerd5z7mI731 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-qKG4Oerd5z7mI731 .rough-node .label text,#mermaid-svg-qKG4Oerd5z7mI731 .node .label text,#mermaid-svg-qKG4Oerd5z7mI731 .image-shape .label,#mermaid-svg-qKG4Oerd5z7mI731 .icon-shape .label{text-anchor:middle;}#mermaid-svg-qKG4Oerd5z7mI731 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-qKG4Oerd5z7mI731 .rough-node .label,#mermaid-svg-qKG4Oerd5z7mI731 .node .label,#mermaid-svg-qKG4Oerd5z7mI731 .image-shape .label,#mermaid-svg-qKG4Oerd5z7mI731 .icon-shape .label{text-align:center;}#mermaid-svg-qKG4Oerd5z7mI731 .node.clickable{cursor:pointer;}#mermaid-svg-qKG4Oerd5z7mI731 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-qKG4Oerd5z7mI731 .arrowheadPath{fill:#333333;}#mermaid-svg-qKG4Oerd5z7mI731 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-qKG4Oerd5z7mI731 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-qKG4Oerd5z7mI731 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-qKG4Oerd5z7mI731 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-qKG4Oerd5z7mI731 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-qKG4Oerd5z7mI731 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-qKG4Oerd5z7mI731 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-qKG4Oerd5z7mI731 .cluster text{fill:#333;}#mermaid-svg-qKG4Oerd5z7mI731 .cluster span{color:#333;}#mermaid-svg-qKG4Oerd5z7mI731 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-qKG4Oerd5z7mI731 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-qKG4Oerd5z7mI731 rect.text{fill:none;stroke-width:0;}#mermaid-svg-qKG4Oerd5z7mI731 .icon-shape,#mermaid-svg-qKG4Oerd5z7mI731 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-qKG4Oerd5z7mI731 .icon-shape p,#mermaid-svg-qKG4Oerd5z7mI731 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-qKG4Oerd5z7mI731 .icon-shape .label rect,#mermaid-svg-qKG4Oerd5z7mI731 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-qKG4Oerd5z7mI731 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-qKG4Oerd5z7mI731 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-qKG4Oerd5z7mI731 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 有
无
方法调用
AOP 切面拦截
方法上有 @DS 注解?
把数据源 key 写入 ThreadLocal
使用默认 primary=master
执行 SQL
AbstractRoutingDataSource 读取 ThreadLocal
选择对应连接池
方法执行完毕
清除 ThreadLocal

5.3 为什么"读后写"必须走主库

pay() 里的 validateOrder 会先查订单状态、再决定能不能支付。如果这个方法走了从库,可能因为主从延迟读到"还是 WAIT_PAY"的旧数据------用户明明已经支付过了,却又能再支付一次。

这就是读写分离最重要的一条业务铁律:判断依据不能来自可能延迟的数据源。

六、异常与并发边界:五个必须知道的坑

6.1 主从延迟(最核心的代价)

主从复制是异步的,主库写完,从库不是立刻有。典型翻车场景:

  • 用户下单 → 主库删购物车 → 用户秒刷购物车列表(走从库)→ 刚删的项又冒出来了
  • 用户支付 → 主库改 PAID → 用户秒进订单详情(走从库)→ 还显示"待支付"

避坑策略从简单到硬核分三档:

  1. 写后必须立刻读准的 → 强制读主库 (@DS("master")):业务层底线,永远先做这一招;
  2. 半同步复制(semi-sync):主库提交前等从库确认收到 binlog,牺牲写性能换强一致;
  3. 从库并行复制(MySQL 5.7 MTS):从库 SQL 线程并行重放,降低延迟。

本项目的处理方式是第 1 招:读方法都是"列表展示类",容忍秒级延迟,放从库没问题;凡是要"读来改"的都留在了主库。

6.2 主从复制静默失效

如前所述,CHANGE MASTER TO 不执行不报错。排查口诀 :从库查不到主库刚写的数据 → 先 SHOW SLAVE STATUS\G 看两个 Yes,再确认 MASTER_LOG_FILE/POS 是否对得上。

6.3 端口冲突导致起不来

本机 MySQL 服务占用 3306 与 docker 映射冲突,报 Only one usage of each socket address。属于环境问题,但生产上是"容器与宿主机服务争端口"的通用教训。

6.4 @DS 注解失效的两个边界

由于切换依赖 AOP 代理 + ThreadLocal,有两个必须知道的失效场景:

  • 同类内部自调用 :this.detail() 这种不走代理的调用,AOP 不会生效,@DS("slave") 形同虚设(会走默认的 master)。想要走从库,得通过注入的代理对象调用,或者把方法拆到另一个 Bean;
  • 异步/线程池 :ThreadLocal 不跨线程传递,@Async 或线程池里执行的 SQL 拿不到外层的数据源 key。本项目里两个定时调度器和 MQ 消费者都明确走了主库,也回避了这个问题。

6.5 事务与数据源切换

一个 @Transactional 方法在进入时就绑定了连接,方法内部的 @DS 切换不一定能生效。所以不要在一个事务里指望"前半段读从库、后半段写主库" ------这类"读后写"逻辑,正确做法是整段走主库。本项目正是这么做的:pay()、cancel() 全程主库。

五个坑的定位与规避路径,用一张决策图总结:
#mermaid-svg-gF70vCSbjApT3px8{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-gF70vCSbjApT3px8 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-gF70vCSbjApT3px8 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-gF70vCSbjApT3px8 .error-icon{fill:#552222;}#mermaid-svg-gF70vCSbjApT3px8 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-gF70vCSbjApT3px8 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-gF70vCSbjApT3px8 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-gF70vCSbjApT3px8 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-gF70vCSbjApT3px8 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-gF70vCSbjApT3px8 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-gF70vCSbjApT3px8 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-gF70vCSbjApT3px8 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-gF70vCSbjApT3px8 .marker.cross{stroke:#333333;}#mermaid-svg-gF70vCSbjApT3px8 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-gF70vCSbjApT3px8 p{margin:0;}#mermaid-svg-gF70vCSbjApT3px8 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-gF70vCSbjApT3px8 .cluster-label text{fill:#333;}#mermaid-svg-gF70vCSbjApT3px8 .cluster-label span{color:#333;}#mermaid-svg-gF70vCSbjApT3px8 .cluster-label span p{background-color:transparent;}#mermaid-svg-gF70vCSbjApT3px8 .label text,#mermaid-svg-gF70vCSbjApT3px8 span{fill:#333;color:#333;}#mermaid-svg-gF70vCSbjApT3px8 .node rect,#mermaid-svg-gF70vCSbjApT3px8 .node circle,#mermaid-svg-gF70vCSbjApT3px8 .node ellipse,#mermaid-svg-gF70vCSbjApT3px8 .node polygon,#mermaid-svg-gF70vCSbjApT3px8 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-gF70vCSbjApT3px8 .rough-node .label text,#mermaid-svg-gF70vCSbjApT3px8 .node .label text,#mermaid-svg-gF70vCSbjApT3px8 .image-shape .label,#mermaid-svg-gF70vCSbjApT3px8 .icon-shape .label{text-anchor:middle;}#mermaid-svg-gF70vCSbjApT3px8 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-gF70vCSbjApT3px8 .rough-node .label,#mermaid-svg-gF70vCSbjApT3px8 .node .label,#mermaid-svg-gF70vCSbjApT3px8 .image-shape .label,#mermaid-svg-gF70vCSbjApT3px8 .icon-shape .label{text-align:center;}#mermaid-svg-gF70vCSbjApT3px8 .node.clickable{cursor:pointer;}#mermaid-svg-gF70vCSbjApT3px8 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-gF70vCSbjApT3px8 .arrowheadPath{fill:#333333;}#mermaid-svg-gF70vCSbjApT3px8 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-gF70vCSbjApT3px8 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-gF70vCSbjApT3px8 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-gF70vCSbjApT3px8 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-gF70vCSbjApT3px8 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-gF70vCSbjApT3px8 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-gF70vCSbjApT3px8 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-gF70vCSbjApT3px8 .cluster text{fill:#333;}#mermaid-svg-gF70vCSbjApT3px8 .cluster span{color:#333;}#mermaid-svg-gF70vCSbjApT3px8 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-gF70vCSbjApT3px8 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-gF70vCSbjApT3px8 rect.text{fill:none;stroke-width:0;}#mermaid-svg-gF70vCSbjApT3px8 .icon-shape,#mermaid-svg-gF70vCSbjApT3px8 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-gF70vCSbjApT3px8 .icon-shape p,#mermaid-svg-gF70vCSbjApT3px8 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-gF70vCSbjApT3px8 .icon-shape .label rect,#mermaid-svg-gF70vCSbjApT3px8 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-gF70vCSbjApT3px8 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-gF70vCSbjApT3px8 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-gF70vCSbjApT3px8 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 读不到刚写的数据
从库始终查不到新数据
服务起不来
@DS 不生效
事务内切换无效
读写分离踩坑排查
现象是什么?
主从延迟
写后必须立刻读准 → 强制 @DS('master')
容忍秒级延迟 → 保持走从库
复制静默失效
SHOW SLAVE STATUS 看两个 Yes
核对 MASTER_LOG_FILE / POS
端口冲突
停本机 MySQL80 服务
AOP 失效边界
同类自调用 this.xxx()
异步线程池 ThreadLocal 不传递
事务绑定连接
读后写整段走主库

七、一次下单 + 一次查单的完整数据流

bash 复制代码
用户下单 POST /api/order
   │
   ▼ Gateway :9000 转发
order-service :8083
   │  ① 生成雪花订单号(低 8 位嵌入 userId 分片基因)
   │  ② Feign 调 stock-service :8084 原子扣库存(幂等键 order_no+product_id)
   │  ③ 本地事务:写 t_order / t_order_item(无 @DS → 主库 :3306)
   │  ④ 同事务写 t_event_record(WAIT),由 EventRetryScheduler 异步补发 MQ
   ▼
用户秒进订单详情 GET /api/order/{id}
   │
   ▼ order-service.detail() 带 @DS("slave")
   从库 :3307 读取 → 主从复制异步追平(容忍秒级延迟)

注意 ③ 和查询是两个不同的数据源,中间隔着一个异步复制窗口------这就是"下单后立刻查看"可能读到旧数据的物理原因。

把「下单 + 查单」的完整数据流画成时序图,能更直观地看到主从延迟的物理原因:
从库 :3307 主库 :3306 stock-service :8084 order-service :8083 Gateway :9000 用户 从库 :3307 主库 :3306 stock-service :8084 order-service :8083 Gateway :9000 用户 #mermaid-svg-8fR0ECROTJOtYn1N{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-8fR0ECROTJOtYn1N .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-8fR0ECROTJOtYn1N .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-8fR0ECROTJOtYn1N .error-icon{fill:#552222;}#mermaid-svg-8fR0ECROTJOtYn1N .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-8fR0ECROTJOtYn1N .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-8fR0ECROTJOtYn1N .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-8fR0ECROTJOtYn1N .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-8fR0ECROTJOtYn1N .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-8fR0ECROTJOtYn1N .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-8fR0ECROTJOtYn1N .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-8fR0ECROTJOtYn1N .marker{fill:#333333;stroke:#333333;}#mermaid-svg-8fR0ECROTJOtYn1N .marker.cross{stroke:#333333;}#mermaid-svg-8fR0ECROTJOtYn1N svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-8fR0ECROTJOtYn1N p{margin:0;}#mermaid-svg-8fR0ECROTJOtYn1N .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-8fR0ECROTJOtYn1N text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-8fR0ECROTJOtYn1N .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-8fR0ECROTJOtYn1N .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-8fR0ECROTJOtYn1N .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-8fR0ECROTJOtYn1N .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-8fR0ECROTJOtYn1N #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-8fR0ECROTJOtYn1N .sequenceNumber{fill:white;}#mermaid-svg-8fR0ECROTJOtYn1N #sequencenumber{fill:#333;}#mermaid-svg-8fR0ECROTJOtYn1N #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-8fR0ECROTJOtYn1N .messageText{fill:#333;stroke:none;}#mermaid-svg-8fR0ECROTJOtYn1N .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-8fR0ECROTJOtYn1N .labelText,#mermaid-svg-8fR0ECROTJOtYn1N .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-8fR0ECROTJOtYn1N .loopText,#mermaid-svg-8fR0ECROTJOtYn1N .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-8fR0ECROTJOtYn1N .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-8fR0ECROTJOtYn1N .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-8fR0ECROTJOtYn1N .noteText,#mermaid-svg-8fR0ECROTJOtYn1N .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-8fR0ECROTJOtYn1N .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-8fR0ECROTJOtYn1N .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-8fR0ECROTJOtYn1N .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-8fR0ECROTJOtYn1N .actorPopupMenu{position:absolute;}#mermaid-svg-8fR0ECROTJOtYn1N .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-8fR0ECROTJOtYn1N .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-8fR0ECROTJOtYn1N .actor-man circle,#mermaid-svg-8fR0ECROTJOtYn1N line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-8fR0ECROTJOtYn1N :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} ③ 写主库与⑤读从库之间隔着异步复制窗口 POST /api/order转发下单请求① 生成雪花订单号② Feign 原子扣库存扣减成功③ 写 t_order / t_order_item④ 写 t_event_record(WAIT)异步复制 binlogGET /api/order/{id}转发查单请求⑤ detail() 带 @DS("slave") 读从库

八、面试与简历亮点

  1. 三层结构讲得清楚 :能区分"复制层 / 数据源层 / 路由层",而不是只会背 @DS 注解;
  2. 有真实踩坑:主从复制静默失效(配了参数没建连接、不报错)、端口冲突,都是能体现排查能力的一手经验;
  3. 选型有判断:明确说清为什么"只需要读写分离"时选轻量方案,以及将来升级 ShardingSphere 的演进路径------体现"不为用而用"的工程判断;
  4. 一致性策略明确 :读走从、写走主、判断走主,一句话说清一致性边界在哪;
  5. 默认值设计有理由 :primary: master 是选"出错后果更轻"的一侧。

九、场景题

Q1:用户下单后立刻跳转订单详情,偶尔显示"订单不存在"或"待支付"。你怎么定位?

思路:先确认现象是否符合"写后立刻读"模式------是的,create() 走主库、detail() 走从库,中间是异步复制窗口。定位手段:查看该接口方法上是否有 @DS("slave");对比主从两台库分别查这条订单号,确认是从库还没追平而不是数据没写进去(这一条能直接区分"延迟"和"丢数据")。解决:该场景强制 @DS("master"),或者下单成功后直接返回主库写入的订单对象,前端不必再查一次。要做到"延迟但不错"而不是"看起来随机"。

Q2:SHOW SLAVE STATUS 两个 Yes,但业务查从库始终查不到新数据,可能是什么原因?

排查清单(按概率排序):

  1. 注解没生效 ------方法上根本没加 @DS("slave"),或者被同类内部 this.xxx() 自调用绕过代理;
  2. 注解加错了位置 ------加在了"先读后写"的方法上,走从库读到旧数据(这就是 pay() 不能加注解的原因);
  3. 复制位点不对 ------MASTER_LOG_FILE/POS 是旧位点,线程在跑但追的是旧 binlog;
  4. 没做端到端验证------只看了两个 Yes 就以为通了,实际上主库写的那张表压根没同步过;
  5. 根本连的是同一个库 ------master 和 slave 的 url 端口写重了(3306/3307 写成一个)。

排查顺序建议从"应用层"往"数据库层"走:先确认 SQL 实际落在哪个数据源,再去看复制状态,避免一上来就钻数据库。


一句话总结 :读写分离的完整链路是「主从复制(地基)→ 多数据源(容器)→ @DS 路由(开关)」;它的收益是读性能,代价是主从延迟;工程上的核心决策就一条------读走从、写走主、判断走主。

推荐标签:MySQL、读写分离、Spring Boot、微服务、数据库

相关推荐
goehou2 小时前
MCP 查数实战:让 LLM 安全地查数据库(只读账号、SQL 校验、参数漂移)
数据库·ai·llm·教程·mcp·function calling
我不会插花弄玉2 小时前
7.函数【由浅入深-MySQL】
android·数据库·mysql
周小董2 小时前
[1369]Windows上安装PostgreSQL
数据库·windows·postgresql
一只积极向上的小咸鱼5 小时前
第一性原理思考:从拆解问题到用实验检验答案
数据库
这个DBA有点耶5 小时前
时序数据库InfluxDB高基数问题深度拆解:内存索引膨胀机制与OOM完整链路
数据库·架构·dba
SL_staff5 小时前
运维救火实录:钉钉考勤API变更引发工资计算异常,我们用JVS-Logic可视化逻辑引擎实现分钟级热修复
java·mysql·全栈
加油yx5 小时前
MySQL数据类型
数据库·mysql
蓝胖的四次元口袋6 小时前
Redis知识梳理(1)
数据库·redis·缓存
夜雪一千6 小时前
MySQL 数据加密实战:从传输、存储到字段级加密,分清场景不踩坑
数据库·mysql