记一次RocketMQ消费非顺序消息引起的线上事故

应用场景

C端用户提交工单、工单创建完成之后、会发布一条工单创建完成的消息事件(异步消息)、MQ消费者收到消息之后、会通知各处理器处理该消息、各处理器处理完后都会发布一条将该工单写入搜索引擎的消息、最终该工单出现在搜索引擎、被工单处理人检索和处理。

事故异常体现

1、异常体现

从工单的流转记录发现、工单的状态从A->C->B、理论上 工单的状态只能从A->B->C。此处能得出两个结论、1、各处理器处理完工单之后、状态有误;2、写入到搜索引擎的工单数据被本该更早写入引擎的数据覆盖了。

从监控数据发现、结论1排除。

2、背景解释

一个工单创建完之后、会经历

  • 工单创建完城-->状态新建、将新工单信息更新至ES(搜索引擎)
  • 工单内容审核-->状态已审核、将新工单信息更新至ES
  • 工单分配给指定工作人员-->状态已分配、将最新工单信息更新至ES
  • 其他操作-->状态改变-->将最新工单信息更新至ES

说明:

所有工单的操作都是异步的、没有固定顺序。

保证点:

写到ES之前从数据库所获取的工单信息都是最新的工单信息、无误。

案例中异常情况:

工单实际已经分配了工作人员(已分配状态)、可以查询到被分配的人、但是工单的状态显示是新建状态。

3、事故异常分析

1、创单的顺序是优先于派单的

2、创单之后抛出一个写ES的MQ消息--消息A

3、派单之后也会抛出一个写ES的MQ消息--消息B

4、如果MQ是有顺序的、按照顺序消费消息、MQ消费者消费第一个消息(消息A)肯定是比第二份消息(消息B)要快的、正常情况、工单是绝对没有问题。

(非正常情况、可能是ES自身写消息到集群、有快慢之分、第二个消息会先写完、此种情况基本忽略不计)

5、此处异常是、MQ消息消费无序

正常拿到消息A、查询数据库的状态正确、可以推送ES消息、再接着拿到消息B、查询数据库的状态正确、可以推送ES消息、因为推送ES没有加分布式锁、导致消息B那个时刻的工单数据被先写入ES、消息A那个时刻的工单数据后写入ES、导致ES的数据被覆盖、最终ES的最新版本的工单状态和数据库的工单状态不一致。

4、解决方式

背景说明(续):写ES的MQ消息可以理解为并发出现、在工单创建的那一刻、所有改动工单状态的消息基本会在ms级别时间内到达。

前提:在写ES的MQ消息中、携带更新完工单之后该工单的时间戳、作为工单的版本号。

方式一:

暴力方式、直接在写ES的接口新增分布式锁、通过对比消息的版本号和工单数据库中的版本号、即可判定消息要舍弃还是写入ES(不采取、会严重降低写ES的效率)

方式二:

仅对工单使用分布式锁、同时、在一定时间内(秒级)收集写ES的消息、并且对消息进行排序过滤、仅处理最新版本的工单消息写入ES。

5、感想

1、有序的MQ消息资源会比较贵,还是要代码层保证数据稳定性。

2、验证异常论断的方式是、第一个大胆猜想、第二个必须保证有日志可追踪查询论证。

相关推荐
手握乄风云7 天前
快速了解高并发解决方案
并发
vd_vd20 天前
并发容器简介
java·面试·并发·并发容器
嚯呀怪怪怪21 天前
从零基础学Go(九)——Go的Goroutine
golang·线程·多线程·并发·编译原理·协程·gorountine
ComputerInBook1 个月前
C++概观:并发及实用工具(A Tour of C++: Concurrency and Utilities)
开发语言·c++·多线程·并发·并发编程·c++并发编程·c++实用工具
promise5241 个月前
Oracle统计API并发访问量
linux·运维·数据库·sql·oracle·api·并发
functionMC1 个月前
编程深水区之并发⑤:C#的Thread线程
c#·多线程·并发·thread
威哥爱编程2 个月前
Volatile不保证原子性及解决方案
java·并发·威哥爱编程
许野平3 个月前
Rust:Future、async 异步代码机制示例与分析
rust·进程·并发·async·future·异步·并行
剑海风云3 个月前
通过自定义分配器解决 ZGC中的碎片问题
java·算法·并发·zgc·内存碎片·0字节标头·zgc小页面