如何保证消息队列消费消息的顺序性

消息顺序消费的场景

一个订单产生了三条消息分别是订单创建、订单付款、订单完成。消费时要按照这个顺序消费才能有意义,但是同时订单之间是可以并行消费的。

顺序消息又分为全局顺序消息和局部顺序消息

全局顺序是指某个Topic下的所有消息都要保证顺序;

局部顺序消息只要保证每一组消息(同一订单产生的消息)被顺序消费即可。

一般需要实现的都是局部顺序消费


实现顺序消费

  • 生产者有序发送

RocketMQ生产者生产的消息会将其放入某个队列中,基于队列先进先出的特性,可以保证存入同一队列的消息可以顺序消费。因此,只需要保证一组消息放入相同的队列就能保证生产者有序存储

如果想要实现全局顺序消息,那么只能使用一个队列,以及单个生产者,这是会严重影响性能

普通发送模式下,生产者采用的是轮询的方式将消费均匀的分发到不同的队列中,然后被不同的消费者消费,如果同一组的消息在不同的队列,就无法保证消息消费的有序性

RocketMQ支持生产者在投放消息的时候自定义投放策略,我们实现一个MessageQueueSelector接口,使用Hash取模法保证同一个订单在同一个队列中就行了,即通过订单ID%队列数量得到该ID的订单所投放的队列在队列列表中的索引,然后该订单的所有消息都会被投放到这个队列中。

生产者发送消息的方法中就有一些添加队列选择器的方法,保证消息发送顺序

另外,顺序消息必须使用同步发送的方式才能保证生产者发送的消息有序

风险:如果某个broker挂了,那么队列就会减少一部分,如果采用取余的方式投递,将可能导致同一个业务中的不同消息被发送到不同的队列中,导致同一个业务的不同消息被存入不同的队列中,短暂的造成部分消息无序。同样的,如果增加了服务器,那么也会造成短暂的造成部分消息无序


  • 消费者有序消费

虽然生产者存储消息有序了,但是不实现消费者有序消费仍然不能实现消息顺序消费。

RockerMQ的MessageListener回调函数提供了两种消费模式,有序消费模式MessageListenerOrderly和并发消费模式MessageListenerConcurrently

在消费的时候,还需要保证消费者注册MessageListenerOrderly类型的回调接口实现顺序消费,如果消费者采用Concurrently并行消费,则仍然不能保证消息消费顺序

实际上,每一个消费者的消费端都是采用线程池实现多线程消费的模式,即消费端是多线程消费。虽然MessageListenerOrderly被称为有序消费模式,但是仍然是使用的线程池去消费消息,那它是如何实现顺序消费的呢?

顺序消费模式使用3把锁来保证消费的顺序性:

Broker 端的分布式锁:保证 同一个消费者组下,同一个队列只会被分配给一个消费者实例

MessageQueue 的本地 synchronized 锁:保证同一时刻,同一个队列只能由一个线程消费。

ProcessQueue 的本地 ReentrantLock 锁:防止负载均衡时,该MessageQueue被重新分配,导致消息重复消费

模拟创建订单的业务场景这3种锁冲突:

  • Consumer A 和 Consumer B 启动,同时竞争 MessageQueue1:
    • Consumer A 先获取分布式锁,因此它独占 MessageQueue1。
    • Consumer B 锁获取失败,只能等待。
  • Consumer A 内部的线程池 竞争消费:
    • 线程 T1 获取 MessageQueue1 的 本地 synchronized 锁,消费 Order1001: 创建订单。
    • 线程 T2 必须等待,直到 T1 释放锁,才能消费 Order1001: 支付订单。
  • 负载均衡触发:
    • Broker 尝试把 MessageQueue1 重新分配给 Consumer B。
    • Consumer A 仍在消费,并持有 ProcessQueue 的 consumeLock。
    • Consumer B 无法解锁 MessageQueue1,只得等待 Consumer A 释放锁后再尝试接管

风险:前一个消息消费阻塞时后面消息都会被阻塞。如果遇到消费失败的消息,会自动对当前消息进行重试(每次间隔时间为1秒),无法自动跳过,重试最大次数是Integer.MAX_VALUE,这将导致当前队列消费暂停,因此通常需要设定有一个最大消费次数,以及处理好所有可能的异常情况

你可能感兴趣的:(java,开发语言)