作者:一个被分布式事务折磨过999次的10年老Java程序员
座右铭:能用分布式解决的问题,绝不用单体!能用补偿的问题,绝不用锁!💪
📚 目录
🎬 引子:那个让我秃头的需求 {#引子}
从前从前,在一个风和日丽的周一早晨...
产品经理小王兴冲冲地跑过来:"老李啊,咱们系统要重构成微服务了!"
我:"哦?为啥?"
小王:"因为老板说竞对都在用微服务,咱们也得用!而且听说微服务特别高大上!"
我(内心OS):完了,又是一个看了几篇技术博客就来指点江山的... 😓
结果一个月后,系统拆成了三个服务:
- 📦 订单服务:负责创建订单
- 🏪 库存服务:负责扣减库存
- 💰 账户服务:负责扣款
然后问题就来了...
🔥 灾难现场
某天凌晨3点,我被一通电话吵醒:
运维小哥:"老李,出大事了!用户投诉说钱扣了,但是订单没创建成功!"
我一个激灵爬起来,打开监控一看:
订单创建:✅ 成功
库存扣减:✅ 成功
账户扣款:✅ 成功
但是... 有20%的订单数据对不上!😱
这就是分布式事务的经典问题:在单体架构里,一个@Transactional注解就能解决的事儿,拆成微服务后,变成了世纪难题!
🎓 理论篇:先把原理整明白 {#理论篇}
什么是事务?用吃火锅来理解
想象你去吃火锅自助:
- 点菜(创建订单)
- 厨房拿食材(扣减库存)
- 结账(扣款)
在单体架构里,这就像你一个人承包了整个流程,要么全做完,要么一件不做。这就是ACID特性:
🎯 ACID特性(单体事务的四大金刚)
| 特性 | 说明 | 生活例子 |
|---|---|---|
| Atomicity(原子性) | 要么全做,要么全不做 | 要么火锅吃完结账,要么一口不吃不给钱 |
| Consistency(一致性) | 数据前后保持一致 | 你的钱包-100,商家的钱包+100 |
| Isolation(隔离性) | 互不干扰 | 你吃你的火锅,我吃我的,别抢我的肉! |
| Durability(持久性) | 做完就是做完了 | 吃完了就是吃完了,不能反悔 |
分布式系统的噩梦:CAP定理
当你把系统拆成微服务,就像把火锅店拆成了三家店:
🏠 点菜店(订单服务)
🏠 食材店(库存服务)
🏠 收银店(账户服务)
这时候,CAP定理告诉我们一个残酷的现实:鱼与熊掌不可兼得!
📊 CAP定理图解
C (一致性)
/ \
/ \
/ \
/ \
/ 😱 \
/ 只能选2 \
/______________\
A P
(可用性) (分区容错)
- C (Consistency):所有节点看到的数据一致
- A (Availability):系统随时可用,不宕机
- P (Partition Tolerance):网络出问题了,系统还能撑住
重点来了:在分布式系统中,P是必选项(网络故障是常态),所以你只能在C和A之间二选一!
BASE理论:退而求其次的智慧
既然CAP告诉我们"不可能三角",那咋办?聪明的架构师们提出了BASE理论:
- BA (Basically Available):基本可用,系统有点慢没关系,但别挂掉
- S (Soft state):软状态,可以有中间状态
- E (Eventually consistent):最终一致性,暂时不一致没事,最后一致就行
🍜 用点外卖来理解BASE
1. 你下单了外卖(订单创建)
2. 商家接单了(库存扣减)
3. 支付成功了(账户扣款)
BUT... 外卖还在路上,还没到你手里!
这是"软状态",过程中暂时不一致
但最终,要么你吃到外卖,要么退款,达到"最终一致性"
⚔️ 方案篇:江湖上的六大门派 {#方案篇}
老司机告诉你,分布式事务的解决方案就像武侠小说里的各大门派,各有优劣,适用场景不同。
1️⃣ 两阶段提交(2PC)- 刚猛派 💪
原理图
协调者(老大)
|
___________________
| | |
参与者1 参与者2 参与者3
(订单) (库存) (账户)
阶段一:准备阶段
老大:"兄弟们,准备好了吗?"
小弟1:"我准备好了!"
小弟2:"我也准备好了!"
小弟3:"我也OK!"
阶段二:提交阶段
老大:"都准备好了,一起提交!"
所有小弟:"收到!提交完成!"
优点
- ✅ 强一致性,严格的ACID保证
- ✅ 逻辑简单,易于理解
缺点
- ❌ 同步阻塞:所有小弟都得等老大的命令,干等着,浪费资源
- ❌ 单点故障:老大挂了,所有小弟都傻眼
- ❌ 数据不一致:网络问题可能导致部分提交成功,部分失败
- ❌ 性能差:大家都在等,吞吐量低
生活例子
就像组团开黑打游戏:
- 队长问:"大家准备好了吗?"
- 所有人都要回:"准备好了!"
- 队长:"好,一起冲!"
但如果队长突然掉线,所有人都不知道该不该冲... 😅
代码示例(伪代码)
// 协调者
public class TransactionCoordinator {
public void execute2PC() {
// 阶段一:准备
boolean phase1 = prepare();
// 阶段二:提交或回滚
if (phase1) {
commit();
} else {
rollback();
}
}
private boolean prepare() {
boolean orderOk = orderService.prepare(); // 订单准备
boolean inventoryOk = inventoryService.prepare(); // 库存准备
boolean accountOk = accountService.prepare(); // 账户准备
return orderOk && inventoryOk && accountOk;
}
private void commit() {
orderService.commit();
inventoryService.commit();
accountService.commit();
}
private void rollback() {
orderService.rollback();
inventoryService.rollback();
accountService.rollback();
}
}
2️⃣ 三阶段提交(3PC)- 改良派 🔧
2PC的弟弟,在2PC基础上加了个预备阶段,缓解了一些问题,但本质还是同步阻塞,现在基本没人用了。
我的评价:理论上很美好,实际上基本没用。跳过!⏭️
3️⃣ TCC模式 - 稳健派 🛡️
Try-Confirm-Cancel,这是我个人比较推荐的方案!
原理图
业务操作拆成三步:
Try阶段(尝试):
订单服务:冻结订单状态为"处理中"
库存服务:冻结库存(预扣)
账户服务:冻结金额(预扣款)
⬇️ 如果都成功
Confirm阶段(确认):
订单服务:订单状态改为"已完成"
库存服务:真正扣减库存
账户服务:真正扣款
⬇️ 如果任何一步失败
Cancel阶段(取消):
订单服务:删除订单
库存服务:释放冻结的库存
账户服务:释放冻结的金额
🍔 用麦当劳点餐理解TCC
Try阶段:
- 你:"我要一个巨无霸套餐"
- 收银员:"稍等,我看看有没有..."(检查库存,冻结食材)
- 你:"我看看钱包够不够..."(冻结余额)
Confirm阶段:
- 收银员:"有货!请付款!"
- 你:"好的"(真正扣款)
- 厨房:"开始制作"(真正扣减食材)
Cancel阶段:
- 收银员:"抱歉,汉堡肉刚好卖完了..."
- 你:"那算了"(释放冻结的钱)
- 厨房:"不用做了"(释放冻结的食材)
代码示例
// 订单服务
@Service
public class OrderTccService {
// Try:尝试创建订单,订单状态为"处理中"
@Tcc(confirmMethod = "confirmOrder", cancelMethod = "cancelOrder")
public void tryCreateOrder(OrderRequest request) {
Order order = new Order();
order.setStatus("PROCESSING"); // 冻结状态
order.setAmount(request.getAmount());
orderRepository.save(order);
log.info("Try阶段:订单已冻结,orderId={}", order.getId());
}
// Confirm:确认订单
public void confirmOrder(OrderRequest request) {
Order order = orderRepository.findById(request.getOrderId());
order.setStatus("COMPLETED"); // 确认完成
orderRepository.save(order);
log.info("Confirm阶段:订单已确认,orderId={}", order.getId());
}
// Cancel:取消订单
public void cancelOrder(OrderRequest request) {
Order order = orderRepository.findById(request.getOrderId());
order.setStatus("CANCELLED"); // 取消
orderRepository.save(order);
log.info("Cancel阶段:订单已取消,orderId={}", order.getId());
}
}
// 库存服务
@Service
public class InventoryTccService {
@Tcc(confirmMethod = "confirmReduce", cancelMethod = "cancelReduce")
public void tryReduceInventory(String productId, int quantity) {
// 冻结库存:可用库存-quantity,冻结库存+quantity
Inventory inventory = inventoryRepository.findByProductId(productId);
inventory.setAvailable(inventory.getAvailable() - quantity);
inventory.setFrozen(inventory.getFrozen() + quantity);
inventoryRepository.save(inventory);
log.info("Try阶段:库存已冻结,productId={}, quantity={}", productId, quantity);
}
public void confirmReduce(String productId, int quantity) {
// 真正扣减:冻结库存-quantity
Inventory inventory = inventoryRepository.findByProductId(productId);
inventory.setFrozen(inventory.getFrozen() - quantity);
inventoryRepository.save(inventory);
log.info("Confirm阶段:库存已扣减,productId={}, quantity={}", productId, quantity);
}
public void cancelReduce(String productId, int quantity) {
// 释放冻结:可用库存+quantity,冻结库存-quantity
Inventory inventory = inventoryRepository.findByProductId(productId);
inventory.setAvailable(inventory.getAvailable() + quantity);
inventory.setFrozen(inventory.getFrozen() - quantity);
inventoryRepository.save(inventory);
log.info("Cancel阶段:库存已释放,productId={}, quantity={}", productId, quantity);
}
}
// 账户服务
@Service
public class AccountTccService {
@Tcc(confirmMethod = "confirmDeduct", cancelMethod = "cancelDeduct")
public void tryDeductBalance(String userId, BigDecimal amount) {
// 冻结金额
Account account = accountRepository.findByUserId(userId);
account.setAvailable(account.getAvailable().subtract(amount));
account.setFrozen(account.getFrozen().add(amount));
accountRepository.save(account);
log.info("Try阶段:金额已冻结,userId={}, amount={}", userId, amount);
}
public void confirmDeduct(String userId, BigDecimal amount) {
// 真正扣款
Account account = accountRepository.findByUserId(userId);
account.setFrozen(account.getFrozen().subtract(amount));
accountRepository.save(account);
log.info("Confirm阶段:金额已扣除,userId={}, amount={}", userId, amount);
}
public void cancelDeduct(String userId, BigDecimal amount) {
// 释放冻结
Account account = accountRepository.findByUserId(userId);
account.setAvailable(account.getAvailable().add(amount));
account.setFrozen(account.getFrozen().subtract(amount));
accountRepository.save(account);
log.info("Cancel阶段:金额已释放,userId={}, amount={}", userId, amount);
}
}
优点
- ✅ 不依赖数据库层面的事务:每个服务独立管理
- ✅ 性能较好:可以异步化
- ✅ 灵活性高:业务逻辑清晰
缺点
- ❌ 开发成本高:需要实现Try、Confirm、Cancel三个方法
- ❌ 业务侵入性强:需要改造现有业务代码
- ❌ 需要考虑幂等性:Confirm和Cancel可能被重复调用
4️⃣ Saga模式 - 流派 🌊
Saga是目前最流行的分布式事务解决方案之一!把一个大事务拆成多个小事务,每个小事务都有对应的补偿操作。
两种实现方式
📮 1. 事件编排(Choreography)- 去中心化
订单服务 --[OrderCreated事件]--> 库存服务
|
[InventoryReduced事件]
⬇️
账户服务
|
[PaymentCompleted事件]
⬇️
完成!
特点:
- 没有中央协调者
- 每个服务监听事件,自主决定下一步
- 就像接力赛跑,每个人跑完自己的棒次
🎯 2. 命令协调(Orchestration)- 中心化
Saga协调器(总指挥)
|
___________|____________
| | |
订单服务 库存服务 账户服务
特点:
- 有一个中央协调者
- 协调者负责调用各个服务,管理整个流程
- 就像乐队指挥,统一协调
🎭 用拍电影理解Saga
正常流程:
导演:"Action!"
演员A:演完第一场 ✅
演员B:演完第二场 ✅
演员C:演完第三场 ✅
导演:"Cut!完美!"
出问题了:
导演:"Action!"
演员A:演完第一场 ✅
演员B:演完第二场 ✅
演员C:忘词了!❌
导演:"Cut!重来!"
演员B:重拍第二场(补偿操作)
演员A:重拍第一场(补偿操作)
代码示例(事件编排方式)
// 订单服务
@Service
public class OrderSagaService {
@Autowired
private EventPublisher eventPublisher;
@Transactional
public void createOrder(OrderRequest request) {
try {
// 1. 创建订单
Order order = new Order();
order.setUserId(request.getUserId());
order.setProductId(request.getProductId());
order.setQuantity(request.getQuantity());
order.setAmount(request.getAmount());
order.setStatus("PENDING");
orderRepository.save(order);
// 2. 发布订单创建事件
OrderCreatedEvent event = new OrderCreatedEvent();
event.setOrderId(order.getId());
event.setProductId(request.getProductId());
event.setQuantity(request.getQuantity());
event.setUserId(request.getUserId());
event.setAmount(request.getAmount());
eventPublisher.publish(event);
log.info("订单创建成功,等待后续处理,orderId={}", order.getId());
} catch (Exception e) {
log.error("订单创建失败", e);
throw new BusinessException("订单创建失败");
}
}
// 监听库存扣减失败事件,进行补偿
@EventListener
public void onInventoryReduceFailed(InventoryReduceFailedEvent event) {
log.warn("库存扣减失败,开始补偿订单,orderId={}", event.getOrderId());
Order order = orderRepository.findById(event.getOrderId());
order.setStatus("CANCELLED");
orderRepository.save(order);
log.info("订单补偿完成,orderId={}", event.getOrderId());
}
}
// 库存服务
@Service
public class InventorySagaService {
@Autowired
private EventPublisher eventPublisher;
// 监听订单创建事件
@EventListener
@Transactional
public void onOrderCreated(OrderCreatedEvent event) {
try {
// 1. 扣减库存
Inventory inventory = inventoryRepository.findByProductId(event.getProductId());
if (inventory.getQuantity() < event.getQuantity()) {
throw new BusinessException("库存不足");
}
inventory.setQuantity(inventory.getQuantity() - event.getQuantity());
inventoryRepository.save(inventory);
// 2. 发布库存扣减成功事件
InventoryReducedEvent reducedEvent = new InventoryReducedEvent();
reducedEvent.setOrderId(event.getOrderId());
reducedEvent.setUserId(event.getUserId());
reducedEvent.setAmount(event.getAmount());
eventPublisher.publish(reducedEvent);
log.info("库存扣减成功,orderId={}", event.getOrderId());
} catch (Exception e) {
log.error("库存扣减失败,orderId={}", event.getOrderId(), e);
// 发布库存扣减失败事件
InventoryReduceFailedEvent failedEvent = new InventoryReduceFailedEvent();
failedEvent.setOrderId(event.getOrderId());
eventPublisher.publish(failedEvent);
}
}
// 监听支付失败事件,进行补偿
@EventListener
public void onPaymentFailed(PaymentFailedEvent event) {
log.warn("支付失败,开始补偿库存,orderId={}", event.getOrderId());
// 恢复库存
OrderCreatedEvent originalEvent = event.getOriginalEvent();
Inventory inventory = inventoryRepository.findByProductId(originalEvent.getProductId());
inventory.setQuantity(inventory.getQuantity() + originalEvent.getQuantity());
inventoryRepository.save(inventory);
log.info("库存补偿完成,orderId={}", event.getOrderId());
}
}
// 账户服务
@Service
public class AccountSagaService {
@Autowired
private EventPublisher eventPublisher;
// 监听库存扣减成功事件
@EventListener
@Transactional
public void onInventoryReduced(InventoryReducedEvent event) {
try {
// 1. 扣款
Account account = accountRepository.findByUserId(event.getUserId());
if (account.getBalance().compareTo(event.getAmount()) < 0) {
throw new BusinessException("余额不足");
}
account.setBalance(account.getBalance().subtract(event.getAmount()));
accountRepository.save(account);
// 2. 发布支付成功事件
PaymentCompletedEvent completedEvent = new PaymentCompletedEvent();
completedEvent.setOrderId(event.getOrderId());
eventPublisher.publish(completedEvent);
log.info("支付成功,orderId={}", event.getOrderId());
} catch (Exception e) {
log.error("支付失败,orderId={}", event.getOrderId(), e);
// 发布支付失败事件
PaymentFailedEvent failedEvent = new PaymentFailedEvent();
failedEvent.setOrderId(event.getOrderId());
failedEvent.setOriginalEvent(event);
eventPublisher.publish(failedEvent);
}
}
}
优点
- ✅ 异步执行:性能好,吞吐量高
- ✅ 松耦合:服务之间通过事件通信
- ✅ 适合长事务:可以跨天的交易也能处理
缺点
- ❌ 最终一致性:不是实时一致
- ❌ 补偿逻辑复杂:要考虑各种异常情况
- ❌ 调试困难:链路长,排查问题麻烦
5️⃣ 本地消息表 - 朴实派 📝
这是一个非常朴实但有效的方案!
原理
订单服务数据库:
+------------+ +----------------+
| 订单表 | | 本地消息表 |
+------------+ +----------------+
| order_id | | msg_id |
| user_id | | topic |
| amount | | content |
| status | | status |
+------------+ | retry_count |
+----------------+
在同一个事务中:
1. 插入订单记录
2. 插入消息记录
然后:
3. 定时任务扫描消息表
4. 发送消息到MQ
5. 发送成功后,标记消息为已发送
🚚 用快递理解本地消息表
你在网上买了东西:
-
下单:商家同时做两件事
- 记录你的订单(订单表)
- 记录要给快递公司的通知(消息表)
-
发货:商家后台有个小助理
- 定期检查消息表:"有没有要发的货?"
- 通知快递公司来取货
- 快递确认收货后,标记"已发货"
-
容错:如果快递没来
- 小助理会不断重试
- 直到快递确认收货为止
代码示例
// 订单服务
@Service
public class OrderServiceWithLocalMessage {
@Autowired
private OrderRepository orderRepository;
@Autowired
private LocalMessageRepository messageRepository;
@Autowired
private MessageSender messageSender;
@Transactional
public void createOrder(OrderRequest request) {
// 1. 创建订单
Order order = new Order();
order.setUserId(request.getUserId());
order.setAmount(request.getAmount());
order.setStatus("PENDING");
orderRepository.save(order);
// 2. 在同一个事务中,保存本地消息
LocalMessage message = new LocalMessage();
message.setTopic("order-created");
message.setContent(JSON.toJSONString(order));
message.setStatus("PENDING");
message.setRetryCount(0);
message.setMaxRetry(5);
messageRepository.save(message);
log.info("订单和消息已保存,orderId={}", order.getId());
}
// 定时任务:扫描并发送消息
@Scheduled(fixedDelay = 5000) // 每5秒执行一次
public void scanAndSendMessage() {
List<LocalMessage> pendingMessages = messageRepository
.findByStatusAndRetryCountLessThan("PENDING", 5);
for (LocalMessage message : pendingMessages) {
try {
// 发送消息到MQ
messageSender.send(message.getTopic(), message.getContent());
// 标记为已发送
message.setStatus("SENT");
messageRepository.save(message);
log.info("消息发送成功,messageId={}", message.getId());
} catch (Exception e) {
// 发送失败,增加重试次数
message.setRetryCount(message.getRetryCount() + 1);
if (message.getRetryCount() >= message.getMaxRetry()) {
message.setStatus("FAILED");
log.error("消息发送失败,已达最大重试次数,messageId={}", message.getId());
}
messageRepository.save(message);
}
}
}
}
// 库存服务:消费消息
@Service
public class InventoryMessageConsumer {
@KafkaListener(topics = "order-created")
public void onOrderCreated(String message) {
try {
Order order = JSON.parseObject(message, Order.class);
// 扣减库存(幂等性处理)
reduceInventory(order.getProductId(), order.getQuantity());
log.info("库存扣减成功,orderId={}", order.getId());
} catch (Exception e) {
log.error("库存扣减失败", e);
// 可以发送补偿消息
}
}
private void reduceInventory(String productId, int quantity) {
// 实现库存扣减逻辑
// 注意:要保证幂等性!
}
}
优点
- ✅ 实现简单:不需要额外的框架
- ✅ 可靠性高:消息不会丢失
- ✅ 对现有系统改造小:只需加张表
缺点
- ❌ 耦合度较高:消息表和业务表在一起
- ❌ 需要定时任务:额外的资源消耗
- ❌ 不适合高并发:扫描表有性能瓶颈
6️⃣ 最大努力通知 - 佛系派 🙏
适用于对一致性要求不那么高的场景。
原理
服务A --调用--> 服务B
|
|--失败了?没关系!
|
|--重试1次
|--重试2次
|--重试3次
|
|--还是失败?
|
└---> 发送通知,人工处理
🎁 用送礼物理解
你要给朋友送生日礼物:
- 第一次送,朋友不在家 ❌
- 第二次送,朋友还是不在 ❌
- 第三次送,朋友还是不在 ❌
- 算了,给朋友打个电话:"礼物放你家门口了,记得拿!"
优点
- ✅ 实现最简单
- ✅ 性能最好
缺点
- ❌ 可靠性最低
- ❌ 可能需要人工介入
🎯 实战篇:订单-库存-账户的血泪史 {#实战篇}
好了,理论讲完了,该来点实战了!让我用一个完整的案例,手把手教你怎么实现!
业务场景
电商系统下单流程: 用户点击"立即购买"→创建订单→扣减库存→扣款→完成
技术选型
经过10年老司机的血泪教训,我推荐:
- 强一致性场景(如金融交易):选TCC
- 弱一致性场景(如电商订单):选Saga + 本地消息表
- 超高并发场景:选最大努力通知
我们这里选择 Saga + 本地消息表 的组合方案!
系统架构图
客户端
|
⬇️
API网关
|
______________|______________
| | |
⬇️ ⬇️ ⬇️
订单服务 库存服务 账户服务
| | |
⬇️ ⬇️ ⬇️
订单DB 库存DB 账户DB
| | |
|__________ MQ _____________|
(Kafka/RocketMQ)
数据库设计
订单服务
-- 订单表
CREATE TABLE `t_order` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`order_no` VARCHAR(64) UNIQUE COMMENT '订单号',
`user_id` BIGINT COMMENT '用户ID',
`product_id` BIGINT COMMENT '商品ID',
`quantity` INT COMMENT '数量',
`amount` DECIMAL(10,2) COMMENT '金额',
`status` VARCHAR(20) COMMENT '状态:PENDING/SUCCESS/FAILED/CANCELLED',
`create_time` DATETIME,
`update_time` DATETIME,
INDEX idx_user_id (`user_id`),
INDEX idx_order_no (`order_no`)
) ENGINE=InnoDB COMMENT='订单表';
-- 本地消息表
CREATE TABLE `t_local_message` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`message_id` VARCHAR(64) UNIQUE COMMENT '消息ID',
`topic` VARCHAR(100) COMMENT '主题',
`content` TEXT COMMENT '消息内容',
`status` VARCHAR(20) COMMENT '状态:PENDING/SENT/FAILED',
`retry_count` INT DEFAULT 0 COMMENT '重试次数',
`max_retry` INT DEFAULT 5 COMMENT '最大重试次数',
`create_time` DATETIME,
`update_time` DATETIME,
INDEX idx_status (`status`)
) ENGINE=InnoDB COMMENT='本地消息表';
库存服务
-- 库存表
CREATE TABLE `t_inventory` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`product_id` BIGINT UNIQUE COMMENT '商品ID',
`quantity` INT COMMENT '库存数量',
`version` INT COMMENT '版本号(乐观锁)',
`create_time` DATETIME,
`update_time` DATETIME,
INDEX idx_product_id (`product_id`)
) ENGINE=InnoDB COMMENT='库存表';
-- 库存操作日志表(防止重复扣减)
CREATE TABLE `t_inventory_log` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`order_no` VARCHAR(64) UNIQUE COMMENT '订单号',
`product_id` BIGINT COMMENT '商品ID',
`quantity` INT COMMENT '扣减数量',
`type` VARCHAR(20) COMMENT '类型:REDUCE/RESTORE',
`create_time` DATETIME,
INDEX idx_order_no (`order_no`)
) ENGINE=InnoDB COMMENT='库存操作日志';
账户服务
-- 账户表
CREATE TABLE `t_account` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`user_id` BIGINT UNIQUE COMMENT '用户ID',
`balance` DECIMAL(10,2) COMMENT '余额',
`version` INT COMMENT '版本号(乐观锁)',
`create_time` DATETIME,
`update_time` DATETIME,
INDEX idx_user_id (`user_id`)
) ENGINE=InnoDB COMMENT='账户表';
-- 账户操作日志表(防止重复扣款)
CREATE TABLE `t_account_log` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`order_no` VARCHAR(64) UNIQUE COMMENT '订单号',
`user_id` BIGINT COMMENT '用户ID',
`amount` DECIMAL(10,2) COMMENT '金额',
`type` VARCHAR(20) COMMENT '类型:DEDUCT/REFUND',
`create_time` DATETIME,
INDEX idx_order_no (`order_no`)
) ENGINE=InnoDB COMMENT='账户操作日志';
完整代码实现
1. 订单服务(核心)
@Service
@Slf4j
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private LocalMessageMapper messageMapper;
/**
* 创建订单(Saga的第一步)
*/
@Transactional(rollbackFor = Exception.class)
public String createOrder(CreateOrderRequest request) {
// 1. 生成订单号
String orderNo = generateOrderNo();
// 2. 创建订单
Order order = new Order();
order.setOrderNo(orderNo);
order.setUserId(request.getUserId());
order.setProductId(request.getProductId());
order.setQuantity(request.getQuantity());
order.setAmount(request.getAmount());
order.setStatus("PENDING");
orderMapper.insert(order);
// 3. 在同一事务中保存本地消息(关键!)
LocalMessage message = new LocalMessage();
message.setMessageId(UUID.randomUUID().toString());
message.setTopic("order-created");
// 消息内容
OrderCreatedEvent event = new OrderCreatedEvent();
event.setOrderNo(orderNo);
event.setUserId(request.getUserId());
event.setProductId(request.getProductId());
event.setQuantity(request.getQuantity());
event.setAmount(request.getAmount());
message.setContent(JSON.toJSONString(event));
message.setStatus("PENDING");
message.setRetryCount(0);
message.setMaxRetry(5);
messageMapper.insert(message);
log.info("订单创建成功,orderNo={}", orderNo);
return orderNo;
}
/**
* 监听库存扣减失败事件,进行补偿
*/
@KafkaListener(topics = "inventory-reduce-failed")
public void onInventoryReduceFailed(String message) {
InventoryReduceFailedEvent event = JSON.parseObject(message, InventoryReduceFailedEvent.class);
log.warn("库存扣减失败,开始补偿订单,orderNo={}", event.getOrderNo());
// 更新订单状态为FAILED
Order order = orderMapper.selectByOrderNo(event.getOrderNo());
if (order != null) {
order.setStatus("FAILED");
orderMapper.updateById(order);
log.info("订单补偿完成,orderNo={}", event.getOrderNo());
}
}
/**
* 监听支付失败事件,进行补偿
*/
@KafkaListener(topics = "payment-failed")
public void onPaymentFailed(String message) {
PaymentFailedEvent event = JSON.parseObject(message, PaymentFailedEvent.class);
log.warn("支付失败,开始补偿订单,orderNo={}", event.getOrderNo());
// 更新订单状态为FAILED
Order order = orderMapper.selectByOrderNo(event.getOrderNo());
if (order != null) {
order.setStatus("FAILED");
orderMapper.updateById(order);
log.info("订单补偿完成,orderNo={}", event.getOrderNo());
}
}
/**
* 监听支付成功事件
*/
@KafkaListener(topics = "payment-completed")
public void onPaymentCompleted(String message) {
PaymentCompletedEvent event = JSON.parseObject(message, PaymentCompletedEvent.class);
log.info("支付成功,更新订单状态,orderNo={}", event.getOrderNo());
// 更新订单状态为SUCCESS
Order order = orderMapper.selectByOrderNo(event.getOrderNo());
if (order != null) {
order.setStatus("SUCCESS");
orderMapper.updateById(order);
log.info("订单完成,orderNo={}", event.getOrderNo());
}
}
private String generateOrderNo() {
// 生成订单号:时间戳 + 随机数
return "ORD" + System.currentTimeMillis() + RandomUtils.nextInt(1000, 9999);
}
}
2. 消息发送定时任务
@Component
@Slf4j
public class MessageSendScheduler {
@Autowired
private LocalMessageMapper messageMapper;
@Autowired
private KafkaTemplate<String, String> kafkaTemplate;
/**
* 定时扫描并发送消息
*/
@Scheduled(fixedDelay = 5000) // 每5秒执行一次
public void scanAndSendMessage() {
// 查询待发送的消息
List<LocalMessage> messages = messageMapper.selectPendingMessages();
if (messages.isEmpty()) {
return;
}
log.info("开始发送消息,数量={}", messages.size());
for (LocalMessage message : messages) {
try {
// 发送消息到Kafka
kafkaTemplate.send(message.getTopic(), message.getContent()).get();
// 标记为已发送
message.setStatus("SENT");
messageMapper.updateById(message);
log.info("消息发送成功,messageId={}, topic={}",
message.getMessageId(), message.getTopic());
} catch (Exception e) {
log.error("消息发送失败,messageId={}", message.getMessageId(), e);
// 增加重试次数
message.setRetryCount(message.getRetryCount() + 1);
// 超过最大重试次数,标记为失败
if (message.getRetryCount() >= message.getMaxRetry()) {
message.setStatus("FAILED");
log.error("消息发送失败,已达最大重试次数,messageId={}", message.getMessageId());
// TODO: 发送告警,人工介入
}
messageMapper.updateById(message);
}
}
}
}
3. 库存服务
@Service
@Slf4j
public class InventoryService {
@Autowired
private InventoryMapper inventoryMapper;
@Autowired
private InventoryLogMapper inventoryLogMapper;
@Autowired
private KafkaTemplate<String, String> kafkaTemplate;
/**
* 监听订单创建事件,扣减库存
*/
@KafkaListener(topics = "order-created")
@Transactional(rollbackFor = Exception.class)
public void onOrderCreated(String message) {
OrderCreatedEvent event = JSON.parseObject(message, OrderCreatedEvent.class);
log.info("收到订单创建事件,开始扣减库存,orderNo={}", event.getOrderNo());
try {
// 幂等性检查:判断是否已经处理过
InventoryLog existLog = inventoryLogMapper.selectByOrderNo(event.getOrderNo());
if (existLog != null) {
log.warn("订单已处理过,跳过,orderNo={}", event.getOrderNo());
return;
}
// 扣减库存(乐观锁)
Inventory inventory = inventoryMapper.selectByProductId(event.getProductId());
if (inventory == null) {
throw new BusinessException("商品不存在");
}
if (inventory.getQuantity() < event.getQuantity()) {
throw new BusinessException("库存不足");
}
int rows = inventoryMapper.reduceInventory(
event.getProductId(),
event.getQuantity(),
inventory.getVersion()
);
if (rows == 0) {
// 乐观锁失败,重试
throw new BusinessException("库存扣减失败,请重试");
}
// 记录日志
InventoryLog log = new InventoryLog();
log.setOrderNo(event.getOrderNo());
log.setProductId(event.getProductId());
log.setQuantity(event.getQuantity());
log.setType("REDUCE");
inventoryLogMapper.insert(log);
// 发送库存扣减成功事件
InventoryReducedEvent reducedEvent = new InventoryReducedEvent();
reducedEvent.setOrderNo(event.getOrderNo());
reducedEvent.setUserId(event.getUserId());
reducedEvent.setAmount(event.getAmount());
kafkaTemplate.send("inventory-reduced", JSON.toJSONString(reducedEvent));
this.log.info("库存扣减成功,orderNo={}, productId={}, quantity={}",
event.getOrderNo(), event.getProductId(), event.getQuantity());
} catch (Exception e) {
log.error("库存扣减失败,orderNo={}", event.getOrderNo(), e);
// 发送库存扣减失败事件
InventoryReduceFailedEvent failedEvent = new InventoryReduceFailedEvent();
failedEvent.setOrderNo(event.getOrderNo());
failedEvent.setReason(e.getMessage());
kafkaTemplate.send("inventory-reduce-failed", JSON.toJSONString(failedEvent));
}
}
/**
* 监听支付失败事件,恢复库存
*/
@KafkaListener(topics = "payment-failed")
@Transactional(rollbackFor = Exception.class)
public void onPaymentFailed(String message) {
PaymentFailedEvent event = JSON.parseObject(message, PaymentFailedEvent.class);
log.warn("支付失败,开始恢复库存,orderNo={}", event.getOrderNo());
try {
// 查询原始扣减记录
InventoryLog reduceLog = inventoryLogMapper.selectByOrderNoAndType(
event.getOrderNo(), "REDUCE");
if (reduceLog == null) {
log.warn("未找到扣减记录,orderNo={}", event.getOrderNo());
return;
}
// 幂等性检查:判断是否已经恢复过
InventoryLog restoreLog = inventoryLogMapper.selectByOrderNoAndType(
event.getOrderNo(), "RESTORE");
if (restoreLog != null) {
log.warn("库存已恢复过,跳过,orderNo={}", event.getOrderNo());
return;
}
// 恢复库存
inventoryMapper.addInventory(
reduceLog.getProductId(),
reduceLog.getQuantity()
);
// 记录恢复日志
InventoryLog log = new InventoryLog();
log.setOrderNo(event.getOrderNo());
log.setProductId(reduceLog.getProductId());
log.setQuantity(reduceLog.getQuantity());
log.setType("RESTORE");
inventoryLogMapper.insert(log);
this.log.info("库存恢复成功,orderNo={}, productId={}, quantity={}",
event.getOrderNo(), reduceLog.getProductId(), reduceLog.getQuantity());
} catch (Exception e) {
log.error("库存恢复失败,orderNo={}", event.getOrderNo(), e);
}
}
}
4. 账户服务
@Service
@Slf4j
public class AccountService {
@Autowired
private AccountMapper accountMapper;
@Autowired
private AccountLogMapper accountLogMapper;
@Autowired
private KafkaTemplate<String, String> kafkaTemplate;
/**
* 监听库存扣减成功事件,进行扣款
*/
@KafkaListener(topics = "inventory-reduced")
@Transactional(rollbackFor = Exception.class)
public void onInventoryReduced(String message) {
InventoryReducedEvent event = JSON.parseObject(message, InventoryReducedEvent.class);
log.info("收到库存扣减成功事件,开始扣款,orderNo={}", event.getOrderNo());
try {
// 幂等性检查
AccountLog existLog = accountLogMapper.selectByOrderNo(event.getOrderNo());
if (existLog != null) {
log.warn("订单已处理过,跳过,orderNo={}", event.getOrderNo());
return;
}
// 扣款(乐观锁)
Account account = accountMapper.selectByUserId(event.getUserId());
if (account == null) {
throw new BusinessException("账户不存在");
}
if (account.getBalance().compareTo(event.getAmount()) < 0) {
throw new BusinessException("余额不足");
}
int rows = accountMapper.deductBalance(
event.getUserId(),
event.getAmount(),
account.getVersion()
);
if (rows == 0) {
throw new BusinessException("扣款失败,请重试");
}
// 记录日志
AccountLog log = new AccountLog();
log.setOrderNo(event.getOrderNo());
log.setUserId(event.getUserId());
log.setAmount(event.getAmount());
log.setType("DEDUCT");
accountLogMapper.insert(log);
// 发送支付成功事件
PaymentCompletedEvent completedEvent = new PaymentCompletedEvent();
completedEvent.setOrderNo(event.getOrderNo());
kafkaTemplate.send("payment-completed", JSON.toJSONString(completedEvent));
this.log.info("扣款成功,orderNo={}, userId={}, amount={}",
event.getOrderNo(), event.getUserId(), event.getAmount());
} catch (Exception e) {
log.error("扣款失败,orderNo={}", event.getOrderNo(), e);
// 发送支付失败事件
PaymentFailedEvent failedEvent = new PaymentFailedEvent();
failedEvent.setOrderNo(event.getOrderNo());
failedEvent.setReason(e.getMessage());
kafkaTemplate.send("payment-failed", JSON.toJSONString(failedEvent));
}
}
}
时序图
用户 订单服务 MQ 库存服务 账户服务
| | | | |
|--下单---->| | | |
| | | | |
| |--保存订单+消息 | |
| |(同一事务) | | |
| | | | |
|<--成功----| | | |
| | | | |
| 定时任务 | | |
| |--发送消息->| | |
| | | | |
| | |--订单创建事件--> |
| | | | |
| | | 扣减库存 |
| | | | |
| | |<-库存扣减成功事件-- |
| | | | |
| | |--库存扣减成功事件----->|
| | | | 扣款
| | | | |
| | |<------支付成功事件-----|
| | | | |
| |<-支付成功事件 | |
| | | | |
| 更新订单状态 | | |
| | | | |
🚨 避坑指南:我踩过的那些坑 {#避坑指南}
坑1:忘记幂等性处理 😭
场景:消息可能会重复消费,导致重复扣库存、重复扣款!
解决方案:
// 方案1:唯一键约束
CREATE UNIQUE INDEX uk_order_no ON t_inventory_log(order_no);
// 方案2:先查询再插入
InventoryLog existLog = inventoryLogMapper.selectByOrderNo(orderNo);
if (existLog != null) {
log.warn("Already processed, skip");
return;
}
坑2:忘记处理消息发送失败 😱
场景:订单保存成功了,但消息没发出去,后续流程卡住了!
解决方案:本地消息表 + 定时任务扫描重试
坑3:补偿操作没做好 🤦
场景:支付失败了,但库存没恢复!
解决方案:
// 每个正向操作都要有对应的补偿操作
正向:reduceInventory()
补偿:restoreInventory()
正向:deductBalance()
补偿:refundBalance()
坑4:死锁问题 💀
场景:高并发下,乐观锁失败率太高,导致大量重试!
解决方案:
// 方案1:限流
@RateLimiter(value = 100, timeout = 1000)
// 方案2:分段锁(把库存分成多份)
// 原来:商品A总库存100
// 现在:商品A库存分成10份,每份10个
坑5:消息堆积 📮
场景:下游服务挂了,消息堆积几百万条!
解决方案:
// 1. 设置消息过期时间
message.setExpireTime(30 * 60 * 1000); // 30分钟
// 2. 死信队列
@KafkaListener(topics = "order-created-dlq")
public void handleDeadLetter(String message) {
// 人工处理
}
// 3. 限流
@RateLimiter
坑6:分布式锁的滥用 🔒
错误示例:
// 千万别这样!
String lockKey = "inventory:" + productId;
if (redisLock.tryLock(lockKey)) {
try {
reduceInventory();
} finally {
redisLock.unlock(lockKey);
}
}
问题:
- 锁的粒度太粗,性能差
- 锁超时了咋办?
- 锁被别人释放了咋办?
正确做法:用乐观锁(version字段)
UPDATE t_inventory
SET quantity = quantity - #{quantity},
version = version + 1
WHERE product_id = #{productId}
AND version = #{version}
AND quantity >= #{quantity}
🚀 性能优化:从龟速到飞起 {#性能优化}
优化1:异步化 ⚡
Before:
// 同步调用,慢死了
createOrder();
reduceInventory(); // 等待
deductBalance(); // 等待
After:
// 异步调用,飞快
createOrder();
发送消息("order-created"); // 不等待,直接返回
效果:RT从3000ms降到100ms!
优化2:批量处理 📦
Before:
for (Message msg : messages) {
kafkaTemplate.send(topic, msg); // 一条一条发
}
After:
List<Message> batch = new ArrayList<>();
for (Message msg : messages) {
batch.add(msg);
if (batch.size() >= 100) {
kafkaTemplate.send(topic, batch); // 批量发送
batch.clear();
}
}
效果:TPS从1000提升到10000!
优化3:缓存预热 🔥
// 把热点商品的库存放到Redis
String cacheKey = "inventory:" + productId;
Integer stock = redisTemplate.get(cacheKey);
if (stock == null) {
// 缓存未命中,查数据库
stock = inventoryMapper.selectByProductId(productId).getQuantity();
redisTemplate.set(cacheKey, stock, 60, TimeUnit.SECONDS);
}
效果:数据库压力降低90%!
优化4:消息压缩 📉
// 消息太大,网络传输慢
String message = JSON.toJSONString(event);
// 压缩
byte[] compressed = gzip(message.getBytes());
// 发送压缩后的消息
kafkaTemplate.send(topic, compressed);
效果:网络带宽占用降低70%!
优化5:数据库优化 💾
-- 添加索引
CREATE INDEX idx_status ON t_local_message(status);
CREATE INDEX idx_order_no ON t_order(order_no);
CREATE INDEX idx_product_id ON t_inventory(product_id);
-- 分区表(按时间分区)
CREATE TABLE t_order (
...
) PARTITION BY RANGE (YEAR(create_time)) (
PARTITION p2023 VALUES LESS THAN (2024),
PARTITION p2024 VALUES LESS THAN (2025),
PARTITION p2025 VALUES LESS THAN (2026)
);
-- 定期清理历史数据
DELETE FROM t_local_message
WHERE status = 'SENT'
AND create_time < DATE_SUB(NOW(), INTERVAL 7 DAY);
🎓 企业级框架推荐
Seata - 阿里出品 🌟
官网:seata.io/
支持的模式:
- AT模式(自动)
- TCC模式(手动)
- Saga模式(长事务)
- XA模式(强一致)
快速开始:
<!-- 引入依赖 -->
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.7.0</version>
</dependency>
// 只需加一个注解!
@GlobalTransactional
public void placeOrder() {
orderService.createOrder();
inventoryService.reduceInventory();
accountService.deductBalance();
}
优点:
- ✅ 阿里生产环境验证
- ✅ 支持多种模式
- ✅ 对业务侵入小
缺点:
- ❌ 需要部署TC(事务协调器)
- ❌ 学习成本较高
Apache ServiceComb Pack
特点:
- 支持Saga模式
- Apache顶级项目
- 文档完善
自研方案
如果你的业务比较特殊,也可以自研!
核心组件:
- 本地消息表
- 定时任务
- 消息队列
- 补偿逻辑
🎯 选型决策树
开始
|
⬇️
是否需要强一致性?
| \
是 否
| \
⬇️ ⬇️
TCC 是否能接受最终一致?
| | \
| 是 否
| | \
| ⬇️ ⬇️
| Saga 重新评估需求
| +本地消息表
| |
| ⬇️
| 是否高并发?
| | \
| 是 否
| | \
| ⬇️ ⬇️
| + Redis 基础方案
| + 分段锁
| |
└-------┴--------⬇️
生产可用!
📚 总结
核心要点
-
理论基础
- CAP定理:不可能三角
- BASE理论:最终一致性
- ACID vs BASE
-
方案选择
- 强一致性 → TCC
- 弱一致性 → Saga
- 超高并发 → 异步化
-
关键技术
- 本地消息表
- 幂等性处理
- 补偿机制
- 乐观锁
-
避坑指南
- 幂等性!幂等性!幂等性!
- 补偿操作要完善
- 消息堆积要处理
- 性能优化要到位
最佳实践
/**
* 分布式事务最佳实践清单
*/
✅ 1. 每个操作都要有幂等性
✅ 2. 每个正向操作都要有补偿操作
✅ 3. 消息发送要可靠(本地消息表)
✅ 4. 要有重试机制(指数退避)
✅ 5. 要有超时控制
✅ 6. 要有监控告警
✅ 7. 要有降级方案
✅ 8. 要有人工介入机制
✅ 9. 数据要有日志记录
✅ 10. 要有压测验证
🎤 老司机的肺腑之言
作为一个被分布式事务折磨过无数次的10年老Java程序员,我想说:
-
不要过度设计
能用单体就用单体,别一上来就微服务! -
选择合适的方案
没有银弹,只有最合适的! -
一定要做好测试
正常流程测、异常流程测、并发测、压力测! -
监控告警很重要
出问题能第一时间发现,别等用户投诉! -
文档要写好
半年后你自己都看不懂,别说新人了! -
持续优化
上线不是终点,是起点!
🔗 参考资料
- 《分布式系统原理与范型》- Andrew S. Tanenbaum
- 《数据密集型应用系统设计》- Martin Kleppmann
- Seata官方文档:seata.io/
- 《微服务设计》- Sam Newman
- 阿里技术公众号系列文章
📮 最后
如果这篇文档对你有帮助,请点个赞👍!
如果你有问题,欢迎评论区交流!
如果你也踩过类似的坑,欢迎分享你的经验!
记住:分布式事务不可怕,可怕的是不了解它就乱用!😄
后记:写这篇文档的时候,我又想起了那个被分布式事务支配的恐惧...
不过还好,现在的我已经是个老司机了!💪
希望这篇文档能帮你少走弯路,少踩坑!Keep Coding, Keep Learning! 🚀
—— 一个秃了但变强了的Java程序员
全文完 ✨