第 15 章:Seata 实战
学习目标
- 掌握 Seata AT/TCC/Saga/XA 模式
- 学会部署 Seata Server
- 用 Spring Cloud 集成 Seata 解决分布式事务
- 避免 AT 模式脏写、隔离级别不足的坑
一、Seata 是什么?
阿里开源的分布式事务中间件,提供了 AT、TCC、Saga、XA 四种模式。
text
应用层
↓
[Seata Client(TC)]
↓
[Seata Server(TC)]
├── 协调全局事务
├── 记录分支事务
└── 推动 Commit/Rollback二、AT 模式(默认,推荐)
工作机制:两阶段 + 写前镜像 + 写后镜像
text
阶段 1:解析 SQL,生成 undo log,直接提交本地事务
阶段 2:全局事务提交,删除 undo log;或回滚,根据 undo 还原xml
<!-- 引入依赖 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>yaml
# application.yml
spring:
cloud:
alibaba:
seata:
tx-service-group: order_tx_group
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/order_db
username: root
password: rootjava
// 订单服务
@Service
public class OrderService {
@Resource
private OrderRepository orderRepository;
@GlobalTransactional(name = "place-order", rollbackFor = Exception.class)
public void placeOrder(OrderDto dto) {
// 1. 订单服务本地事务
orderRepository.save(new Order(dto));
// 2. 远程调用扣库存(走 Seata 代理)
inventoryClient.deduct(dto.skuId, dto.quantity);
}
}java
// 库存服务
@Service
public class InventoryService {
@Transactional
public void deduct(Long skuId, int quantity) {
inventoryRepository.deduct(skuId, quantity);
}
}⚠️ 坑 1:AT 模式依赖undo log 表,每个业务库都要建
undo_log表,否则全局回滚找不到快照。
sql
-- 每个业务库都要建
CREATE TABLE undo_log (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
branch_id BIGINT NOT NULL,
xid VARCHAR(100) NOT NULL,
context VARCHAR(128) NOT NULL,
rollback_info LONGBLOB NOT NULL,
log_status INT NOT NULL,
log_created DATETIME NOT NULL,
log_modified DATETIME NOT NULL,
UNIQUE KEY ux_undo_log (xid, branch_id)
);三、Seata Server 部署
yaml
# docker-compose.yml
version: '3.8'
services:
seata-server:
image: seataio/seata-server:1.8.0
ports:
- "8091:8091"
environment:
- SEATA_PORT=8091
- STORE_MODE=db
- DB_URL=jdbc:mysql://mysql:3306/seata
- DB_USER=root
- DB_PASSWORD=rootsql
-- Seata Server 数据库
CREATE TABLE branch_table (
branch_id BIGINT PRIMARY KEY,
xid VARCHAR(128) NOT NULL,
transaction_id BIGINT,
resource_group_id VARCHAR(32),
resource_id VARCHAR(256),
branch_type VARCHAR(8),
status TINYINT,
client_id VARCHAR(64),
application_data VARCHAR(2000),
gmt_create DATETIME,
gmt_modified DATETIME
);
CREATE TABLE global_table (
xid VARCHAR(128) PRIMARY KEY,
transaction_id BIGINT,
status TINYINT NOT NULL,
application_id VARCHAR(32),
transaction_service_group VARCHAR(32),
transaction_name VARCHAR(128),
timeout INT,
begin_time BIGINT,
application_data VARCHAR(2000),
gmt_create DATETIME,
gmt_modified DATETIME
);四、AT 隔离级别与脏写
AT 模式默认读未提交,可能出现脏写:
java
// 场景:两个事务同时改同一行
// 事务 A:UPDATE product SET stock = 90
// 事务 B:UPDATE product SET stock = 80
// A 回滚,B 提交 → 最终 80,正确
// 但如果:
// 事务 A:UPDATE product SET stock = 90(后回滚)
// 事务 B:UPDATE product SET stock = 80(后提交)
// A 的写前镜像记录 stock=100,回滚还原 100
// B 提交后是 80,回滚后是 100 ❌ B 的修改被回滚覆盖java
// 解决:加全局锁
@GlobalLock
public Product getProduct(Long id) {
return productRepository.findById(id);
}
// 写时强制 SELECT FOR UPDATE
@GlobalTransactional
public void updateProduct(Long id, int newStock) {
Product p = productRepository.findByIdForUpdate(id); // SELECT FOR UPDATE
p.setStock(newStock);
productRepository.save(p);
}⚠️ 坑 2:AT 模式不保证"读已提交",如需防止脏读,业务 SQL 加
FOR UPDATE。
五、TCC 模式
java
// 订单服务 TCC
@LocalTCC
public interface OrderTccAction {
@TwoPhaseBusinessAction(name = "orderTry", commitMethod = "confirm", rollbackMethod = "cancel")
boolean tryOrder(BusinessActionContext context, @BusinessActionContextParameter Long orderId);
boolean confirmOrder(BusinessActionContext context);
boolean cancelOrder(BusinessActionContext context);
}
@Component
public class OrderTccActionImpl implements OrderTccAction {
@Override
public boolean tryOrder(BusinessActionContext context, Long orderId) {
// 订单状态 = PENDING
orderDao.updateStatus(orderId, "PENDING");
return true;
}
@Override
public boolean confirmOrder(BusinessActionContext context) {
orderDao.updateStatus(context.getActionContext("orderId", Long.class), "CONFIRMED");
return true;
}
@Override
public boolean cancelOrder(BusinessActionContext context) {
orderDao.updateStatus(context.getActionContext("orderId", Long.class), "CANCELLED");
return true;
}
}java
// 调用方
@GlobalTransactional
public void placeOrder(OrderDto dto) {
orderTccAction.tryOrder(null, dto.id);
inventoryTccAction.tryFreeze(null, dto.skuId, dto.quantity);
}六、Saga 模式
java
// 状态机定义
public class OrderSaga {
@SagaStart
public void placeOrder(OrderDto dto) {
// 1. 订单
orderService.create(dto);
// 2. 支付
paymentService.pay(dto);
// 3. 库存
inventoryService.deduct(dto);
}
@Compensation(forward = "placeOrder", compensation = "cancelOrder")
public void cancelOrder(OrderDto dto) {
orderService.cancel(dto);
paymentService.refund(dto);
inventoryService.restore(dto);
}
}text
优点:长事务、跨服务
缺点:补偿逻辑靠业务写,容易遗漏七、XA 模式(强一致)
java
// 启动 XA 模式
spring:
cloud:
alibaba:
seata:
mode: xa
// 代码与本地事务一致
@GlobalTransactional
public void placeOrder(OrderDto dto) {
orderRepository.save(new Order(dto));
inventoryClient.deduct(dto.skuId, dto.quantity);
}text
XA 模式:数据库层 2PC,强一致,性能差
AT 模式:应用层 2PC,最终一致,性能好⚠️ 坑 3:XA 模式需要数据库支持 XA 协议,MySQL 8 InnoDB 支持,部分云数据库不支持。
八、实战避坑
java
// 1. Feign 调用要走 Seata 代理
@FeignClient(name = "inventory-service", configuration = FeignSeataConfig.class)
public interface InventoryClient {
@PostMapping("/api/inventory/deduct")
void deduct(@RequestBody DeductRequest req);
}
// 2. 异步调用要传递 XID
RestTemplate restTemplate = new RestTemplate();
// Header 注入 XIDjava
// 3. 异常处理
@GlobalTransactional(noRollbackFor = BusinessException.class)
public void placeOrder(OrderDto dto) {
// 业务异常不回滚全局事务
}本章小结
| 模式 | 一致性 | 性能 | 复杂度 |
|---|---|---|---|
| AT | 最终 | 高 | 低 |
| TCC | 最终 | 中 | 高 |
| Saga | 最终 | 中 | 中 |
| XA | 强 | 低 | 中 |
| 选型 | 建议 |
|---|---|
| 默认 | AT 模式 |
| 支付 | TCC |
| 长流程 | Saga |
| 强一致 | XA |
动手练习
- Server 部署:用 Docker 启动 Seata Server + MySQL
- AT 集成:写 order + inventory 两个服务,演示 Seata AT 全局提交和回滚
- 脏写测试:两个并发事务同时改一行,加全局锁修复
- TCC 实现:把订单库存 TCC 化,验证 confirm / cancel 行为
下一章:第 16 章:最终一致性 →