深夜,小艾接到了一通突如其来的电话,是物流系统的负责人曹工焦急的声音。他火急火燎地反馈了一个严重的问题——大批用户投诉物流信息异常,订单状态与实际情况不符,用户已完成支付,但物流单还是待支付状态。
小艾立刻警觉起来,意识到这个问题可能对公司的业务以及用户体验造成重大影响。她一边安抚曹工的情绪,一边迅速启动紧急响应机制,通知QA对线上变更进行回滚。
随着回滚进程的推进,系统逐步恢复正常。紧接着,他手工导出上线以来的全部订单,并与曹工一起进行数据核对,对问题数据进行修复。终于忙完了,天空已经微微发亮……
上午稍微补了个觉,小艾洗漱完毕后对这件事进行分析:订单已支付,物流单待支付。
现在订单和物流的系统交互如下:
图片
在正常的业务流程中,订单发布事件和物流监听事件紧密相连。
在正常情况下,没有出现不一致的情况。小艾想到了最近的系统变更:
最近上线的一项新功能——礼品赠送。为了降低对下游系统的影响,小艾通过在应用层对流程进行编排的方式实现该功能,简单来说,就是系统先创建订单,然后模拟支付成功,这样既能满足礼品赠送的需求,又能保障下游契约消息没有变化。新流程如下所示:
图片
整个流程与原来的方案没有差别,问题究竟出现在哪呢?无奈的小艾只好打开 idea 查看源码,终于发现问题所在:
@Servicepublic 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 秒以上的延迟,在这种情况下该实现可以保障逻辑的执行顺序。然而在礼品赠送这个场景,系统先创建订单,然后模拟支付成功,导致“订单已创建”和“订单已支付”两个事件几乎同时发出,在接收端就有可能先收到支付成功事件,再收到订单已创建事件,从而导致订单状态和物流单状态不一致,具体流程如下:
图片
如果顺序错了,就会导致业务状态不一致:
问题终于找到了!!!
既然是顺序问题,那最简的方法就是对支付成功消息进行延时发送。
方案如下:
图片
中间增加一个延时组件便能解决这个问题,但不同的方案影响巨大:
定时器方案,核心代码如下:
@TransactionalEventListenerpublic void handle(OrderPaidEvent event){ // 创建Runnable任务 Runnable task = () -> { rocketMQTemplate.convertAndSend("order_paid_event", event); }; // 使用ScheduledExecutorService schedule方法在5秒后执行任务 executor.schedule(task, 5, TimeUnit.SECONDS);}
该方案存在几个比较严重的问题:
现在不少 MQ 提供顺序消息的支持,比如常见的 RocketMQ 提供了两种类型的顺序消息:全局顺序消息和分区顺序消息。
分区顺序消息整体设计如下:
图片
核心代码如下:
@TransactionalEventListenerpublic 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);}@TransactionalEventListenerpublic 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);}
代码仓库:https://gitee.com/litao851025/learnFromBug
代码地址:https://gitee.com/litao851025/learnFromBug/tree/master/src/main/java/com/geekhalo/demo/mq/disorder
本文链接:http://www.28at.com/showinfo-26-80867-0.html故障现场 | MQ消息乱序造成的业务事故
声明:本网页内容旨在传播知识,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。邮件:2376512515@qq.com
上一篇: 三分钟学会消息队列实践
下一篇: 你最擅长使用哪个异步编程模式?