多线程与微服务下的事务

在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. 主事务在此方法结束时提交
    }
}

可能发生的问题

  1. 数据不一致:如果主线程事务成功提交,但随后子线程中的短信发送失败,系统状态将不一致(用户信息已更新,但通知未发出)。
  2. 异常处理脱节 :子线程抛出的异常无法被主线程的@Transactional机制捕获,因此子线程的失败不会导致主事务回滚。
  3. 连接资源耗尽:每个子线程都可能创建新的数据库连接,在高并发下可能导致数据库连接池被快速耗尽。
  4. 死锁风险:如果多个线程并发操作同一数据,且未合理使用锁机制,极易引发死锁。

💡 多线程事务的解决方案

针对上述问题,可以根据业务对一致性的要求程度,选择不同的解决方案。

方案一:最终一致性方案(推荐用于非核心业务)

对于非核心、可补偿的操作(如发送通知、记录日志),追求最终一致性通常是更平衡的选择。核心思想是先记录后处理

  1. 本地事务表/日志记录 :在主事务中,将异步任务的信息(如SmsTask)与业务数据在同一个数据库事务中持久化。
  2. 异步处理:主事务提交后,由一个独立的异步组件从表中读取任务并执行。
  3. 补偿机制:如果异步任务执行失败,记录状态,并通过定时任务进行重试。
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等同步工具,协调主线程和所有子线程的事务状态,实现近似"跨线程事务"的效果。

  • 核心机制
    1. 主线程作为协调者 :主线程方法上标注自定义注解(如@MainTransaction),并声明子线程数量。
    2. 子线程作为参与者 :子线程方法上标注另一注解(如@SonTransaction)。
    3. AOP拦截与同步:AOP切面会拦截这些方法。主线程会等待所有子线程业务逻辑执行完毕,然后根据所有线程的执行结果(成功或失败)统一决定是提交还是回滚每个线程的独立事务。
  • 实现复杂度:这种方法需要编写复杂的AOP代码,并谨慎处理线程同步和异常,稍有不慎可能导致死锁或长时间等待。
方案四:分布式事务框架(用于强一致性核心业务)

如果业务要求多个线程的操作必须强一致(如金融交易中的核心步骤),那么最可靠的方案是引入分布式事务管理器,如Seata ,或使用支持事务消息的消息队列(如RocketMQ)。

这些框架通过全局事务ID(XID)将不同线程(甚至不同服务)中的本地事务绑定在一起,通过两阶段提交(2PC)等协议来保证所有参与者同时提交或回滚。

优势 :能实现真正的跨线程/跨服务事务强一致性。
劣势:性能开销大,架构复杂,通常用于分布式微服务场景。

⚙️ 实践建议与最佳实践

无论选择哪种方案,以下几点都至关重要:

  1. 合理配置线程池 :避免使用Executors直接创建无界线程池,推荐使用ThreadPoolTaskExecutor进行显式配置,控制核心线程数、最大线程数和队列容量,防止资源耗尽。
  2. 明确事务传播行为 :在子线程方法上,根据场景使用@Transactional(propagation = Propagation.REQUIRES_NEW)明确指定需要新事务。
  3. 精细化异常处理:确保在子线程内部捕获异常并妥善处理(如记录状态),避免异常抛出导致线程意外终止,而主线程却无法感知。
  4. 保持事务短小精悍:长时间的事务会持有数据库锁,增加死锁概率,并影响系统吞吐量。尽量避免在事务中进行远程调用、文件IO等耗时操作。
  5. 避免@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端。

其基本工作流程可概括为以下步骤:

  1. 开启全局事务:TM向TC申请开启一个全局事务,TC生成一个全局唯一的XID。
  2. 传播XID:该XID会在微服务调用链的上下文中进行传播。
  3. 注册分支事务:每个服务的RM将本地事务作为分支事务注册到TC,纳入该XID对应的全局事务管理。
  4. 决议全局事务:TM根据所有分支事务的执行结果,向TC发起全局提交或回滚的决议。
  5. 驱动分支事务: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协议实现的两阶段提交。一阶段准备但不提交事务;二阶段协调所有参与者统一提交或回滚。 强一致性;对业务无侵入。 同步阻塞,性能差;资源锁定时间长,易影响并发。 需要强一致性且并发压力不大的内部应用,通常在企业内部系统中使用。

🛠️ 重要机制与特性

  1. 全局锁与写隔离

    在AT模式下,Seata通过全局锁来实现写隔离,防止不同全局事务同时修改同一数据产生脏写。分支事务在一阶段提交前会尝试获取全局锁,只有获取成功才能提交,否则会重试或回滚。

  2. 高可用部署

    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)来实现数据回滚,对业务代码无任何侵入。

  • 实现步骤

    1. 添加依赖与配置 :在 pom.xml 中加入 Seata 依赖,并在 application.yml 中配置 Seata Server 的连接信息(如注册中心地址)。
    2. 创建用于存储 Seata 全局事务和分支事务信息的数据库表(如 global_table, branch_table)。
    3. 使用注解 :在全局事务的发起方法上添加 @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(服务任务) :这是核心节点,用于执行具体的业务逻辑。
    • ServiceNameServiceMethod 指定要调用的服务与方法。
    • 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文件的位置等信息 。

⚙️ 配置与运行要点
  1. 状态机配置 :确保状态机JSON文件(如 statelang/*.json)被正确加载 。
  2. 服务注册 :实现的服务类(如 InventoryActionImpl)需要被Spring容器管理(使用 @Service 并注明状态机JSON中定义的名称)。
  3. 异常处理 :在状态机的JSON定义中,通过 Status 块和 Catch 配置(示例JSON已展示)来映射服务执行结果(成功SU、失败FA、未知UN)并决定状态流转 。
  4. 补偿触发 :当某个 ServiceTask 执行失败(返回false或抛出异常)时,Seata会根据状态机定义自动触发已成功节点的补偿操作,顺序与执行顺序相反 。
🧪 测试SAGA事务
  • 成功场景:库存扣减 → 余额扣减 → 订单完成。
  • 失败与回滚场景 :如果余额扣减失败,Seata会自动调用已成功的"库存扣减"服务的补偿方法(compensateReduce)进行回滚 。
4. XA 模式

XA 模式依赖数据库本身的 XA 协议,实现强一致性,但资源锁定时间长,性能最低。

  • 配置方式 :在 application.yml 中简单配置即可开启。

    yaml 复制代码
    seata:
      data-source-proxy-mode: XA
  • 使用方式 :与 AT 模式类似,在事务起点使用 @GlobalTransactional 注解。

💎 总结与选型建议

选择哪种模式,归根结底是对一致性、性能、开发复杂度进行权衡:

  • 首选 AT 模式:对于绝大多数基于关系型数据库的业务,AT 模式因其无侵入和良好的性能平衡是最佳选择。
  • 考虑 TCC 模式:当业务对性能有极致要求,或需要接入像 Redis、第三方 API 这类非事务性资源时,可以选择 TCC,但要接受其编码复杂度。
  • SAGA 适用长流程:如果业务链路非常长,且可以接受最终一致性,SAGA 模式是合适的。
  • XA 用于强一致:仅在需要强一致性且并发不高的内部系统中考虑。
相关推荐
zhglhy2 小时前
QLExpress Java动态脚本引擎使用指南
java
小瓦码J码2 小时前
使用AWS SDK实现S3桶策略配置
java
廋到被风吹走2 小时前
【Spring】Spring Cloud 配置中心动态刷新与 @RefreshScope 深度原理
java·spring·spring cloud
牧小七2 小时前
springboot 配置访问上传图片
java·spring boot·后端
七夜zippoe2 小时前
微服务架构:FastAPI实战指南与Kubernetes部署
微服务·架构·负载均衡·fastapi·consul
攀登的牵牛花2 小时前
前端向架构突围系列 - 框架设计(六):解析接口职责的单一与隔离
前端·架构
涵涵(互关)2 小时前
JavaScript 对大整数(超过 2^53 - 1)的精度丢失问题
java·javascript·vue.js
进击的丸子2 小时前
基于虹软Linux Pro SDK的多路RTSP流并发接入、解码与帧级处理实践
java·后端·github
小北方城市网2 小时前
微服务架构设计实战指南:从拆分到落地,构建高可用分布式系统
java·运维·数据库·分布式·python·微服务