Skip to content

分布式系统商业场景训练营

这页专门训练“把分布式原理落到商业系统”的能力。分布式不是背 CAP、TCC、Saga、Seata、Redisson 这些词,而是面对订单、支付、库存、采集、搜索、缓存和消息这些真实链路时,能判断哪里会失败、失败后数据会变成什么状态、怎么重试、怎么补偿、怎么对账、怎么排查。

训练目标

学完这页,你要能做到:

  1. 解释远程调用超时为什么不是失败,也不是成功。
  2. 给写接口设计幂等键、唯一约束和状态机。
  3. 按业务风险选择 XA/JTA/2PC、TCC、Saga、本地消息表、事务消息、Seata 或最大努力通知。
  4. 解释 CAP、BASE 在注册中心、订单、搜索、缓存中的实际取舍。
  5. 设计订单、支付、库存、ES 同步、缓存更新、采集任务的最终一致方案。
  6. 解释分布式锁能做什么,不能做什么,为什么还要数据库约束兜底。
  7. 能按日志、消息、事务表、补偿任务和对账结果排查数据不一致。

分布式问题总图

mermaid
flowchart TD
    A["业务请求"] --> B["远程调用"]
    B --> C{"成功、失败、超时"}
    C --> D["幂等和状态机"]
    D --> E["本地事务"]
    E --> F{"是否跨服务一致"}
    F -- "强一致" --> G["XA/JTA/2PC 或 TCC"]
    F -- "最终一致" --> H["本地消息表、事务消息、Saga"]
    H --> I["重试、死信、补偿"]
    G --> I
    I --> J["对账和告警"]

初学者要先记住一句话:

分布式系统不是让故障消失,而是让每种故障都有可追踪、可重试、可补偿、可对账的闭环。

商业场景一:下单、锁库存、支付

业务目标

用户下单时常见步骤:

  1. 创建订单。
  2. 锁定库存。
  3. 使用优惠券。
  4. 创建支付单。
  5. 支付成功后更新订单。
  6. 发送通知、积分、搜索同步等后置动作。

拆成微服务后可能是:

mermaid
flowchart TD
    A["order-service<br/>订单库"] --> B["stock-service<br/>库存库"]
    A --> C["coupon-service<br/>优惠券库"]
    A --> D["pay-service<br/>支付库"]
    A --> E["message-service<br/>通知"]
    A --> F["search-sync<br/>ES"]

这里没有一个天然的本地事务可以包住所有库,所以必须设计一致性方案。

远程调用的三种状态

调用库存服务锁库存:

mermaid
flowchart TD
    A["订单服务发送锁库存请求"] --> B["库存服务按orderNo执行或幂等复用"]
    B --> C["响应超时或丢失"]
    C --> D["调用方不能确认库存是否已锁定"]
    D --> E["查询锁定流水、幂等重试或补偿"]

调用方看到超时,只能说明“没有拿到结果”,不能说明“库存没有锁定”。如果直接重试,而库存接口没有幂等,就可能重复锁库存。

正确做法:

  1. 请求带 orderNobizNo 作为幂等键。
  2. 库存服务写锁库存流水,唯一键保证同一订单只锁一次。
  3. 超时后先查询锁库存状态,或者重试幂等接口。
  4. 长期不确定时进入补偿任务和人工告警。

幂等设计 Demo

幂等表

sql
create table idempotent_record (
  id bigint primary key auto_increment,
  biz_type varchar(64) not null,
  biz_key varchar(128) not null,
  status varchar(32) not null,
  result_json text null,
  created_at datetime not null,
  updated_at datetime not null,
  unique key uk_biz (biz_type, biz_key)
);

订单状态机

sql
create table t_order (
  id bigint primary key auto_increment,
  order_no varchar(64) not null,
  status varchar(32) not null,
  amount bigint not null,
  version int not null default 0,
  unique key uk_order_no (order_no)
);

支付回调幂等处理

java
@Transactional
public void handlePayCallback(PayCallback callback) {
    boolean first = idempotentRepository.tryInsert("PAY_CALLBACK", callback.payNo());
    if (!first) {
        return;
    }

    int updated = orderRepository.markPaid(
            callback.orderNo(),
            "WAIT_PAY",
            "PAID",
            callback.payNo()
    );

    if (updated == 0) {
        Order order = orderRepository.findByOrderNo(callback.orderNo());
        if (!"PAID".equals(order.status())) {
            throw new IllegalStateException("订单状态不允许支付回调: " + order.status());
        }
    }
}

核心点:

  1. 幂等记录用唯一键挡重复请求。
  2. 订单更新带前置状态,避免状态乱跳。
  3. 已支付订单再次收到回调,直接返回成功。
  4. 非法状态要告警,而不是静默吞掉。

如果不会这样设计,重复回调、MQ 重投、接口重试都可能造成重复扣款、重复发券或订单状态回退。

CAP 和 BASE 放到业务里怎么理解

CAP 不是背三选二,而是网络分区时的取舍。

mermaid
flowchart TD
    A["网络分区发生"] --> B{"还要继续接收写请求吗"}
    B -- "继续" --> C["可用性更好<br/>可能短暂不一致"]
    B -- "拒绝部分写" --> D["一致性更强<br/>可用性下降"]

商业场景:

场景更偏向原因
ZooKeeper 选主CP不能出现两个 Leader
Eureka 服务发现AP注册信息短暂不一致可接受
余额扣减C错账代价高
商品浏览量A短暂不准可接受
ES 搜索结果最终一致MySQL 是事实源,ES 是查询视图
Redis 缓存最终一致缓存可重建,数据库才是事实源

BASE 不是“不管一致性”。BASE 允许短暂中间状态,但要求最后能靠重试、补偿、死信、对账和告警回到正确状态。

分布式事务选型决策树

mermaid
flowchart TD
    A["跨服务数据一致性"] --> B{"能否调整模型避免跨服务事务"}
    B -- "能" --> C["合并事务边界或改成异步后置动作"]
    B -- "不能" --> D{"是否必须立即强一致"}
    D -- "是" --> E{"业务是否能资源预留"}
    E -- "能" --> F["TCC"]
    E -- "不能" --> G["XA/JTA/2PC 或 Seata XA/AT"]
    D -- "否" --> H{"是否长流程多步骤"}
    H -- "是" --> I["Saga"]
    H -- "否" --> J["本地消息表或 MQ 事务消息"]
    J --> K["幂等、重试、死信、补偿、对账"]
    I --> K
    F --> K
    G --> K

方案对比

方案适合场景优点代价
XA/JTA/2PC短事务、强一致、多资源支持 XA一致性强锁时间长、性能低、资源要求高
TCC库存、余额、券预留业务可控,确认/取消明确侵入大,要处理空回滚、悬挂、幂等
Saga订单履约、审批、物流适合长流程补偿复杂,只能最终一致
本地消息表订单后置通知、积分、ES 同步落地稳,依赖少有延迟,需要投递任务
MQ 事务消息本地事务和发消息一致消息链路更自然消费端仍要幂等和补偿
Seata ATSQL 场景快速接入业务侵入较低undo_log、全局锁、SQL 兼容限制
最大努力通知支付回调、三方通知适合通知类依赖重试和主动查询

本地消息表落地 Demo

本地消息表是商业项目里非常常用的最终一致方案。

表结构

sql
create table local_message (
  id bigint primary key auto_increment,
  biz_type varchar(64) not null,
  biz_key varchar(128) not null,
  topic varchar(128) not null,
  body text not null,
  status varchar(32) not null,
  retry_count int not null default 0,
  next_retry_time datetime not null,
  created_at datetime not null,
  updated_at datetime not null,
  unique key uk_biz_msg (biz_type, biz_key)
);

创建订单时写消息

java
@Transactional
public void createOrder(CreateOrderCommand command) {
    Order order = Order.create(command);
    orderRepository.save(order);

    LocalMessage message = LocalMessage.of(
            "ORDER_CREATED",
            order.orderNo(),
            "order-created-topic",
            JsonUtils.toJson(new OrderCreatedEvent(order.orderNo(), order.skuId()))
    );
    localMessageRepository.save(message);
}

这一步用同一个本地事务保证:订单创建成功,消息记录也一定存在;订单创建失败,消息记录也不会提交。

投递任务

java
public void publishPendingMessages() {
    List<LocalMessage> messages = localMessageRepository.findPending(100);
    for (LocalMessage message : messages) {
        try {
            mqProducer.send(message.topic(), message.body());
            localMessageRepository.markSuccess(message.id());
        } catch (Exception ex) {
            localMessageRepository.markRetry(message.id(), ex.getMessage());
        }
    }
}

消费端幂等

java
@Transactional
public void consumeOrderCreated(OrderCreatedEvent event) {
    boolean first = consumeLogRepository.tryInsert("ORDER_CREATED", event.orderNo());
    if (!first) {
        return;
    }

    stockService.lockStock(event.orderNo(), event.skuId());
}

这套方案的关键不是“发了 MQ”,而是:

  1. 本地事务保证业务数据和消息记录一起提交。
  2. 后台投递失败可重试。
  3. 消费端必须幂等。
  4. 长期失败进入异常表、死信或告警。
  5. 对账任务能发现订单和库存状态不一致。

TCC 适合什么

TCC 是 Try、Confirm、Cancel。

以库存锁定为例:

mermaid
flowchart TD
    A["Try 锁定库存"] --> B{"所有参与者 Try 成功吗"}
    B -- "是" --> C["Confirm 确认扣减"]
    B -- "否" --> D["Cancel 释放锁定库存"]

Try 阶段不是“试一下不落库”,而是要真实预留资源。例如:

sql
update stock
set available = available - 1,
    locked = locked + 1
where sku_id = 1001
  and available >= 1;

Confirm:

sql
update stock
set locked = locked - 1,
    sold = sold + 1
where sku_id = 1001
  and locked >= 1;

Cancel:

sql
update stock
set available = available + 1,
    locked = locked - 1
where sku_id = 1001
  and locked >= 1;

TCC 必须处理:

问题含义处理
幂等Confirm/Cancel 可能重复分支事务表唯一键
空回滚Try 没执行,Cancel 先来了记录空回滚并返回成功
悬挂Cancel 后 Try 又到Try 前检查是否已空回滚
超时Try 后长时间没 Confirm定时 Cancel 或事务协调器处理

Saga 适合什么

Saga 适合长流程,比如订单履约:

mermaid
flowchart TD
    A["创建订单"] --> B["锁库存"]
    B --> C["生成支付单"]
    C --> D["创建物流单"]
    D --> E{"物流失败"}
    E -- "是" --> F["取消支付单"]
    F --> G["释放库存"]
    G --> H["关闭订单"]

Saga 的每一步都是本地事务,失败后执行补偿事务。它不适合资金强一致的核心扣款,但适合审批、履约、物流、通知、采集流程这种长链路。

不这样会怎样:

  1. 任意一步失败后,前面成功的动作没人撤销。
  2. 补偿动作如果不幂等,重复补偿会造成新的错误。
  3. 没有状态机,流程可能从失败又跳回成功。

Seata 怎么理解

Seata 不是分布式事务的全部,它是一个框架,提供 AT、TCC、Saga、XA 等模式。

AT 简化流程

mermaid
flowchart TD
    A["业务 SQL 执行前"] --> B["记录 before image"]
    B --> C["执行业务 SQL"]
    C --> D["记录 after image 和 undo_log"]
    D --> E["本地事务提交"]
    E --> F{"全局事务成功吗"}
    F -- "成功" --> G["删除 undo_log"]
    F -- "失败" --> H["根据 undo_log 反向补偿"]

AT 模式好处是业务侵入低,但要注意:

  1. 依赖 SQL 解析和 undo_log
  2. 有全局锁,热点数据会竞争。
  3. 不是所有复杂 SQL 都适合。
  4. 长事务会放大锁冲突。

最大努力通知

支付回调常见最大努力通知:

mermaid
flowchart TD
    A["支付平台通知商户"] --> B{"商户返回成功吗"}
    B -- "是" --> C["停止通知"]
    B -- "否" --> D["按规则重试通知"]
    D --> E{"超过最大次数"}
    E -- "否" --> A
    E -- "是" --> F["商户主动查询或人工处理"]

业务系统不能只依赖对方通知。正确做法:

  1. 回调接口幂等。
  2. 回调验签。
  3. 订单状态机更新。
  4. 定时主动查询支付平台状态。
  5. 对账文件或对账接口校验最终结果。

分布式锁边界

Redis 锁适合短时间互斥,不适合作为最终正确性的唯一保证。

正确释放锁:

lua
if redis.call("get", KEYS[1]) == ARGV[1] then
  return redis.call("del", KEYS[1])
else
  return 0
end

为什么锁不够:

  1. 锁可能过期。
  2. 业务执行可能超过租约。
  3. 网络抖动可能导致客户端误判。
  4. 主从切换可能造成锁状态丢失。
  5. 即使加锁失败,也必须保证数据库层不会写错。

关键业务要加兜底:

防线作用
Redis 锁降低并发冲突
唯一索引防重复写入
状态机防非法状态跳转
乐观锁防并发覆盖
幂等表防重复请求
补偿对账修复长期不一致

缓存一致性和 ES 一致性

Redis 缓存和 ES 索引都不是事实源。事实源通常是 MySQL、PostgreSQL、Oracle 或业务主库。

Redis 与数据库

mermaid
flowchart TD
    A["写请求"] --> B["更新数据库"]
    B --> C["事务提交"]
    C --> D["删除缓存"]
    D --> E{"删除成功吗"}
    E -- "是" --> F["后续读请求回源重建缓存"]
    E -- "否" --> G["记录重试任务和告警"]

MySQL 与 ES

mermaid
flowchart TD
    A["MySQL 变更"] --> B["MQ 或 Binlog CDC"]
    B --> C["同步消费者"]
    C --> D["写 ES"]
    D --> E{"写入成功吗"}
    E -- "是" --> F["搜索视图更新"]
    E -- "否" --> G["重试、死信、补偿"]
    G --> H["必要时按 MySQL 重建索引"]

如果 ES 更新失败,不能静默丢弃。要记录事件、重试、死信、告警、补偿,必要时用别名重建索引。否则用户会在搜索里看到旧数据,而详情页看到新数据。

采集任务分布式设计

医疗数据采集常见链路:

mermaid
flowchart TD
    A["调度中心触发任务"] --> B["多个执行器分片"]
    B --> C["读取医院数据"]
    C --> D["原始数据落库"]
    D --> E["清洗转换"]
    E --> F["业务表入库"]
    F --> G["发送资产变更消息"]
    G --> H["同步 ES 和缓存"]

关键设计:

  1. 调度分片只能提高并行度,不能替代幂等。
  2. 每条采集数据要有业务唯一键,例如 hospitalId + sourceId + recordNo
  3. 原始数据最好先落库或落对象存储,避免处理中断后无法追溯。
  4. 清洗入库使用唯一约束防重复。
  5. 资产变更通过 MQ 同步 ES。
  6. ES 同步失败进入重试和死信。
  7. 定时对账发现 MySQL 和 ES 差异。

生产排查流程

数据不一致

mermaid
flowchart TD
    A["发现数据不一致"] --> B["确定事实源"]
    B --> C["查业务状态机"]
    C --> D["查本地事务是否提交"]
    D --> E["查消息是否发送"]
    E --> F["查消费日志和幂等表"]
    F --> G["查重试、死信、补偿任务"]
    G --> H["按事实源修复并补告警"]

分布式事务卡住

mermaid
flowchart TD
    A["事务卡住"] --> B["查全局事务状态"]
    B --> C["查分支事务状态"]
    C --> D["查业务库锁等待"]
    D --> E["查 undo_log 或事务日志"]
    E --> F["查协调器和网络"]
    F --> G["决定重试、补偿或人工介入"]

MQ 堆积导致最终一致延迟

mermaid
flowchart TD
    A["消息堆积"] --> B["看生产 TPS 和消费 TPS"]
    B --> C["看 Lag 分布"]
    C --> D["判断是整体慢还是个别分区慢"]
    D --> E["查消费者线程、DB、锁、慢消息"]
    E --> F["扩容、限流、修慢查询、处理热点"]

面试标准回答

分布式事务怎么选

text
我会先判断能不能避免跨服务事务,能通过调整领域边界或异步后置动作解决,就不要强行上分布式事务。必须跨服务时,如果是短事务且资源支持 XA,可以考虑 XA/JTA/2PC 或 Seata XA/AT;如果业务能做资源预留,比如库存、余额、券,适合 TCC;如果是履约、审批、物流这种长流程,适合 Saga;如果是订单后置通知、积分、ES 同步,通常用本地消息表或事务消息做最终一致。无论哪种方案,都必须有幂等、重试、补偿、死信、对账和告警。

为什么超时不能直接判失败

text
远程调用超时只说明调用方没有收到响应,不代表下游没有执行。下游可能没收到请求,也可能已经执行业务但响应丢失。如果调用方直接按失败处理或直接无脑重试,就可能造成重复扣款、重复锁库存或状态不一致。正确做法是接口设计幂等键,支持按业务流水查询真实状态,并配合重试、补偿和对账。

最终一致怎么保证不是“最终不一致”

text
最终一致不是不保证一致,而是允许短暂中间状态。成熟方案需要本地事务保证事实源正确,通过本地消息表、事务消息或 CDC 推动下游更新;消费端要幂等;失败要重试;超过阈值进死信或异常表;定时补偿和对账发现长期差异;重要差异要告警或人工处理。只有这些闭环都存在,最终一致才可靠。

关联知识点