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

相关推荐
a7769957998 天前
驱动里的并发控制--互斥锁
并发·driver
小Y._10 天前
ConcurrentHashMap高效并发机制深度解析
java·并发·juc·concurrenthashmap
趣魂12 天前
五种并发/异步模型整理
并发·异步
lee_curry13 天前
Java中关于“锁”的那些事
java·线程·并发·juc
SudosuBash14 天前
[A Primer On MC and CC] 2.1 Memory Consistency 1 - 指令重排序和 SC 模型
并发·进程和线程·内存缓存一致性·多核编程·a primer on mc and cc
坐吃山猪14 天前
Python29_并发编程
开发语言·网络·python·并发
lee_curry16 天前
JUC第一章 java中基础概念和CompletableFuture
java·多线程·并发·juc
丁劲犇20 天前
改造传统Qt6Widgets程序为多会话MCPServer生产力工具-技巧与实现
qt·ai·agent·并发·mcp·mcpserver·widgets
Echoo华地21 天前
Gatling压测案例
java·jmeter·压力测试·并发·scale·压测·gatling
ん贤23 天前
Go 并发高频十问:goroutine 与线程的区别是什么?select 底层原理是什么?
开发语言·golang·并发