10 订单系统如何保证原子性和最终一致性

vvEcho 2025-02-19 18:51:30
Categories: > Tags:

在微服务架构下,订单服务与库存服务如何实现跨服务的库存扣减最终一致性?若采用可靠消息+本地事务表方案,请结合您佣金系统的JCQ消息拆分经验,详细描述以下问题

1.如何保证消息生产端(订单服务)的本地事务与消息发送的原子性?
2.消息消费端(库存服务)如何实现幂等性控制?请给出与您简历中Redis分布式锁方案不同的实现方式。
3.当遇到网络分区(CAP中的P)时,该方案可能面临什么风险?如何通过设计补偿机制确保数据最终一致?

  1. 消息生产端的原子性保障
    订单创建与消息发送必须保持原子性,避免出现订单创建成功但消息未发送的情况

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    // 订单服务本地事务表方案
    @Transactional
    public void createOrder(OrderDTO order) {
    // 1. 写入订单表
    orderMapper.insert(order);

    // 2. 写入本地消息表(原子操作)
    LocalMessage message = new LocalMessage();
    message.setBizId(order.getOrderNo());
    message.setContent(JSON.toJSONString(order.getItems()));
    message.setStatus(MessageStatus.UNSENT);
    localMessageMapper.insert(message); // 与订单表同库事务

    // 3. 异步发送消息(非事务内)
    CompletableFuture.runAsync(() -> {
    try {
    jcqTemplate.send("stock_reduce_topic", message.getContent());
    localMessageMapper.updateStatus(message.getId(), MessageStatus.SENT);
    } catch (Exception e) {
    log.error("消息发送失败", e);
    // 触发补偿任务
    }
    });
    }

    关键设计:
    事务边界:订单创建与本地消息写入在同一个数据库事务中,确保原子性。
    异步发送:消息发送与业务事务解耦,避免因MQ不可用导致事务阻塞
    补偿机制:定时扫描status=UNSENT的消息重试发送(类似您项目中的数据对账机制)

  2. 消费端幂等性控制
    利用数据的唯一主键这一特性保障幂等,天然支持分布式环境

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 库存服务消费逻辑
    public void handleStockReduce(String message) {
    StockReduceEvent event = parseMessage(message);
    // 基于订单号+商品ID+操作版本号构建唯一键
    String uniqueKey = event.getOrderNo() + "_" + event.getSkuId() + "_v" + event.getVersion();

    // 数据库唯一约束实现幂等
    try {
    processedEventMapper.insert(uniqueKey); // 唯一索引约束
    stockMapper.reduceStock(event.getSkuId(), event.getQuantity());
    } catch (DuplicateKeyException e) {
    log.info("消息已处理: {}", uniqueKey);
    }
    }

    或者消息体都携带版本号,根据乐观锁+状态机的来保障只消费一遍

  3. 网络分区风险与补偿机制
    风险场景:
    生产端分区:订单服务与MQ集群失联,导致本地消息表积压无法发送
    消费端分区:库存服务无法消费消息,导致库存未扣减但订单已创建

补偿方案设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 生产端补偿任务 生产端消息未发送
@XxlJob("retryUnsendedMessages")
public void retryMessages() {
List<LocalMessage> messages = localMessageMapper.selectUnsended(1000);
messages.forEach(msg -> {
if (jcqTemplate.send(msg.getContent())) {
localMessageMapper.updateStatus(msg.getId(), MessageStatus.SENT);
}
});
}

// 消费端对账服务 消费端消费失败,客户已支付但库存未扣减
@Scheduled(cron = "0 0/5 * * * ?")
public void reconcileStock() {
// 1. 查询订单表已支付未扣减库存的订单(状态为已支付但库存未扣减)
List<Order> orders = orderMapper.selectUnprocessedOrders();

// 2. 对比库存流水表
orders.forEach(order -> {
if (!stockFlowMapper.exists(order.getOrderNo())) {
// 3. 触发补偿扣减
stockService.forceReduceStock(order);
// 4. 记录补偿日志
alarmService.send("发现库存不一致订单:" + order.getOrderNo());
}
});
}

容灾策略:
熔断降级:当分区持续时间超过阈值(如30分钟),暂停新订单创建
人工干预:通过您开发的运维工具生成修复脚本

总结:通过本地事务表+异步消息实现生产端原子性,结合唯一约束/乐观锁保障消费端幂等性,配合定时对账+熔断降级应对网络分区,可构建高可用的最终一致性方案