故障现场 | MQ消息乱序造成的业务事故

开发 前端
深夜,小艾接到了一通突如其来的电话,是物流系统的负责人曹工焦急的声音。他火急火燎地反馈了一个严重的问题——大批用户投诉物流信息异常,订单状态与实际情况不符,用户已完成支付,但物流单还是待支付状态。

1. 问题&分析

1.1. 案例

深夜,小艾接到了一通突如其来的电话,是物流系统的负责人曹工焦急的声音。他火急火燎地反馈了一个严重的问题——大批用户投诉物流信息异常,订单状态与实际情况不符,用户已完成支付,但物流单还是待支付状态。

小艾立刻警觉起来,意识到这个问题可能对公司的业务以及用户体验造成重大影响。她一边安抚曹工的情绪,一边迅速启动紧急响应机制,通知QA对线上变更进行回滚。

随着回滚进程的推进,系统逐步恢复正常。紧接着,他手工导出上线以来的全部订单,并与曹工一起进行数据核对,对问题数据进行修复。终于忙完了,天空已经微微发亮……

1.2. 问题分析

上午稍微补了个觉,小艾洗漱完毕后对这件事进行分析:订单已支付,物流单待支付。

现在订单和物流的系统交互如下:

图片图片

在正常的业务流程中,订单发布事件和物流监听事件紧密相连。

  • 订单系统发布一个“订单已创建”事件时,物流系统会立即响应并在其内部创建一条对应的物流单据。
  • 当支付环节完成并触发“订单已支付”事件时,物流系统会找到关联的物流单据并更新其为待发货状态。

在正常情况下,没有出现不一致的情况。小艾想到了最近的系统变更:

最近上线的一项新功能——礼品赠送。为了降低对下游系统的影响,小艾通过在应用层对流程进行编排的方式实现该功能,简单来说,就是系统先创建订单,然后模拟支付成功,这样既能满足礼品赠送的需求,又能保障下游契约消息没有变化。新流程如下所示:

图片图片

整个流程与原来的方案没有差别,问题究竟出现在哪呢?无奈的小艾只好打开 idea 查看源码,终于发现问题所在:

@Service
public class RocketMQProducer {
    @Autowired
    private RocketMQTemplate rocketMQTemplate;

    @TransactionalEventListener
    public void handle(OrderCreatedEvent event){
        rocketMQTemplate.convertAndSend("order_created_event", event);
    }

    @TransactionalEventListener
    public void handle(OrderPaidEvent event){
        rocketMQTemplate.convertAndSend("order_paid_event", event);
    }
}

下单和支付成功使用两个不同的 topic,两个 topic 相互独立,根本就无法保障投递顺序。在手动支付场景下,由于用户从订单创建到支付完成通常会有 5 秒以上的延迟,在这种情况下该实现可以保障逻辑的执行顺序。然而在礼品赠送这个场景,系统先创建订单,然后模拟支付成功,导致“订单已创建”和“订单已支付”两个事件几乎同时发出,在接收端就有可能先收到支付成功事件,再收到订单已创建事件,从而导致订单状态和物流单状态不一致,具体流程如下:

图片图片

如果顺序错了,就会导致业务状态不一致:

  • 物流先接到支付成功事件,在查询物流单时由于找不到物流单所以更新失败。
  • 随后物流接到订单创建事件,根据逻辑创建一条待支付的物流单,但由于该订单的支付成功事件在上一步已经错过,所以物流一直停留在待支付状态。

问题终于找到了!!!

2. 解决方案

2.1. 方案一:主动延时

既然是顺序问题,那最简的方法就是对支付成功消息进行延时发送。

方案如下:

图片图片

中间增加一个延时组件便能解决这个问题,但不同的方案影响巨大:

  • sleep 方案,会导致大量线程处于阻塞状态,增加接口响应时间,同时降低系统的吞吐。在线上绝对不允许这种方案的出现!
  • 定时器方案,下单完成后,注册一个定时调度任务,时间到达时调度器将自动执行任务。

定时器方案,核心代码如下:

@TransactionalEventListener
public void handle(OrderPaidEvent event){
    // 创建Runnable任务
    Runnable task = () -> {
        rocketMQTemplate.convertAndSend("order_paid_event", event);
    };
    // 使用ScheduledExecutorService schedule方法在5秒后执行任务
    executor.schedule(task, 5, TimeUnit.SECONDS);
}

该方案存在几个比较严重的问题:

  • 全内存操作,容易操作任务的丢失。当遇到非优雅关机时,内存中的 task 就会丢失,从而导致业务逻辑不完整;
  • 异步执行,容易造成错觉。用户完成任务提交并不代表任务肯定会成功运行
  • 资源管理困难,如果任务量太大会大量消耗内存资源,甚至引起整个服务 OOM

2.2. 方案二:顺序消息

现在不少 MQ 提供顺序消息的支持,比如常见的 RocketMQ 提供了两种类型的顺序消息:全局顺序消息和分区顺序消息。

  • 全局顺序消息要求所有的消息都在一个队列上发送和消费,因此只适用于少量队列(通常是1个队列,否则就无法做到全局顺序)。
  • 分区顺序消息则允许基于(分片键)进行分区,相同的消息会被发送到同一队列中,从而在每个分区内部实现顺序。

分区顺序消息整体设计如下:

图片图片

核心代码如下:

@TransactionalEventListener
public void handle(OrderCreatedEvent event){
    Long orderId = event.getOrderId();
    Message<OrderCreatedEvent> message = MessageBuilder.withPayload(event)
            .setHeader(RocketMQHeaders.KEYS, orderId) // 设置 Sharding Key,即订单ID
            .setHeader(RocketMQHeaders.TAGS, "OrderCreatedEvent") // 设置 Tag
            .build();
    // 发送至统一的 order_event_topic
    rocketMQTemplate.send("order_event_topic", message);
}

@TransactionalEventListener
public void handle(OrderPaidEvent event){
    Long orderId = event.getOrderId();
    Message<OrderPaidEvent> message = MessageBuilder.withPayload(event)
            .setHeader(RocketMQHeaders.KEYS, orderId) // 设置 Sharding Key,即订单ID
            .setHeader(RocketMQHeaders.TAGS, "OrderCreatedEvent") // 设置 Tag
            .build();
    // 发送至统一的 order_event_topic
    rocketMQTemplate.send("order_event_topic", message);
}

3. 示例&源码

代码仓库:https://gitee.com/litao851025/learnFromBug

代码地址:https://gitee.com/litao851025/learnFromBug/tree/master/src/main/java/com/geekhalo/demo/mq/disorder

责任编辑:武晓燕 来源: geekhalo
相关推荐

2024-03-18 09:24:12

RocketMQ消息模型分布式

2009-06-12 16:55:10

VPN客户端故障

2024-01-29 09:22:59

死锁线程池服务

2009-12-23 09:37:38

集线器故障

2019-01-16 09:20:42

架构设计JVM FullGC宕机事故

2020-09-07 08:42:13

宕机业务运维

2013-01-22 09:57:32

2017-11-09 09:06:29

流量暴增优化

2022-11-16 08:00:00

雪花算法原理

2009-06-14 17:18:55

ibmdwWebSphereMQ

2023-06-29 10:10:06

Rocket MQ消息中间件

2023-10-24 07:50:18

消息中间件MQ

2013-01-21 09:41:00

路由器设备故障设置参数

2011-05-27 09:04:39

Skype宕机

2015-12-04 15:21:43

2009-07-07 17:22:34

光纤链路测试故障

2011-04-08 15:56:02

路由器Ip

2009-09-22 13:54:57

VMware驱动VMware后门系统故障

2019-08-23 12:12:49

MQ消息队列

2021-03-08 10:19:59

MQ消息磁盘
点赞
收藏

51CTO技术栈公众号