复杂业务场景事务适配案例(嵌套事务+跨服务事务)
以下结合电商订单履约这一典型复杂场景,分别说明嵌套事务、跨服务事务的适配方案与落地实现,覆盖核心逻辑、问题痛点、解决方案及关键代码示例。
一、场景背景
电商平台“订单支付后履约”核心流程:
- 订单支付成功后,触发核心操作:更新订单状态→扣减商品库存→生成物流单;
- 扩展操作:扣减用户优惠券→增加用户积分→推送支付成功通知;
- 跨系统依赖:库存系统、物流系统、用户积分系统为独立微服务,订单系统为核心协调方;
- 核心要求:所有操作需保证数据一致性(如库存扣减失败则订单回滚,积分增加失败需补偿)。

二、案例1:嵌套事务(单服务内多层级事务)
1. 业务痛点
订单服务内,“更新订单状态”作为父事务,“扣减库存(本地库存表)+ 生成物流单(本地物流表)”作为子事务:
- 若子事务(如库存扣减)失败,需回滚子事务且触发父事务(订单状态)回滚;
- 若仅物流单生成失败,需仅回滚物流单子事务,保留库存扣减(避免重复扣减),同时订单状态标记为“履约异常”。
2. 适配方案(Spring 声明式事务+保存点)
利用 Spring 事务传播行为(Propagation)+ 保存点(Savepoint),实现嵌套事务的精细化控制:
| 事务层级 | 传播行为 | 作用 |
|---|---|---|
| 父事务 | REQUIRED(默认) | 订单状态更新,作为核心事务,所有子事务复用该事务,子事务失败则整体回滚 |
| 子事务1 | NESTED(嵌套) | 库存扣减,绑定父事务保存点,失败仅回滚该保存点范围,不影响父事务核心逻辑 |
| 子事务2 | NESTED(嵌套) | 物流单生成,独立保存点,失败仅回滚自身,父事务可捕获异常并标记订单状态 |
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 会记录事务状态,服务重启后可重试补偿操作,避免数据残留。
四、共性适配注意事项
- 避免长事务:嵌套事务中减少大事务范围(如拆分非核心操作至事务外),跨服务事务中拆分长流程为多个短事务,降低锁占用和超时风险;
- 日志追踪:所有事务操作绑定唯一事务ID(XID/订单ID),记录操作日志(正向/补偿),便于问题定位和人工兜底;
- 幂等与防重:所有操作需保证幂等(如基于XID/业务ID校验),避免重复执行导致数据错误;
- 降级兜底:跨服务事务中配置降级策略(如积分服务不可用时,先标记积分待补发,后续异步补偿),避免核心流程阻塞。
几种特殊的应用场景
允许子事务部分失败的业务场景及跨事务解决方案
一、允许子事务部分失败的场景:成因、示例与实现
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个阶段,保证原子性:
- Try阶段:账户服务冻结A的1万元(标记为“待转账”),检查B账户状态;
- Confirm阶段:账户服务扣减A的冻结金额,增加B的余额;流水服务生成转账流水;通知服务推送成功通知;
- 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)
- 流程设计:通过“消息队列+本地事务表”实现:
- ERP系统扣减库存,同时向本地事务表插入“库存扣减+订单同步”任务;
- 本地事务提交后,向MQ发送“库存扣减完成”消息;
- 电商平台消费消息,生成订单并扣减线上库存;
- 若电商平台消费失败,MQ重试(最多3次),仍失败则标记为异常,人工介入;
- 定时任务扫描ERP本地事务表,补发未成功发送的消息;
- 核心优势:适配跨系统、跨语言的场景,无需引入分布式事务框架,企业改造成本低;
- 企业落地细节:消息体携带唯一业务ID(如门店订单号),消费端做幂等校验,消息设置死信队列(失败消息归档)。
三、核心总结
| 场景类型 | 企业诉求 | 典型解决方案 | 适用行业 |
|---|---|---|---|
| 子事务部分失败 | 核心流程保障,非核心容错 | 嵌套事务+保存点 | 电商、零售 |
| 跨服务最终一致性 | 允许短时间不一致,最终一致 | SAGA(Seata) | 电商、物流 |
| 跨服务强一致性 | 无中间状态,全成或全败 | TCC(Seata) | 金融、支付 |
| 跨系统数据同步 | 低成本、最终一致 | 可靠消息+本地事务表 | 零售、政企 |
企业选择方案的核心原则:优先适配业务一致性诉求,而非追求技术完美——如电商场景无需强一致性,用SAGA即可;金融场景必须强一致,才用TCC;跨系统场景优先用消息队列,降低架构复杂度。
以上案例覆盖了单服务内嵌套事务的精细化控制、跨服务分布式事务的最终一致性保障,是电商、金融等复杂业务场景中事务适配的典型落地方式。
173万+

被折叠的 条评论
为什么被折叠?



