RocketMQ理解(顺序消费)

1.场景分析

顺序消费是指消息的产生顺序和消费顺序相同

假设有个下单场景,每个阶段需要发邮件通知用户订单状态变化。用户付款完成时系统给用户发送订单已付款邮件,订单已发货时给用户发送订单已发货邮件,订单完成时给用户发送订单已完成邮件。

发送邮件的操作为了不阻塞订单主流程,可以通过mq消息来解耦,下游邮件服务器收到mq消息后发送具体邮件,已付款邮件、已发货邮件、订单已完成邮件这三个消息,下游的邮件服务器需要顺序消费这3个消息并且顺序发送邮件才有意义。否则就会出现已发货邮件先发出,已付款邮件后发出的情况。

但是mq消费者往往是集群部署,一个消费组内存在多个消费者,同一个消费者内部,也可能存在多个消费线程并行消费,如何在消费者集群环境中,如何保证邮件mq消息发送与消费的顺序性呢?

顺序消费又分两种,全局顺序消费和局部顺序消费

1.1全局顺序消费

什么是全局顺序消费?所有发到mq的消息都被顺序消费,类似数据库中的binlog,需要严格保证全局操作的顺序性

那么RocketMQ中如何做才能保证全局顺序消费呢?

这就需要设置topic下读写队列数量为1

为什么要设置读写队列数量为1呢?
假设读写队列有多个,消息就会存储在多个队列中,消费者负载时可能会分配到多个消费队列同时进行消费,多队列并发消费时,无法保证消息消费顺序性

那么全局顺序消费有必要么?
A、B都下了单,B用户订单的邮件先发送,A的后发送,不行么?其实,大多数场景下,mq下只需要保证局部消息顺序即可,即A的付款消息先于A的发货消息即可,A的消息和B的消息可以打乱,这样系统的吞吐量会更好,将队列数量置为1,极大的降低了系统的吞吐量,不符合mq的设计初衷

举个例子来说明局部顺序消费。假设订单A的消息为A1,A2,A3,发送顺序也如此。订单B的消息为B1,B2,B3,A订单消息先发送,B订单消息后发送

消费顺序如下
A1,A2,A3,B1,B2,B3是全局顺序消息,严重降低了系统的并发度
A1,B1,A2,A3,B2,B3是局部顺序消息,可以被接受
A2,B1,A1,B2,A3,B3不可接收,因为A2出现在了A1的前面

1.2 局部顺序消费

那么在RocketMQ里局部顺序消息又是如何怎么实现的呢?

要保证消息的顺序消费,有三个关键点

消息顺序发送
消息顺序存储
消息顺序消费
第一点,消息顺序发送,多线程发送的消息无法保证有序性,因此,需要业务方在发送时,针对同一个业务编号(如同一笔订单)的消息需要保证在一个线程内顺序发送,在上一个消息发送成功后,在进行下一个消息的发送。对应到mq中,消息发送方法就得使用同步发送,异步发送无法保证顺序性

第二点,消息顺序存储,mq的topic下会存在多个queue,要保证消息的顺序存储,同一个业务编号的消息需要被发送到一个queue中。对应到mq中,需要使用MessageQueueSelector来选择要发送的queue,即对业务编号进行hash,然后根据队列数量对hash值取余,将消息发送到一个queue中

第三点,消息顺序消费,要保证消息顺序消费,同一个queue就只能被一个消费者所消费,因此对broker中消费队列加锁是无法避免的。同一时刻,一个消费队列只能被一个消费者消费,消费者内部,也只能有一个消费线程来消费该队列。即,同一时刻,一个消费队列只能被一个消费者中的一个线程消费

上面第一、第二点中提到,要保证消息顺序发送和消息顺序存储需要使用mq的同步发送和MessageQueueSelector来保证,具体Demo会有体现

至于第三点中的加锁操作会结合源码来具体分析

Producer
 
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
 
import com.alibaba.rocketmq.client.exception.MQBrokerException;
import com.alibaba.rocketmq.client.exception.MQClientException;
import com.alibaba.rocketmq.client.producer.DefaultMQProducer;
import com.alibaba.rocketmq.client.producer.MessageQueueSelector;
import com.alibaba.rocketmq.client.producer.SendResult;
import com.alibaba.rocketmq.common.message.Message;
import com.alibaba.rocketmq.common.message.MessageQueue;
import com.alibaba.rocketmq.remoting.exception.RemotingException;
 

/**
 * Producer,发送顺序消息
 */
public class Producer {
	
    public static void main(String[] args) throws IOException {
        try {
            DefaultMQProducer producer = new DefaultMQProducer("sequence_producer");
 
            producer.setNamesrvAddr("192.168.159.128:9876;192.168.159.129:9876");
 
            producer.start();
 
            String[] tags = new String[] { "TagA", "TagC", "TagD" };
            
            // 订单列表
            List<OrderDO> orderList =  new Producer().buildOrders();
            
            Date date = new Date();
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            String dateStr = sdf.format(date);
            for (int i = 0; i < 10; i++) {
                // 加个时间后缀
                String body = dateStr + " Hello RocketMQ " + orderList.get(i).getOrderId()+orderList.get(i).getDesc();
                Message msg = new Message("SequenceTopicTest", tags[i % tags.length], "KEY" + i, body.getBytes());
 
                SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
                    @Override
                    public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
                        Long id = Long.valueOf((String)arg);
                        long index = id % mqs.size();
                        return mqs.get((int)index);
                    }
                }, orderList.get(i).getOrderId());//通过订单id来获取对应的messagequeue
 
                System.out.println(sendResult + ", body:" + body);
            }
            
            producer.shutdown();
 
        } catch (MQClientException e) {
            e.printStackTrace();
        } catch (RemotingException e) {
            e.printStackTrace();
        } catch (MQBrokerException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.in.read();
    }
    
    /**
     * 生成模拟订单数据 
     */
    private List<OrderDO> buildOrders() {
    	List<OrderDO> orderList = new ArrayList<OrderDO>();
 
    	OrderDO OrderDO = new OrderDO();
        OrderDO.setOrderId("15103111039");
    	OrderDO.setDesc("创建");
    	orderList.add(OrderDO);
    	
    	OrderDO = new OrderDO();
    	OrderDO.setOrderId("15103111065");
    	OrderDO.setDesc("创建");
    	orderList.add(OrderDO);
    	
    	OrderDO = new OrderDO();
    	OrderDO.setOrderId("15103111039");
    	OrderDO.setDesc("付款");
    	orderList.add(OrderDO);
    	
    	OrderDO = new OrderDO();
    	OrderDO.setOrderId("15103117235");
    	OrderDO.setDesc("创建");
    	orderList.add(OrderDO);
    	
    	OrderDO = new OrderDO();
    	OrderDO.setOrderId("15103111065");
    	OrderDO.setDesc("付款");
    	orderList.add(OrderDO);
    	
    	OrderDO = new OrderDO();
    	OrderDO.setOrderId("15103117235");
    	OrderDO.setDesc("付款");
    	orderList.add(OrderDO);
    	
    	OrderDO = new OrderDO();
    	OrderDO.setOrderId("15103111065");
    	OrderDO.setDesc("完成");
    	orderList.add(OrderDO);
    	
    	OrderDO = new OrderDO();
    	OrderDO.setOrderId("15103111039");
    	OrderDO.setDesc("推送");
    	orderList.add(OrderDO);
    	
    	OrderDO = new OrderDO();
    	OrderDO.setOrderId("15103117235");
    	OrderDO.setDesc("完成");
    	orderList.add(OrderDO);
    	
    	OrderDO = new OrderDO();
    	OrderDO.setOrderId("15103111039");
    	OrderDO.setDesc("完成");
    	orderList.add(OrderDO);
    	return orderList;
    }
}

此处需要注意,producer.send(msg, new MessageQueueSelector()),如果需要全局有序,只需要使new MessageQueueSelector().select(List mqs, Message msg, Object arg)方法返回值唯一且不变,例如:

SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
                    @Override
                    public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
                        Long id = Long.valueOf((String)arg);
                        long index = id % mqs.size();
                        return mqs.get((int)index);
                    }
                }, orderList.get(0).getOrderId());//通过订单id来获取对应的messagequeue

这边获取到的queue永远都是唯一的且确定的(此处只是举个简单的例子,orderList.get(i).getOrderId()改为0亦可)

错误的Consumer

import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeUnit;
 
import com.alibaba.rocketmq.client.consumer.DefaultMQPushConsumer;
import com.alibaba.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
import com.alibaba.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import com.alibaba.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import com.alibaba.rocketmq.client.exception.MQClientException;
import com.alibaba.rocketmq.common.consumer.ConsumeFromWhere;
import com.alibaba.rocketmq.common.message.MessageExt;
 

public class WrongConsumer {
	public static void main(String[] args) throws MQClientException {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name_3");
        consumer.setNamesrvAddr("192.168.159.128:9876;192.168.159.129:9876");
        /**
         * 设置Consumer第一次启动是从队列头部开始消费还是队列尾部开始消费<br>
         * 如果非第一次启动,那么按照上次消费的位置继续消费
         */
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
 
        consumer.subscribe("SequenceTopicTest", "TagA || TagC || TagD");
 		//这里错了
        consumer.registerMessageListener(new MessageListenerConcurrently() {
 
            Random random = new Random();
 
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
                System.out.print(Thread.currentThread().getName() + " Receive New Messages: " );
                for (MessageExt msg: msgs) {
                    System.out.println(msg + ", content:" + new String(msg.getBody()));
                }
                try {
                    //模拟业务逻辑处理中...
                    TimeUnit.SECONDS.sleep(random.nextInt(10));
                } catch (Exception e) {
                    e.printStackTrace();
                }
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
 
        consumer.start();
 
        System.out.println("Consumer Started.");
    }
}
正确的Consumer
 
import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeUnit;
 
import com.alibaba.rocketmq.client.consumer.DefaultMQPushConsumer;
import com.alibaba.rocketmq.client.consumer.listener.ConsumeOrderlyContext;
import com.alibaba.rocketmq.client.consumer.listener.ConsumeOrderlyStatus;
import com.alibaba.rocketmq.client.consumer.listener.MessageListenerOrderly;
import com.alibaba.rocketmq.client.exception.MQClientException;
import com.alibaba.rocketmq.common.consumer.ConsumeFromWhere;
import com.alibaba.rocketmq.common.message.MessageExt;
 

/**
 * 顺序消息消费,带事务方式(应用可控制Offset什么时候提交)
 */
public class Consumer {
 
    public static void main(String[] args) throws MQClientException {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name_3");
        consumer.setNamesrvAddr("192.168.159.128:9876;192.168.159.129:9876");
        /**
         * 设置Consumer第一次启动是从队列头部开始消费还是队列尾部开始消费<br>
         * 如果非第一次启动,那么按照上次消费的位置继续消费
         */
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
 
        consumer.subscribe("SequenceTopicTest", "TagA || TagC || TagD");
 
        consumer.registerMessageListener(new MessageListenerOrderly() {
 
            Random random = new Random();
 
            @Override
            public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
                context.setAutoCommit(true);
                System.out.print(Thread.currentThread().getName() + " Receive New Messages: " );
                for (MessageExt msg: msgs) {
                    System.out.println(msg + ", content:" + new String(msg.getBody()));
                }
                try {
                    //模拟业务逻辑处理中...
                    TimeUnit.SECONDS.sleep(random.nextInt(10));
                } catch (Exception e) {
                    e.printStackTrace();
                }
                return ConsumeOrderlyStatus.SUCCESS;
            }
        });
 
        consumer.start();
        System.out.println("Consumer Started.");
    }
}
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值