AI摘要
北海のAI

一个下订单的业务中扣减库存、创建订单、扣减余额等在分布式情况下由于位于不同的分支事务,存在OpenFeign这种远程调用,以及@Transactional注解这种不可以跨服务的去回滚事务,所以如何解决这种分布式事务在企业中是常见的问题,下面是我的思路与总结,如果你有更好的方案或我这里有错误请在评论区去指出

Seata解决

Seta是阿里巴巴提供的这种解决分布式事务一站式的方案,其中默认模式是AT,其次是XA、TCC、Saga,现在依次对这些不同的解决方式去介绍

flowchart TB
  subgraph Legend["📋 图例说明"]
      direction LR
      L1["🔵 TC: 事务协调器(Transaction Coordinator)"]:::tc
      L2["🟢 TM: 事务管理器(Transaction Manager)"]:::tm
      L3["🟡 RM: 资源管理器(Resource Manager)"]:::rm
      L4["🔴 存储层:持久化数据"]:::storage
  end

  subgraph Core["Seata 核心架构组成"]
      direction TB
      
      subgraph TC["🔵 TC Server(事务协调器)
维护全局事务和分支事务状态,驱动全局提交或回滚"] direction TB TC1["事务协调器集群
TC Cluster"] --> TC2["全局事务管理
Global Transaction Manager"] TC2 --> TC3["分支事务注册中心
Branch Registry"] TC3 --> TC4["全局锁管理器
Global Lock Manager"] TC4 --> TC5["事务状态机
Transaction State Machine"] TC5 --> TC6["二阶段协调器
2-Phase Coordinator"] TC6 --> TC7["提交处理器
Commit Processor"] TC6 --> TC8["回滚处理器
Rollback Processor"] TC7 & TC8 --> TC9["异步任务执行器
Async Worker"] end subgraph Storage["🔴 存储层(TC 持久化)"] direction TB S1[("事务会话存储
Session Store")] --> S2["全局事务表
global_table"] S1 --> S3["分支事务表
branch_table"] S1 --> S4["全局锁表
lock_table"] S1 --> S5["分布式 KV 存储
DB / Redis / Raft"] end subgraph TM["🟢 TM(事务管理器)
定义全局事务范围,开启/提交/回滚全局事务"] direction TB TM1["@GlobalTransactional
注解拦截器"] --> TM2["全局事务上下文
Global Transaction Context"] TM2 --> TM3["XID 传播器
XID Propagator"] TM3 --> TM4["事务拦截器
Transaction Interceptor"] TM4 --> TM5["事务模板
Transactional Template"] TM5 --> TM6["开启全局事务
Begin Global TX"] TM5 --> TM7["提交全局事务
Commit Global TX"] TM5 --> TM8["回滚全局事务
Rollback Global TX"] end subgraph RM["🟡 RM(资源管理器)
管理分支事务处理资源,驱动分支事务提交或回滚"] direction TB RM1["资源管理器注册
RM Registry"] --> RM2["分支事务处理器
Branch Handler"] RM2 --> RM3["AT 模式处理器
UNDO_LOG Manager"] RM2 --> RM4["TCC 模式处理器
Try/Confirm/Cancel"] RM2 --> RM5["Saga 模式处理器
State Machine Engine"] RM2 --> RM6["XA 模式处理器
XA Resource Manager"] RM3 --> RM7["数据源代理
DataSource Proxy"] RM4 --> RM8["TCC 代理
TCC Proxy"] RM5 --> RM9["Saga 执行器
Saga Executor"] RM6 --> RM10["XA 连接池
XA Connection Pool"] RM7 --> RM11["SQL 解析器
SQL Parser"] RM7 --> RM12["UNDO_LOG 生成器
UNDO_LOG Generator"] RM7 --> RM13["本地事务执行器
Local TX Executor"] end subgraph Client["🟣 Seata Client(集成在业务应用)"] direction TB C1["业务应用程序
Business Application"] --> C2["Seata Client SDK"] C2 --> C3["配置中心客户端
Config Client"] C2 --> C4["注册中心客户端
Registry Client"] C2 --> C5["Netty 通信客户端
Netty Remoting Client"] end end subgraph Interaction["组件交互关系"] direction TB I1["TM 开启全局事务"] -->|"1. 申请 XID"| I2["TC 分配 XID"] I2 -->|"2. 返回 XID"| I3["TM 执行业务"] I3 -->|"3. 调用服务"| I4["RM 注册分支"] I4 -->|"4. 申请全局锁"| I5["TC 管理锁"] I5 -->|"5. 批准/拒绝"| I6["RM 执行本地事务"] I6 -->|"6. 上报状态"| I7["TC 更新状态"] I7 -->|"7. 发起二阶段"| I8["TC 通知 RM"] I8 -->|"8. 提交/回滚"| I9["RM 完成分支"] end %% 连接关系 TC -->|"持久化"| Storage TM -.->|"注册全局事务"| TC RM -.->|"注册分支事务"| TC RM -.->|"申请/释放全局锁"| TC Client -->|"包含"| TM Client -->|"包含"| RM %% 样式定义 classDef tc fill:#E3F2FD,stroke:#1976D2,stroke-width:3px,color:#000 classDef tm fill:#E8F5E9,stroke:#388E3C,stroke-width:2px,color:#000 classDef rm fill:#FFF8E1,stroke:#FBC02D,stroke-width:2px,color:#000 classDef storage fill:#FFEBEE,stroke:#C62828,stroke-width:2px,color:#000 classDef client fill:#F3E5F5,stroke:#7B1FA2,stroke-width:2px,color:#000 class TC,TC1,TC2,TC3,TC4,TC5,TC6,TC7,TC8,TC9 tc; class TM,TM1,TM2,TM3,TM4,TM5,TM6,TM7,TM8 tm; class RM,RM1,RM2,RM3,RM4,RM5,RM6,RM7,RM8,RM9,RM10,RM11,RM12,RM13 rm; class Storage,S1,S2,S3,S4,S5 storage; class Client,C1,C2,C3,C4,C5 client; class Legend,L1,L2,L3,L4 default;

AT模式

AT模式是采用两阶段去提交即一阶段去提交 -> 二阶段靠undo_log去补偿,使用全局锁 + 本地锁 + undo_log,锁开销中等,适用于通用业务,并发适中

  • 第一阶段:执行各自的本地分支事务,并且更新各自的undo_log数据表
  • 第二阶段
    • 成功:删除各自的undo_log数据表中数据
    • 失败:对照各自的undo_log数据表中前后数据,进行回滚
1
2
3
4
5
6
7
8
9
// 业务代码(开发者视角,无感知)
@GlobalTransactional
public void purchase() {
// 1. 扣减库存(UPDATE stock SET count = count - 1 WHERE id = 100)
stockMapper.decrease(100, 1);

// 2. 创建订单(INSERT INTO orders ...)
orderMapper.insert(order);
}

XA模式

两阶段提交,一阶段不提交 -> 二阶段由数据库原生支持提交/回滚 ,这种方式XA 阻塞性更强,一致性更强,性能更低,金融核心用它稳,高并发下容易性能崩; 锁久阻塞是大坑,短事务低并发才可行。

  • 一阶段:业务SQL去执行但是不提交,持有数据库的锁
  • 二阶段:
    • 成功:TC通知后,自动XA Commit
    • 失败:自动XA Rollback
1
2
3
4
5
6
# application.yml - 订单服务
seata:
enabled: true
application-id: order-service
tx-service-group: my_tx_group
data-source-proxy-mode: XA # ⚠️ 关键:指定 XA 模式(默认是 AT)

TCC模式

三阶段提交:Try - Confirm - Cancel

  • 一阶段Try(资源预留):执行业务检查,预留表要的资源,比如冻结库存、预扣金额,结果是资源被锁定,等待后续指令
  • 二阶段Confirm(确认执行):真正执行业务,使用Try预留的资源,比如将冻结库存转换成实际扣减,预扣金额转成实际扣减。这里必须成功,失败就需要人工去接入

数据库表设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
-- 库存表:增加 frozen 字段,记录"冻结"的库存
CREATE TABLE stock (
id BIGINT PRIMARY KEY,
sku_id BIGINT NOT NULL,
available INT NOT NULL DEFAULT 0, -- 可售库存(用户能看到的)
frozen INT NOT NULL DEFAULT 0, -- 冻结库存(被预留但未确认)
total INT NOT NULL DEFAULT 0, -- 总库存 = available + frozen
version INT DEFAULT 0
);

-- 初始状态:iPhone 有 100 件
INSERT INTO stock VALUES (1, 100, 100, 0, 100, 0);

-- 冻结日志表:记录谁冻结了多少,防止重复处理
CREATE TABLE stock_freeze_log (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
xid VARCHAR(128) NOT NULL, -- Seata 全局事务ID
sku_id BIGINT NOT NULL,
quantity INT NOT NULL,
status TINYINT DEFAULT 0, -- 0-FROZEN, 1-COMMITTED, 2-CANCELLED
UNIQUE KEY uk_xid (xid) -- 保证幂等
);

Java代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
/**
* 库存 TCC 服务
* @LocalTCC 告诉 Seata:这个类用 TCC 模式
*/
@LocalTCC
public interface StockTccService {

/**
* ====== Try 方法:尝试冻结库存 ======
* @TwoPhaseBusinessAction 标记这是 Try 方法
* - name: 全局唯一标识
* - commitMethod: 成功时调用的方法名
* - rollbackMethod: 失败时调用的方法名
*/
@TwoPhaseBusinessAction(
name = "stockTccAction",
commitMethod = "commit",
rollbackMethod = "rollback"
)
boolean tryFreeze(
@BusinessActionContextParameter(paramName = "skuId") Long skuId,
@BusinessActionContextParameter(paramName = "quantity") Integer quantity
);

/**
* ====== Confirm 方法:确认扣减 ======
* 用户付款成功,TC 自动调用这个方法
*/
boolean commit(BusinessActionContext context);

/**
* ====== Cancel 方法:取消回滚 ======
* 用户取消或余额不足,TC 自动调用这个方法
*/
boolean rollback(BusinessActionContext context);
}

@Service
public class StockTccServiceImpl implements StockTccService {

@Autowired
private StockMapper stockMapper;

@Autowired
private StockFreezeLogMapper freezeLogMapper;

/**
* ============================================
* Try 阶段:检查库存,冻结资源
* ============================================
* 场景:用户点击"提交订单",Seata 自动调用
*/
@Override
@Transactional
public boolean tryFreeze(Long skuId, Integer quantity) {
// 获取 Seata 全局事务ID(用于幂等判断)
String xid = RootContext.getXID();
System.out.println("【Try】开始冻结,xid=" + xid + ", skuId=" + skuId + ", quantity=" + quantity);

// 1. 检查库存是否充足(查 available,不是 total)
Stock stock = stockMapper.selectBySkuId(skuId);
if (stock.getAvailable() < quantity) {
System.out.println("【Try】库存不足,available=" + stock.getAvailable());
return false; // 返回 false,Seata 会触发全局 Cancel
}

// 2. 冻结库存:available 减少,frozen 增加
// SQL: UPDATE stock SET available = available - 1, frozen = frozen + 1 WHERE sku_id = 100
int rows = stockMapper.freezeStock(skuId, quantity);
if (rows == 0) {
return false; // 并发冲突,冻结失败
}

// 3. 记录冻结日志(用于 Confirm/Cancel 时查找)
StockFreezeLog log = new StockFreezeLog();
log.setXid(xid);
log.setSkuId(skuId);
log.setQuantity(quantity);
log.setStatus(0); // FROZEN
freezeLogMapper.insert(log);

System.out.println("【Try】冻结成功,available=" + (stock.getAvailable()-quantity)
+ ", frozen=" + (stock.getFrozen()+quantity));
return true; // 返回 true,表示 Try 成功
}

/**
* ============================================
* Confirm 阶段:确认扣减(用户付款成功)
* ============================================
* 场景:用户付款完成,Seata TC 自动调用
* 注意:这个方法可能被重复调用,必须幂等!
*/
@Override
@Transactional
public boolean commit(BusinessActionContext context) {
String xid = context.getXid();
System.out.println("【Confirm】确认扣减,xid=" + xid);

// 1. 幂等性检查:查日志,看是否已经处理过
StockFreezeLog log = freezeLogMapper.selectByXid(xid);
if (log == null) {
System.out.println("【Confirm】警告:找不到冻结记录,可能 Try 没执行");
return true; // 认为是成功,避免阻塞
}
if (log.getStatus() == 1) { // 1 = COMMITTED
System.out.println("【Confirm】幂等拦截:已经处理过了");
return true; // 已经处理过,直接返回
}

// 2. 真正扣减:frozen 减少(available 已经在 Try 扣过了)
// SQL: UPDATE stock SET frozen = frozen - 1 WHERE sku_id = 100
stockMapper.confirmFreeze(log.getSkuId(), log.getQuantity());

// 3. 更新日志状态
log.setStatus(1); // COMMITTED
freezeLogMapper.updateById(log);

System.out.println("【Confirm】扣减完成,库存最终减少");
return true;
}

/**
* ============================================
* Cancel 阶段:取消回滚(用户取消或余额不足)
* ============================================
* 场景:用户取消订单,或账户余额不足,Seata TC 自动调用
* 注意:这个方法也可能被重复调用,必须幂等!
*/
@Override
@Transactional
public boolean rollback(BusinessActionContext context) {
String xid = context.getXid();
System.out.println("【Cancel】取消回滚,xid=" + xid);

// 1. 幂等性检查
StockFreezeLog log = freezeLogMapper.selectByXid(xid);
if (log == null) {
System.out.println("【Cancel】找不到记录,可能 Try 没执行或已回滚");
return true; // 认为是成功
}
if (log.getStatus() == 2) { // 2 = CANCELLED
System.out.println("【Cancel】幂等拦截:已经回滚过了");
return true;
}

// 2. 释放冻结:frozen 减少,available 增加(退回去)
// SQL: UPDATE stock SET frozen = frozen - 1, available = available + 1 WHERE sku_id = 100
stockMapper.unfreezeStock(log.getSkuId(), log.getQuantity());

// 3. 更新日志状态
log.setStatus(2); // CANCELLED
freezeLogMapper.updateById(log);

System.out.println("【Cancel】回滚完成,库存恢复原状");
return true;
}
}

TM入口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
@Service
public class OrderService {

@Autowired
private StockTccService stockTccService;

@Autowired
private OrderTccService orderTccService;

@Autowired
private AccountTccService accountTccService;

/**
* 用户下单入口
* @GlobalTransactional 开启 Seata 全局事务
*/
@GlobalTransactional(name = "create-order-tcc", rollbackFor = Exception.class)
public Order createOrder(Long userId, Long skuId, Integer quantity, BigDecimal amount) {

// ========== 第一阶段:Try ==========
// Seata 自动调用三个服务的 tryXxx 方法

// 1. 库存 Try:冻结 1 件 iPhone
boolean stockTry = stockTccService.tryFreeze(skuId, quantity);
if (!stockTry) {
throw new RuntimeException("库存不足");
}

// 2. 订单 Try:创建预备订单
Order order = orderTccService.tryCreate(userId, skuId, quantity, amount);

// 3. 账户 Try:冻结 100 元
boolean accountTry = accountTccService.tryFreeze(userId, amount);
if (!accountTry) {
// 抛出异常,Seata 自动触发三个服务的 Cancel
throw new RuntimeException("余额不足");
}

// ========== Try 全部成功 ==========
// 方法正常返回,Seata 自动调用三个服务的 commit 方法(Confirm)

// 如果这里抛出异常,Seata 自动调用三个服务的 rollback 方法(Cancel)

return order;
}
}