MQ出现消息乱序了如何解决?
消息中间件如Kafka、RocketMQ等,普通的消息是有可能存在乱序的,比如说因为网络延迟导致某个消息发送晚了,因为系统异常导致第一个消息处理失败了,等等原因都可能会导致消息乱序
举个简单的例子,一次下单过程中,有一个支付消息、一个发货消息。按理说支付一定在发货之前。所以消息的顺序也是先处理支付消息、再处理发货消息。但是对于一些特殊的业务,比如那种虚拟商品,可能支付后马上就自动发货了。这时候如果有一点点网络延迟,就可能导致发货消息优先于支付消息投递。这就是所谓的消息乱序。
一般来说,消息乱序会导致系统处理异常,比如A消息你还没出来就处理B消息的话可能会失败,也有可能你直接把B消息处理成功了,导致A再来的时候无法处理。等等一系列问题。所以这个乱序的问题是非常关键的。
一般来说,我们有几种办法来解决这个消息乱序的问题。
1、顺序消息
对于那些明确的有顺序的消息,比如像支付消息和发货消息,这个就可以在发送的时候就用顺序消息的方式发送。把他们按照顺序投递到同一个partition(队列上)上,利用分区的顺序性保证消息的顺序投递。
同时还需要确保只有一个消费者进行串行消费,这样才能完全避免消息的乱序。
2、前置状态判断
对于上面说的支付消息和发货消息,我们可以在消息体中增加一个前置状态的信息,比如beforeStatus。
作为一个消费支付消息和发货消息的系统,我们可以基于这个beforeStatus来判断和我系统中的当前状态是否一致,如果是一致的,说明我是可以处理的,那么我就处理这个消息,如果是不一致的,那说明我要的消息还没来,那我就把这个消息处理失败,让MQ下次再重投给我。
这个方案有两个要求:
- 1、消息要能推进单据状态,比如支付消息可以把订单从待支付推进到已支付。
- 2、消息的状态是单向的,不能出现那种从待支付推进到已支付了,过了一会又变成待支付了。
这样就能通过状态来确保消息的有序。
3、增加序列号
如果无法完全保证发送和处理的顺序,又没有状态来做前置判断,可以在消息中引入序列号,消费端根据序列号重排。例如:
- 1.在消息中附加一个递增的序列号。
- 2.消费端使用缓冲区缓存收到的消息,根据序列号重新排序后再处理。
缺点是会增加系统复杂度,并且需要设置缓存超时时间来处理丢失的消息。
4、自己实现排序
还有一种方案,其实是对上面第2个和第3个方案的优化。
第二个方案存在一个问题,那就是依赖MQ的重新投递,有可能会导致最终这个消息丢失了,因为一旦长时间无法消费,消息就会不再重投了。而且不断地让MQ重试,也可能会导致消息堆积,并且对系统造成一定的压力。
第三个方案的问题就是需要再内存中维护一个队列,来进行排序,太麻烦了。
那么,我们有一个做法。是这样的流程:
- 1、接到消息之后,做基本的前置校验,如消息幂等、参数齐全等,如果校验不通过,直接返回失败。
- 2、一旦消息校验成功,把消息体转成一个内部的事件,这个事件是自己定义的,方便后续解析和处理。
- 3、把这个事件存到数据库中,状态设置为待处理。
- 4、如果数据库保存成功,返回消息处理成功。
- 5、再第四步返回之前,开启异步线程处理这个事件,执行他需要执行的代码,如果成功,则把消息状态设置为已处理。如果没成功,不用改消息状态(或者改为失败也可以),然后在执行次数上+1
- 6、起一个异步任务,定时扫描事件表中的未成功的事件进行重试。
这么做,就能确保所有的事件我都有存储,并且存储后立刻返回,避免消息重投和堆积。消息存储下来之后,我就可以基于这些消息做排序,以及重试了,如果某个消息处理失败了,也不怕,不断重试即可。当达到了一定次数之后,报警出来人工跟进。
进一步优化
为了减少这个方案的定时任务带来的延迟,我们可以在写入消息表的时候,在redis中存一条记录,业务单号(比如订单号)当作key,然后把存入的消息的主键id当作value存进去。
这样再有消息过来的时候,先正常处理,如果处理成功了,去redis中查一下是不是存在相同业务单号的待处理的消息,有的话,根据存储的主键id查询对应的事件,放线程池中进行处理。