顺序消息是消息队列 RocketMQ 提供的一种高级消息类型。
对于一个指定的Topic,消息严格按照先进先出(FIFO)的原则进行消息发布和消费。
- 即先发送的消息先消费,后发送的消息后消费。
顺序消息适用于对消息发送和消费顺序有严格要求的情况。
应用场景
顺序消息和普通消息的对比如下:
消息类型 | 消费顺序 | 性能 | 适用场景 |
普通消息 | 无顺序 | 高 | 适用于对吞吐量要求高,且对生产和消费顺序无要求 |
顺序消息 | 指定的 Topic 内的消息遵循先入先出(FIFO)规则 | 一般 | 吞吐量要求一般 但是要求特定的 |
订单创建场景:
在一些电商系统中,同一个订单相关的创建订单消息、订单支付消息、订单退款消息、订单物流消息。
必须严格按照先后顺序来进行生产或者消费,否则消费中传递订单状态会发生紊乱,影响业务的正常进行。
因此,该订单的消息必须按照一定的顺序在客户端和消息队列中进行生产和消费。
- 同时消息之间有先后的依赖关系,后一条消息需要依赖于前一条消息的处理结果。
顺序消息分为全局有序和局部有序。
全局有序
可以为Topic设置一个消息队列,使用一个生产者单线程发送数据,消费者端也使用单线程进行消费。
从而保证消息的全局有序,但是这种方式效率低,一般不使用。
局部有序
假设一个Topic分配了两个消息队列,生产者在发送消息的时候,可以对消息设置一个路由ID。
- 比如想保证一个订单的相关消息有序,那么就使用订单ID当做路由ID。
在发送消息的时候,通过订单ID对消息队列的个数取余,根据取余结果选择消息队列。
- 这样同一个订单的数据就可以保证发送到一个消息队列中。
消费者端使用MessageListenerOrderly处理有序消息。
这就是RocketMQ的局部有序,保证消息在某个消息队列中有序。
图片
实现原理
消费者在启动时会调用DefaultMQPushConsumerImpl的start方法。
图片
在DefaultMQPushConsumerImpl的start方法中,对消息监听器类型进行了判断。
如果类型是MessageListenerOrderly表示要进行顺序消费。
此时使用ConsumeMessageOrderlyService对ConsumeMessageService进行实例化。
- 然后调用它的start方法进行启动。
图片
加锁定时任务
进入到ConsumeMessageOrderlyService的start方法中。
可以看到,如果是集群模式,会启动一个定时加锁的任务,周期性的对订阅的消息队列进行加锁。
具体是通过调用RebalanceImpl的lockAll方法实现的。
图片
为什么集群模式下需要加锁?
因为广播模式下,消息队列会分配给消费者下的每一个消费者。
而在集群模式下,一个消息队列同一时刻只能被同一个消费组下的某一个消费者进行。
- 所以在广播模式下不存在竞争关系,也就不需要对消息队列进行加锁。
而在集群模式下,有可能因为负载均衡等原因将某一个消息队列分配到了另外一个消费者中。
- 因此在集群模式下就要加锁,当某个消息队列被锁定时,其他的消费者不能进行消费。
整个顺序消费过程涉及了三把锁,它们分别对应不同的情况。
向Broker申请的消息队列锁
集群模式下一个消息队列同一时刻只能被同一个消费组下的某一个消费者进行。
为了避免负载均衡等原因引起的变动,消费者会向Broker发送请求对消息队列进行加锁。
如果加锁成功,记录到消息队列对应的ProcessQueue中的locked变量中,它是boolean类型的。
public class ProcessQueue {
private volatile boolean locked = false;
}
消费者处理拉取消息时的消息队列锁
消费者在处理拉取到的消息时,由于可以开启多线程进行处理。
所以处理消息前通过MessageQueueLock中的mqLockTable获取到了消息队列对应的锁。
锁住要处理的消息队列,这里加消息队列锁主要是处理多线程之间的竞争。
public class MessageQueueLock {
private ConcurrentMap<MessageQueue, Object> mqLockTable =
new ConcurrentHashMap<MessageQueue, Object>();
消息消费锁
消费者在调用consumeMessage方法之前会加消费锁。
主要是为了避免在消费消息时,由于负载均衡等原因,ProcessQueue被删除。
public class ProcessQueue {
private final Lock consumeLock = new ReentrantLock();
}
图片
顺序消息缺陷
消费顺序消息的并行度依赖于队列的数量。
队列热点问题,个别队列由于哈希不均导致消息过多,消费速度跟不上,产生消息堆积问题。
遇到消息失败的消息,无法跳过,当前队列消费暂停。
热点问题,只能通过拆分MessageQueue和优化路由方法来尽量均衡的将消息分配到不同的MessageQueue。
消费并行度理论上不会有太大问题,因为MessageQueue的数量可以调整。
消费失败的无法跳过是不可避免的。
因为跳过可能导致后续的数据处理都是错误的。
不过可以提供一些策略,由用户根据错误类型来决定是否跳过,并且提供重试队列之类的功能。
- 在跳过之后用户可以在其他地方重新消费到这条消息。
资料分享:
参考:
丁威、周继锋《RocketMQ技术内幕》
https://rocketmq.apache.org/zh/docs/featureBehavior/03fifomessage/