Spring 核心思想与企业级最佳特性(实践级)事务相关

ModelEngine·创作计划征文活动 10w+人浏览 1.6k人参与

复杂业务场景事务适配案例(嵌套事务+跨服务事务)

以下结合电商订单履约这一典型复杂场景,分别说明嵌套事务、跨服务事务的适配方案与落地实现,覆盖核心逻辑、问题痛点、解决方案及关键代码示例。

一、场景背景

电商平台“订单支付后履约”核心流程:

  1. 订单支付成功后,触发核心操作:更新订单状态→扣减商品库存→生成物流单;
  2. 扩展操作:扣减用户优惠券→增加用户积分→推送支付成功通知;
  3. 跨系统依赖:库存系统、物流系统、用户积分系统为独立微服务,订单系统为核心协调方;
  4. 核心要求:所有操作需保证数据一致性(如库存扣减失败则订单回滚,积分增加失败需补偿)。
    在这里插入图片描述

二、案例1:嵌套事务(单服务内多层级事务)

1. 业务痛点

订单服务内,“更新订单状态”作为父事务,“扣减库存(本地库存表)+ 生成物流单(本地物流表)”作为子事务:

  • 若子事务(如库存扣减)失败,需回滚子事务且触发父事务(订单状态)回滚;
  • 若仅物流单生成失败,需仅回滚物流单子事务,保留库存扣减(避免重复扣减),同时订单状态标记为“履约异常”。

2. 适配方案(Spring 声明式事务+保存点)

利用 Spring 事务传播行为(Propagation)+ 保存点(Savepoint),实现嵌套事务的精细化控制:

事务层级传播行为作用
父事务REQUIRED(默认)订单状态更新,作为核心事务,所有子事务复用该事务,子事务失败则整体回滚
子事务1NESTED(嵌套)库存扣减,绑定父事务保存点,失败仅回滚该保存点范围,不影响父事务核心逻辑
子事务2NESTED(嵌套)物流单生成,独立保存点,失败仅回滚自身,父事务可捕获异常并标记订单状态

3. 代码实现

@Service
@Transactional // 父事务:REQUIRED,默认传播行为
public class OrderFulfillmentService {

    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private StockService stockService;
    @Autowired
    private LogisticsService logisticsService;

    /**
     * 订单履约核心方法(父事务)
     */
    public void fulfillOrder(Long orderId, Long skuId, Integer num) {
        try {
            // 1. 父事务核心:更新订单状态为“已支付”
            orderMapper.updateStatus(orderId, "PAID");

            // 2. 子事务1:扣减本地库存(嵌套事务,绑定保存点)
            stockService.deductStock(skuId, num);

            // 3. 子事务2:生成本地物流单(嵌套事务,独立保存点)
            try {
                logisticsService.createLogisticsOrder(orderId, skuId);
            } catch (Exception e) {
                // 物流单生成失败:仅回滚子事务2,父事务不回滚,标记订单异常
                orderMapper.updateStatus(orderId, "FULFILL_EXCEPTION");
                log.error("物流单生成失败,订单标记为异常:{}", orderId, e);
                // 无需抛出异常,避免父事务回滚
            }
        } catch (Exception e) {
            // 库存扣减失败:触发父事务整体回滚(订单状态恢复为“未支付”)
            log.error("订单履约失败,整体回滚:{}", orderId, e);
            throw new BusinessException("履约失败,订单回滚"); // 运行时异常触发事务回滚
        }
    }
}

// 库存子事务(嵌套)
@Service
public class StockService {
    @Autowired
    private StockMapper stockMapper;

    @Transactional(propagation = Propagation.NESTED) // 嵌套事务,基于保存点
    public void deductStock(Long skuId, Integer num) {
        // 校验库存
        StockDO stock = stockMapper.selectBySkuId(skuId);
        if (stock.getAvailableNum() < num) {
            throw new BusinessException("库存不足,扣减失败");
        }
        // 扣减库存
        stockMapper.deduct(skuId, num);
    }
}

// 物流单子事务(嵌套)
@Service
public class LogisticsService {
    @Autowired
    private LogisticsMapper logisticsMapper;

    @Transactional(propagation = Propagation.NESTED) // 嵌套事务,独立保存点
    public void createLogisticsOrder(Long orderId, Long skuId) {
        // 生成物流单(模拟异常:如物流系统临时故障)
        if (RandomUtils.nextBoolean()) {
            throw new BusinessException("物流系统异常,生成物流单失败");
        }
        logisticsMapper.insert(new LogisticsDO(orderId, skuId, "PENDING"));
    }
}

4. 关键效果

  • 库存扣减失败:父事务(订单状态更新)+ 所有子事务全部回滚,订单回到“未支付”状态,数据无不一致;
  • 物流单生成失败:仅物流单子事务回滚,库存扣减、订单状态更新保留,订单标记为“履约异常”,后续可人工介入处理。

三、案例2:跨服务事务(多微服务协同)

在这里插入图片描述

1. 业务痛点

订单履约流程中,“扣减用户优惠券”(用户服务)、“增加用户积分”(积分服务)为独立微服务,存在以下问题:

  • 跨服务调用无天然事务保证,若订单服务扣减库存后,积分服务调用超时,会导致“库存已扣、积分未加”的数据不一致;
  • 服务间网络波动、节点故障可能导致部分操作执行成功,部分失败。

2. 适配方案(SAGA模式+Seata 分布式事务)

选择 SAGA 模式(长事务拆分+补偿)适配电商“最终一致性”需求(无需强实时一致性,允许短时间数据不一致,最终补偿至一致),基于 Seata 框架落地:

(1)SAGA 核心流程
步骤正向操作补偿操作(反向)负责服务
1订单服务:更新订单状态订单服务:回滚订单状态为“未支付”订单服务
2库存服务:扣减商品库存库存服务:恢复商品库存库存服务
3用户服务:扣减优惠券用户服务:恢复用户优惠券用户服务
4积分服务:增加用户积分积分服务:扣减用户新增积分积分服务
5通知服务:推送支付通知通知服务:推送“履约失败”补偿通知通知服务
(2)Seata 适配关键
  • 事务协调:Seata TC(事务协调器)统一生成 XID(分布式事务ID),串联所有服务的操作;
  • 补偿触发:若某一步正向操作失败,Seata 自动触发前序步骤的补偿操作;
  • 幂等设计:所有正向/补偿操作需保证幂等(如扣减积分时校验XID是否已执行),避免重复操作。

3. 代码实现

(1)订单服务(SAGA 发起方)
@Service
public class OrderDistributedService {

    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private StockFeignClient stockFeign; // 库存服务Feign客户端
    @Autowired
    private UserFeignClient userFeign; // 用户服务Feign客户端
    @Autowired
    private PointFeignClient pointFeign; // 积分服务Feign客户端

    /**
     * Seata SAGA 分布式事务入口
     */
    @SagaTransactional(
        businessKey = "orderId", // 事务标识
        compensation = "rollbackFulfillOrder" // 补偿方法
    )
    public void fulfillOrderDistributed(Long orderId, Long skuId, Integer num, Long userId) {
        // 步骤1:更新订单状态(正向操作)
        orderMapper.updateStatus(orderId, "PAID");
        // 步骤2:调用库存服务扣减库存
        stockFeign.deductStock(skuId, num, orderId);
        // 步骤3:调用用户服务扣减优惠券
        userFeign.deductCoupon(userId, orderId);
        // 步骤4:调用积分服务增加积分
        pointFeign.addPoint(userId, num * 10, orderId); // 消费1件商品加10积分
    }

    /**
     * SAGA 补偿方法(反向操作)
     */
    public void rollbackFulfillOrder(Long orderId, Long skuId, Integer num, Long userId) {
        // 补偿步骤1:回滚订单状态
        orderMapper.updateStatus(orderId, "UNPAID");
        // 补偿步骤2:恢复库存
        stockFeign.recoverStock(skuId, num, orderId);
        // 补偿步骤3:恢复优惠券
        userFeign.recoverCoupon(userId, orderId);
        // 补偿步骤4:扣减新增积分
        pointFeign.deductPoint(userId, num * 10, orderId);
    }
}
(2)积分服务(参与方,含幂等设计)
@Service
public class PointService {

    @Autowired
    private PointMapper pointMapper;
    @Autowired
    private PointLogMapper pointLogMapper; // 积分操作日志(幂等校验)

    /**
     * 正向操作:增加用户积分
     */
    @GlobalTransactional // Seata 标记分布式事务参与方
    public void addPoint(Long userId, Integer point, String xid) {
        // 幂等校验:XID已执行则直接返回
        if (pointLogMapper.existsByXid(xid)) {
            return;
        }
        // 增加积分
        pointMapper.increase(userId, point);
        // 记录操作日志(绑定XID)
        pointLogMapper.insert(new PointLogDO(xid, userId, point, "ADD"));
    }

    /**
     * 补偿操作:扣减新增积分
     */
    public void deductPoint(Long userId, Integer point, String xid) {
        // 幂等校验
        if (!pointLogMapper.existsByXid(xid)) {
            return;
        }
        // 扣减积分
        pointMapper.decrease(userId, point);
        // 更新日志状态为补偿完成
        pointLogMapper.updateStatus(xid, "COMPENSATE");
    }
}

4. 关键效果

  • 正常流程:所有服务正向操作执行完成,分布式事务提交,数据最终一致;
  • 异常流程(如积分服务超时):Seata 触发补偿流程,自动回滚订单状态、恢复库存、恢复优惠券,扣减未生效的积分,保障最终数据一致性;
  • 故障恢复:若某服务宕机,Seata TC 会记录事务状态,服务重启后可重试补偿操作,避免数据残留。

四、共性适配注意事项

  1. 避免长事务:嵌套事务中减少大事务范围(如拆分非核心操作至事务外),跨服务事务中拆分长流程为多个短事务,降低锁占用和超时风险;
  2. 日志追踪:所有事务操作绑定唯一事务ID(XID/订单ID),记录操作日志(正向/补偿),便于问题定位和人工兜底;
  3. 幂等与防重:所有操作需保证幂等(如基于XID/业务ID校验),避免重复执行导致数据错误;
  4. 降级兜底:跨服务事务中配置降级策略(如积分服务不可用时,先标记积分待补发,后续异步补偿),避免核心流程阻塞。

几种特殊的应用场景

允许子事务部分失败的业务场景及跨事务解决方案

一、允许子事务部分失败的场景:成因、示例与实现

1. 场景出现的核心原因

企业级业务中,核心流程必须保证完成,非核心流程允许失败且不影响核心结果 是这类场景的底层逻辑,具体成因包括:

  • 核心诉求:核心业务(如订单支付、库存扣减)的原子性必须保障,但附属业务(如物流单生成、消息推送)的失败不应该阻断核心流程;
  • 资源约束:非核心业务依赖的第三方系统(如物流中台、短信网关)可能临时不可用,若强绑定核心事务会导致核心流程阻塞;
  • 成本权衡:非核心业务的实时一致性可让步于最终一致性(如物流单生成失败可后续异步重试,无需回滚已完成的库存扣减);
  • 用户体验:核心操作(如支付)完成后,用户更关注“订单生效”,而非即时看到物流单,局部失败可通过后续补偿修复。

2. 典型业务示例(电商订单履约)

场景描述

用户在电商平台支付订单后,核心流程是“更新订单状态为已支付 + 扣减商品库存”(必须成功),附属流程包括“生成物流单、推送支付成功短信、增加用户积分”(允许部分失败)。

  • 核心诉求:只要订单状态更新和库存扣减完成,订单就视为“履约中”,物流单生成失败(如物流系统宕机)、短信推送失败(如短信网关限流)不能回滚订单和库存;
  • 失败处理:非核心流程失败后,标记“履约异常”,通过异步任务(如定时重试)补全物流单/积分,短信失败则通过APP推送兜底。
技术实现(Spring 嵌套事务 + 保存点)
@Service
@Transactional(rollbackFor = Exception.class) // 外层核心事务
public class OrderService {
    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private StockService stockService;
    @Autowired
    private LogisticsService logisticsService;
    @Autowired
    private SmsService smsService;
    @Autowired
    private PointService pointService;

    public void fulfillOrder(Long orderId, Long skuId, Integer num, Long userId) {
        try {
            // 1. 核心操作:更新订单状态(必须成功,失败则整体回滚)
            orderMapper.updateStatus(orderId, "PAID");
            // 2. 核心操作:扣减库存(必须成功,失败则整体回滚)
            stockService.deductStock(skuId, num);

            // 3. 非核心操作1:生成物流单(允许失败,不影响核心)
            try {
                logisticsService.createLogisticsOrder(orderId, skuId);
            } catch (Exception e) {
                log.error("物流单生成失败,订单ID:{}", orderId, e);
                // 标记异常,后续异步重试
                orderMapper.markException(orderId, "LOGISTICS_FAILED");
            }

            // 4. 非核心操作2:推送短信(允许失败,不影响核心)
            try {
                smsService.sendPaidSms(userId, orderId);
            } catch (Exception e) {
                log.error("短信推送失败,用户ID:{}", userId, e);
                // 兜底:推送APP消息
                appMsgService.sendPaidMsg(userId, orderId);
            }

            // 5. 非核心操作3:增加积分(允许失败,不影响核心)
            try {
                pointService.addPoint(userId, num * 10);
            } catch (Exception e) {
                log.error("积分增加失败,用户ID:{}", userId, e);
                // 记录积分补发任务,定时任务处理
                pointTaskMapper.addRetryTask(userId, num * 10, orderId);
            }
        } catch (Exception e) {
            // 核心操作失败,整体回滚
            log.error("订单核心流程失败,订单ID:{}", orderId, e);
            throw new BusinessException("订单履约失败,请重试");
        }
    }
}

// 库存服务:核心子事务(失败则外层回滚)
@Service
public class StockService {
    @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
    public void deductStock(Long skuId, Integer num) {
        StockDO stock = stockMapper.selectBySkuId(skuId);
        if (stock.getStock() < num) {
            throw new BusinessException("库存不足");
        }
        stockMapper.deduct(skuId, num);
    }
}

// 物流服务:非核心子事务(失败仅捕获,不抛异常)
@Service
public class LogisticsService {
    @Transactional(propagation = Propagation.NESTED) // 嵌套事务,失败仅回滚自身
    public void createLogisticsOrder(Long orderId, Long skuId) {
        // 模拟物流系统宕机异常
        if (logisticsSystemUnavailable()) {
            throw new BusinessException("物流系统临时不可用");
        }
        logisticsMapper.insert(new LogisticsDO(orderId, skuId, "PENDING"));
    }
}
关键效果
  • 核心流程(订单状态、库存)失败 → 整体回滚,用户订单回到“未支付”状态,无数据不一致;
  • 非核心流程(物流单、短信、积分)任意一个失败 → 仅记录异常/重试任务,核心流程保留,用户感知“订单已支付”,后台异步修复失败项。

二、跨服务事务的典型企业场景与解决方案

跨服务事务的核心痛点是“多微服务协同操作时,无法通过本地事务保证数据一致性”,以下是企业最常用的3类场景及落地方案:

场景1:电商“下单-支付-履约”全链路(最终一致性)

场景描述

订单服务、支付服务、库存服务、物流服务为独立微服务,用户下单后需完成:创建订单→扣减库存→发起支付→支付成功后更新订单状态→生成物流单。

  • 痛点:支付服务回调超时/失败,会导致“库存已扣、订单未更新”;物流服务失败,会导致“订单已支付、物流单未生成”;
  • 企业级诉求:无需强实时一致性,允许短时间数据不一致,但最终必须一致(如库存扣减后支付失败,需恢复库存)。
解决方案:SAGA模式(基于Seata)
  • 流程设计:拆分为“正向操作+补偿操作”,由Seata TC(事务协调器)统一协调:
    正向操作补偿操作
    订单服务:创建订单订单服务:删除订单
    库存服务:扣减库存库存服务:恢复库存
    支付服务:发起支付支付服务:取消支付
    订单服务:更新为已支付订单服务:回滚为未支付
    物流服务:生成物流单物流服务:删除物流单
  • 核心优势:支持长事务拆分,失败后自动触发补偿,适配电商“最终一致性”诉求,是企业最常用的分布式事务方案;
  • 企业落地细节:所有操作加幂等校验(基于订单ID/XID),补偿操作异步重试(避免同步阻塞),关键步骤记录操作日志(便于人工兜底)。

场景2:金融“转账-记账-通知”(强一致性)

场景描述

银行转账场景:用户A转账1万元给用户B,需完成:扣减A账户余额→增加B账户余额→生成转账流水→推送转账通知。涉及账户服务、流水服务、通知服务3个微服务。

  • 痛点:金融场景要求“强一致性”,不允许“A扣钱、B未加钱”的情况;
  • 企业级诉求:所有操作要么全部成功,要么全部失败,无中间状态。
解决方案:TCC模式(Try-Confirm-Cancel)
  • 流程设计:将每个操作拆分为3个阶段,保证原子性:
    1. Try阶段:账户服务冻结A的1万元(标记为“待转账”),检查B账户状态;
    2. Confirm阶段:账户服务扣减A的冻结金额,增加B的余额;流水服务生成转账流水;通知服务推送成功通知;
    3. Cancel阶段:若Confirm失败,账户服务解冻A的冻结金额,流水服务删除草稿流水,通知服务推送失败通知;
  • 核心优势:强一致性,适配金融、支付等对数据一致性要求极高的场景;
  • 企业落地细节:Try阶段预留资源(冻结金额),Confirm/Cancel阶段保证幂等,通过分布式锁避免并发问题。

Confirm/Cancel 执行多次的原因及幂等/分布式锁的作用

一、Confirm/Cancel 执行多次的核心原因

在 TCC 分布式事务模式中,Confirm(确认)、Cancel(取消)阶段出现重复执行,本质是分布式环境下的网络/节点不确定性,核心场景可归纳为 3 类:

1. 网络异常导致的重试

  • 场景1:事务协调器(TC)向业务服务发送 Confirm 指令,服务执行成功但网络中断,TC 未收到“执行成功”响应,判定为执行失败并触发重试;
  • 场景2:业务服务接收 Confirm 指令后,执行过程中网络超时(如数据库慢查询),TC 未在超时时间内收到反馈,启动重试机制。

2. 节点故障导致的重试

  • 场景1:业务服务执行 Confirm 过程中,服务节点宕机(如 JVM 崩溃、服务器断电),TC 感知节点不可用后,将指令路由至其他节点重试;
  • 场景2:TC 自身故障重启,恢复后发现未完成的 TCC 事务,重新触发 Confirm/Cancel 指令。

3. 人工兜底触发的重复执行

企业级场景中,若 TCC 事务长时间未完成(如超过 10 分钟),运维人员会通过后台系统手动触发 Confirm/Cancel 操作,可能与自动重试形成重复执行。

典型例子(金融转账)

用户 A 转账 1 万元给用户 B,Try 阶段冻结 A 的 1 万元后,TC 发起 Confirm 指令扣减 A 冻结金额、增加 B 余额:

  • 若 Confirm 执行成功,但 TC 未收到响应,会再次发送 Confirm 指令;
  • 若未做幂等控制,会导致 A 被重复扣减(扣 2 万)、B 被重复加钱(加 2 万),引发资金账目错误。

二、Confirm/Cancel 保证幂等的核心目的

幂等性(多次执行同一操作,结果与单次执行一致)是解决重复执行的关键,针对 TCC 场景的核心价值:

1. 避免数据错误

  • Confirm 幂等:即使多次执行,仅扣减一次 A 的冻结金额、增加一次 B 的余额,不会重复记账;
  • Cancel 幂等:即使多次执行,仅解冻一次 A 的冻结金额,不会重复释放资源。

2. 适配重试机制

TCC 框架(如 Seata TCC)的重试是“默认策略”,幂等性保证了重试不会破坏数据一致性,无需额外判断“是否已执行过”。

幂等实现的典型方式(企业级落地)

实现方式适用场景示例(转账场景)
唯一业务 ID 校验所有 TCC 场景基于转账订单号+操作类型(Confirm/Cancel)创建唯一索引,执行前校验是否已处理
状态机控制有明确状态流转的场景转账流水状态为“待确认”时才执行 Confirm,执行后改为“已确认”,重复执行时直接返回成功
版本号(CAS)高并发更新场景账户表增加 version 字段,更新时带版本号,重复执行时版本不匹配则失败

三、分布式锁的补充作用

幂等性解决“重复执行同一操作的结果一致性”,但无法解决“并发执行同一操作的竞争问题”,分布式锁的核心价值是避免并发执行

1. 并发问题场景

若 TC 同时向两个节点发送同一笔转账的 Confirm 指令,即使做了幂等校验,也可能出现:

  • 节点 1 校验“未处理”→ 准备扣减 A 金额;
  • 节点 2 同时校验“未处理”→ 也准备扣减 A 金额;
  • 最终两个节点都执行扣减,突破幂等校验(因为校验和执行非原子操作)。

2. 分布式锁的作用

  • 执行 Confirm/Cancel 前,先获取基于“转账订单号”的分布式锁(如 Redis 锁、Zookeeper 锁);
  • 只有获取锁的节点能执行操作,其他节点需等待或直接返回;
  • 保证同一笔事务的 Confirm/Cancel 操作“串行执行”,避免并发竞争导致的幂等校验失效。

四、核心总结

问题产生原因解决方案核心目标
Confirm/Cancel 多次执行网络超时、节点故障、人工兜底幂等性多次执行结果与单次一致
Confirm/Cancel 并发执行多节点同时接收重试指令分布式锁同一操作串行执行,避免竞争

企业级 TCC 落地中,幂等是基础,分布式锁是补充:幂等保证“重复执行无害”,分布式锁减少“重复执行的概率”,两者结合才能彻底解决 Confirm/Cancel 阶段的一致性问题。

场景3:零售“库存同步-订单同步”(跨系统数据同步)

场景描述

零售企业的线下门店系统(ERP)和线上商城系统(电商平台)为独立系统,线下门店卖出商品后,需同步扣减线上商城的库存,同时同步生成线上订单记录。

  • 痛点:跨系统调用无事务保证,ERP扣减库存后,电商平台订单同步失败,导致“线上库存与线下不一致”;
  • 企业级诉求:数据最终一致,且适配跨系统(非微服务)的通信场景。
解决方案:可靠消息最终一致性(基于RocketMQ/ Kafka)
  • 流程设计:通过“消息队列+本地事务表”实现:
    1. ERP系统扣减库存,同时向本地事务表插入“库存扣减+订单同步”任务;
    2. 本地事务提交后,向MQ发送“库存扣减完成”消息;
    3. 电商平台消费消息,生成订单并扣减线上库存;
    4. 若电商平台消费失败,MQ重试(最多3次),仍失败则标记为异常,人工介入;
    5. 定时任务扫描ERP本地事务表,补发未成功发送的消息;
  • 核心优势:适配跨系统、跨语言的场景,无需引入分布式事务框架,企业改造成本低;
  • 企业落地细节:消息体携带唯一业务ID(如门店订单号),消费端做幂等校验,消息设置死信队列(失败消息归档)。

三、核心总结

场景类型企业诉求典型解决方案适用行业
子事务部分失败核心流程保障,非核心容错嵌套事务+保存点电商、零售
跨服务最终一致性允许短时间不一致,最终一致SAGA(Seata)电商、物流
跨服务强一致性无中间状态,全成或全败TCC(Seata)金融、支付
跨系统数据同步低成本、最终一致可靠消息+本地事务表零售、政企

企业选择方案的核心原则:优先适配业务一致性诉求,而非追求技术完美——如电商场景无需强一致性,用SAGA即可;金融场景必须强一致,才用TCC;跨系统场景优先用消息队列,降低架构复杂度。

以上案例覆盖了单服务内嵌套事务的精细化控制、跨服务分布式事务的最终一致性保障,是电商、金融等复杂业务场景中事务适配的典型落地方式。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Coder_Boy_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值