跳到主要内容

分布式事务故障排查

问题

微服务架构下,跨服务调用出现数据不一致(如订单创建了但库存没扣,钱扣了但积分没加),如何排查和修复?

答案

排查思路

常见不一致场景

场景描述解决
订单创建但库存未扣库存服务调用超时TCC / Saga 补偿
扣款成功但订单创建失败网络抖动本地消息表 + 重试
MQ 消息丢失生产端未确认 / Broker 故障ACK + 对账
重复扣款超时重试时接口不幂等幂等性设计

本地消息表方案(最常用)

本地消息表实现
@Transactional
public void createOrder(OrderDTO orderDTO) {
// 1. 创建订单(同库事务)
Order order = orderMapper.insert(orderDTO);

// 2. 写本地消息表(同库事务保证原子性)
LocalMessage message = new LocalMessage();
message.setMessageId(UUID.randomUUID().toString());
message.setTopic("stock-deduct");
message.setPayload(JSON.toJSONString(new StockDeductDTO(order.getId(), order.getItems())));
message.setStatus("PENDING");
localMessageMapper.insert(message);
}

// 定时任务扫描未发送的消息
@Scheduled(fixedDelay = 5000)
public void scanPendingMessages() {
List<LocalMessage> messages = localMessageMapper.selectPending();
for (LocalMessage msg : messages) {
try {
mqProducer.send(msg.getTopic(), msg.getPayload());
msg.setStatus("SENT");
} catch (Exception e) {
msg.setRetryCount(msg.getRetryCount() + 1);
if (msg.getRetryCount() > 5) {
msg.setStatus("FAILED"); // 人工介入
}
}
localMessageMapper.update(msg);
}
}

对账系统

定时对账
@Scheduled(cron = "0 0 2 * * ?")  // 每天凌晨 2 点
public void reconcile() {
LocalDate yesterday = LocalDate.now().minusDays(1);

// 查询订单服务昨天的已支付订单
List<String> orderIds = orderMapper.selectPaidOrderIds(yesterday);

// 查询库存服务昨天的扣减记录
List<String> deductIds = stockClient.getDeductedOrderIds(yesterday);

// 找差集:订单已支付但库存未扣减
Set<String> missing = new HashSet<>(orderIds);
missing.removeAll(deductIds);

if (!missing.isEmpty()) {
log.error("发现不一致数据 {} 条: {}", missing.size(), missing);
// 发起补偿
for (String orderId : missing) {
compensationService.deductStock(orderId);
}
}
}

常见面试问题

Q1: 分布式事务有哪些方案?各有什么优缺点?

答案

方案一致性性能复杂度适用场景
2PC强一致低(同步阻塞)数据库层面
TCC最终一致高(三个接口)资金/库存
Saga最终一致长事务
本地消息表最终一致最通用
MQ 事务消息最终一致已有 RocketMQ

详见 分布式事务

Q2: TCC 的空回滚和悬挂问题怎么解决?

答案

  • 空回滚:Try 未执行但 Cancel 被调用 → Cancel 检查是否有 Try 记录,没有则直接返回
  • 悬挂:Cancel 先于 Try 执行 → Try 检查是否已有 Cancel 记录,已取消则不执行
  • 通过事务控制表记录每个分支事务的状态来解决

Q3: 如何保证消息不丢?

答案

三端保障:

  • 生产端:同步发送 + 确认 ACK + 失败重试
  • Broker 端:持久化 + 多副本同步
  • 消费端:手动 ACK + 幂等消费

详见 消息可靠性

相关链接