面试知识库
困难

微服务数据一致性#

一句话答案#

微服务间数据一致性靠分布式事务(TCC/SAGA/事务消息),原则:能异步不同步,能最终一致不强一致。

核心要点

方案选择:

场景推荐
核心资金TCC
长流程编排SAGA
异步解耦事务消息
数据同步Canal + MQ

原则: 能异步不同步,能最终一致不强一致

三大方案的落地机制(深挖)#

决策树解决「选哪个」,下面是面试深挖「怎么实现的」——这三套机制是分布式事务的高频追问点。

RocketMQ 事务消息:半消息(half message)机制

要解决的核心矛盾:本地事务(扣自己的库)和发消息(通知下游)怎么保证原子,不能出现「事务提交了消息没发」或「消息发了事务回滚了」。

  1. 发半消息:生产者先发一条「半消息」到 Broker。半消息会写入 CommitLog 但不建 ConsumeQueue 索引——RocketMQ 消费者是靠 ConsumeQueue 索引找消息的,没索引消费者就看不见这条消息(对下游不可见)。
  2. 执行本地事务:Broker 回 ACK 后,生产者执行本地事务(扣库存/改订单)。
  3. 二阶段提交/回滚:本地事务成功 → 发 commit,Broker 给这条半消息补建 ConsumeQueue 索引,消息变可见、下游开始消费;本地事务失败 → 发 rollback,Broker 把半消息标记删除(实际是写一条删除标记),下游永远看不到。
  4. 事务状态回查:若生产者发完半消息后宕机/超时没发 commit/rollback,Broker 会定时遍历未决的半消息,反向回调生产者的 checkLocalTransaction 询问「这笔本地事务到底成没成」,据此补 commit 或 rollback——这是保证最终一致的兜底。

关键点:「写 CommitLog 但不建 ConsumeQueue 索引」就是半消息「对消费者不可见」的实现原理;commit 的动作本质就是补索引。

TCC 三大坑及解法(都靠事务记录表)

TCC = Try(预留资源)/ Confirm(确认提交)/ Cancel(释放预留)。分布式 + 网络乱序会暴露三个坑:

现象解法
空回滚Try 还没执行(如网络丢包没到),事务管理器却先发了 Cancel;此时 Cancel 找不到对应的预留资源事务记录表先记一条事务状态;Cancel 时查表发现没有 Try 记录,则识别为空回滚,直接返回成功、不做实际回滚
悬挂Try 请求因网络延迟「迟到」,在 Cancel 之后才到达,预留的资源再没人 Confirm/Cancel,永久悬挂Cancel 时在事务表里标记「已回滚」;Try 执行前先查表,发现该事务已被 Cancel 过则拒绝执行,避免迟到的 Try 占资源
幂等Confirm/Cancel 因超时重试被调多次,重复加钱/重复扣减事务表记录 Try/Confirm/Cancel 各阶段状态,每个操作执行前查状态,已执行则直接返回成功,保证多次调用结果一致

三坑同源:分布式调用「可能重复、可能乱序、可能丢」。统一用一张事务记录表记录每笔事务到达了哪个阶段,每个动作执行前先查表判断,是 TCC 框架(如 Seata TCC、Hmily)落地的标准做法。

Seata AT 模式:undo_log + 全局锁

AT(Auto Transaction)是「无侵入的自动补偿」,业务只写普通 SQL,Seata 自动生成回滚逻辑:

  • undo_log 存反向 SQL(前后镜像):执行业务 SQL 前,Seata 解析 SQL,查出前镜像(before image,改之前的行数据);执行后再查后镜像(after image,改之后的数据),把前后镜像一起写入业务库的 undo_log 表。回滚(二阶段 rollback)时拿前镜像把数据还原,相当于自动生成的反向 SQL;正常提交(二阶段 commit)则异步删掉 undo_log。
  • 全局锁防写隔离丢更新:一阶段提交本地事务前,要先向 TC(事务协调器)申请全局锁(锁住被改的行 PK)。其他全局事务想改同一行,拿不到全局锁就等待——防止「事务 A 改了行但全局事务还没结束,事务 B 又改同一行并提交」导致 A 回滚时按旧前镜像覆盖、把 B 的更新丢掉(脏写/丢更新)。
  • 脏写校验:回滚还原前会比对「当前数据库的值」和「后镜像」,不一致说明被别的非 Seata 事务改过(脏写),此时不能直接还原,需人工介入告警。
面试回答(2分钟版)

微服务拆分后每个服务有独立数据库,跨服务的数据一致性是必须面对的问题。核心原则是能异步不同步、能最终一致不强一致。选型思路用一棵决策树来说:先问能不能异步——如果业务允许异步就用事务消息,这是最轻量的方案,RocketMQ 原生支持半消息机制保证本地事务和消息发送的原子性;如果必须同步再问是不是资金核心场景——是就用 TCC,Try 预留资源 Confirm 确认 Cancel 回滚,手动挡控制力强但开发成本高;不是资金核心就用 SAGA,每步操作定义正向和补偿操作,失败时逆序补偿,适合长流程编排如订单创建涉及库存、积分、优惠券多个服务。另外还有本地消息表方案,在业务表所在的库建一张消息表,用本地事务保证业务操作和消息记录原子性,再通过定时任务发送消息,适合任意 MQ 但要维护额外的表和定时任务。不管用哪种方案,都要有对账机制做兜底,定期比对各服务数据发现并修复不一致。

追问与易错

追问方向:

  • “跨服务一致性有哪些方案?”→ 本地消息表(事务写消息表+定时补偿)、TCC(Try-Confirm-Cancel 三阶段补偿)、Saga(编排/协调长事务链)、Seata AT 模式(自动生成反向 SQL);大多数场景用最终一致性即可
  • “怎么保证消息发送和事务原子性?”→ 本地消息表:业务操作和写消息表在同一个本地事务中,定时任务扫描消息表发送 MQ,发送成功后更新状态;RocketMQ 事务消息也能解决,半消息+回查机制
  • “对账机制怎么设计?”→ 定时任务对比上下游系统的关键数据(如订单状态、金额),不一致的记录到差异表并触发告警;核心是明确对账粒度、对账频率和差异处理策略(自动补偿或人工介入)
  • “RocketMQ 半消息为什么消费者看不见?”→ 半消息写入 CommitLog 但不建 ConsumeQueue 索引,消费者靠 ConsumeQueue 找消息所以看不到;二阶段 commit 时补建索引消息才可见,rollback 则标记删除。生产者宕机没回执时 Broker 定时遍历未决半消息回查本地事务状态兜底
  • “TCC 的空回滚和悬挂怎么解决?”→ 都靠事务记录表。空回滚:Cancel 查表无 Try 记录则直接成功不实际回滚;悬挂:Cancel 时标记已回滚,迟到的 Try 执行前查表发现已 Cancel 就拒绝执行,防止占用资源没人释放
  • “Seata AT 怎么自动回滚?undo_log 存什么?”→ 业务 SQL 执行前后各查一次拿到前镜像和后镜像存进 undo_log;回滚时用前镜像还原数据(等于自动反向 SQL),提交则异步删 undo_log。还原前比对当前值和后镜像,被别人改过则报脏写
  • “Seata AT 的全局锁解决什么问题?”→ 防写隔离丢更新:一阶段提交前申请行级全局锁,别的全局事务改同一行要等锁,避免 A 未结束时 B 改了同行并提交、A 回滚按旧前镜像覆盖把 B 的更新丢掉

易错点:

  • ❌ 微服务用分布式事务——尽量避免用最终一致
  • ❌ 跨服务直接调用保证一致——网络不可靠需补偿