记一次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、验证异常论断的方式是、第一个大胆猜想、第二个必须保证有日志可追踪查询论证。

相关推荐
bufanjun00112 小时前
JUC并发工具---ThreadLocal
java·jvm·面试·并发·并发基础
码农飞飞22 天前
详解Rust多线程编程
rust·多线程·条件变量·并发··线程同步·线程通信
桃园码工22 天前
第七章:并发编程 1.Goroutines --Go 语言轻松入门
服务器·网络·golang·并发
小哈里24 天前
【后端开发】Go语言编程实践,Goroutines和Channels,基于共享变量的并发,反射与底层编程
开发语言·后端·golang·编程·并发
雪碧聊技术1 个月前
多线程4:线程池、并发、并行、综合案例-抢红包游戏
java·线程池·多线程·并发·并行·复用线程
慕木沐1 个月前
【JUC并发】cmpxchg和lock指令
后端·并发·juc·汇编指令
Hello-Brand1 个月前
Java核心知识体系10-线程管理
java·高并发·多线程·并发·多线程模型·线程管理
幸苦的马侬2 个月前
并发-线程
线程·并发
aristo_boyunv3 个月前
【线程池】ThreadPoolExecutor应用
java·线程池·并发
手握乄风云3 个月前
快速了解高并发解决方案
并发