分布式事务故障排查
问题
微服务架构下,跨服务调用出现数据不一致(如订单创建了但库存没扣,钱扣了但积分没加),如何排查和修复?
答案
排查思路
常见不一致场景
| 场景 | 描述 | 解决 |
|---|---|---|
| 订单创建但库存未扣 | 库存服务调用超时 | 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 + 幂等消费
详见 消息可靠性。