微服务下单跨服务数据不一致问题分析笔记
微服务下单跨服务数据不一致问题完整分析笔记
一、问题全景定位
1.1 故障场景复现
业务流程原始串行逻辑
- 订单服务:本地事务创建订单,事务提交持久化
- 库存服务:本地事务扣减库存,事务提交持久化
- 支付服务:调用时抛出网络异常/服务宕机/数据库报错,流程中断
脏数据现象
- 库存:商品库存已扣减,库存流水存在扣减记录
- 订单:订单状态停留在待支付,无支付成功流水
- 账务:库存成本已扣、支付收入无入账,每日对账产生大额差额
- 核心特征:单服务本地事务执行无报错,仅跨多服务调用出现数据错乱
1.2 底层根本原因
- 单机ACID事务边界限制:订单、库存、支付分属独立微服务,各自独立数据库,本地事务仅管控当前服务库,无法跨节点原子提交/回滚
- 流程执行顺序设计缺陷:前置资源扣减、后置支付校验,支付失败无逆向补偿链路
- 无分布式事务协调机制:缺少全局事务协调器记录全流程执行状态,故障后丢失中间节点信息
- 无可靠消息持久化流程节点:同步调用内存流转,服务中断后无法追溯已完成步骤
- 缺少兜底对账与自动补偿任务:异常单据无法自动修复,仅靠人工核对账务
- 接口未实现幂等性保障:重试调用易重复扣减库存、重复创建订单加剧脏数据
1.3 故障树分层拆解(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 改造前故障架构图
用户请求 -> 订单服务(本地事务新建订单,提交) ↓同步调用 库存服务(本地事务扣库存,提交) ↓同步调用 支付服务【异常宕机/报错】 数据状态:订单已创建、库存已扣、支付无记录 → 脏数据1.4.2 改造后Seata AT分布式事务架构图
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:任意服务故障,解冻库存、作废订单、释放资金
- 树形结构示例
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半事务消息)
- 核心原理:半消息+本地事务绑定,本地事务成功才对外投递消息,消费失败死信补偿
- 适用场景:电商高并发下单、商品秒杀、大流量订单场景
- 执行树形示例
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
<!-- 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事务发起方)
importio.seata.spring.annotation.GlobalTransactional;importorg.springframework.boot.SpringApplication;importorg.springframework.boot.autoconfigure.SpringBootApplication;importorg.springframework.cloud.openfeign.EnableFeignClients;@SpringBootApplication@EnableFeignClientspublicclassOrderApplication{publicstaticvoidmain(String[]args){SpringApplication.run(OrderApplication.class,args);}}3.1.3 下单核心业务Service(全局事务注解)
importio.seata.spring.annotation.GlobalTransactional;importorg.springframework.stereotype.Service;importjavax.annotation.Resource;@ServicepublicclassOrderServiceImplimplementsOrderService{@ResourceprivateOrderMapperorderMapper;@ResourceprivateStockFeignClientstockFeignClient;@ResourceprivatePayFeignClientpayFeignClient;/** * 全局分布式事务:任意远程调用异常,全部分支自动回滚 */@Override@GlobalTransactional(rollbackFor=Exception.class)publicvoidcreateOrder(LonggoodsId,Integernum,LonguserId){// 1.创建订单Orderorder=newOrder();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远程调用接口
// 库存远程调用@FeignClient("stock-service")publicinterfaceStockFeignClient{@PostMapping("/stock/deduct")Result<Void>deductStock(@RequestParamLonggoodsId,@RequestParamIntegernum);}// 支付远程调用@FeignClient("pay-service")publicinterfacePayFeignClient{@PostMapping("/pay/create")Result<Void>createPay(@RequestParamLongorderId,@RequestParamLonguserId);}3.2 本地消息表分布式任务表建表SQL(MySQL)
CREATETABLE`distribute_task`(`id`bigintNOTNULLAUTO_INCREMENTCOMMENT'主键ID',`biz_no`varchar(64)NOTNULLCOMMENT'业务单号(订单号)',`task_stage`tinyintNOTNULLCOMMENT'执行阶段 1扣库存 2发起支付',`task_status`tinyintNOTNULLCOMMENT'任务状态 0待执行 1执行成功 2执行失败待补偿',`biz_params`varchar(1024)DEFAULT''COMMENT'业务参数JSON',`retry_count`intDEFAULT0COMMENT'已重试次数',`next_execute_time`datetimeDEFAULTCURRENT_TIMESTAMPCOMMENT'下次执行时间',`create_time`datetimeDEFAULTCURRENT_TIMESTAMP,`update_time`datetimeDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMP,PRIMARYKEY(`id`),UNIQUEKEYuk_biz_stage(`biz_no`,`task_stage`))ENGINE=InnoDBDEFAULTCHARSET=utf8mb4COMMENT'分布式本地消息任务表';3.3 定时补偿任务SpringBoot代码
importorg.springframework.scheduling.annotation.Scheduled;importorg.springframework.stereotype.Component;importjavax.annotation.Resource;importjava.util.List;@ComponentpublicclassTaskCompensateJob{@ResourceprivateDistributeTaskMappertaskMapper;@ResourceprivateStockFeignClientstockFeignClient;// 每30秒扫描待补偿任务@Scheduled(fixedRate=30000)publicvoidscanCompensateTask(){List<DistributeTask>failTaskList=taskMapper.selectWaitCompensateTask();for(DistributeTasktask:failTaskList){// 幂等校验,防止重复补偿if(task.getRetryCount()>5){// 超过最大重试次数,推送告警人工处理sendAlarm(task);continue;}// 逆向补偿:恢复库存stockFeignClient.revertStock(task.getBizParams());// 更新任务为补偿完成task.setTaskStatus(1);taskMapper.updateById(task);}}privatevoidsendAlarm(DistributeTasktask){// 短信/企业微信告警逻辑省略}}3.4 每日对账修复定时任务代码片段
/** * 凌晨2点执行三方对账:订单、库存、支付流水核对修复脏数据 */@Scheduled(cron="0 0 2 * * ?")publicvoidorderStockPayCheck(){// 查询已扣库存但无支付成功记录的异常订单List<OrderStockAbnormalDTO>abnormalList=checkMapper.selectAbnormalOrder();for(OrderStockAbnormalDTOdto:abnormalList){// 自动补偿恢复库存stockFeignClient.revertStock(dto.getGoodsId(),dto.getBuyNum());// 更新订单状态为支付失败关闭orderMapper.updateOrderClose(dto.getOrderId());// 记录对账修复日志saveRepairLog(dto);}}四、方案选型对比树与落地规范
4.1 分布式事务方案选型决策树
选型决策树 └── 业务一致性要求 ├── 强一致、零脏数据、金融/核心库存 │ ├── 系统可改造代码,接受少量侵入 → TCC │ └── 不想侵入业务代码,快速开发 → Seata AT └── 高并发秒杀,允许短暂中间态,追求性能 ├── 已部署RocketMQ消息中间件 → 事务消息最终一致性 └── 无中间件、老旧单体拆分微服务 → 本地消息表+定时补偿 └── 所有方案必须配套:定时对账兜底任务4.2 生产环境落地强制规范无序列表
- 所有远程RPC接口必须实现幂等性,使用业务唯一单号作为幂等键
- 分布式事务场景必须配置定时补偿任务,重试次数上限控制在5次以内
- 每日离线对账任务不可省略,作为数据一致性最后兜底防线
- 支付、库存类核心业务禁止直接物理扣减,优先使用冻结中间态
- 异常单据必须接入告警渠道(企业微信、短信、邮件),及时发现账务差额
- 分布式事务框架TC协调器必须集群部署,避免单点故障导致事务卡死
五、故障预防优化补充
5.1 业务流程顺序优化方案
- 原高危流程:创建订单 → 直接扣库存 → 发起支付
- 优化安全流程:创建待支付订单 → 发起支付 → 支付成功后扣减库存
- 配套机制:30分钟未支付订单定时任务自动取消,释放预占用库存
5.2 监控观测指标
- 分布式事务回滚次数监控
- 定时补偿任务执行失败数量告警
- 每日对账修复脏单据数量统计
- 库存与支付账务差额监控阈值告警
