分布式事务Seata_分布式 事务
Seata 是一款开源的分布式事务解决方案,提供了 AT(自动补偿型)、TCC(手动补偿型)、Saga(长事务模式) 等模式,能有效地解决微服务架构下的数据一致性问题。下面我用 Java 代码实例(主要基于 Spring Cloud Alibaba)带你快速上手 Seata 的两种核心模式,并聊聊它们的特点。
先了解 Seata 的几种模式
Seata 主要支持三种分布式事务模式,它们各有适用场景:
模式 | 原理 | 优点 | 缺点 | 适用场景 |
AT | 两阶段提交,自动生成反向 SQL 回滚 | 对业务代码几乎零侵入 | 依赖数据库支持,全局锁 | 大部分业务场景,快速集成 |
TCC | 两阶段提交,需要手动实现 Try/Confirm/Cancel 方法 | 性能好,无全局锁 | 代码侵入性强,实现复杂 | 对性能、一致性要求高的业务 |
Saga | 长事务模型,通过状态机和事件补偿实现 | 适用于长时间的分布式事务 | 需保证补偿逻辑必然成功 | 业务流程长、无需强一致性的场景 |
环境准备
在使用以下代码前,请确保你的项目中已引入 Seata 依赖。以 Maven 为例:
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.4.2</version> <!-- 请使用最新稳定版本 -->
</dependency>
并在 application.yml 中配置 Seata:
spring:
cloud:
alibaba:
seata:
tx-service-group: my_test_tx_group # 事务组名称,需与Seata Server配置匹配
seata:
enabled: true
application-id: your-application-name
tx-service-group: my_test_tx_group
registry:
type: nacos # 以Nacos为例
nacos:
server-addr: localhost:8848
config:
type: nacos
代码示例
1. AT 模式(自动补偿)
AT 模式是 Seata 最常用的模式,通过代理数据源和生成反向 SQL 实现自动回滚。
只需在全局事务的入口方法上添加 @GlobalTransactional 注解:
import io.seata.spring.annotation.GlobalTransactional;
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private InventoryService inventoryService;
// 开启全局事务
@GlobalTransactional(name = "createOrder", rollbackFor = Exception.class)
public void createOrder(Order order) {
// 1. 本地事务:创建订单
orderMapper.insert(order);
// 2. 远程调用:扣减库存(Feign调用)
inventoryService.deduct(order.getProductId(), order.getCount());
// 3. 模拟异常,触发全局回滚
// int i = 1 / 0;
}
}
说明:
- Seata 会自动在分支事务注册时记录快照信息(前置镜像 + 后置镜像),存入 undo_log 表。
- 若发生异常,Seata 服务器通知所有分支事务根据 undo_log 自动回滚。
- 需确保每个分支事务的业务库中都有 undo_log 表(建表 SQL 在 Seata 官方文档中可找到)。
2. TCC 模式(手动补偿)
TCC 模式不依赖数据库事务,而是要求开发者手动实现 Try、Confirm、Cancel 三个方法,更适合对性能要求高或数据库不支持自动补偿的场景。
a. 定义 TCC 接口
import io.seata.rm.tcc.api.BusinessActionContext;
import io.seata.rm.tcc.api.BusinessActionContextParameter;
import io.seata.rm.tcc.api.LocalTCC;
import io.seata.rm.tcc.api.TwoPhaseBusinessAction;
@LocalTCC
public interface InventoryTccService {
@TwoPhaseBusinessAction(name = "inventoryTccService", commitMethod = "commit", rollbackMethod = "cancel")
boolean tryDeduct(@BusinessActionContextParameter(paramName = "productId") String productId,
@BusinessActionContextParameter(paramName = "count") Integer count);
boolean commit(BusinessActionContext context);
boolean cancel(BusinessActionContext context);
}
b. 实现 TCC 接口
@Service
public class InventoryTccServiceImpl implements InventoryTccService {
@Autowired
private InventoryMapper inventoryMapper;
@Override
public boolean tryDeduct(String productId, Integer count) {
// Try 阶段:尝试冻结库存(业务检查并预留资源)
int availableStock = inventoryMapper.getAvailableStock(productId);
if (availableStock < count) {
throw new RuntimeException("库存不足");
}
inventoryMapper.freezeStock(productId, count); // 冻结库存
return true;
}
@Override
public boolean commit(BusinessActionContext context) {
// Confirm 阶段:真正扣减冻结的库存
String productId = (String) context.getActionContext("productId");
Integer count = (Integer) context.getActionContext("count");
inventoryMapper.reduceFreezedStock(productId, count); // 扣减已冻结库存
return true;
}
@Override
public boolean cancel(BusinessActionContext context) {
// Cancel 阶段:释放冻结的库存
String productId = (String) context.getActionContext("productId");
Integer count = (Integer) context.getActionContext("count");
inventoryMapper.returnFreezedStock(productId, count); // 释放冻结库存
return true;
}
}
c. 在业务中调用 TCC
@Service
public class OrderService {
@Autowired
private InventoryTccService inventoryTccService;
@GlobalTransactional(name = "createOrderTcc", rollbackFor = Exception.class)
public void createOrderTcc(Order order) {
// 调用 TCC 的 Try 方法
boolean tryResult = inventoryTccService.tryDeduct(order.getProductId(), order.getCount());
if (!tryResult) {
throw new RuntimeException("TCC Try 阶段失败");
}
// ... 其他业务逻辑(如创建订单)
}
}
说明:
- Try:尝试执行业务,完成资源检查和预留(如冻结库存)。
- Confirm:确认执行业务,真正使用预留的资源(如扣减冻结的库存)。要求幂等。
- Cancel:取消执行,释放预留的资源(如解冻库存)。要求幂等。
- TCC 模式无全局锁,性能较好,但需开发者手动实现补偿逻辑。
实践注意事项
- 全局事务 ID(XID)传递:
在微服务调用中,需确保 Seata 的 XID 通过请求头在不同服务间传递。通常可通过配置 Interceptor 或 Filter 实现。例如,使用 RestTemplate 时可添加拦截器:
@Bean
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();
restTemplate.setInterceptors(Collections.singletonList(new SeataRestTemplateInterceptor()));
return restTemplate;
}
使用 Feign 时,Seata 已提供支持,一般无需额外配置。
- 异常处理:
默认情况下,Seata 只在抛出 RuntimeException 或 Error 时回滚。若需其他异常也触发回滚,可使用 @GlobalTransactional(rollbackFor = Exception.class)。 - 超时控制:
Seata 全局事务有超时时间,默认 60 秒。超时后会自动触发回滚。 - AT 模式下的脏回滚与手动干预:
在极少数情况下(如长时间网络分区、Seata Server 异常),AT 模式可能无法自动处理回滚(脏数据回滚不成功),此时可能需要人工干预。 - 手动控制事务回滚与挂起:
在某些特殊场景(如调用第三方 API 并根据返回码判断是否需要回滚,或在大事务中需暂时脱离事务上下文执行一些非事务操作),你可能需要手动回滚或临时挂起分布式事务。
- 手动回滚:
if (需要回滚的条件) {
if (RootContext.inGlobalTransaction()) {
GlobalTransactionContext.reload(RootContext.getXID()).rollback();
}
}
- 临时挂起(解绑并重新绑定 XID):
@GlobalTransactional
public void someMethod() {
String xid = RootContext.getXID(); // 获取当前 XID
RootContext.unbind(); // 解绑 XID,后续操作不在此全局事务内
// 执行一些不希望被分布式事务管理的操作(如记录日志)
logService.saveLog();
RootContext.bind(xid); // 重新绑定 XID,后续操作重新加入全局事务
// ... 其他业务逻辑
}
挂起事务时需谨慎评估数据一致性。
- 数据库支持:
AT 模式需要数据库支持(如 MySQL、PostgreSQL、Oracle 等),并且需要在业务数据库中创建 undo_log 表。 - 幂等性与空回滚:
在 TCC 模式中,务必保证 Confirm 和 Cancel 方法的幂等性,并处理好空回滚(Try 未执行,Cancel 却被调用)和防悬挂(Cancel 比 Try 先执行)问题。
如何选择模式?
- 绝大多数场景:优先使用 AT 模式,对代码侵入低,能快速上线。
- 高性能需求或数据库不支持(如某些 NoSQL、Redis操作):考虑 TCC 模式,但需承担实现复杂度。
- 业务流程长、非核心业务或最终一致性即可:可考虑 Saga 模式(本文未详细展开,其通过状态机定义流程和补偿逻辑)。