saga文件使用
2026/4/18 1:20:16 网站建设 项目流程

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)。
  • 返回值:建议返回voidBooleantrue表示成功,false表示失败);若返回非布尔类型,状态机会默认判定为执行成功。
2. 幂等性要求(核心!)
  • 正向 / 补偿方法都必须保证幂等(因为 SAGA 状态机可能重试失败的步骤);示例:给转账请求加transId唯一索引,方法执行前先检查transId是否已处理。
3. 无事务嵌套
  • Service 层方法不要加@Transactional本地事务(除非是独立的本地逻辑);SAGA 是 “无锁补偿” 模式,本地事务会导致补偿方法无法回滚已提交的业务数据。
4. 异常处理
  • 正向方法抛出异常 → 状态机触发该步骤的补偿方法,然后按 “反向顺序” 执行前面步骤的补偿;
  • 补偿方法建议捕获异常(仅打印日志),避免补偿失败导致状态机卡死。

四、状态机调用 Service 方法的核心原理

  1. Bean 加载:Seata 启动时,会扫描 Spring 容器中的 Service Bean(如accountService),存入 Bean 工厂;
  2. 方法反射调用:状态机执行 Step 时,通过serviceName找到对应的 Bean,通过methodName反射调用方法;
  3. 参数透传:状态机上下文(如transIdamount等)会作为参数传入 Service 方法,保证正向 / 补偿方法能拿到相同的业务参数;
  4. 补偿触发逻辑
    • 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" }

六、关键总结

  1. SAGA 状态机直接配置 Service 层的正向 / 补偿方法,不建议配置 Controller 或 DAO 层(Service 层是业务逻辑聚合层,适合作为补偿粒度);
  2. 正向方法是 “业务推进逻辑”,补偿方法是 “业务回滚逻辑”,两者需一一对应;
  3. Service 方法必须保证幂等、无强事务嵌套,这是 SAGA 补偿成功的核心;
  4. 状态机通过 “反射 + 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 启动时:

  1. 扫描resources/seata/saga下的所有 JSON/YAML 配置;
  2. 解析每个配置的name字段,作为唯一标识;
  3. 将状态机定义存入saga_state_machine表(name字段对应表的name列);
  4. 同时加载到内存中的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';

核心总结

  1. 状态机 JSON 中的name唯一标识,启动时 Seata 自动扫描并注册;
  2. 业务代码触发状态机时,必须通过name指定要执行的状态机
  3. 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; } }

三、关键注意事项

  1. 目录权限(生产环境)若配置文件放在服务器本地路径(如file:/opt/seata/saga/),需保证 Seata 进程有该目录的读权限,否则会加载失败。

  2. 配置文件命名规范无强制命名规则,但建议按 “业务名 + Saga + 格式后缀” 命名(如transferSaga.json),便于维护;避免同名文件(否则会覆盖)。

  3. 热加载支持(Seata 1.5.x+)高版本 Seata 支持状态机配置热加载:修改配置文件后,调用StateMachineRepository.refresh()即可重新加载,无需重启应用,核心还是基于配置目录扫描。

  4. 集群环境配置同步若 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 外的关键判断)

  1. 条件分支(condition):按业务参数走不同正向流程(SpEL 表达式);
  2. 结果判断(resultExpression):按方法返回值判定成功 / 失败,替代异常捕获;
  3. 终止判断(end/terminate):主动控制状态机结束(成功 / 失败 / 暂停);
  4. 循环判断(repeat):重复执行步骤(重试场景);
  5. 事件判断(event):监听外部事件触发流程(异步场景)。

五、实操建议

  1. 优先使用resultExpression替代 “抛异常”:减少异常开销,更贴合业务语义;
  2. 条件分支使用 SpEL 时,避免复杂表达式(可封装为 Service 方法调用,如${riskService.checkAmount(transContext.amount)});
  3. 循环重试必须加次数限制,避免死循环;
  4. 事件触发需设置超时时间,防止状态机永久暂停。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询