消息队列—顺序消息

消息队列——顺序消息

消息队列中的消息可以被分为5种类型:

  1. 普通消息
  2. 延迟消息
  3. 顺序消息
  4. 批量消息
  5. 事务消息

顺序消息

​ 顺序消息就是希望消费者能按照发布消息的顺序去消费消息。

常见于订单场景:

​ 创建订单 -> 付款 -> 厂家发货 -> 等待用户收件 -> 完成订单

我们需要先创建订单,才能付款,然后让厂家发货,等客户收到快递之后,才能完成订单。

一个顺序消息只能由一个生产者发送,并且只能被发送到同一个队列中,但是不同的顺序消息可以发送到不同队列中实现并发执行。

倘若顺序消息被发送到不同的队列中,就算它们是按照顺序发送的(比如:队列1:创建订单 - > 队列2 :付款 -> 队列3 : 厂家发货),仍然可能会收到消费者消费速度的不同导致消息执行顺序错乱。

在这里插入图片描述

因此要把顺序消息放到一个队列中。如果把所有的订单操作都放在一个队列中,叫做全局顺序消息

但是对于不同订单的下单操作,可以将它们放到不同的队列中,这样并行地去处理提高性能。这个被称为分区顺序消息

对于分区顺序消息,它是通过一个叫 sharding key 来进行分区的。我们可以指定消息中的某个字段作为 sharding key,例如订单号(orderId)所有属于相同的订单的消息都有相同的 orderId,所以可以通过: orderId % queue.size() 来实现队列的分配。

对于顺序消息来说,不同的顺序消息可以通过使用分区顺序消息,可以提高并发度,加快消息的消费速度。

顺序消息的实现原理

顺序消息的生产者只能单一

首先我们来思考一下,顺序消息是否能让多个生产者来生产?

​ 可以想象地到,多个生产者就算按照逻辑顺序或者时间顺序去发送顺序消息,它们仍然难以保证能够正确地发送顺序消息。因为它们发送到 Broker 的实际情况可能很难把控,有点消息发送早,有的消息发送晚。因此,顺序消息必须由单个生产者发送。

顺序消息只能是单线程发送的

​ 虽然 RocketMQ 支持多线程发送消息,但是对于顺序消息来说,如果将其并发地发送,即使在发送时间上它们是对的,也无法保证最终到达 Broker 的顺序。可能受限于网络,也可能是先发送消息的线程突然挂了。

对于顺序消息来说,单生产者和单线程串行发送是必须的

Broker存储的顺序性实现

​ 消息的存储是按照时间顺序追加写入到 commitlog 中的,同时它会被分发到 consumerQueue 中,而一个 consumerQueue 只能被一个消费者消费。

要想实现存储的顺序性,我们只需要让相关的顺序消息都发配到一个 consumerQueue 中。

例如:

我们把同一笔订单的创建、支付、发货操作都发到一个队列中,让消费者按照顺序消费即可。

发送顺序消息到同一个 consumerQueue 的方法

这里有一个前提,一个相关的顺序消息中,各个消息总有一个可以识别当前消息是属于同一个顺序消息的字段。例如:一个订单的下单、付款、发货三条消息都必须有相同的订单号。

我们可以通过利用这个字段,然后将其和队列总数作取余运算,即可让顺序消息都发往同一个 consumerQueue 中。

对于另外的顺序消息,我们仍然可以用相同的取余操作发到不同的队列中来提高消息发送的并发度,这样就保证了分区顺序消息

SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
    Integer id = (Integer) arg; //订单号
    int index = id % mqs.size(); //对队列数取余
    return mqs.get(index); //选择队列
}
}, orderId);

如果你希望保证所有订单的顺序性,那么你可以选择将消息全部发送到一个队列中,这个被称为全局顺序消息。由于全部都发往一个队列了,那么并发度被限制,所以全局顺序消息的性能偏低。

在这里插入图片描述

消费过程的顺序性实现

经过我们正常发送顺序消息,存储顺序消息,消费者只需要老老实实地按照拉取到地消息顺序消费,就可以保证顺序性。

但是,仍然会出现一些问题,比如消费者如果消费失败了,对于一个普通消息来说,它就会进入消息重试,如果默认设置的16次重试都失败了,消息就会进入死信队列,需要人工介入处理。

这样对于顺序消息来说可能是不可接受的,因为前置的消息消费失败了,后续的消息若是消费成功,消息的顺序性就被打乱了。

比如下单操作,用户下单的消息消费失败了,相当于订单未能创建,但是付款的消息却执行成功了,用户一看,钱都扣了咋订单没了??于是客服电话又双叒叕被打爆了!

因此我们需要考虑顺序消息能不能重试,能不能放入死信队列?

RocketMQ 对于顺序消息的重试默认实现是执行 Integer.MAX_VALUE 次。 若是一直无法消费成功,那么就可能会把业务堵塞了。

因此顺序消息消费失败的处理的一个方法是将可能需要支持相关联的所有消息都直接失败,然后找个地方持久化保存这些消息,等待后续修复后再重新消费。

重平衡机制的影响

消费者的重平衡机制也会影响消息的顺序消费。当一个消费者突然宕机了,当前队列没有消费者,那么就会触发重平衡机制,让其他消费者顶替这个挂掉的消费者的位置。

如果这个挂掉的消费者此时没有消费消息倒是还好,新来的消费者就能继续完成后续消费。但是如果挂掉的消费者刚好消费消息到一半,那么此时它还未去修改 Broker 的消费位点,那么就会导致新来的消费者重复消费,甚至可能使得顺序不一致。

顺序消费的三把锁

第一把锁:分布式锁

RocketMQ 提供了一个 ConsumeMessageOrderlyService 类来保证顺序消费。

这个 service 启动的时候会向 Broker 申请当前消费者负责的队列锁,会将消费组、客户端ID、以及负责的队列发往 Broker。 Broker 会将这个队列与当前消费者进行绑定,将关系存储到本地。

这个锁实现了同一个消费者组内,只有一个消费者可以消费这个队列。

这个锁有过期时间,消费者会定期(默认20s)给这个锁续期,确保对分布式锁的占有。

别的消费者要想消费队列,就必须也来加分布式锁,但是如果队列已经被别的消费者给绑定了,那么就无法消费。

第二把锁: Synchronized

这把锁确保了同一时刻只能有一个线程去消费这个队列。

这里需要了解一个前提:

一个消费者可以同时消费多个队列,而一个队列某刻只能被一个消费者消费。

由于占有分布式锁的消费者拿到消息后,它仍然是丢到线程池(因为消费者可能在消费多个队列,并发消费不同队列可以增加性能)去并发消费,但是我们的顺序消息必须保证有序,因此只能让一个线程去消费顺序消息。

第三把锁:ReentrantLock

线程获取到 Synchronized 锁之后,还需要再到 ProcessQueue 中获取到 consumeLock 锁。这是一个 ReentrantLock。

这把锁是为了表明消费者正在消费消息

由于 RocketMQ 存在重平衡机制,我们前面了解到,如果当前消费者正在处理消费消息,还未向 Broker 提交消费点位,如果当前的消费者挂了,触发重平衡机制,那么新来的消费者就可能导致重复消费。

这个锁的功能就是表明当前这个队列还有消息正在被消费,无法重平衡,等待下一次重平衡。

当消费者去请求占有这个锁的时候,如果获取失败,就说明队列正在被消费,则重平衡失败。如果获取锁成功,那么就表明当前队列没有被消费消息,就可以去 Broker 中解除分布式锁,让新的消费者接管这个队列了。

实际上,这三把锁并不能完全保障顺序性和不重复

​ 例如:当一个 Broker 挂了,那么 Broker 对应的队列就全部不可用了,此时会让集群中其他的 Broker 顶替这个挂掉的 Broker,原来队列中相关的消息只能被发送到别的队列里,那么就会被别的消费者消费。

要想保证顺序性,那么就得牺牲可用性,不能让消息发送到别得队列。要想保证可用性又会牺牲顺序性。

RocketMQ 对这两个模式都提供了方案:如果要绝对的顺序性,则创建 Topic 时要指定 -o 参数(–order)为true,且 NameServer 中的配置 orderMessageEnable 和 returnOrderTopicConfigToBroker 必须是 true。

的消息只能被发送到别的队列里,那么就会被别的消费者消费。

要想保证顺序性,那么就得牺牲可用性,不能让消息发送到别得队列。要想保证可用性又会牺牲顺序性。

RocketMQ 对这两个模式都提供了方案:如果要绝对的顺序性,则创建 Topic 时要指定 -o 参数(–order)为true,且 NameServer 中的配置 orderMessageEnable 和 returnOrderTopicConfigToBroker 必须是 true。

上面的条件只要有任何一个是 false 则是选择了可用性。

  • 13
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值