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)}); - 循环重试必须加次数限制,避免死循环;
- 事件触发需设置超时时间,防止状态机永久暂停。