RocketMQ 如何保证消息的顺序性

和 Kafka 只支持同一个 Partition 内消息的顺序性一样,RocketMQ 中也提供了基于队列(分区)的顺序消费。即同一个队列内的消息可以做到有序,但是不同队列内的消息是无序的

需要从 Product 发送消息顺序,broker 中的 Queue 的接收顺序,Consumer 消费的顺序三个方面来保证。

如何保证 Product 发送消息顺序?

Apache RocketMQ 通过生产者和服务端的协议保障单个生产者串行地发送消息,并按序存储和持久化。
如需保证消息生产的顺序性,则必须满足以下条件:

  • 单一生产者:消息生产的顺序性仅支持单一生产者,不同生产者分布在不同的系统,即使设置相同的消息组,不同生产者之间产生的消息也无法判定其先后顺序。
  • 串行发送(同步发送):Apache RocketMQ 生产者客户端支持多线程安全访问,但如果生产者使用多线程并行发送,则不同线程间产生的消息将无法判定其先后顺序。

如何保证 Queue 中数据的顺序?

当我们作为MQ的生产者需要发送顺序消息时,需要在 send 方法中,传入一个 MessageQueueSelector
MessageQueueSelector 中需要实现一个select方法,这个方法就是用来定义要把消息发送到哪个MessageQueue 的,通常可以使用取模法进行路由:

SendResult sendResult = producer.send(msg, new MessageQueueSelector(){
    @Override
    //mqs:该Topic下所有可选的MessageQueue
    //msg:待发送的消息
    //arg:发送消息时传递的参数
    public MessageQueue select(List<MessageQueue>mgs,Message msg,Object arg){
        Integer id=(Integer)arg;
        //根据参数,计算出一个要接收消息的MessageQueue的下标
        int index= id % mgs.size();
        //返回这个MessageQueue
        return mgs.get(index);
    }
}, orderId);

通过以上形式就可以将需要有序的消息发送到同一个队列中。需要注意的时候,这里需要使用同步发送的方式

将顺序消息发送至 Apache RocketMQ 后,会保证设置了同一消息组的消息,按照发送顺序存储在同一队列中。服务端顺序存储逻辑如下:

  • 相同消息组的消息按照先后顺序被存储在同一个队列。
  • 不同消息组的消息可以混合在同一个队列中,且不保证连续。

image.png
如上图所示,消息组 1 和消息组 4 的消息混合存储在队列 1 中, Apache RocketMQ 保证消息组 1 中的消息G1-M1、G1-M2、G1-M3 是按发送顺序存储,且消息组 4 的消息 G4-M1、G4-M2 也是按顺序存储,但消息组 1 和消息组 4 中的消息不涉及顺序关系。

如何保证 Consumer 消费顺序?

RocketMQ的MessageListener回调函数提供了两种消费模式,

  1. 有序消费模式 MessagelistenerOrderly
  2. 发消费模式 MessageListenerConcurrently。

所以,想要实现顺序消费,需要使用 MessageListenerOrderly 模式接收消息:

consumer.registerMessageListener(new MessageListenerOrderly(){
    Override
    public Consume0rderlystatus consumeMessage(List<MessageExt>msgs ,ConsumeOrderlyContext context) {
        System.out.printf("Receive order msg:"+ new string(msgs.get(@).getBody()));
        return ConsumeOrderlystatus.SUCCESS;
    }
});

当我们用以上方式注册一个消费之后,为了保证同一个队列中的有序消息可以被顺序消费,就要保证 RocketMQ 的 Broker 只会把消息发送到同一个消费者上,这时候就需要加锁了,

在实现中,ConsumeMessageOrderyService 初始化的时候,会启动一个定时任务会尝试向** Broker 为当前消费者客户端申请分布式锁**。如果获取成功,那么后续消息将会只发给这个Consumer。(第一把锁)

接下来在消息拉取的过程中,消费者会一次性拉取多条消息的,并且会将拉取到的消息放入 ProcessQueue,同时将消息提交到消费线程池进行执行

那么拉取之后的消费过程,怎么保证顺序消费呢?这里就需要更多的锁了

RocketMQ 在消费的过程中,需要申请 MessageQueue 锁,确保在同一时间,一个队列中只有一个线程能处理队列中的消息.(第二把锁)

获取到 MessageQueue 的锁后,就可以从 ProcessQueue 中依次拉取一批消息处理了,但是这个过程中,为了保证消息不会出现重复消费,还需要对** ProcessQueue 进行加锁**,(第三把锁)

第三把锁有什么用?

前面介绍客户端加锁过程中,一共加了三把锁,那么,有没有想过这样一个问题,第三把锁如果不加的话,是不是也没问题?

因为我们已经对 MessageQueue 加锁了,为啥还需要对 ProcessQueue 再次加锁呢?

这里其实主要考虑的是重平衡的问题。

当我们的消费者集群,新增了一些消费者,发生重平衡的时候,某个队列可能会原来属于客户端 A 消费的,但是现在要重新分配给客户端 B 了。

这时候客户端 A 就需要把自己加在 Broker上 的锁解掉,而在这个解锁的过程中,就需要确保消息不能在消费过程中就被移除了,因为如果客户端 A 可能正在处理一部分消息,但是位点信息还没有提交,如果客户端 B 立马去消费队列中的消息,那存在一部分数据会被重复消费。

那么如何判断消息是否正在消费中呢,就需要通过这个 ProcessQueue 上面的锁来判断了,也就是说在解锁的线程也需要尝试对 ProcessQueue 进行加锁,加锁成功才能进行解锁操作。以避免过程中有消息消费。

顺序消费存在的问题

通过上面的介绍,我们知道了 RocketMQ 的顺序消费是通过在消费者上多次加锁实现的,这种方式带来的问题就是会降低吞吐量,并且如果前面的消息阻塞,会导致更多消息阻塞。所以,顺序消息需要慎用。

总结

  1. 顺序消费需要由两个阶段消息发送消息消费协同配合,底层支撑依靠的是 RocketMQ 的存储模型;
  2. 顺序消费服务启动后,通过三把锁的机制,使得消费者实例单线程的消费重平衡分配的消费队列;
  3. 假如发生扩容,消费者重启,或者 Broker 宕机 ,顺序消费也会有一定几率较短时间内乱序,所以消费者的业务逻辑还是要保障幂等
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值