saga指定的方法是spring bean的bean名称
一、核心结论:状态机配置的是 Service 层方法
SAGA 状态机的核心是「步骤(Step)」,每个 Step 对应一个 Service 层方法,分为两类:
| 方法类型 | 作用 | 配置关键字(JSON/YAML) |
|---|---|---|
| 正向方法 | 执行业务逻辑(如扣款、加款、创建订单),对应 SAGA 的 “前进” 步骤 | serviceName/methodName |
| 补偿(逆向)方法 | 正向方法执行失败后,回滚业务逻辑(如退款、撤销订单),对应 SAGA 的 “回退” 步骤 | compensateServiceName/compensateMethodName |
二、状态机配置示例(JSON 格式)
以转账场景为例(用户 A 扣款 → 用户 B 加款,失败则反向补偿),状态机配置直接指向 Service 层方法:
json
{ "name": "transferSaga", "comment": "转账SAGA状态机", "startState": "deductMoney", "states": { // 第一步:扣用户A的钱(正向方法) "deductMoney": { "type": "ServiceTask", "serviceName": "accountService", // Service 层 Bean 名称 "methodName": "deductMoney", // Service 层正向方法名 "compensateServiceName": "accountService", // 补偿用的 Service Bean "compensateMethodName": "refundMoney", // Service 层补偿方法名 "next": "addMoney" // 执行成功后进入下一步 }, // 第二步:加用户B的钱(正向方法) "addMoney": { "type": "ServiceTask", "serviceName": "accountService", "methodName": "addMoney", "compensateServiceName": "accountService", "compensateMethodName": "reduceMoney", "end": true // 执行成功则状态机结束 } } }对应的 Service 层代码示例:
java
运行
@Service("accountService") // 对应状态机中的 serviceName public class AccountServiceImpl implements AccountService { // 正向:扣用户A的钱 public void deductMoney(TransferDTO dto) { // 业务逻辑:update account set balance = balance - dto.getAmount() where userId = dto.getFromUserId() } // 补偿:给用户A退款(扣钱的逆向) public void refundMoney(TransferDTO dto) { // 业务逻辑:update account set balance = balance + dto.getAmount() where userId = dto.getFromUserId() } // 正向:给用户B加钱 public void addMoney(TransferDTO dto) { // 业务逻辑:update account set balance = balance + dto.getAmount() where userId = dto.getToUserId() } // 补偿:扣用户B的钱(加钱的逆向) public void reduceMoney(TransferDTO dto) { // 业务逻辑:update account set balance = balance - dto.getAmount() where userId = dto.getToUserId() } }三、核心要求:Service 层方法需满足这些规则
SAGA 状态机调用 Service 方法时,有严格的约束(否则会执行失败):
1. 方法参数 / 返回值规则
- 参数:建议统一使用 DTO 作为入参(如上述
TransferDTO),状态机会将全局上下文参数透传给方法;支持的参数类型:基本类型、DTO(需序列化)、Map,不支持多参数(多参数需封装为一个 DTO)。 - 返回值:建议返回
void或Boolean(true表示成功,false表示失败);若返回非布尔类型,状态机会默认判定为执行成功。
2. 幂等性要求(核心!)
- 正向 / 补偿方法都必须保证幂等(因为 SAGA 状态机可能重试失败的步骤);示例:给转账请求加
transId唯一索引,方法执行前先检查transId是否已处理。
3. 无事务嵌套
- Service 层方法不要加
@Transactional本地事务(除非是独立的本地逻辑);SAGA 是 “无锁补偿” 模式,本地事务会导致补偿方法无法回滚已提交的业务数据。
4. 异常处理
- 正向方法抛出异常 → 状态机触发该步骤的补偿方法,然后按 “反向顺序” 执行前面步骤的补偿;
- 补偿方法建议捕获异常(仅打印日志),避免补偿失败导致状态机卡死。
四、状态机调用 Service 方法的核心原理
- Bean 加载:Seata 启动时,会扫描 Spring 容器中的 Service Bean(如
accountService),存入 Bean 工厂; - 方法反射调用:状态机执行 Step 时,通过
serviceName找到对应的 Bean,通过methodName反射调用方法; - 参数透传:状态机上下文(如
transId、amount等)会作为参数传入 Service 方法,保证正向 / 补偿方法能拿到相同的业务参数; - 补偿触发逻辑:
- 若
addMoney执行失败 → 先触发addMoney的补偿方法reduceMoney→ 再触发deductMoney的补偿方法refundMoney; - 补偿完成后,状态机状态更新为 “失败”,并写入
saga_state_machine_inst表。
- 若
五、进阶配置:Service 方法的高级用法
1. 多参数封装
若 Service 方法需要多个参数,需封装为一个 DTO,状态机通过inputParams配置参数映射:
json
"deductMoney": { "type": "ServiceTask", "serviceName": "accountService", "methodName": "deductMoney", "inputParams": { "fromUserId": "${transContext.fromUserId}", // 从上下文取参数 "amount": "${transContext.amount}" }, "compensateMethodName": "refundMoney", "compensateInputParams": { // 补偿方法的参数映射 "fromUserId": "${transContext.fromUserId}", "amount": "${transContext.amount}" }, "next": "addMoney" }2. 异步执行 Service 方法
高并发场景下,可配置 Service 方法异步执行(避免状态机阻塞):
json
"deductMoney": { "type": "ServiceTask", "serviceName": "accountService", "methodName": "deductMoney", "async": true, // 异步执行 "asyncTimeout": 3000, // 异步超时时间 "compensateMethodName": "refundMoney" }六、关键总结
- SAGA 状态机直接配置 Service 层的正向 / 补偿方法,不建议配置 Controller 或 DAO 层(Service 层是业务逻辑聚合层,适合作为补偿粒度);
- 正向方法是 “业务推进逻辑”,补偿方法是 “业务回滚逻辑”,两者需一一对应;
- Service 方法必须保证幂等、无强事务嵌套,这是 SAGA 补偿成功的核心;
- 状态机通过 “反射 + Bean 工厂” 调用 Service 方法,参数通过上下文透传,无需硬编码调用。
saga状态机启动saga方法里是指定状态机json文件的name
步骤 1:状态机配置中定义name(JSON 示例)
json
{ "name": "transferSaga", // 状态机唯一名称(核心!) "comment": "用户转账SAGA状态机", "startState": "deductMoney", "states": { "deductMoney": { /* 扣钱步骤 */ }, "addMoney": { /* 加钱步骤 */ } } }- 该配置文件通常放在
resources/seata/saga目录下(Seata 默认扫描路径),Seata 启动时会自动加载并注册,注册 Key 就是transferSaga。
步骤 2:Seata 启动时自动注册状态机(无需手动指定)
Seata 内置的SagaStateMachineConfigProcessor会在 Spring 启动时:
- 扫描
resources/seata/saga下的所有 JSON/YAML 配置; - 解析每个配置的
name字段,作为唯一标识; - 将状态机定义存入
saga_state_machine表(name字段对应表的name列); - 同时加载到内存中的
StateMachineFactory,供业务代码调用。
步骤 3:业务代码中通过name触发状态机(核心操作)
业务代码(如 Controller/Service)中,通过SagaEngine触发状态机,必须指定name:
java
运行
@RestController public class TransferController { // 注入 SAGA 引擎(Seata 自动配置) @Autowired private SagaEngine sagaEngine; @PostMapping("/transfer") public String transfer(TransferDTO dto) { // 1. 构建状态机上下文(传递业务参数) Map<String, Object> context = new HashMap<>(); context.put("fromUserId", dto.getFromUserId()); context.put("toUserId", dto.getToUserId()); context.put("amount", dto.getAmount()); context.put("transId", dto.getTransId()); // 幂等标识 // 2. 触发状态机:指定 name = "transferSaga"(匹配 JSON 中的 name) SagaInstance sagaInstance = sagaEngine.start( "transferSaga", // 核心:指定要执行的状态机名称 dto.getTransId(), // 全局事务ID(建议用业务唯一ID,便于追踪) context // 业务参数上下文 ); // 3. 检查执行结果 if (sagaInstance.getStatus() == SagaInstanceStatus.SUCCEEDED) { return "转账成功"; } else { return "转账失败:" + sagaInstance.getExMsg(); } } }三、关键细节:name的约束与进阶用法
1.name的唯一性约束
- 同一 Seata 集群中,状态机
name必须唯一(如不能同时有两个transferSaga); - 若重复,Seata 启动时会抛出
StateMachineDuplicateException异常。
2. 多环境 / 多租户:name结合tenant_id
若需多租户 / 多环境隔离,可在配置中增加tenantId,触发时指定:
json
{ "name": "transferSaga", "tenantId": "tenant_01", // 租户ID "startState": "deductMoney" }触发时指定租户:
java
运行
sagaEngine.start("transferSaga", "tenant_01", dto.getTransId(), context);3. 动态加载状态机(无需重启 Seata)
若需在运行时修改状态机配置,可通过 Seata 提供的StateMachineRepository手动注册,核心还是指定name:
java
运行
@Autowired private StateMachineRepository stateMachineRepository; public void reloadStateMachine(String jsonConfig) { // 解析 JSON 配置 StateMachineConfig config = StateMachineConfigParser.parse(jsonConfig); // 注册(name 作为唯一 Key) stateMachineRepository.save(config); }四、状态机name与全局事务的关联
- 触发状态机时,Seata 会自动创建全局事务(生成 XID),并将
name(状态机名称)写入saga_state_machine_inst表的machine_id字段; - 可通过
name查询某业务的所有状态机执行记录:sql
SELECT * FROM saga_state_machine_inst WHERE machine_id = 'transferSaga';
核心总结
- 状态机 JSON 中的
name是唯一标识,启动时 Seata 自动扫描并注册; - 业务代码触发状态机时,必须通过
name指定要执行的状态机; name关联了配置文件、内存中的状态机工厂、数据库中的执行实例,是 SAGA 状态机的核心索引。
saga状态机文件路径
Seata SAGA 状态机配置文件的存放目录有默认约定路径和自定义路径两种,核心遵循 “框架默认扫描 + 配置扩展” 的逻辑,以下按优先级和使用场景详细说明:
一、默认扫描目录(无需额外配置,开箱即用)
Seata 官方默认约定的状态机配置文件存放路径为:
plaintext
项目根目录/resources/seata/saga/- 支持的文件格式:
JSON(推荐)、YAML/YML; - 扫描规则:Seata 启动时(Spring 环境下),通过
SagaStateMachineConfigProcessor自动扫描该目录下所有.json/.yml/.yaml文件,解析并注册状态机(按name字段作为唯一标识); - 适用场景:开发 / 测试环境、单租户 / 单业务场景,无需自定义路径时优先使用。
示例目录结构:
plaintext
your-project/ ├── src/ │ └── main/ │ ├── resources/ │ │ ├── seata/ │ │ │ └── saga/ # 默认SAGA配置目录 │ │ │ ├── transferSaga.json # 转账业务状态机 │ │ │ └── orderSaga.yaml # 下单业务状态机 │ │ ├── application.yml # 项目配置文件 │ │ └── logback.xml │ └── java/ └── pom.xml二、自定义目录(通过配置指定,适配多环境 / 多目录)
若默认目录不符合项目规范(如想放在resources/saga/config/),可通过以下方式自定义扫描路径:
方式 1:Spring 配置文件(application.yml/application.properties)
yaml
# application.yml seata: saga: state-machine: config: location: classpath:saga/config/ # 自定义扫描目录(支持classpath/文件绝对路径) suffix: json,yaml # 可选:指定扫描的文件后缀,默认json,yaml,yml- 路径支持:
classpath:(类路径)、file:(本地绝对路径,如file:/opt/seata/saga/); - 多目录配置:用英文逗号分隔,如
classpath:saga/transfer/,classpath:saga/order/。
方式 2:编程式指定(动态加载)
若需在代码中动态指定配置目录(如多租户场景),可通过StateMachineConfigLoader手动加载:
java
运行
@Configuration public class SagaConfig { @Bean public StateMachineRepository stateMachineRepository() { // 自定义配置目录 String configLocation = "classpath:saga/custom/"; // 加载配置文件并注册 StateMachineConfigLoader loader = new StateMachineConfigLoader(); List<StateMachineConfig> configs = loader.load(configLocation); StateMachineRepository repository = new InMemoryStateMachineRepository(); for (StateMachineConfig config : configs) { repository.save(config); // 按name注册状态机 } return repository; } }三、关键注意事项
目录权限(生产环境)若配置文件放在服务器本地路径(如
file:/opt/seata/saga/),需保证 Seata 进程有该目录的读权限,否则会加载失败。配置文件命名规范无强制命名规则,但建议按 “业务名 + Saga + 格式后缀” 命名(如
transferSaga.json),便于维护;避免同名文件(否则会覆盖)。热加载支持(Seata 1.5.x+)高版本 Seata 支持状态机配置热加载:修改配置文件后,调用
StateMachineRepository.refresh()即可重新加载,无需重启应用,核心还是基于配置目录扫描。集群环境配置同步若 Seata 集群部署,建议将状态机配置文件放在 Nacos/Apollo 等配置中心(而非本地目录),通过以下配置读取:
yaml
seata: saga: state-machine: config: type: nacos # 配置中心类型(nacos/apollo/configmap) data-id: seata-saga-config # 配置中心的dataId group: SEATA_GROUP # 配置分组
四、核心总结
| 场景 | 推荐目录 | 配置方式 |
|---|---|---|
| 开发 / 测试环境 | resources/seata/saga/(默认) | 无需配置,开箱即用 |
| 自定义本地目录 | resources/saga/config/等 | seata.saga.state-machine.config.location |
| 集群 / 多环境 | Nacos/Apollo 配置中心 | seata.saga.state-machine.config.type |
saga状态机文件实例
| 判断类型 | 核心作用 | 配置关键字 | 典型场景 |
|---|---|---|---|
| 异常捕获(catch) | 步骤执行抛出异常时,触发补偿 | catch/compensateMethod | 扣款失败→退款、加款失败→扣回 |
| 条件分支判断 | 根据业务参数,选择不同的正向执行路径 | condition/next | 转账金额 > 1000→风控审核,否则直接加款 |
| 执行结果判断 | 根据步骤返回值,判定 “成功 / 失败”,决定是否继续 / 补偿 | resultExpression | 加款返回false→触发补偿 |
| 状态机终止判断 | 手动 / 自动判定流程是否终止(成功 / 失败 / 暂停) | end/terminate | 风控审核不通过→终止状态机 |
| 循环判断 | 满足条件时重复执行某步骤(支持次数 / 条件终止) | repeat/repeatUntil | 接口调用失败→重试 3 次 |
| 事件触发判断 | 监听外部事件,满足条件后触发步骤执行 | event/listener | 接收到 “风控通过” 事件→继续转账 |
二、核心判断逻辑(除 catch 外的关键能力)
1. 条件分支判断(condition):按业务参数走不同流程
作用:正向流程中,根据上下文参数(如金额、用户等级)选择不同的执行步骤,实现 “分支逻辑”。配置示例(转账金额 > 1000 需风控审核):
json
{ "name": "transferSaga", "startState": "deductMoney", "states": { "deductMoney": { "type": "ServiceTask", "serviceName": "accountService", "methodName": "deductMoney", "compensateMethodName": "refundMoney", // 条件分支:金额>1000→风控步骤,否则直接加款 "next": { "condition": "${transContext.amount > 1000}", // SpEL 表达式判断 "true": "riskCheck", "false": "addMoney" } }, "riskCheck": { // 风控审核步骤 "type": "ServiceTask", "serviceName": "riskService", "methodName": "check", "compensateMethodName": "cancelCheck", "next": "addMoney" // 审核通过→加款 }, "addMoney": { "type": "ServiceTask", "serviceName": "accountService", "methodName": "addMoney", "compensateMethodName": "reduceMoney", "end": true // 执行完成→状态机结束 } } }核心:使用 Spring EL(SpEL)表达式解析上下文参数,支持所有 SpEL 语法(如${transContext.userId == 1001}、${transContext.status eq 'SUCCESS'})。
2. 执行结果判断(resultExpression):按方法返回值判定结果
作用:不依赖 “抛异常”,而是根据 Service 方法的返回值,判定步骤是否执行成功,决定是否继续 / 补偿。配置示例(加款返回 false 触发补偿):
json
"addMoney": { "type": "ServiceTask", "serviceName": "accountService", "methodName": "addMoney", "compensateMethodName": "reduceMoney", // 结果判断:返回 true 则成功,否则失败(触发补偿) "resultExpression": "${returnObject == true}", "end": true }说明:
returnObject代表 Service 方法的返回值;- 若表达式结果为
false,状态机会判定该步骤失败,触发补偿流程; - 相比 “抛异常”,更适合 “非异常类失败”(如业务规则校验不通过)。
3. 状态机终止判断(end/terminate):手动控制流程结束
作用:主动判定流程是否终止,支持 “成功终止”“失败终止”“暂停终止”。
(1)正常终止(end: true)
json
"addMoney": { "type": "ServiceTask", "serviceName": "accountService", "methodName": "addMoney", "end": true // 执行成功→状态机正常终止,标记为 SUCCEEDED }(2)条件终止(terminate)
json
"riskCheck": { "type": "ServiceTask", "serviceName": "riskService", "methodName": "check", "compensateMethodName": "cancelCheck", // 风控不通过→直接终止状态机(失败) "next": { "condition": "${returnObject == 'PASS'}", "true": "addMoney", "false": { "terminate": true, // 终止状态机 "status": "FAILED", // 标记为失败 "message": "风控审核不通过" } } }4. 循环判断(repeat/repeatUntil):重复执行某步骤
作用:满足条件时重复执行某步骤(如接口重试),支持 “次数限制”“条件终止”。配置示例(加款失败重试 3 次):
json
"addMoney": { "type": "ServiceTask", "serviceName": "accountService", "methodName": "addMoney", "compensateMethodName": "reduceMoney", // 循环判断:最多重试 3 次,直到返回 true "repeat": { "times": 3, // 最大重试次数 "interval": 1000, // 重试间隔(毫秒) "repeatUntil": "${returnObject == true}" // 满足条件则停止重试 }, "end": true }注意:重试的步骤仍需保证幂等性(如通过transId防重复执行)。
5. 事件触发判断(event):监听外部事件执行步骤
作用:状态机暂停,等待外部事件触发后继续执行(如异步审核、MQ 消息触发),属于 “事件驱动型判断”。配置示例(等待风控审核事件):
json
"riskCheck": { "type": "EventTask", // 事件类型步骤 "eventName": "RISK_CHECK_PASS", // 监听的事件名 "timeout": 30000, // 超时时间(30秒) // 事件触发后执行下一步,超时则触发补偿 "onEvent": "addMoney", "onTimeout": { "compensate": true, // 超时触发补偿 "terminate": true, "status": "FAILED" } }触发外部事件代码:
java
运行
// 风控审核通过后,发送事件触发状态机继续执行 sagaEngine.triggerEvent("RISK_CHECK_PASS", transId, null);适用场景:异步流程(如人工审核、跨系统异步回调)。
三、进阶:组合判断(多规则叠加)
实际业务中,可组合多种判断逻辑,例如:
json
"deductMoney": { "type": "ServiceTask", "serviceName": "accountService", "methodName": "deductMoney", "compensateMethodName": "refundMoney", "resultExpression": "${returnObject == true}", // 结果判断 "catch": [ // 异常捕获(兜底) { "exception": "java.lang.Exception", // 捕获所有异常 "compensate": true, "terminate": true } ], "next": { // 条件分支 "condition": "${transContext.amount > 1000}", "true": "riskCheck", "false": "addMoney" } }逻辑优先级:catch(异常)> resultExpression(返回值)> condition(分支)。
四、核心总结(除 catch 外的关键判断)
- 条件分支(condition):按业务参数走不同正向流程(SpEL 表达式);
- 结果判断(resultExpression):按方法返回值判定成功 / 失败,替代异常捕获;
- 终止判断(end/terminate):主动控制状态机结束(成功 / 失败 / 暂停);
- 循环判断(repeat):重复执行步骤(重试场景);
- 事件判断(event):监听外部事件触发流程(异步场景)。
五、实操建议
- 优先使用
resultExpression替代 “抛异常”:减少异常开销,更贴合业务语义; - 条件分支使用 SpEL 时,避免复杂表达式(可封装为 Service 方法调用,如
${riskService.checkAmount(transContext.amount)}); - 循环重试必须加次数限制,避免死循环;
- 事件触发需设置超时时间,防止状态机永久暂停。