标题: 分布式事务太难?TCC、Saga、本地消息表一网打尽!
副标题: 从理论到实践,订单库存一致性全攻略
🎬 开篇:一次灾难性的数据不一致
电商大促当天:
用户下单 -> 订单服务创建订单成功 ✅
-> 库存服务扣减库存... 网络超时 ❌
结果:
- 订单创建成功(已生成订单号)
- 库存未扣减(还是原来的数量)
- 用户支付成功
- 仓库发不了货(库存数据不对)
客服:为什么我有订单但是库存没扣?😱
运营:数据对不上,怎么发货?😰
老板:这个月奖金全扣!💀
排查发现:
- 订单服务和库存服务是独立的微服务
- 使用了本地事务,无法保证一致性
- 没有任何补偿机制
- 数据不一致导致库存混乱
损失:
- 库存混乱:无法统计
- 财务对账失败:通宵加班
- 用户投诉:爆炸
- 技术债:3个月才修复完
教训:分布式系统必须解决事务一致性问题!
🤔 什么是分布式事务?
想象你和朋友之间转账:
- 本地事务: 你银行账户-100,朋友账户+100(同一个银行,一次完成)
- 分布式事务: 你的支付宝-100,朋友的微信+100(不同系统,需要协调)
分布式事务:跨多个服务/数据库的事务,要么都成功,要么都失败!
📚 知识地图
分布式事务解决方案
├── 💪 2PC/3PC(强一致性,性能差)
├── 🎯 TCC(Try-Confirm-Cancel)
├── 📜 Saga(长事务,补偿机制)
├── 📨 本地消息表(推荐!)
├── 🔔 事务消息(RocketMQ)
└── 📊 最大努力通知
🎯 方案1:TCC模式
🌰 生活中的例子
订酒店房间:
- Try(尝试): 预订房间,锁定房间(房间标记为"已预订")
- Confirm(确认): 支付成功,确认入住(房间分配给你)
- Cancel(取消): 支付失败,取消预订(释放房间)
💻 技术实现
/**
* TCC模式实现
*/
// ========== 订单服务 ==========
/**
* 订单TCC接口
*/
public interface OrderTccService {
/**
* Try:创建预订单
*/
@TwoPhaseBusinessAction(
name = "orderTccService",
commitMethod = "confirmOrder",
rollbackMethod = "cancelOrder"
)
String tryCreateOrder(
@BusinessActionContextParameter(paramName = "orderDTO") OrderDTO orderDTO
);
/**
* Confirm:确认订单
*/
boolean confirmOrder(BusinessActionContext context);
/**
* Cancel:取消订单
*/
boolean cancelOrder(BusinessActionContext context);
}
/**
* 订单TCC实现
*/
@Service
public class OrderTccServiceImpl implements OrderTccService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private StockTccService stockTccService; // Dubbo远程调用
/**
* Try阶段:创建预订单
*/
@Override
@Transactional(rollbackFor = Exception.class)
public String tryCreateOrder(OrderDTO orderDTO) {
log.info("【Try阶段】创建预订单:{}", orderDTO);
// 1. 创建预订单(状态为TRYING)
Order order = Order.builder()
.orderNo(generateOrderNo())
.userId(orderDTO.getUserId())
.productId(orderDTO.getProductId())
.quantity(orderDTO.getQuantity())
.totalAmount(orderDTO.getTotalAmount())
.status(OrderStatus.TRYING) // ⚡ TRYING状态
.createTime(LocalDateTime.now())
.build();
orderMapper.insert(order);
// 2. 调用库存服务Try(预扣库存)
boolean stockResult = stockTccService.tryDeductStock(
orderDTO.getProductId(),
orderDTO.getQuantity(),
order.getOrderNo()
);
if (!stockResult) {
throw new BusinessException("库存不足");
}
log.info("【Try阶段】预订单创建成功:orderNo={}", order.getOrderNo());
return order.getOrderNo();
}
/**
* Confirm阶段:确认订单
*/
@Override
@Transactional(rollbackFor = Exception.class)
public boolean confirmOrder(BusinessActionContext context) {
String orderNo = context.getActionContext("orderDTO", OrderDTO.class)
.getOrderNo();
log.info("【Confirm阶段】确认订单:orderNo={}", orderNo);
// 1. 查询预订单
Order order = orderMapper.selectByOrderNo(orderNo);
if (order == null) {
log.warn("订单不存在:orderNo={}", orderNo);
return true; // 幂等性:已处理过
}
if (order.getStatus() == OrderStatus.CONFIRMED) {
log.warn("订单已确认:orderNo={}", orderNo);
return true; // 幂等性
}
// 2. 更新订单状态为已确认
order.setStatus(OrderStatus.CONFIRMED);
order.setConfirmTime(LocalDateTime.now());
orderMapper.updateById(order);
log.info("【Confirm阶段】订单确认成功:orderNo={}", orderNo);
return true;
}
/**
* Cancel阶段:取消订单
*/
@Override
@Transactional(rollbackFor = Exception.class)
public boolean cancelOrder(BusinessActionContext context) {
String orderNo = context.getActionContext("orderDTO", OrderDTO.class)
.getOrderNo();
log.info("【Cancel阶段】取消订单:orderNo={}", orderNo);
// 1. 查询预订单
Order order = orderMapper.selectByOrderNo(orderNo);
if (order == null) {
log.warn("订单不存在:orderNo={}", orderNo);
return true; // 幂等性
}
if (order.getStatus() == OrderStatus.CANCELLED) {
log.warn("订单已取消:orderNo={}", orderNo);
return true; // 幂等性
}
// 2. 更新订单状态为已取消
order.setStatus(OrderStatus.CANCELLED);
order.setCancelTime(LocalDateTime.now());
orderMapper.updateById(order);
log.info("【Cancel阶段】订单取消成功:orderNo={}", orderNo);
return true;
}
}
// ========== 库存服务 ==========
/**
* 库存TCC接口
*/
public interface StockTccService {
/**
* Try:预扣库存
*/
@TwoPhaseBusinessAction(
name = "stockTccService",
commitMethod = "confirmDeduct",
rollbackMethod = "cancelDeduct"
)
boolean tryDeductStock(
@BusinessActionContextParameter(paramName = "productId") Long productId,
@BusinessActionContextParameter(paramName = "quantity") Integer quantity,
@BusinessActionContextParameter(paramName = "orderNo") String orderNo
);
/**
* Confirm:确认扣减
*/
boolean confirmDeduct(BusinessActionContext context);
/**
* Cancel:取消扣减(回滚)
*/
boolean cancelDeduct(BusinessActionContext context);
}
/**
* 库存TCC实现
*/
@Service
public class StockTccServiceImpl implements StockTccService {
@Autowired
private StockMapper stockMapper;
@Autowired
private StockFreezeMapper freezeMapper;
/**
* Try阶段:冻结库存
*/
@Override
@Transactional(rollbackFor = Exception.class)
public boolean tryDeductStock(Long productId, Integer quantity, String orderNo) {
log.info("【Try阶段】冻结库存:productId={}, quantity={}, orderNo={}",
productId, quantity, orderNo);
// 1. 检查是否已经冻结过(幂等性)
StockFreeze existFreeze = freezeMapper.selectByOrderNo(orderNo);
if (existFreeze != null) {
log.warn("库存已冻结:orderNo={}", orderNo);
return true;
}
// 2. 查询库存
Stock stock = stockMapper.selectByProductId(productId);
if (stock == null || stock.getAvailableStock() < quantity) {
log.warn("库存不足:productId={}, available={}, need={}",
productId, stock != null ? stock.getAvailableStock() : 0, quantity);
return false;
}
// 3. ⚡ 扣减可用库存,增加冻结库存
int updated = stockMapper.freezeStock(productId, quantity);
if (updated == 0) {
log.error("冻结库存失败:productId={}", productId);
return false;
}
// 4. 记录冻结记录
StockFreeze freeze = StockFreeze.builder()
.orderNo(orderNo)
.productId(productId)
.quantity(quantity)
.status(FreezeStatus.TRYING)
.createTime(LocalDateTime.now())
.build();
freezeMapper.insert(freeze);
log.info("【Try阶段】库存冻结成功:orderNo={}", orderNo);
return true;
}
/**
* Confirm阶段:确认扣减
*/
@Override
@Transactional(rollbackFor = Exception.class)
public boolean confirmDeduct(BusinessActionContext context) {
String orderNo = context.getActionContext("orderNo", String.class);
log.info("【Confirm阶段】确认扣减库存:orderNo={}", orderNo);
// 1. 查询冻结记录
StockFreeze freeze = freezeMapper.selectByOrderNo(orderNo);
if (freeze == null) {
log.warn("冻结记录不存在:orderNo={}", orderNo);
return true;
}
if (freeze.getStatus() == FreezeStatus.CONFIRMED) {
log.warn("库存已确认扣减:orderNo={}", orderNo);
return true; // 幂等性
}
// 2. 更新冻结记录状态
freeze.setStatus(FreezeStatus.CONFIRMED);
freeze.setConfirmTime(LocalDateTime.now());
freezeMapper.updateById(freeze);
// 注意:库存已在Try阶段扣减,这里只需更新状态
log.info("【Confirm阶段】库存扣减确认成功:orderNo={}", orderNo);
return true;
}
/**
* Cancel阶段:回滚扣减
*/
@Override
@Transactional(rollbackFor = Exception.class)
public boolean cancelDeduct(BusinessActionContext context) {
String orderNo = context.getActionContext("orderNo", String.class);
Long productId = context.getActionContext("productId", Long.class);
Integer quantity = context.getActionContext("quantity", Integer.class);
log.info("【Cancel阶段】回滚库存:orderNo={}, productId={}, quantity={}",
orderNo, productId, quantity);
// 1. 查询冻结记录
StockFreeze freeze = freezeMapper.selectByOrderNo(orderNo);
if (freeze == null) {
log.warn("冻结记录不存在:orderNo={}", orderNo);
return true;
}
if (freeze.getStatus() == FreezeStatus.CANCELLED) {
log.warn("库存已回滚:orderNo={}", orderNo);
return true; // 幂等性
}
// 2. ⚡ 释放冻结库存
int updated = stockMapper.unfreezeStock(productId, quantity);
if (updated == 0) {
log.error("释放库存失败:productId={}", productId);
return false;
}
// 3. 更新冻结记录状态
freeze.setStatus(FreezeStatus.CANCELLED);
freeze.setCancelTime(LocalDateTime.now());
freezeMapper.updateById(freeze);
log.info("【Cancel阶段】库存回滚成功:orderNo={}", orderNo);
return true;
}
}
/**
* Mapper实现
*/
@Mapper
public interface StockMapper {
/**
* 冻结库存
*/
@Update("UPDATE stock " +
"SET available_stock = available_stock - #{quantity}, " +
" frozen_stock = frozen_stock + #{quantity} " +
"WHERE product_id = #{productId} " +
"AND available_stock >= #{quantity}")
int freezeStock(@Param("productId") Long productId,
@Param("quantity") Integer quantity);
/**
* 解冻库存
*/
@Update("UPDATE stock " +
"SET available_stock = available_stock + #{quantity}, " +
" frozen_stock = frozen_stock - #{quantity} " +
"WHERE product_id = #{productId}")
int unfreezeStock(@Param("productId") Long productId,
@Param("quantity") Integer quantity);
}
/**
* 数据库表结构
*/
CREATE TABLE stock (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
product_id BIGINT NOT NULL,
total_stock INT NOT NULL DEFAULT 0 COMMENT '总库存',
available_stock INT NOT NULL DEFAULT 0 COMMENT '可用库存',
frozen_stock INT NOT NULL DEFAULT 0 COMMENT '冻结库存',
UNIQUE KEY uk_product_id (product_id)
) COMMENT='库存表';
CREATE TABLE stock_freeze (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_no VARCHAR(50) NOT NULL,
product_id BIGINT NOT NULL,
quantity INT NOT NULL,
status VARCHAR(20) NOT NULL COMMENT 'TRYING/CONFIRMED/CANCELLED',
create_time DATETIME NOT NULL,
confirm_time DATETIME,
cancel_time DATETIME,
UNIQUE KEY uk_order_no (order_no),
INDEX idx_product_id (product_id)
) COMMENT='库存冻结记录表';
/**
* 优点:
* ✅ 强一致性
* ✅ 实时性好
* ✅ 无需人工干预
*
* 缺点:
* ❌ 实现复杂(每个操作都要3个方法)
* ❌ 性能开销大(需要3次调用)
* ❌ 代码侵入性强
*
* 适用场景:
* ✅ 对一致性要求极高
* ✅ 业务逻辑相对简单
* ✅ 交易类场景(支付、转账)
*/
📨 方案2:本地消息表(推荐!)
🌰 生活中的例子
寄快递:
- 写纸条: "帮我转账100元给张三"(本地消息)
- 放进包裹: 和订单一起打包(同一个事务)
- 快递送达: 对方收到包裹和纸条(消息送达)
- 执行任务: 对方看到纸条,转账给张三(消费消息)
💻 技术实现
/**
* 本地消息表方案
*/
// ========== 订单服务 ==========
/**
* 本地消息表
*/
CREATE TABLE local_message (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
message_id VARCHAR(50) NOT NULL COMMENT '消息ID',
message_type VARCHAR(50) NOT NULL COMMENT '消息类型',
message_body TEXT NOT NULL COMMENT '消息内容',
status VARCHAR(20) NOT NULL DEFAULT 'PENDING' COMMENT '状态:PENDING/SUCCESS/FAIL',
retry_count INT NOT NULL DEFAULT 0 COMMENT '重试次数',
max_retry INT NOT NULL DEFAULT 3 COMMENT '最大重试次数',
next_retry_time DATETIME COMMENT '下次重试时间',
create_time DATETIME NOT NULL,
update_time DATETIME,
UNIQUE KEY uk_message_id (message_id),
INDEX idx_status_retry (status, next_retry_time)
) COMMENT='本地消息表';
/**
* 订单服务实现
*/
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private LocalMessageMapper messageMapper;
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 创建订单
*/
@Transactional(rollbackFor = Exception.class)
public String createOrder(OrderDTO dto) {
// 1. 创建订单
Order order = Order.builder()
.orderNo(generateOrderNo())
.userId(dto.getUserId())
.productId(dto.getProductId())
.quantity(dto.getQuantity())
.totalAmount(dto.getTotalAmount())
.status(OrderStatus.PENDING)
.createTime(LocalDateTime.now())
.build();
orderMapper.insert(order);
// 2. ⚡ 创建本地消息(和订单在同一个事务中)
LocalMessage message = LocalMessage.builder()
.messageId(UUID.randomUUID().toString())
.messageType("STOCK_DEDUCT")
.messageBody(JSON.toJSONString(Map.of(
"orderNo", order.getOrderNo(),
"productId", dto.getProductId(),
"quantity", dto.getQuantity()
)))
.status(MessageStatus.PENDING)
.createTime(LocalDateTime.now())
.build();
messageMapper.insert(message);
log.info("订单创建成功,已记录本地消息:orderNo={}, messageId={}",
order.getOrderNo(), message.getMessageId());
return order.getOrderNo();
}
}
/**
* 定时任务:扫描并发送本地消息
*/
@Component
public class LocalMessageScheduler {
@Autowired
private LocalMessageMapper messageMapper;
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 每分钟扫描一次待发送的消息
*/
@Scheduled(cron = "0 * * * * ?")
public void scanAndSendMessages() {
log.info("开始扫描本地消息");
// 1. 查询待发送的消息
List<LocalMessage> messages = messageMapper.selectPendingMessages(100);
log.info("查询到{}条待发送消息", messages.size());
// 2. 发送消息
for (LocalMessage message : messages) {
try {
sendMessage(message);
} catch (Exception e) {
log.error("发送消息失败:messageId=" + message.getMessageId(), e);
handleSendFail(message);
}
}
}
/**
* 发送消息
*/
private void sendMessage(LocalMessage message) {
// 1. 发送到MQ
rabbitTemplate.convertAndSend(
"stock.exchange",
"stock.deduct",
message.getMessageBody()
);
// 2. 更新消息状态为成功
message.setStatus(MessageStatus.SUCCESS);
message.setUpdateTime(LocalDateTime.now());
messageMapper.updateById(message);
log.info("消息发送成功:messageId={}", message.getMessageId());
}
/**
* 处理发送失败
*/
private void handleSendFail(LocalMessage message) {
message.setRetryCount(message.getRetryCount() + 1);
if (message.getRetryCount() >= message.getMaxRetry()) {
// 超过最大重试次数,标记为失败
message.setStatus(MessageStatus.FAIL);
log.error("消息发送失败,已达最大重试次数:messageId={}",
message.getMessageId());
// 发送告警
alertService.sendAlert("本地消息发送失败", message.getMessageId());
} else {
// 设置下次重试时间(指数退避)
int delayMinutes = (int) Math.pow(2, message.getRetryCount());
message.setNextRetryTime(
LocalDateTime.now().plusMinutes(delayMinutes)
);
}
message.setUpdateTime(LocalDateTime.now());
messageMapper.updateById(message);
}
}
// ========== 库存服务 ==========
/**
* MQ消费者:扣减库存
*/
@Component
public class StockDeductConsumer {
@Autowired
private StockService stockService;
@RabbitListener(queues = "stock.deduct.queue")
public void handleStockDeduct(String messageBody) {
log.info("收到扣减库存消息:{}", messageBody);
try {
// 1. 解析消息
Map<String, Object> data = JSON.parseObject(messageBody, Map.class);
String orderNo = (String) data.get("orderNo");
Long productId = (Long) data.get("productId");
Integer quantity = (Integer) data.get("quantity");
// 2. 幂等性检查
if (stockService.isAlreadyDeducted(orderNo)) {
log.info("库存已扣减,跳过:orderNo={}", orderNo);
return;
}
// 3. 扣减库存
boolean success = stockService.deductStock(
productId,
quantity,
orderNo
);
if (!success) {
log.error("扣减库存失败:orderNo={}", orderNo);
throw new BusinessException("库存不足");
}
log.info("库存扣减成功:orderNo={}", orderNo);
} catch (Exception e) {
log.error("处理扣减库存消息失败", e);
// 重新抛出异常,触发消息重试
throw new AmqpRejectAndDontRequeueException("处理失败", e);
}
}
}
/**
* 库存服务实现
*/
@Service
public class StockService {
@Autowired
private StockMapper stockMapper;
@Autowired
private StockLogMapper logMapper;
/**
* 扣减库存
*/
@Transactional(rollbackFor = Exception.class)
public boolean deductStock(Long productId, Integer quantity, String orderNo) {
// 1. 扣减库存
int updated = stockMapper.deductStock(productId, quantity);
if (updated == 0) {
log.warn("库存不足:productId={}, quantity={}", productId, quantity);
return false;
}
// 2. 记录扣减日志(用于幂等性)
StockLog log = StockLog.builder()
.orderNo(orderNo)
.productId(productId)
.quantity(quantity)
.type(StockLogType.DEDUCT)
.createTime(LocalDateTime.now())
.build();
logMapper.insert(log);
return true;
}
/**
* 幂等性检查
*/
public boolean isAlreadyDeducted(String orderNo) {
StockLog log = logMapper.selectByOrderNo(orderNo);
return log != null;
}
}
/**
* 优点:
* ✅ 实现简单
* ✅ 可靠性高(消息持久化)
* ✅ 性能好(异步处理)
* ✅ 业务侵入小
*
* 缺点:
* ⚠️ 最终一致性(有延迟)
* ⚠️ 需要定时任务扫描
* ⚠️ 需要幂等性设计
*
* 适用场景:
* ✅ 对实时性要求不高
* ✅ 高并发场景
* ✅ 微服务架构(推荐)⭐⭐⭐⭐⭐
*/
📊 方案对比总结
| 方案 | 一致性 | 性能 | 复杂度 | 实时性 | 推荐度 |
|---|---|---|---|---|---|
| 2PC/3PC | 强一致 | 低 | 高 | 高 | ⭐⭐ |
| TCC | 强一致 | 中 | 高 | 高 | ⭐⭐⭐ |
| Saga | 最终一致 | 高 | 中 | 中 | ⭐⭐⭐⭐ |
| 本地消息表 | 最终一致 | 高 | 低 | 中 | ⭐⭐⭐⭐⭐ |
| 事务消息 | 最终一致 | 高 | 低 | 中 | ⭐⭐⭐⭐⭐ |
✅ 最佳实践
生产环境推荐:本地消息表 or RocketMQ事务消息
设计原则:
□ 优先选择最终一致性方案
□ 根据业务特点选择合适方案
□ 重试机制必须幂等
□ 记录完整的日志便于排查
□ 监控消息堆积和失败率
容错处理:
□ 消息重试(指数退避)
□ 最大重试次数限制
□ 死信队列处理
□ 人工介入机制
□ 补偿接口
性能优化:
□ 异步处理(MQ)
□ 批量操作
□ 消息去重
□ 限流保护
监控告警:
□ 消息发送失败
□ 消息消费失败
□ 消息堆积
□ 库存不一致
🎉 总结
核心要点
分布式事务解决方案:
1️⃣ 强一致性场景 -> TCC
- 金融转账
- 支付场景
- 实时性要求高
2️⃣ 最终一致性场景 -> 本地消息表/事务消息
- 订单+库存
- 积分+优惠券
- 大部分业务场景(推荐)
3️⃣ 长事务场景 -> Saga
- 工作流
- 审批流程
- 多步骤业务
4️⃣ 关键设计:
- 幂等性
- 补偿机制
- 监控告警
- 人工介入
记住:分布式事务的核心是"最终一致性+补偿机制"! 🔄
文档编写时间:2025年10月24日
作者:热爱分布式的事务工程师
版本:v1.0
愿你的数据始终一致! 🔄✨