微服务下单跨服务数据不一致问题完整分析笔记
一、问题全景定位
1.1 故障场景复现
业务流程原始串行逻辑
- 订单服务:本地事务创建订单,事务提交持久化
- 库存服务:本地事务扣减库存,事务提交持久化
- 支付服务:调用时抛出网络异常/服务宕机/数据库报错,流程中断
脏数据现象
- 库存:商品库存已扣减,库存流水存在扣减记录
- 订单:订单状态停留在待支付,无支付成功流水
- 账务:库存成本已扣、支付收入无入账,每日对账产生大额差额
- 核心特征:单服务本地事务执行无报错,仅跨多服务调用出现数据错乱
1.2 底层根本原因
- 单机ACID事务边界限制:订单、库存、支付分属独立微服务,各自独立数据库,本地事务仅管控当前服务库,无法跨节点原子提交/回滚
- 流程执行顺序设计缺陷:前置资源扣减、后置支付校验,支付失败无逆向补偿链路
- 无分布式事务协调机制:缺少全局事务协调器记录全流程执行状态,故障后丢失中间节点信息
- 无可靠消息持久化流程节点:同步调用内存流转,服务中断后无法追溯已完成步骤
- 缺少兜底对账与自动补偿任务:异常单据无法自动修复,仅靠人工核对账务
- 接口未实现幂等性保障:重试调用易重复扣减库存、重复创建订单加剧脏数据
1.3 故障树分层拆解(Plaintext树形结构)
plaintext
下单跨服务数据不一致故障树
└── 顶层故障:库存扣减但支付失败,三方数据对账失衡
├── 分支1:分布式事务能力缺失
│ ├── 子节点1:仅依赖单库本地事务,无全局事务协调
│ ├── 子节点2:未引入Seata/TCC/事务消息分布式一致性方案
│ └── 子节点3:无undo_log/事务状态表记录分支执行快照
├── 分支2:业务执行流程顺序不合理
│ ├── 子节点1:先扣库存,后发起支付,前置资源不可逆占用
│ └── 子节点2:无库存冻结中间态,直接物理扣减无法快速回滚
├── 分支3:流程状态无持久化存储
│ ├── 子节点1:同步串行调用,流程状态存于服务内存
│ ├── 子节点2:无本地消息表/状态机记录表留存执行步骤
│ └── 子节点3:支付异常中断后,系统无法识别已扣减库存
├── 分支4:异常补偿机制缺失
│ ├── 子节点1:无失败重试、死信队列存储异常任务
│ ├── 子节点2:未开发逆向补偿接口(库存回加、订单作废)
│ └── 子节点3:无定时补偿任务扫描待修复异常单据
└── 分支5:兜底校验机制空白
├── 子节点1:无定时对账任务核对订单/库存/支付三方流水
├── 子节点2:异常单据无告警推送,无法及时发现差额
└── 子节点3:远程接口未做幂等,重试引发二次脏数据
1.4 架构对比示意图(Plaintext)
1.4.1 改造前故障架构图
plaintext
用户请求 -> 订单服务(本地事务新建订单,提交)
↓同步调用
库存服务(本地事务扣库存,提交)
↓同步调用
支付服务【异常宕机/报错】
数据状态:订单已创建、库存已扣、支付无记录 → 脏数据
1.4.2 改造后Seata AT分布式事务架构图
plaintext
TC事务协调器
↑全局事务注册
用户请求 -> 订单服务(TM开启@GlobalTransactional,生成undo_log)
↓RPC分支调用
库存服务(RM记录undo_log,执行扣减)
↓RPC分支调用
支付服务(抛出异常)
↓通知回滚
TC下发回滚指令 → 库存RM读取undo_log自动恢复库存
→ 订单RM读取undo_log作废订单
数据状态:全链路数据统一回滚,无脏数据
二、分布式事务多类型完整分类与实例
2.1 强一致性方案(零脏数据,资金/库存核心场景)
2.1.1 TCC(Try-Confirm-Cancel)三段式事务
- 核心原理:每个微服务实现预留、确认、回滚三套接口,全局协调器统一调度
- 适用场景:高账务敏感、库存不可出错场景(电商下单、金融支付)
- 流程示例:
- Try:订单预创建、库存冻结、支付余额预授权
- Confirm:全部服务无异常,真实扣减库存、完成扣款、订单生效
- Cancel:任意服务故障,解冻库存、作废订单、释放资金
- 树形结构示例
plaintext
TCC事务执行树
└── 全局事务协调器
├── 阶段1 Try预检查
│ ├── 订单服务Try:生成预订单,锁定订单号
│ ├── 库存服务Try:冻结商品库存10件
│ └── 支付服务Try:冻结用户账户余额100元
├── 分支判断:全部Try成功?
│ ├── 是 → 阶段2 Confirm确认提交
│ │ ├── 订单Confirm:订单改为已支付生效
│ │ ├── 库存Confirm:真实扣减冻结库存
│ │ └── 支付Confirm:完成资金扣款入账
│ └── 否 → 阶段3 Cancel补偿回滚
│ ├── 订单Cancel:删除预订单
│ ├── 库存Cancel:解冻全部冻结库存
│ └── 支付Cancel:释放冻结余额
2.1.2 Seata AT自动事务(无业务侵入)
- 核心原理:基于undo_log快照自动生成回滚SQL,AOP拦截业务方法实现分布式事务
- 适用场景:中小微服务系统,快速改造现有业务代码
- 优缺点:代码改动极小;高并发秒杀场景存在行锁性能损耗
2.2 最终一致性方案(高并发、允许短暂中间态)
2.2.1 可靠事务消息(RocketMQ半事务消息)
- 核心原理:半消息+本地事务绑定,本地事务成功才对外投递消息,消费失败死信补偿
- 适用场景:电商高并发下单、商品秒杀、大流量订单场景
- 执行树形示例
plaintext
RocketMQ事务消息流程树
└── 订单服务生产者
├── 1.发送半事务消息至MQ服务端(消息不可见)
├── 2.开启本地事务:创建待支付订单 + 写入本地任务表
├── 3.本地事务提交成功 → 提交半消息,消费者可见
└── 3.本地事务回滚 → 删除半消息,流程终止
↓MQ消费者消费
├── 消费者1:库存服务扣减库存,写入库存流水,ACK确认
└── 消费者2:支付服务发起支付请求
├── 支付成功:更新订单状态完成,流程结束
└── 支付失败:消息重试3次,转入死信队列
└── 定时死信补偿任务:调用库存回加接口、关闭订单
2.2.2 本地消息表+定时补偿(无中间件轻量化方案)
- 核心原理:业务表与分布式任务表同本地事务写入,定时任务扫描异常任务逆向补偿
- 适用场景:老旧系统无法引入MQ、分布式事务框架场景
2.3 柔性事务轻量方案
2.3.1 状态机驱动流程
- 核心原理:订单全生命周期固化状态流转,仅允许按预设状态变更,拦截非法中间态
- 订单标准状态流转:待创建→库存冻结→待支付→支付成功→已完成;支付失败→待补偿→库存恢复→订单关闭
2.3.2 定时对账兜底补偿
- 核心原理:离线定时任务关联订单、库存、支付三张流水表,自动修复差额并告警
- 核对规则:存在库存扣减流水无支付记录 → 自动回补库存、作废订单
三、落地完整代码实现
3.1 Seata AT 分布式事务SpringBoot业务代码
3.1.1 依赖pom.xml
xml
<!-- Seata 分布式事务核心依赖 -->
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.7.1</version>
</dependency>
<!-- SpringCloud OpenFeign 远程调用 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
3.1.2 订单服务启动类(TM事务发起方)
java
import io.seata.spring.annotation.GlobalTransactional;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableFeignClients
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}
3.1.3 下单核心业务Service(全局事务注解)
java
import io.seata.spring.annotation.GlobalTransactional;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service
public class OrderServiceImpl implements OrderService {
@Resource
private OrderMapper orderMapper;
@Resource
private StockFeignClient stockFeignClient;
@Resource
private PayFeignClient payFeignClient;
/**
* 全局分布式事务:任意远程调用异常,全部分支自动回滚
*/
@Override
@GlobalTransactional(rollbackFor = Exception.class)
public void createOrder(Long goodsId, Integer num, Long userId) {
// 1.创建订单
Order order = new Order();
order.setGoodsId(goodsId);
order.setBuyNum(num);
order.setUserId(userId);
order.setOrderStatus(0); // 待支付
orderMapper.insert(order);
// 2.远程调用库存扣减
stockFeignClient.deductStock(goodsId, num);
// 3.远程调用支付服务,模拟支付异常
payFeignClient.createPay(order.getId(), userId);
}
}
3.1.4 Feign远程调用接口
java
// 库存远程调用
@FeignClient("stock-service")
public interface StockFeignClient {
@PostMapping("/stock/deduct")
Result<Void> deductStock(@RequestParam Long goodsId, @RequestParam Integer num);
}
// 支付远程调用
@FeignClient("pay-service")
public interface PayFeignClient {
@PostMapping("/pay/create")
Result<Void> createPay(@RequestParam Long orderId, @RequestParam Long userId);
}
3.2 本地消息表分布式任务表建表SQL(MySQL)
sql
CREATE TABLE `distribute_task` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`biz_no` varchar(64) NOT NULL COMMENT '业务单号(订单号)',
`task_stage` tinyint NOT NULL COMMENT '执行阶段 1扣库存 2发起支付',
`task_status` tinyint NOT NULL COMMENT '任务状态 0待执行 1执行成功 2执行失败待补偿',
`biz_params` varchar(1024) DEFAULT '' COMMENT '业务参数JSON',
`retry_count` int DEFAULT 0 COMMENT '已重试次数',
`next_execute_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '下次执行时间',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY uk_biz_stage (`biz_no`,`task_stage`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT '分布式本地消息任务表';
3.3 定时补偿任务SpringBoot代码
java
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.List;
@Component
public class TaskCompensateJob {
@Resource
private DistributeTaskMapper taskMapper;
@Resource
private StockFeignClient stockFeignClient;
// 每30秒扫描待补偿任务
@Scheduled(fixedRate = 30000)
public void scanCompensateTask() {
List<DistributeTask> failTaskList = taskMapper.selectWaitCompensateTask();
for (DistributeTask task : failTaskList) {
// 幂等校验,防止重复补偿
if(task.getRetryCount() > 5){
// 超过最大重试次数,推送告警人工处理
sendAlarm(task);
continue;
}
// 逆向补偿:恢复库存
stockFeignClient.revertStock(task.getBizParams());
// 更新任务为补偿完成
task.setTaskStatus(1);
taskMapper.updateById(task);
}
}
private void sendAlarm(DistributeTask task) {
// 短信/企业微信告警逻辑省略
}
}
3.4 每日对账修复定时任务代码片段
java
/**
* 凌晨2点执行三方对账:订单、库存、支付流水核对修复脏数据
*/
@Scheduled(cron = "0 0 2 * * ?")
public void orderStockPayCheck() {
// 查询已扣库存但无支付成功记录的异常订单
List<OrderStockAbnormalDTO> abnormalList = checkMapper.selectAbnormalOrder();
for(OrderStockAbnormalDTO dto : abnormalList){
// 自动补偿恢复库存
stockFeignClient.revertStock(dto.getGoodsId(), dto.getBuyNum());
// 更新订单状态为支付失败关闭
orderMapper.updateOrderClose(dto.getOrderId());
// 记录对账修复日志
saveRepairLog(dto);
}
}
四、方案选型对比树与落地规范
4.1 分布式事务方案选型决策树
plaintext
选型决策树
└── 业务一致性要求
├── 强一致、零脏数据、金融/核心库存
│ ├── 系统可改造代码,接受少量侵入 → TCC
│ └── 不想侵入业务代码,快速开发 → Seata AT
└── 高并发秒杀,允许短暂中间态,追求性能
├── 已部署RocketMQ消息中间件 → 事务消息最终一致性
└── 无中间件、老旧单体拆分微服务 → 本地消息表+定时补偿
└── 所有方案必须配套:定时对账兜底任务
4.2 生产环境落地强制规范无序列表
- 所有远程RPC接口必须实现幂等性,使用业务唯一单号作为幂等键
- 分布式事务场景必须配置定时补偿任务,重试次数上限控制在5次以内
- 每日离线对账任务不可省略,作为数据一致性最后兜底防线
- 支付、库存类核心业务禁止直接物理扣减,优先使用冻结中间态
- 异常单据必须接入告警渠道(企业微信、短信、邮件),及时发现账务差额
- 分布式事务框架TC协调器必须集群部署,避免单点故障导致事务卡死
五、故障预防优化补充
5.1 业务流程顺序优化方案
- 原高危流程:创建订单 → 直接扣库存 → 发起支付
- 优化安全流程:创建待支付订单 → 发起支付 → 支付成功后扣减库存
- 配套机制:30分钟未支付订单定时任务自动取消,释放预占用库存
5.2 监控观测指标
- 分布式事务回滚次数监控
- 定时补偿任务执行失败数量告警
- 每日对账修复脏单据数量统计
- 库存与支付账务差额监控阈值告警