摘要:在微服务架构下,如何保证跨服务的数据一致性?是选 Seata 还是 RocketMQ?TCC 模式真的那么难写吗?本文带你从零理解分布式事务核心原理,并手把手在 Spring Boot 中落地三种主流解决方案。
🧐 一、为什么我们需要分布式事务?
先来看一个经典的 "下单扣库存" 场景:
- 订单服务:创建订单
- 库存服务:扣减库存
- 账户服务:扣减余额
在单体应用中,一个 @Transactional 注解就能保证 ACID,要么全成功,要么全回滚。
但在微服务架构下,三个服务连接着三个独立的数据库。当订单创建成功,但库存扣减失败时,就会出现 "订单已生成但库存未减少" 的严重数据不一致问题。
分布式事务,就是为了解决这种跨多个独立数据节点(数据库、消息队列、缓存等)的事务一致性问题而生的技术方案。
📚 二、理论基础:从 ACID 到 CAP 与 BASE
在深入代码之前,我们需要明确两个核心概念:
2.1 本地事务的 ACID
- Atomicity:原子性
- Consistency:一致性
- Isolation:隔离性
- Durability:持久性
2.2 分布式环境下的权衡
分布式系统无法同时满足 CAP 定理中的三点,由于分区容错性(P)是必然属性,我们通常只能在一致性(C)和可用性(A)之间做权衡。
这催生了 BASE 理论:
- Basically Available(基本可用)
- Soft state(软状态)
- Eventually consistent(最终一致性)
核心结论 :大多数分布式事务方案不再追求强一致性(实时一致),而是转向最终一致性。
⚖️ 三、主流方案大比拼:谁是你的菜?
目前国内企业应用最广的是 Seata AT模式 和 RocketMQ事务消息。以下是详细对比:
| 方案 | 原理 | 一致性 | 性能 | 复杂度 | 适用场景 |
|---|---|---|---|---|---|
| XA / 2PC | 两阶段提交 | 强一致 | 低 | 中 | 银行、金融等对一致性要求极高的场景 |
| TCC | Try-Confirm-Cancel | 强一致 | 中 | 高 | 核心交易、高并发场景 |
| 可靠消息 | 本地事务+MQ | 最终一致 | 高 | 中 | 异步解耦、对实时性要求不高的场景 |
| Saga | 长事务+补偿 | 最终一致 | 高 | 高 | 长流程、跨多服务的事务 |
| Seata AT | 自动补偿(数据快照) | 强一致 | 中 | 低 | 希望无侵入改造的现有项目 |
💻 四、Spring Boot/Cloud 实战:三种方案代码实现
技术栈:Spring Boot 2.7.x + Spring Cloud Alibaba 2021.x + Seata 1.6.1 + RocketMQ 4.9.x + Nacos
4.1 方案一:Seata AT模式(无侵入首选)
Seata AT模式通过代理数据源,自动生成回滚SQL,业务代码几乎零改造。
步骤 1:引入依赖
xml
<!-- seata spring cloud starter -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
步骤 2:配置 application.yml
yaml
seata:
enabled: true
application-id: ${spring.application.name}
tx-service-group: my_test_tx_group # 事务组名称,需与Seata Server一致
registry:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
步骤 3:全局发起方添加 @GlobalTransactional
这是最关键的一步,只需在入口方法添加注解:
java
@Service
@Slf4j
public class OrderService {
@GlobalTransactional(name = "create-order", rollbackFor = Exception.class)
public void createOrder(OrderDTO orderDTO) {
// 1. 本地创建订单
orderMapper.insert(order);
// 2. 远程调用库存服务(Feign)
inventoryFeign.deduct(orderDTO.getProductId(), orderDTO.getQuantity());
// 3. 远程调用账户服务(Feign)
accountFeign.debit(orderDTO.getUserId(), orderDTO.getAmount());
// 4. 更新订单状态
orderMapper.updateById(order);
}
}
效果:当任何一步异常时,Seata 会自动利用 undo_log 表进行全局回滚。
4.2 方案二:RocketMQ事务消息(高吞吐、最终一致)
适用于对实时一致性要求不高,但要求高吞吐和最终一致的场景(如电商下单)。
步骤 1:发送事务消息(半消息)
java
@Service
public class OrderService {
@Autowired
private RocketMQTemplate rocketMQTemplate;
public void createOrder(OrderDTO orderDTO) {
// 发送半消息,此时消费者不可见
TransactionSendResult sendResult = rocketMQTemplate.sendMessageInTransaction(
"order-topic",
MessageBuilder.withPayload(orderDTO).build(),
orderDTO
);
}
}
步骤 2:实现本地事务监听器
java
@Component
@RocketMQTransactionListener(txProducerGroup = "order-producer-group")
public class OrderTransactionListener implements RocketMQLocalTransactionListener {
// 执行本地事务
@Override
public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
try {
// 1. 执行本地数据库操作:创建订单
orderMapper.insert(order);
// 2. 成功则提交消息,消费者可见
return RocketMQLocalTransactionState.COMMIT;
} catch (Exception e) {
// 3. 失败则回滚消息
return RocketMQLocalTransactionState.ROLLBACK;
}
}
// 事务状态回查(防止网络抖动导致状态未知)
@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
// 根据业务ID查询订单是否存在
return orderMapper.exists(...) ? RocketMQLocalTransactionState.COMMIT : RocketMQLocalTransactionState.ROLLBACK;
}
}
⚠️ 注意 :消费方(库存/账户服务)必须做幂等设计,因为MQ可能会重发消息。
4.3 方案三:TCC模式(高性能、手动补偿)
TCC 适用于高一致性要求且需要精准控制资源的场景,但开发量较大。
步骤 1:定义 TCC 接口
java
@LocalTCC
public interface AccountTccAction {
// Try阶段:资源检查与冻结
@TwoPhaseBusinessAction(name = "debit", commitMethod = "commit", rollbackMethod = "rollback")
boolean debit(BusinessActionContext context, @BusinessActionContextParameter(paramName = "userId") Long userId, @BusinessActionContextParameter(paramName = "amount") BigDecimal amount);
// Confirm阶段:确认执行(扣减冻结金额)
boolean commit(BusinessActionContext context);
// Cancel阶段:取消执行(解冻金额)
boolean rollback(BusinessActionContext context);
}
步骤 2:实现逻辑
- Try:冻结资金(如:余额100 -> 可用80,冻结20)。
- Confirm:扣除冻结资金(如:冻结20 -> 0)。
- Cancel:解冻资金(如:冻结20 -> 0,可用80 -> 100)。
难点 :TCC 需要处理幂等 、空回滚 (Try没执行直接Cancel)和悬挂问题,建议直接使用 Seata 框架提供的支持。
💡 五、方案选型建议
| 场景描述 | 推荐方案 | 理由 |
|---|---|---|
| 金融支付、核心交易 | Seata AT / XA | 要求强一致性,且并发量通常可控 |
| 高并发、短事务 | RocketMQ事务消息 | 允许短暂不一致,追求高吞吐和削峰填谷 |
| 长业务流程 | Saga + 状态机 | 涉及多服务、多阶段,可能需要人工介入补偿 |
| 旧系统改造 | Seata AT | 只需代理数据源,代码侵入性最小 |
| 秒杀场景 | 本地事务 + 异步MQ | 对一致性容忍度高,优先保证系统可用性 |
🛡️ 六、总结与避坑指南
分布式事务不是银弹,在落地时请务必注意以下几点:
- 幂等性是基石:无论用哪种方案(尤其是MQ和TCC),消费端和服务提供方都必须实现幂等,防止重复扣款或扣库存。
- 不要重复造轮子:尽量选择 Seata、RocketMQ 等成熟框架,不要手写底层逻辑。
- 超时与重试:必须配置合理的超时时间和退避策略,无限制重试可能引发雪崩。
- 监控与告警:最终一致性意味着可能存在中间状态,需要定时扫描"悬挂事务",并配置告警以便人工介入。
分布式事务的尽头往往不是技术,而是业务上的合理折衷。 希望本文能帮助你在项目中做出最合适的选择!
📢 关注 《卷毛的技术笔记》 ,专注后端硬核技术分享,拒绝套路,只聊落地的技术。
如果觉得文章对你有帮助,欢迎点赞、收藏、关注!