一、什么是顺序消费
RocketMQ 支持两种类型的顺序消息消费:分区顺序消息(Partition Ordered Messages)和全局顺序消息(Global Ordered Messages)。这两种方式都能确保消息的顺序消费,但它们的工作机制有所不同。
二、分区顺序消息
在分区顺序消息中,RocketMQ 保证在同一个消息队列(Message Queue)中的消息按照发送的顺序被消费。为了实现这一点,你需要做以下几点:
-
消息分发:在发送消息时,你需要通过某种键(Sharding Key)将相关联的消息发送到相同的队列中。通常,这个键是业务逻辑中的一部分,比如用户ID或者订单ID。
-
消费控制:在消费端,你需要配置一个顺序消费的消费者(Ordered Consumer),这种消费者在处理完当前的消息之前不会接收到下一个消息。RocketMQ 会确保每个队列只被一个线程消费,从而保证顺序。
三、全局顺序消息
全局顺序消息比分区顺序消息更进一步,它不仅要求在同一个队列中的消息有序,而且要求整个主题的所有消息都按照发送的顺序被消费。这意味着所有的消息都必须进入同一个队列中,这在高并发场景下可能会成为一个瓶颈。
为了实现全局顺序消息,你同样需要配置一个顺序消费的消费者,但是你只能有一个消息队列,所以所有消息都会被发送到这个队列中,从而确保全局顺序。
实现顺序消费的步骤
-
配置消费者:在消费者的配置中,你需要明确指定这是一个顺序消费者。这可以通过实现
MQPushConsumer
接口并重写consumeMessage
方法来完成,同时需要在setConsumeFromWhere
和subscribe
方法中正确配置。 -
消息发送:在发送消息时,使用Sharding Key来确保相关联的消息被发送到相同的队列中(对于分区顺序消息)。
-
处理消息:在
consumeMessage
方法中,确保按顺序处理消息。RocketMQ 的设计会保证消息在一个队列中的顺序消费。
注意事项
-
性能影响:顺序消费会降低系统的吞吐量,因为它限制了并行处理的能力。
-
错误处理:如果在消费过程中发生错误,需要妥善处理,以避免阻塞后续消息的消费。
在实际应用中,你可能需要根据业务需求权衡是否需要顺序消费,以及选择哪种顺序消费策略。在某些情况下,分区顺序消息可能已经足够满足业务需求,而无需使用全局顺序消息。
四、实现例子
全局顺序和分区顺序,主要根据推送设置hashKey来实现的。
发送消息:
下面简单实现一个并发推送消息,来验证消息的顺序处理。
private static final String TOPIC = "TEST_TOPIC";
@Resource
private RocketMQTemplate rocketMQTemplate;
@Test
public void send(){
VolatileExample example = new VolatileExample();
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
int temp = example.increment();
Test test= new Test();
test.setNum(temp+"");
// 指定相同的hashKey, 确保所有消息推送到同一个队列
// 如何消息中用关联id,例如订单id, 可以将订单id作用hashKey, 就能够实现分区顺序
rocketMQTemplate.syncSendOrderly(TOPIC, test, "hashKey");
System.out.println(temp);
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
int temp = example.increment();
Test test= new Test();
test.setNum(temp+"");
// 指定相同的hashKey, 确保所有消息推送到同一个队列
// 如何消息中用关联id,例如订单id, 可以将订单id作用hashKey, 就能够实现分区顺序
rocketMQTemplate.syncSendOrderly(TOPIC, test, "hashKey");
System.out.println(temp);
}
});
Thread thread3 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
int temp = example.increment();
Test test= new Test();
test.setNum(temp+"");
// 指定相同的hashKey, 确保所有消息推送到同一个队列
// 如何消息中用关联id,例如订单id, 可以将订单id作用hashKey, 就能够实现分区顺序
rocketMQTemplate.syncSendOrderly(TOPIC, test, "hashKey");
System.out.println(temp);
}
});
thread1.start();
thread2.start();
thread3.start();
try {
thread1.join();
thread2.join();
thread3.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Counter: " + example.counter);
}
static class VolatileExample {
volatile int counter = 0;
public synchronized int increment() {
return counter++;
}
}
消费消息:
消息消费默认是并发消费,并发消费无法保证顺序性,所以需要修改配置,设置顺序消费。
/** ConsumeMode.ORDERLY 顺序消费 */
@RocketMQMessageListener(topic = "TEST_TOPIC", nameServer = "${rocketmq.name-server}", consumerGroup = "TEST_GROUP" , consumeMode = ConsumeMode.ORDERLY)
@Component
@Slf4j
public class UsersComsumerListener implements RocketMQListener<String> {
@Override
public void onMessage(String msg) {
// 事件处理
log.info("MQ消息:{}", msg);
}
}
五、源码分析
/**
* Same to {@link #syncSend(String, Object)} with send orderly with hashKey by specified.
*
* @param destination formats: topicName:tags
* @param payload the Object to use as payload
* @param hashKey use this key to select queue. for example: orderId, productId ...
* @return {@link SendResult}
*/
public SendResult syncSendOrderly(String destination, Object payload, String hashKey) {
// hashKey 决定消息推送到哪个queue
return syncSendOrderly(destination, payload, hashKey, producer.getSendMsgTimeout());
}
// 可以根据推送源码查询下去,看到选择队列的方法
方法链路:
org.apache.rocketmq.spring.core.RocketMQTemplate#syncSendOrderly(java.lang.String, java.lang.Object, java.lang.String)
org.apache.rocketmq.spring.core.RocketMQTemplate#syncSendOrderly(java.lang.String, java.lang.Object, java.lang.String, long)
org.apache.rocketmq.spring.core.RocketMQTemplate#syncSendOrderly(java.lang.String, org.springframework.messaging.Message<?>, java.lang.String, long)
org.apache.rocketmq.client.producer.DefaultMQProducer#send(org.apache.rocketmq.common.message.Message, org.apache.rocketmq.client.producer.MessageQueueSelector, java.lang.Object, long)
org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl#send(org.apache.rocketmq.common.message.Message, org.apache.rocketmq.client.producer.MessageQueueSelector, java.lang.Object, long)
org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl#sendSelectImpl
org.apache.rocketmq.client.producer.selector.SelectMessageQueueByHash#select
找到了选择队列的方法:
@Override
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
int value = arg.hashCode();
if (value < 0) {
// 小于0时,取绝对值,为正数
value = Math.abs(value);
}
// 根据队列数,模运算找到对应的队列数(标识)
value = value % mqs.size();
// 根据hashCode选择队列
return mqs.get(value);
}
设置消费者注解
@RocketMQMessageListener 中 consumeMode 设置了顺序消息 ConsumeMode.ORDERLY
看一下 ConsumeMode.ORDERLY 是如何实现顺序消费
org.apache.rocketmq.spring.autoconfigure.ListenerContainerConfiguration#afterSingletonsInstantiated
org.apache.rocketmq.spring.autoconfigure.ListenerContainerConfiguration#registerContainer
org.apache.rocketmq.spring.support.DefaultRocketMQListenerContainer
在初始化消费者方法
org.apache.rocketmq.spring.support.DefaultRocketMQListenerContainer#initRocketMQPushConsumer
private void initRocketMQPushConsumer() throws MQClientException {
Assert.notNull(rocketMQListener, "Property 'rocketMQListener' is required");
Assert.notNull(consumerGroup, "Property 'consumerGroup' is required");
Assert.notNull(nameServer, "Property 'nameServer' is required");
Assert.notNull(topic, "Property 'topic' is required");
RPCHook rpcHook = RocketMQUtil.getRPCHookByAkSk(applicationContext.getEnvironment(),
this.rocketMQMessageListener.accessKey(), this.rocketMQMessageListener.secretKey());
boolean enableMsgTrace = rocketMQMessageListener.enableMsgTrace();
if (Objects.nonNull(rpcHook)) {
consumer = new DefaultMQPushConsumer(consumerGroup, rpcHook, new AllocateMessageQueueAveragely(),
enableMsgTrace, this.applicationContext.getEnvironment().
resolveRequiredPlaceholders(this.rocketMQMessageListener.customizedTraceTopic()));
consumer.setVipChannelEnabled(false);
consumer.setInstanceName(RocketMQUtil.getInstanceName(rpcHook, consumerGroup));
} else {
log.debug("Access-key or secret-key not configure in " + this + ".");
consumer = new DefaultMQPushConsumer(consumerGroup, enableMsgTrace,
this.applicationContext.getEnvironment().
resolveRequiredPlaceholders(this.rocketMQMessageListener.customizedTraceTopic()));
}
String customizedNameServer = this.applicationContext.getEnvironment().resolveRequiredPlaceholders(this.rocketMQMessageListener.nameServer());
if (customizedNameServer != null) {
consumer.setNamesrvAddr(customizedNameServer);
} else {
consumer.setNamesrvAddr(nameServer);
}
if (accessChannel != null) {
consumer.setAccessChannel(accessChannel);
}
consumer.setConsumeThreadMax(consumeThreadMax);
if (consumeThreadMax < consumer.getConsumeThreadMin()) {
consumer.setConsumeThreadMin(consumeThreadMax);
}
consumer.setConsumeTimeout(consumeTimeout);
consumer.setInstanceName(this.name);
switch (messageModel) {
case BROADCASTING:
consumer.setMessageModel(org.apache.rocketmq.common.protocol.heartbeat.MessageModel.BROADCASTING);
break;
case CLUSTERING:
consumer.setMessageModel(org.apache.rocketmq.common.protocol.heartbeat.MessageModel.CLUSTERING);
break;
default:
throw new IllegalArgumentException("Property 'messageModel' was wrong.");
}
switch (selectorType) {
case TAG:
consumer.subscribe(topic, selectorExpression);
break;
case SQL92:
consumer.subscribe(topic, MessageSelector.bySql(selectorExpression));
break;
default:
throw new IllegalArgumentException("Property 'selectorType' was wrong.");
}
// 在这个地方可以看到,根据不同的消费模式设置消息监听者
switch (consumeMode) {
case ORDERLY:
consumer.setMessageListener(new DefaultMessageListenerOrderly());
break;
case CONCURRENTLY:
consumer.setMessageListener(new DefaultMessageListenerConcurrently());
break;
default:
throw new IllegalArgumentException("Property 'consumeMode' was wrong.");
}
if (rocketMQListener instanceof RocketMQPushConsumerLifecycleListener) {
((RocketMQPushConsumerLifecycleListener) rocketMQListener).prepareStart(consumer);
}
}
默认顺序消息侦听器
org.apache.rocketmq.spring.support.DefaultRocketMQListenerContainer.DefaultMessageListenerOrderly
实现 MessageListenerOrderly ,顺序消息监听器默认使用手动ACK的方式
public class DefaultMessageListenerOrderly implements MessageListenerOrderly {
@SuppressWarnings("unchecked")
@Override
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
for (MessageExt messageExt : msgs) {
log.debug("received msg: {}", messageExt);
try {
long now = System.currentTimeMillis();
rocketMQListener.onMessage(doConvertMessage(messageExt));
long costTime = System.currentTimeMillis() - now;
log.info("consume {} cost: {} ms", messageExt.getMsgId(), costTime);
} catch (Exception e) {
log.warn("consume message failed. messageExt:{}", messageExt, e);
// 消费失败时,默认暂时挂起1秒不进行消费
context.setSuspendCurrentQueueTimeMillis(suspendCurrentQueueTimeMillis);
return ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT;
}
}
return ConsumeOrderlyStatus.SUCCESS;
}
}