第 227 章:分布式事务
学习目标
- 理解 CAP / BASE 理论
- 掌握 2PC / TCC / Saga 模式
- 学会 Seata 实战
- 选择合适的事务方案
一、为什么需要分布式事务
本地 @Transactional 只在单服务内有效,跨服务必须用分布式事务。
二、CAP 理论
| 维度 | 含义 |
|---|---|
| Consistency | 所有节点同时看到相同数据 |
| Availability | 每个请求都能得到响应 |
| Partition tolerance | 网络分区时仍能工作 |
定理:三者不可兼得。
| 组合 | 选择 |
|---|---|
| CP | ZooKeeper、Etcd(放弃 A) |
| AP | Eureka、Cassandra(放弃 C) |
| CA | 单机数据库(放弃 P) |
分布式系统P 必选,只能在 C 和 A 之间权衡。
三、BASE 理论
CAP 的"实用妥协"思想:
| 原则 | 含义 |
|---|---|
| BA(Basically Available) | 基本可用,允许降级 |
| S(Soft State) | 软状态,允许中间态不一致 |
| E(Eventually Consistent) | 最终一致 |
主流分布式系统都用 BASE,而不是 ACID。
四、事务模式
4.1 2PC(两阶段提交)
强一致协议,分两阶段:
阶段:
- Prepare:协调器问所有参与者"能不能提交?"
- Commit:所有参与者都 OK,协调器让大家提交
问题:
- 同步阻塞
- 协调器单点故障
- 数据不一致(部分 commit)
4.2 3PC(改进版 2PC)
加CanCommit 阶段,降低阻塞,但仍非完美。
4.3 TCC(补偿事务)
特点:
- 每个业务有 Try / Confirm / Cancel 三个操作
- 业务侵入性强
- 性能较好
4.4 Saga(长事务拆分)
最终一致,拆为多个本地事务 + 补偿。
特点:
- 每个子事务有对应补偿
- 适合业务流程长的场景
- 业务改造成本低
4.5 本地消息表
特点:
- 业务无侵入
- 适合最终一致性场景
- 用本地表保证业务与消息一致
4.6 模式对比
| 模式 | 一致性 | 性能 | 侵入性 | 适用 |
|---|---|---|---|---|
| 2PC | 强 | 低 | 低 | 单库 / XA |
| TCC | 强 | 中 | 高 | 金融 |
| Saga | 最终 | 高 | 中 | 订单 |
| 本地消息表 | 最终 | 高 | 低 | 一般业务 |
| MQ 事务消息 | 最终 | 高 | 低 | 通用 |
| Seata AT | 强 | 中 | 低 | 通用首选 |
五、Seata 实战
Seata 是阿里开源的分布式事务解决方案,支持 AT、TCC、Saga、XA 四种模式。
5.1 核心角色
5.2 部署 Seata Server
yaml
# docker-compose.yml
services:
seata-server:
image: seataio/seata-server:1.6.1
ports: ["8091:8091"]
environment:
- SEATA_PORT=8091
- STORE_MODE=db
- DB_URL=jdbc:mysql://mysql:3306/seata
- DB_USER=seata
- DB_PASSWORD=seata
depends_on:
- mysql注册到 Nacos:
properties
# registry.conf
registry {
type = "nacos"
nacos {
serverAddr = "127.0.0.1:8848"
namespace = "public"
}
}
config {
type = "nacos"
nacos {
serverAddr = "127.0.0.1:8848"
group = "SEATA_GROUP"
}
}5.3 AT 模式(自动)
两阶段 + 补偿,Seata 自动接管本地事务。
客户端依赖
xml
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>配置
yaml
spring:
cloud:
alibaba:
seata:
tx-service-group: my_test_tx_group
seata:
registry:
type: nacos
config:
type: nacos
service:
vgroup-mapping:
my_test_tx_group: default全局事务入口
java
@Service
public class OrderService {
@Autowired
private OrderRepo orderRepo;
@Autowired
private InventoryClient inventoryClient;
@Autowired
private PaymentClient paymentClient;
// 全局事务
@GlobalTransactional(name = "createOrder", rollbackFor = Exception.class)
public void createOrder(OrderRequest req) {
// 1. 本地
Order order = new Order(req);
orderRepo.save(order);
// 2. 远程(库存服务)
inventoryClient.deduct(req.getSkuId(), req.getQuantity());
// 3. 远程(支付服务)
paymentClient.pay(req.getUserId(), req.getAmount());
}
}原理
每个 RM 在本地提交时记录前后镜像(快照 + undo_log),TC 根据情况决定 commit 或 rollback。
数据源代理
AT 模式需要 Seata 接管数据源:
java
@Configuration
public class DataSourceConfig {
@Bean
@Primary
public DataSourceProxy dataSourceProxy(DataSource druidDataSource) {
return new DataSourceProxy(druidDataSource);
}
}5.4 TCC 模式(手工)
适用于性能高或自动接管困难的场景。
接口定义:
java
public interface InventoryTccService {
@TwoPhaseBusinessAction(
name = "deductInventory",
commitMethod = "commit",
rollbackMethod = "rollback"
)
boolean tryDeduct(
@BusinessActionContextParameter("skuId") String skuId,
@BusinessActionContextParameter("qty") int qty
);
boolean commit(BusinessActionContext ctx);
boolean rollback(BusinessActionContext ctx);
}实现:
java
@Service
@Slf4j
public class InventoryTccServiceImpl implements InventoryTccService {
@Autowired
private InventoryMapper inventoryMapper;
@Autowired
private FrozenMapper frozenMapper;
@Override
public boolean tryDeduct(String skuId, int qty) {
// 1. 检查库存
if (inventoryMapper.getStock(skuId) < qty) {
return false;
}
// 2. 冻结
frozenMapper.freeze(skuId, qty);
return true;
}
@Override
public boolean commit(BusinessActionContext ctx) {
// 真正扣减
String skuId = (String) ctx.getActionContext("skuId");
int qty = (int) ctx.getActionContext("qty");
inventoryMapper.deduct(skuId, qty);
frozenMapper.unfreeze(skuId, qty);
return true;
}
@Override
public boolean rollback(BusinessActionContext ctx) {
String skuId = (String) ctx.getActionContext("skuId");
int qty = (int) ctx.getActionContext("qty");
// 解冻
frozenMapper.unfreeze(skuId, qty);
return true;
}
}5.5 Saga 模式
通过状态机编排:
json
{
"name": "createOrder",
"compensateCancel": true,
"steps": [
{
"name": "createOrder",
"compensate": "cancelOrder"
},
{
"name": "deductInventory",
"compensate": "compensateInventory"
},
{
"name": "pay",
"compensate": "refundPay"
}
]
}六、MQ 事务消息
RocketMQ 5.x 支持事务消息。
java
// 发送事务消息
TransactionSendResult result = rocketMQTemplate.sendMessageInTransaction(
"order-events",
MessageBuilder.withPayload(new OrderCreatedEvent(...)).build(),
new TransactionSendCallback() {
@Override
public LocalTransactionState executeLocalTransactionBranch(Message msg, Object arg) {
try {
orderRepo.save(order);
return LocalTransactionState.COMMIT_MESSAGE;
} catch (Exception e) {
return LocalTransactionState.ROLLBACK_MESSAGE;
}
}
}
);七、最佳实践
7.1 选择决策
| 业务 | 推荐方案 |
|---|---|
| 支付 | TCC |
| 订单 | Seata AT / Saga |
| 库存 | TCC / Saga |
| 退款 | MQ 事务消息 |
| 通知 | 本地消息表 |
7.2 何时避免分布式事务
- 能拆分为独立流程的不需要事务
- 用 最终一致 + 补偿 替代强一致
- 用幂等 + 重试代替回滚
- CQRS:读写分离,避开分布式事务
7.3 幂等保证
无论事务如何,业务操作必须幂等。
java
@Idempotent(key = "#req.orderId", type = IdempotentTypeEnum.SPEL)
public Order createOrder(OrderRequest req) {
// 即使重试,只能创建一次订单
if (orderRepo.exists(req.getOrderId())) {
return orderRepo.findById(req.getOrderId());
}
return orderRepo.save(new Order(req));
}7.4 异常场景
| 场景 | 处理 |
|---|---|
| Seata Server 挂 | TC 持久化,重启回放 |
| 业务悬挂(try 后没收到 commit) | 定时补偿(扫描冻结记录) |
| 资源一直加锁 | 加 timeout |
| undo_log 太大 | 定期清理已成功事务 |
八、本章小结
| 模式 | 一致性 | 性能 | 适用 |
|---|---|---|---|
| 2PC | 强 | 低 | 单 DB |
| TCC | 强 | 高 | 金融 |
| Saga | 最终 | 高 | 长流程 |
| AT | 弱强 | 高 | 通用 |
| MQ 事务消息 | 最终 | 高 | 通用 |
动手练习
- 部署 Seata Server(单机版)
- 写一个"下单-扣库存-支付"的 Seata AT 例子,人工触发其中一个失败,观察回滚
- 用 TCC 实现"扣库存"接口
- 用 Saga 配置一个完整退款流程
推荐阅读
下一章:第 228 章:分布式锁进阶