saga文件使用

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. 事件触发需设置超时时间,防止状态机永久暂停。
相关推荐
墨夶6 小时前
交易所安全保卫战:从冷钱包到零知识证明,让黑客连边都摸不着!
java·安全·区块链·零知识证明
山风wind6 小时前
Tomcat三步搭建局域网文件共享
java·tomcat
a努力。6 小时前
网易Java面试被问:偏向锁在什么场景下反而降低性能?如何关闭?
java·开发语言·后端·面试·架构·c#
小新1106 小时前
Spring boot 之 Hello World 番外:如何修改端口号
java·spring boot·后端
百花~6 小时前
Spring Boot 日志~
java·spring boot·后端
李白的粉6 小时前
基于springboot的火锅店管理系统(全套)
java·spring boot·毕业设计·课程设计·源代码·火锅店管理系统
狂奔小菜鸡6 小时前
Day32 | Java Stream流式编程详解
java·后端·java ee
雨中飘荡的记忆6 小时前
Canal深度解析:MySQL增量数据订阅与消费实战
java
hhzz6 小时前
Activiti7工作流(五)流程操作
java·activiti·工作流引擎·工作流