微服务下单跨服务数据不一致问题分析笔记

微服务下单跨服务数据不一致问题完整分析笔记

一、问题全景定位

1.1 故障场景复现

业务流程原始串行逻辑

  1. 订单服务:本地事务创建订单,事务提交持久化
  2. 库存服务:本地事务扣减库存,事务提交持久化
  3. 支付服务:调用时抛出网络异常/服务宕机/数据库报错,流程中断

脏数据现象

  • 库存:商品库存已扣减,库存流水存在扣减记录
  • 订单:订单状态停留在待支付,无支付成功流水
  • 账务:库存成本已扣、支付收入无入账,每日对账产生大额差额
  • 核心特征:单服务本地事务执行无报错,仅跨多服务调用出现数据错乱

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)三段式事务
  • 核心原理:每个微服务实现预留、确认、回滚三套接口,全局协调器统一调度
  • 适用场景:高账务敏感、库存不可出错场景(电商下单、金融支付)
  • 流程示例:
    1. Try:订单预创建、库存冻结、支付余额预授权
    2. Confirm:全部服务无异常,真实扣减库存、完成扣款、订单生效
    3. 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 监控观测指标

  • 分布式事务回滚次数监控
  • 定时补偿任务执行失败数量告警
  • 每日对账修复脏单据数量统计
  • 库存与支付账务差额监控阈值告警
相关推荐
微三云 - 廖会灵 (私域系统开发)1 小时前
电商系统微服务拆分实战:从单体到Spring Cloud的分布式事务踩坑与补偿方案
分布式·spring cloud·微服务
OceanBase数据库官方博客1 小时前
OceanBase Lakebase 架构解析:独立部署与智能叠加层模式的技术实践
架构·oceanbase
白露与泡影1 小时前
慢 SQL 不一定是 SQL 慢:一次 MySQL 连接池耗尽的故障定位
数据库·sql·mysql
AI 小老六2 小时前
Agent 工程化新底座:用 CLI 契约层打通 HTTP 接口与业务能力
网络·人工智能·http·ai·架构
用户8358086187912 小时前
深入剖析Java并发基石:AQS实现原理与自定义同步器实战
架构
威联通安全存储3 小时前
SMT表面贴装线AOI检测网中TS-h2287XU-RP混合架构的物理部署
python·架构
阿坤带你走近大数据3 小时前
Oracle新老SQL写法对比
数据库·sql·oracle
Aray12344 小时前
论服务网格在云原生微服务项目中的设计与落地实践
微服务·云原生·架构