解决这个问需要知道两个背景知识
1 、 一个Topic下的消息会被保存在多个Queue中, 多个Queue是分片保存的关系(类似于es或者mysql的横向分表), 所以全局是无法保证消息的顺序性的, 但是
单个Queue中的消息是有序的
, 所以要想办法将相同业务key的消息发送到同一个Queue中。
2、Broker会根据负载均衡策略, 尽量平均地将消息队列分配给消费组内的所有消费者实例。
不会出现多个实例同时拉取同一个队列中的数据
。这就意味着可以让一个消费者实例固定的消费一个消息队列。
所以RocketMQ顺序消费需要生产者和消费者两方共同保证
消费者
采用同步发送的方式, 在send()方法中传入一个MessageQueueSelector, 来决定将消息按照自己的规则发送到固定的MessageQueue中, 示例代码如下:
public class OrderProducer {
public static void main(String[] args) throws MQClientException, MQBrokerException, RemotingException, InterruptedException {
DefaultMQProducer orderProducer = new DefaultMQProducer("yzjz_producer");
orderProducer.setNamesrvAddr("127.0.0.1:9876");
orderProducer.start();
for (int i = 0; i < 5; i++) {
for (int j = 0; j < 10; j++) {
Message message = new Message("yzjz", "tag_001", ("yzjz_" + i + "_msg_" + j).getBytes(StandardCharsets.UTF_8));
orderProducer.send(message, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
Integer i = (Integer) arg;
int idx = i % mqs.size();
//yzjz 这里可以定制自己的规则
return mqs.get(idx);
}
}, i);
}
}
System.out.println("消息发送成功");
}
}
消费者
RocketMQ提供了两种消费模式MessageListenerConcurrently和MessageListenerOrderly. 要实现顺序消费就要用Orderly模式进行消费,示例代码如下:
public class OrderConsume1 {
public static void main(String[] args) throws MQClientException {
DefaultMQPushConsumer pushConsumer = new DefaultMQPushConsumer("order_consume");
pushConsumer.setNamesrvAddr("60.205.255.220:9876");
pushConsumer.subscribe("order", "*");
pushConsumer.setMessageListener(new MessageListenerOrderly() {
@Override
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> list, ConsumeOrderlyContext context) {
for (MessageExt messageExt : list) {
System.out.println("消费成功_" + new String(messageExt.getBody()));
}
return ConsumeOrderlyStatus.SUCCESS;
}
});
pushConsumer.start();
System.out.println("pushConsumer1启动成功");
}
}
这样Broker就会把消息固定的发送给一个消费者, 实现原理其实就是通过加锁。
1、消费者与队列的绑定:
ConsumeMessageOrderlyService服务在启动时,会周期性地(默认20秒)向Broker发送锁定消息(请求类型LOCK_BATCH_MQ)锁定当前消费者负责的MessageQueue,Broker收到后,就会把队列、消费者组和消费者clientId进行绑定,这样其他客户端就不能从这个MessageQueue拉取消息。
2、MessageQueue锁:
消费者消费消息时,会申请MessageQueue锁,确保同一时间,一个队列只有一个线程处理消息。
3、ProcessQueue锁
从MessageQueue拉取一批消息后,获取ProcessQueue锁,这样保证了只有当前线程可以进行消息处理,同时在重平衡的时候会判断这把锁,防止Rebalance线程把当前处理的MessageQueue移除掉,避免重复消费。
(这里还有一个背景知识, 重平衡之后, consume会从最小offset位点开始拉取消息, 由于重平衡之前可能有些消费者实例正在消费的位点并没有提交, 导致实际位点与最小位点不一致, 中间偏差的这些消息就有可能重新开投递, 导致重复消费, 官方文档也强调过consume要做好幂等控制)