在SpringBoot项目中引入多线程可以提升性能,但也让事务管理变得复杂。其核心挑战在于Spring的事务管理与线程绑定这一特性。
🔍 理解多线程事务的核心挑战
Spring的声明式事务(@Transactional)其底层依赖于ThreadLocal来存储事务上下文信息(如数据库连接)。这意味着事务上下文是线程隔离的。
- 事务上下文无法跨线程传递 :当你在一个线程(称为"主线程")中开启事务后,新创建的子线程无法自动继承这个事务上下文。子线程中执行的数据库操作会在一个独立的新事务中进行。
- 默认行为的结果 :主线程和子线程的事务是完全独立的。主事务的成功提交或回滚不会影响子事务,反之亦然。这直接破坏了业务的原子性(Atomicity)。
⚠️ 典型场景与潜在问题
假设一个业务场景:在更新用户主信息后,需要异步执行一个耗时操作(如发送短信通知或写日志),并要求两者保持原子性。
java
// 错误示例:事务未隔离导致数据不一致
@Service
public class UserService {
@Transactional
public void updateUserAndSendSms(Long userId, String newPhone) {
// 1. 主线程更新用户信息(处于主事务中)
userRepository.updateUserPhone(userId, newPhone);
// 2. 提交子线程任务:发送短信
executor.submit(() -> {
smsClient.sendSms(newPhone, "更新成功"); // 子线程操作,独立事务
});
// 3. 主事务在此方法结束时提交
}
}
可能发生的问题:
- 数据不一致:如果主线程事务成功提交,但随后子线程中的短信发送失败,系统状态将不一致(用户信息已更新,但通知未发出)。
- 异常处理脱节 :子线程抛出的异常无法被主线程的
@Transactional机制捕获,因此子线程的失败不会导致主事务回滚。 - 连接资源耗尽:每个子线程都可能创建新的数据库连接,在高并发下可能导致数据库连接池被快速耗尽。
- 死锁风险:如果多个线程并发操作同一数据,且未合理使用锁机制,极易引发死锁。
💡 多线程事务的解决方案
针对上述问题,可以根据业务对一致性的要求程度,选择不同的解决方案。
方案一:最终一致性方案(推荐用于非核心业务)
对于非核心、可补偿的操作(如发送通知、记录日志),追求最终一致性通常是更平衡的选择。核心思想是先记录后处理。
- 本地事务表/日志记录 :在主事务中,将异步任务的信息(如
SmsTask)与业务数据在同一个数据库事务中持久化。 - 异步处理:主事务提交后,由一个独立的异步组件从表中读取任务并执行。
- 补偿机制:如果异步任务执行失败,记录状态,并通过定时任务进行重试。
java
// 1. 主事务:业务操作 + 任务记录
@Service
public class UserService {
@Transactional
public void updateUserAndPrepareSms(Long userId, String newPhone) {
userRepository.updateUserPhone(userId, newPhone);
SmsTask smsTask = new SmsTask();
smsTask.setPhone(newPhone);
smsTask.setStatus("PENDING");
smsTaskRepository.save(smsTask); // 与用户更新同属一个事务
}
}
// 2. 异步处理任务
@Service
public class SmsAsyncService {
@Async
@Transactional(propagation = Propagation.REQUIRES_NEW) // 开启独立事务
public void asyncSendSms(Long taskId) {
SmsTask task = smsTaskRepository.findById(taskId).orElseThrow();
try {
smsClient.sendSms(task.getPhone(), "更新成功");
task.setStatus("SUCCESS");
} catch (Exception e) {
task.setStatus("FAILED");
// 独立事务回滚,不影响主数据
throw new RuntimeException(e);
} finally {
smsTaskRepository.save(task);
}
}
}
// 3. 补偿服务(可选)
@Service
public class SmsCompensateService {
@Scheduled(cron = "0 */5 * * * ?")
public void compensateFailedTasks() {
List<SmsTask> failedTasks = smsTaskRepository.findByStatus("FAILED");
// 重试失败的任务
}
}
优势 :保证主业务操作不受异步任务影响,性能好,架构清晰。
劣势:达到一致状态有短暂延迟,需要额外的表结构和补偿逻辑。
方案二:编程式事务管理(用于需要较强控制的场景)
当你需要精确控制事务边界,甚至尝试在多个线程间同步事务状态时,可以使用TransactionTemplate进行编程式事务管理。
- 在同一事务内执行(谨慎使用) :通过
TransactionTemplate将任务包装起来,可以在特定场景下(如使用共享连接)在子线程中执行父事务内的操作,但这通常需要复杂的上下文传递且风险较高。 - 更常见的用法:精确控制独立事务:明确为子线程开启一个独立的新事务。
java
@Service
public class OrderService {
private final TransactionTemplate transactionTemplate;
public void processOrder() {
// 主线程业务...
executor.submit(() -> {
// 在子线程中明确开启一个独立事务
transactionTemplate.execute(status -> {
// 这里的数据库操作处于一个独立事务中
logService.saveLog(...);
return null;
});
});
}
}
优势 :事务边界清晰,控制灵活。
劣势:代码侵入性强,需要手动处理事务的提交和回滚。
方案三:借助AOP手动同步事务状态(高级用法)
这是一种更为复杂的方法,通过自定义注解和AOP(面向切面编程),利用CountDownLatch等同步工具,协调主线程和所有子线程的事务状态,实现近似"跨线程事务"的效果。
- 核心机制 :
- 主线程作为协调者 :主线程方法上标注自定义注解(如
@MainTransaction),并声明子线程数量。 - 子线程作为参与者 :子线程方法上标注另一注解(如
@SonTransaction)。 - AOP拦截与同步:AOP切面会拦截这些方法。主线程会等待所有子线程业务逻辑执行完毕,然后根据所有线程的执行结果(成功或失败)统一决定是提交还是回滚每个线程的独立事务。
- 主线程作为协调者 :主线程方法上标注自定义注解(如
- 实现复杂度:这种方法需要编写复杂的AOP代码,并谨慎处理线程同步和异常,稍有不慎可能导致死锁或长时间等待。
方案四:分布式事务框架(用于强一致性核心业务)
如果业务要求多个线程的操作必须强一致(如金融交易中的核心步骤),那么最可靠的方案是引入分布式事务管理器,如Seata ,或使用支持事务消息的消息队列(如RocketMQ)。
这些框架通过全局事务ID(XID)将不同线程(甚至不同服务)中的本地事务绑定在一起,通过两阶段提交(2PC)等协议来保证所有参与者同时提交或回滚。
优势 :能实现真正的跨线程/跨服务事务强一致性。
劣势:性能开销大,架构复杂,通常用于分布式微服务场景。
⚙️ 实践建议与最佳实践
无论选择哪种方案,以下几点都至关重要:
- 合理配置线程池 :避免使用
Executors直接创建无界线程池,推荐使用ThreadPoolTaskExecutor进行显式配置,控制核心线程数、最大线程数和队列容量,防止资源耗尽。 - 明确事务传播行为 :在子线程方法上,根据场景使用
@Transactional(propagation = Propagation.REQUIRES_NEW)明确指定需要新事务。 - 精细化异常处理:确保在子线程内部捕获异常并妥善处理(如记录状态),避免异常抛出导致线程意外终止,而主线程却无法感知。
- 保持事务短小精悍:长时间的事务会持有数据库锁,增加死锁概率,并影响系统吞吐量。尽量避免在事务中进行远程调用、文件IO等耗时操作。
- 避免
@Async和@Transactional在同一方法上注解:这可能导致事务不生效,因为Spring的AOP代理机制可能无法正确处理。
💎 总结
处理SpringBoot多线程事务的关键在于认清 "ThreadLocal导致事务上下文线程隔离" 这一本质。选择解决方案是一场关于一致性、性能与复杂性的权衡。
- 对于大多数场景,最终一致性方案(本地事务表+异步处理) 是平衡性最好的选择。
- 若需要更强控制或简单独立事务,可考虑编程式事务。
- 对于复杂的多线程协同且要求原子性的场景,可评估基于AOP的手动同步方案,但需警惕其复杂度。
- 对于核心的强一致性业务,最终可能需要引入分布式事务框架。
Seate框架
Seata(Simple Extensible Autonomous Transaction Architecture)是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。它最初由阿里巴巴团队开发,并已成为Apache基金会孵化项目。
🔑 核心概念与架构
Seata 的分布式事务模型核心包含三个组件:
| 组件角色 | 缩写 | 职责说明 |
|---|---|---|
| 事务协调者 | TC (Transaction Coordinator) | 维护全局和分支事务的状态,驱动全局事务提交或回滚,是独立部署的Server端。 |
| 事务管理器 | TM (Transaction Manager) | 定义全局事务的边界,负责开启、提交或回滚全局事务,是嵌入应用的Client端。 |
| 资源管理器 | RM (Resource Manager) | 管理分支事务的资源,负责向TC注册分支事务、上报状态,并接收TC的指令来驱动分支事务的提交或回滚,也是嵌入应用的Client端。 |
其基本工作流程可概括为以下步骤:
- 开启全局事务:TM向TC申请开启一个全局事务,TC生成一个全局唯一的XID。
- 传播XID:该XID会在微服务调用链的上下文中进行传播。
- 注册分支事务:每个服务的RM将本地事务作为分支事务注册到TC,纳入该XID对应的全局事务管理。
- 决议全局事务:TM根据所有分支事务的执行结果,向TC发起全局提交或回滚的决议。
- 驱动分支事务:TC调度该XID下的所有分支事务,完成最终的提交或回滚。
所有分支成功
任一分支失败
TM: 开始全局事务
TC: 生成全局事务XID
RM: 注册分支事务
执行本地事务
RM: 报告本地事务状态
TM: 检查所有分支状态
TM: 通知TC提交全局事务
TM: 通知TC回滚全局事务
TC: 通知各分支提交
TC: 通知各分支回滚
RM: 异步清理UNDO_LOG
RM: 根据UNDO_LOG回滚数据
💡 Seata的事务模式
Seata 提供了四种主要的事务模式以适应不同的业务场景。
| 模式 | 原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| AT模式 (自动事务) | 基于两阶段提交的改进。一阶段直接提交本地事务,并生成回滚日志(UNDO_LOG);二阶段如全局提交则异步删除日志,如回滚则根据日志进行数据补偿。 | 对业务无侵入,开发者只需关注业务SQL;性能较好。 | 默认隔离级别为读未提交;回滚时需处理脏写问题(通过全局锁避免)。 | 对业务侵入性要求高、基于支持本地ACID事务的关系型数据库的大多数场景,是Seata的首推模式。 |
| TCC模式 (尝试-确认-取消) | 将业务逻辑分为三个阶段: 1. Try :尝试执行业务,完成资源检查和预留(如冻结资金)。 2. Confirm :确认执行业务,使用预留的资源(如扣减冻结的资金)。 3. Cancel:取消业务,释放预留的资源(如解冻资金)。 | 更高的性能 ,无全局锁;隔离性好。 | 对业务侵入性强 ,需要改造代码实现三个接口;需自行解决空回滚、幂等、悬挂等异常问题。 | 适用于对性能要求高、有自研能力处理复杂性的业务,如金融交易、积分兑换等。 |
| Saga模式 | 将长事务拆分为多个连续的本地子事务。每个子事务都有对应的补偿操作。如果某个子事务失败,则按照相反顺序执行已成功子事务的补偿操作。 | 适用于长事务;参与者异步执行,吞吐量高。 | 不保证隔离性,易出现脏写;补偿操作也可能失败,设计复杂。 | 业务流程长、后续操作不依赖于前序操作结果的场景,如旅行订票系统、订单处理流程。 |
| XA模式 | 基于数据库本身提供的XA协议实现的两阶段提交。一阶段准备但不提交事务;二阶段协调所有参与者统一提交或回滚。 | 强一致性;对业务无侵入。 | 同步阻塞,性能差;资源锁定时间长,易影响并发。 | 需要强一致性且并发压力不大的内部应用,通常在企业内部系统中使用。 |
🛠️ 重要机制与特性
-
全局锁与写隔离
在AT模式下,Seata通过全局锁来实现写隔离,防止不同全局事务同时修改同一数据产生脏写。分支事务在一阶段提交前会尝试获取全局锁,只有获取成功才能提交,否则会重试或回滚。
-
高可用部署
Seata-Server(TC)支持高可用集群部署。可以将TC的会话信息存储到共享数据库(如MySQL)或Redis中,并通过注册中心(如Nacos)进行服务发现,避免单点故障。
📊 应用场景与最佳实践
Seata适用于多种需要保证数据最终一致性的场景,例如:
- 电商下单:创建订单、扣减库存、支付操作需要保持一致。
- 银行转账:转出账户扣款和转入账户加款必须同时成功或失败。
- 物流配送:订单状态更新、库存扣减、配送单生成需要事务性。
在选择模式时,可以遵循以下原则:
- 优先考虑AT模式:对于大多数场景,其无侵入性的优势非常明显。
- 高性能场景考虑TCC:当并发极高且对性能有严苛要求时,可忍受侵入性而选择TCC。
- 长流程业务考虑Saga:如果业务流程长且可以接受最终一致性,Saga是合适的选择。
- 强一致性需求考虑XA:对一致性要求极高且并发不高的内部系统可考虑XA。
💎 总结
Seata通过其多样化的模式(尤其是对业务无侵入的AT模式)和清晰的架构,有效解决了微服务架构下的数据一致性问题。选择哪种模式,取决于业务对一致性、性能、和开发复杂度的权衡。
Seata的使用
Seata 提供的四种主流分布式事务模式(AT、TCC、SAGA、XA)各有其独特的实现方式和适用场景。下面我将通过清晰的对比和具体的代码案例,为你详细介绍每种模式的使用方法。
为了让你快速建立整体印象,下表直观对比了 Seata 四种核心模式的核心特征。
| 模式 | 原理简述 | 一致性 | 业务侵入性 | 性能特点 | 典型适用场景 |
|---|---|---|---|---|---|
| AT 模式 | 两阶段提交,自动生成并解析 SQL 回滚日志(UNDO_LOG)。 | 最终一致性 | 无侵入 | 一阶段直接提交,性能较好。 | 基于关系型数据库的大多数业务场景,如电商下单。 |
| TCC 模式 | 手动编码实现 Try(预留)、Confirm(确认)、Cancel(取消)三个阶段。 | 最终一致性 | 侵入性强 | 无需全局锁,性能最佳。 | 对性能要求高、需接入非事务性资源(如Redis)或外部API的场景。 |
| SAGA 模式 | 长事务模型,由一系列本地事务组成,每个事务有对应的补偿操作。 | 最终一致性 | 侵入性中等 | 参与者可异步执行,吞吐量高。 | 业务流程长、时效性要求不高的场景,如跨系统订单处理。 |
| XA 模式 | 基于数据库 XA 协议的两阶段提交,一阶段准备,二阶段提交/回滚。 | 强一致性 | 无侵入 | 一阶段锁定资源,性能较差。 | 需要强一致性且并发压力不大的内部系统。 |
💡 各模式详解与案例
1. AT 模式(自动事务)
AT 模式是 Seata 的默认模式,通过在运行时自动拦截并解析 SQL,生成回滚日志(UNDO_LOG)来实现数据回滚,对业务代码无任何侵入。
-
实现步骤:
- 添加依赖与配置 :在
pom.xml中加入 Seata 依赖,并在application.yml中配置 Seata Server 的连接信息(如注册中心地址)。 - 创建用于存储 Seata 全局事务和分支事务信息的数据库表(如
global_table,branch_table)。 - 使用注解 :在全局事务的发起方法上添加
@GlobalTransactional注解。
- 添加依赖与配置 :在
-
代码示例 :
在订单服务的业务方法上声明全局事务。当调用库存服务或账户服务失败时,Seata 会自动回滚所有操作。
java@Service public class OrderService { @GlobalTransactional(name = "create-order", rollbackFor = Exception.class) public void createOrder(Order order) { // 1. 本地创建订单 orderMapper.insert(order); // 2. 远程调用库存服务扣减库存 storageFeignClient.deduct(order.getCommodityCode(), order.getCount()); // 3. 远程调用账户服务扣减余额 accountFeignClient.debit(order.getUserId(), order.getMoney()); } }
2. TCC 模式(尝试-确认-取消)
TCC 模式要求开发者手动编写业务逻辑的 Try、Confirm、Cancel 三个方法,适用于需要精细控制事务或整合非事务性资源的场景。
-
核心概念:
- Try:完成资源检查和预留(如将账户金额转入冻结状态)。
- Confirm:执行业务确认(如扣减冻结的金额)。
- Cancel:执行补偿回滚(如将冻结金额解冻)。
- 注意事项 :需要处理空回滚 (Try未执行但执行了Cancel)、幂等性 (防止重复提交或回滚)和业务悬挂(Cancel比Try先执行)等问题。
-
代码示例 :
定义一个 TCC 接口及其实现,用于账户余额的扣减。
java@LocalTCC public interface AccountTCCService { @TwoPhaseBusinessAction(name = "deduct", commitMethod = "confirm", rollbackMethod = "cancel") void deduct(@BusinessActionContextParameter(paramName = "userId") String userId, @BusinessActionContextParameter(paramName = "money") int money); boolean confirm(BusinessActionContext context); boolean cancel(BusinessActionContext context); } @Service public class AccountTCCServiceImpl implements AccountTCCService { @Autowired private AccountMapper accountMapper; // 操作账户主表 @Autowired private AccountFreezeMapper freezeMapper; // 操作冻结记录表 @Override @Transactional public void deduct(String userId, int money) { // 0. 获取全局事务ID,用于幂等控制 String xid = RootContext.getXID(); // 1. 检查是否已存在空回滚记录,防止业务悬挂 if (freezeMapper.selectById(xid) != null) { return; } // 2. 扣减可用余额 (Try: 资源预留) accountMapper.deduct(userId, money); // 3. 记录冻结金额,事务状态为TRY AccountFreeze freeze = new AccountFreeze(xid, userId, money, AccountFreeze.State.TRY); freezeMapper.insert(freeze); } @Override public boolean confirm(BusinessActionContext context) { // Confirm: 删除冻结记录,完成业务 String xid = context.getXid(); int count = freezeMapper.deleteById(xid); return count == 1; } @Override public boolean cancel(BusinessActionContext context) { // Cancel: 补偿操作 String xid = context.getXid(); AccountFreeze freeze = freezeMapper.selectById(xid); // 处理空回滚:如果Try没执行,需要插入一条记录防止脏数据 if (freeze == null) { freeze = new AccountFreeze(xid, (String)context.getActionContext("userId"), 0, AccountFreeze.State.CANCEL); freezeMapper.insert(freeze); return true; } // 幂等性判断:如果已经是CANCEL状态,直接返回 if (freeze.getState() == AccountFreeze.State.CANCEL) { return true; } // 恢复可用余额 accountMapper.refund(freeze.getUserId(), freeze.getFreezeMoney()); // 更新冻结记录状态为CANCEL freeze.setFreezeMoney(0); freeze.setState(AccountFreeze.State.CANCEL); int count = freezeMapper.updateById(freeze); return count == 1; } }
在业务发起方使用 @GlobalTransactional(订单服务):在订单服务的业务方法上,您不需要直接调用Confirm或Cancel,而是通过注解开启全局事务,并调用TCC接口的 Try方法。
java
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
// 注入您已经实现好的TCC服务
@Autowired
private InventoryTccService inventoryTccService;
// 使用此注解开启一个全局事务
@GlobalTransactional(name = "createOrder", rollbackFor = Exception.class)
public void createOrder(Order order) {
// 1. TCC 第一阶段:尝试执行 - 冻结库存
// 您需要手动调用各个TCC服务的Try方法
boolean tryResult = inventoryTccService.tryDeduct(order.getProductId(), order.getCount());
if (!tryResult) {
// 如果尝试失败,抛出异常触发回滚
throw new RuntimeException("库存不足,冻结失败");
}
// 2. 执行本地业务操作 - 创建订单(状态可能是"待支付")
order.setStatus(0); // 0表示初始状态
orderMapper.create(order);
// 3. 这里可能还会有其他TCC调用,比如调用账户服务的Try方法冻结金额...
// accountTccService.tryDebit(...);
// 4. 如果这个方法成功执行完毕,没有抛出异常,Seata框架会自动触发第二阶段的 **Confirm** 操作。
// 5. 如果上述任何一步抛出异常,Seata框架会自动触发第二阶段的 **Cancel** 操作。
}
}
3. SAGA 模式
SAGA 模式通过状态机来编排长业务流程,每个服务节点执行成功后提交本地事务,失败则由协调器触发补偿操作。
在Seata Saga中,一个状态机实例就是一个全局事务,其中的每个节点(状态)对应一个分支事务 。下面是一个简化的"下单"状态机JSON及其核心节点说明 :
- 实现方式:通常需要一个 JSON 文件来定义状态机的流程,包括状态节点和补偿逻辑。
- 代码示例(状态机配置概念):
json
{
"Name": "reduceInventoryAndBalance",
"Comment": "先扣库存再扣余额",
"Version": "0.0.1",
"StartState": "Start",
"States": {
"Start": {
"Type": "Start",
"Next": "InventoryAction"
},
"InventoryAction": {
"Type": "ServiceTask",
"ServiceName": "inventoryAction",
"ServiceMethod": "reduce",
"Next": "ChoiceState",
"CompensateState": "CompensateReduceInventory",
"Input": ["$.[businessKey]", "$.[count]"],
"Output": {"reduceInventoryResult": "$.#root"},
"Status": {
"#root == true": "SU",
"#root == false": "FA",
"$Exception{java.lang.Throwable}": "UN"
}
},
"ChoiceState": {
"Type": "Choice",
"Choices": [{
"Expression": "[reduceInventoryResult] == true",
"Next": "ReduceBalance"
}],
"Default": "Fail"
},
"ReduceBalance": {
"Type": "ServiceTask",
"ServiceName": "balanceAction",
"ServiceMethod": "reduce",
"CompensateState": "CompensateReduceBalance",
"Next": "Succeed"
},
"CompensateReduceInventory": {
"Type": "Compensation",
"ServiceName": "inventoryAction",
"ServiceMethod": "compensateReduce"
},
"Succeed": {"Type": "Succeed"},
"Fail": {"Type": "Fail"}
}
}
关键节点解析:
- ServiceTask(服务任务) :这是核心节点,用于执行具体的业务逻辑。
ServiceName和ServiceMethod指定要调用的服务与方法。CompensateState指定了补偿节点,在事务失败时被触发 。Input使用SpringEL表达式从状态机上下文中取参数,例如$.[businessKey]获取业务键 。
- Choice(选择节点):实现条件分支。如上例中,根据库存扣减结果决定下一步是执行扣款还是失败 。
- Compensation(补偿节点):定义回滚逻辑,其参数需与正向操作的补偿方法匹配 。
💻 编写服务与补偿方法
状态机中引用的服务(如 inventoryAction),需要你创建具体的Java类来实现。
java
// 1. 库存服务实现
@Service("inventoryAction")
public class InventoryActionImpl implements InventoryAction {
@Override
public boolean reduce(String businessKey, BigDecimal count) {
// 执行库存扣减业务逻辑
// 如果扣减失败(如库存不足),返回 false 或抛出异常
System.out.println("减少库存, 订单:" + businessKey + ", 数量:" + count);
// 模拟30%的失败率
if (Math.random() < 0.3) {
throw new RuntimeException("库存不足");
}
return true; // 执行成功返回true
}
// 对应的补偿方法:恢复库存
public boolean compensateReduce(String businessKey) {
// 补偿逻辑:恢复之前扣减的库存
System.out.println("补偿操作:恢复库存, 订单:" + businessKey);
return true;
}
}
// 2. 账户服务实现
@Service("balanceAction")
public class BalanceActionImpl implements BalanceAction {
@Override
public boolean reduce(String businessKey, BigDecimal amount) {
// 执行账户余额扣减
System.out.println("减少余额, 订单:" + businessKey + ", 金额:" + amount);
// 模拟20%的失败率
if (Math.random() < 0.2) {
throw new RuntimeException("余额不足");
}
return true;
}
// 对应的补偿方法:恢复余额
public boolean compensateReduce(String businessKey) {
System.out.println("补偿操作:恢复余额, 订单:" + businessKey);
return true;
}
}
🚀 启动状态机引擎
在业务入口(如下单接口)中,你需要通过 StateMachineEngine 启动定义好的状态机。
java
@Service
public class OrderService {
@Autowired
private StateMachineEngine stateMachineEngine; // 需提前配置Bean
public void createOrder(Order order) {
Map<String, Object> startParams = new HashMap<>();
String businessKey = "ORDER_" + System.currentTimeMillis(); // 生成唯一业务ID
startParams.put("businessKey", businessKey);
startParams.put("count", order.getCount());
startParams.put("amount", order.getAmount());
// ... 设置其他参数
// 同步启动状态机
try {
StateMachineInstance instance = stateMachineEngine.startWithBusinessKey(
"reduceInventoryAndBalance", // 状态机名称,与JSON中Name一致
null, // 租户ID,可为空
businessKey,
startParams
);
if (instance.getStatus() == MachineStatus.SUCCESS) {
System.out.println("订单创建成功!");
} else {
System.out.println("订单创建失败!");
}
} catch (Exception e) {
System.out.println("流程执行异常: " + e.getMessage());
}
}
}
关于 StateMachineEngine 的配置,通常在一个配置类中完成,指定状态机JSON文件的位置等信息 。
⚙️ 配置与运行要点
- 状态机配置 :确保状态机JSON文件(如
statelang/*.json)被正确加载 。 - 服务注册 :实现的服务类(如
InventoryActionImpl)需要被Spring容器管理(使用@Service并注明状态机JSON中定义的名称)。 - 异常处理 :在状态机的JSON定义中,通过
Status块和Catch配置(示例JSON已展示)来映射服务执行结果(成功SU、失败FA、未知UN)并决定状态流转 。 - 补偿触发 :当某个
ServiceTask执行失败(返回false或抛出异常)时,Seata会根据状态机定义自动触发已成功节点的补偿操作,顺序与执行顺序相反 。
🧪 测试SAGA事务
- 成功场景:库存扣减 → 余额扣减 → 订单完成。
- 失败与回滚场景 :如果余额扣减失败,Seata会自动调用已成功的"库存扣减"服务的补偿方法(
compensateReduce)进行回滚 。
4. XA 模式
XA 模式依赖数据库本身的 XA 协议,实现强一致性,但资源锁定时间长,性能最低。
-
配置方式 :在
application.yml中简单配置即可开启。yamlseata: data-source-proxy-mode: XA -
使用方式 :与 AT 模式类似,在事务起点使用
@GlobalTransactional注解。
💎 总结与选型建议
选择哪种模式,归根结底是对一致性、性能、开发复杂度进行权衡:
- 首选 AT 模式:对于绝大多数基于关系型数据库的业务,AT 模式因其无侵入和良好的性能平衡是最佳选择。
- 考虑 TCC 模式:当业务对性能有极致要求,或需要接入像 Redis、第三方 API 这类非事务性资源时,可以选择 TCC,但要接受其编码复杂度。
- SAGA 适用长流程:如果业务链路非常长,且可以接受最终一致性,SAGA 模式是合适的。
- XA 用于强一致:仅在需要强一致性且并发不高的内部系统中考虑。