RocketMq(四)消息分类

在上一篇的基础上,

一、普通消息

1、同步发送消息:指的是Producer发出⼀条消息后,会在收到MQ返回的ACK之后才发下⼀条消息。该方式的消息可靠性最高,但消息发送效率低。

    RocketMq(四)消息分类_第1张图片

 SendResult sendResult = producer.send(msg);

(1)生产者:

@RequestMapping("/sendUser")
    public void sendUser(@RequestBody UserDTO userDTO,int count){
        try{
            String userName = userDTO.getUserName();
            //同步发送多条消息
            for(int i=0;i<=count;i++){
                userDTO.setUserName(userName+i);
                Message msg = new Message(userTopic,userTag, JSON.toJSONString(userDTO).getBytes(StandardCharsets.UTF_8));
                msg.setKeys("key"+i);
                SendResult sendResult = defaultMQProducer.send(msg);
                System.out.println(userDTO.getUserName()+"发送结果:"+sendResult);
            }
            logger.info("发送用户消息成功");
        }catch (Exception e){
            logger.error("发送用户消息失败,errorMessage={}",e.getMessage(),e);
        }
    }

RocketMq(四)消息分类_第2张图片

其中,SendStatus有以下值:

public enum SendStatus {

    /*** 发送成功 */
    SEND_OK,

    /*** 刷盘超时。当Broker设置的刷盘策略为同步刷盘时才可能出现这种异常状态。异步刷盘不会出现 */
    FLUSH_DISK_TIMEOUT,

    /*** Slave同步超时。当Broker集群设置的Master-Slave的复制方式为同步复制时才可能出现这种异常状态。异步复制不会出现 */
    FLUSH_SLAVE_TIMEOUT,

    /*** 没有可用的Slave。当Broker集群设置为Master-Slave的复制方式为同步复制时才可能出现这种异常状态。异步复制不会出现 */
    SLAVE_NOT_AVAILABLE;
}

(2)消费者:

@Override
    public ConsumeConcurrentlyStatus consumeMessage(List list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
       try{
           logger.info("{}消息:{} ", Thread.currentThread().getName(), list);
           Message message = list.get(0);
           String body = new String(message.getBody(), "UTF-8");
           System.out.println("消息:"+body);
           return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
       }catch (Exception e){
           logger.error("接收消息异常{}",e.getMessage(),e);
           return ConsumeConcurrentlyStatus.RECONSUME_LATER;
       }
    }

RocketMq(四)消息分类_第3张图片

2、异步发送消息:

Producer发出消息后无需等待MQ返回ACK,直接发送下⼀条消息。该方式的消息可靠性可以得到保障,消息发送效率也可以。

RocketMq(四)消息分类_第4张图片

      producer.send(msg, new SendCallback() {
                    @Override
                    public void onSuccess(SendResult sendResult) {
                        
                    }

                    @Override
                    public void onException(Throwable throwable) {
                        
                    }
                });

(1)生产者:

@RequestMapping("/sendUser")
    public void sendUser(@RequestBody UserDTO userDTO,int count){
        try{
            String userName = userDTO.getUserName();
            //异步发送多条消息
            for(int i=0;i<=count;i++){
                userDTO.setUserName(userName+i);
                Message msg = new Message(userTopic,userTag, JSON.toJSONString(userDTO).getBytes(StandardCharsets.UTF_8));
                msg.setKeys("key"+i);
                defaultMQProducer.send(msg, new SendCallback() {
                    @Override
                    public void onSuccess(SendResult sendResult) {
                        System.out.println(sendResult);
                    }

                    @Override
                    public void onException(Throwable throwable) {
                        throwable.printStackTrace();
                    }
                });
                System.out.println(userDTO.getUserName()+"发送成功");
            }
            logger.info("发送用户消息成功");
        }catch (Exception e){
            logger.error("发送用户消息失败,errorMessage={}",e.getMessage(),e);
        }
    }

RocketMq(四)消息分类_第5张图片 (2)消费者:代码不变,控制台打印无序

RocketMq(四)消息分类_第6张图片

3、单向发送消息:Producer仅负责发送消息,不等待、不处理MQ的ACK。该发送方式时MQ也不返回ACK。该方式的消息发送效率最高,但消息可靠性较差。

RocketMq(四)消息分类_第7张图片

producer.sendOneway(msg);

(1)生产者:

@RequestMapping("/sendUser")
    public void sendUser(@RequestBody UserDTO userDTO,int count){
        try{
            String userName = userDTO.getUserName();
            //单向发送多条消息
            for(int i=0;i<=count;i++){
                userDTO.setUserName(userName+i);
                Message msg = new Message(userTopic,userTag, JSON.toJSONString(userDTO).getBytes(StandardCharsets.UTF_8));
                msg.setKeys("key"+i);
                defaultMQProducer.sendOneway(msg);
                System.out.println(userDTO.getUserName()+"发送成功");
            }
            logger.info("发送用户消息成功");
        }catch (Exception e){
            logger.error("发送用户消息失败,errorMessage={}",e.getMessage(),e);
        }
    }

RocketMq(四)消息分类_第8张图片

(2)消费者:代码不变

RocketMq(四)消息分类_第9张图片

二、顺序消息

1、介绍:顺序消息指的是,严格按照消息的发送顺序进行消费的消息(FIFO)。mq默认有4个队列,默认情况下生产者会把消息以Round Robin轮询方式发送到不同的Queue分区队列;而消费消息时会从多个Queue上拉取消息,这种情况下的发送和消费是不能保证顺序的。如果将消息仅发送到同一个Queue中,消费时也只从这个Queue上拉取消息,就严格保证了消息的顺序性。根据有序范围的不同,RocketMQ可以严格地保证两种消息的有序性:分区有序与全局有序。

(1)全局有序:

RocketMq(四)消息分类_第10张图片

当发送和消费参与的Queue只有一个时所保证的有序是整个Topic中消息的顺序, 称为全局有序。

注:在创建Topic时指定Queue的数量。有三种指定方式
1)在代码中创建Producer时,可以指定其自动创建的Topic的Queue数量。
2)在RocketMQ可视化控制台中手动创建Topic时指定Queue数量。
3)使用mqadmin命令手动创建Topic时指定Queue数量。

(2)分区有序:如果想要实现全局顺序消息,那么只能使用一个队列,以及单个生产者,这是会严重影响性能。因此我们常说的顺序消息通常是指部分顺序消息。

RocketMq(四)消息分类_第11张图片

如果有多个Queue参与,其仅可保证在该Queue分区队列上的消息顺序,则称为分区有序

2、实现方法: 

(1)生产者有序发送:RocketMQ中生产者生产的消息会放置在某个队列中,基于队列先进先出的特性天然的可以保证存入队列的消息顺序和拉取的消息顺序是一致的,因此,我们只需要保证一组相同的消息按照给定的顺序存入同一个队列中,就能保证生产者有序存储。普通发送消息的模式下,生产者会采用轮询的方式将消费均匀的分发到不同的队列中,然后被不同的消费者消费,因为一组消息在不同的队列,此时就无法使用 RocketMQ 带来的队列有序特性来保证消息有序性了。

RocketMq(四)消息分类_第12张图片

如何实现Queue的选择?在定义Producer时可以指定消息队列选择器,而这个选择器是实现了MessageQueueSelector接口定义的。在定义选择器的选择算法时,一般需要使用选择key。这个选择key可以是消息key也可以是其它数据。但无论谁做选择key,都不能重复,都是唯一的。一般性的选择算法是,让选择key(或其hash值)与该Topic所包含的Queue的数量取模,其结果即为选择出的Queue的QueueId。

取模算法存在一个问题:不同选择key与Queue数量取模结果可能会是相同的,即不同选择key的消息可能会出现在相同的Queue,即同一个Consuemr可能会消费到不同选择key的消息。这个问题如何解决?一般性的作法是,从消息中获取到选择key,对其进行判断。若是当前Consumer需要消费的消息,则直接消费,否则,什么也不做。这种做法要求选择key要能够随着消息一起被Consumer获取到。此时使用消息key作为选择key是比较好的做法。如下为订单一种可能的存放顺序:

RocketMq(四)消息分类_第13张图片

另外,顺序消息必须使用同步发送的方式才能保证生产者发送的消息有序。也即取模计算消息队列id+同步发送实现顺序发送消息。 

(2)消费者有序消费:RockerMQ的MessageListener回调函数提供了两种消费模式,有序消费模式MessageListenerOrderly和并发消费模式MessageListenerConcurrently。使用MessageListenerOrderly类型的回调接口实现顺序消费,如果采用Concurrently并行消费仍然不能保证消息消费顺序。实际上,每一个消费者的的消费端都是采用线程池实现多线程消费的模式,即消费端是多线程消费。虽然MessageListenerOrderly被称为有序消费模式,但是仍然是使用的线程池去消费消息。MessageListenerConcurrently是拉取到新消息之后就提交到线程池去消费,而MessageListenerOrderly则是通过加分布式锁和本地锁保证同时只有一条线程去消费一个队列上的数据。即顺序消费模式使用3把锁来保证消费的顺序性:

1、broker端的分布式锁:
在负载均衡的处理新分配队列的updateProcessQueueTableInRebalance方法,以及ConsumeMessageOrderlyService服务启动时的start方法中,都会尝试向broker申请当前消费者客户端分配到的messageQueue的分布式锁。
broker端的分布式锁存储结构为ConcurrentMap>,该分布式锁保证同一个consumerGroup下同一个messageQueue只会被分配给一个consumerClient。
获取到的broker端的分布式锁,在client端的表现形式为processQueue. locked属性为true,且该分布式锁在broker端默认60s过期,而在client端默认30s过期,因此ConsumeMessageOrderlyService#start会启动一个定时任务,每过20s向broker申请分布式锁,刷新过期时间。而负载均衡服务也是每20s进行一次负载均衡。
broker端的分布式锁最先被获取到,如果没有获取到,那么在负载均衡的时候就不会创建processQueue了也不会提交对应的消费请求了。
2、messageQueue的本地synchronized锁:
在执行消费任务的开头,便会获取该messageQueue的本地锁对象objLock,它是一个Object对象,然后通过synchronized实现锁定。
这个锁的锁对象存储在MessageQueueLock.mqLockTable属性中,结构为ConcurrentMap,所以说,一个MessageQueue对应一个锁,不同的MessageQueue有不同的锁。
因为顺序消费也是通过线程池消费的,所以这个synchronized锁用来保证同一时刻对于同一个队列只有一个线程去消费它。
3、ProcessQueue的本地consumeLock:
在获取到broker端的分布式锁以及messageQueue的本地synchronized锁的之后,在执行真正的消息消费的逻辑messageListener#consumeMessage之前,会获取ProcessQueue的consumeLock,这个本地锁是一个ReentrantLock。
在负载均衡时,如果某个队列C被分配给了新的消费者,那么当前客户端消费者需要对该队列进行释放,它会调用removeUnnecessaryMessageQueue方法对该队列C请求broker端分布式锁的解锁。
而在请求broker分布式锁解锁的时候,一个重要的操作就是首先尝试获取这个messageQueue对应的ProcessQueue的本地consumeLock。只有获取了这个锁,才能尝试请求broker端对该messageQueue的分布式锁解锁。
如果consumeLock加锁失败,表示当前消息队列正在消息,不能解锁。那么本次就放弃解锁了,移除消息队列失败,只有等待下次重新分配消费队列时,再进行移除。
如果没有这把锁,假设该消息队列因为负载均衡而被分配给其他客户端B,但是由于客户端A正在对于拉取的一批消费消息进行消费,还没有提交消费点位,如果此时客户端A能够直接请求broker对该messageQueue解锁,这将导致客户端B获取该messageQueue的分布式锁,进而消费消息,而这些没有commit的消息将会发送重复消费。
所以说这把锁的作用,就是防止在消费消息的过程中,该消息队列因为发生负载均衡而被分配给其他客户端,进而导致的两个客户端重复消费消息的行为。

目前来说,消费者使用MessageListenerOrderly顺序消费有两个问题:使用了很多的锁,降低了吞吐量;前一个消息消费阻塞时后面消息都会被阻塞。如果遇到消费失败的消息,会自动对当前消息进行重试(每次间隔时间为1秒),无法自动跳过,重试最大次数是Integer.MAX_VALUE,这将导致当前队列消费暂停,因此通常需要设定有一个最大消费次数,以及处理好所有可能的异常情况。RocketMQ的消费者消息重试和生产者消息重投。

 3、实例:

(1)解耦:如,现在有TOPIC ORDER_STATUS(订单状态),其下有4个Queue队列,该Topic中的不同消息用于描述当前订单的不同状态。假设订单有状态:未支付、已支付、发货中、发货成功、发货失败。根据以上订单状态,生产者从时序上可以生成如下几个消息:

订单T0000001:未支付
 订单T0000001:已支付
 订单T0000001:发货中
 订单T0000001:发货失败

消息发送到MQ中之后,Queue的选择如果采用轮询策略,消息在MQ的存储可能如下

RocketMq(四)消息分类_第14张图片

RocketMq(四)消息分类_第15张图片

这种情况下,希望Consumer消费消息的顺序和发送是一致的,然而上述MQ的投递和消费方式,无法保证顺序是正确的。对于顺序异常的消息,Consumer即使设置有一定的状态容错,也不能完全处理好这么多种随机出现组合情况。

RocketMq(四)消息分类_第16张图片

基于上述的情况,可以设计如下方案:对于相同订单号的消息,通过一定的策略,将其放置在一个Queue中,然后消费者再采用一定的策略(例如,一个线程独立处理一个queue,保证处理消息的顺序性),能够保证消费的顺序性。

for (int i = 0; i < 100; i++) {
            Integer orderId = i;
            byte[] bytes = ("hi " + i).getBytes();
            Message msg = new Message("topic_A", "tag_A", bytes);
            SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
                @Override
                public MessageQueue select(List list, Message msg, Object arg) {
                    Integer id = (Integer) arg;
                    int index = id % list.size();
                    return list.get(index);
                }
            }, orderId);
            System.out.println(sendResult);
        }

(2)异步处理: 如a系统更新用户学校权限,每更新一个学校,b系统需要更新其他相关权限,并且在最后一个学校处理完传递信息给c系统。为此给消息加上标志,a系统分批发送人校关系消息给b系统,一批5个学校,第一批带上标志start,最后一批带上标志end,b系统接收到end则发送处理完毕消息给c系统。这时a、b消息就需要顺序发送和消费,start...中间...end,可以将用户Id相同的消息放在同一个Queue队列中。

生产者:

 @RequestMapping("/sendSchool")
    public void sendSchool(String userAccount,int schoolCount){
        try{
            //常量
            int PAGE_SIZE_DEFAULT = 5;
            String startStage = "start";
            String endStage = "end";
            String startAndEndStage = "startAndEnd";
            //构造学校id
            List schoolIds = new ArrayList<>();
            for(int i=0;i> schoolIdsPart = Lists.partition(schoolIds, PAGE_SIZE_DEFAULT);
            //顺序发送,同一个人 先发送start再发送end,start...end均在同一个queue队列上
            int totalPage = schoolIdsPart.size();
            String uUID = UUID.randomUUID().toString();
            for(int i = 0;i< schoolIdsPart.size();i++){
                String stage = i == 0 ? startStage : (i == totalPage-1 ? endStage : null);
                if(totalPage == 1){
                    stage = startAndEndStage;
                }
                SchoolDTO schoolDTO = new SchoolDTO();
                schoolDTO.setStage(stage);
                schoolDTO.setSchoolIds(schoolIdsPart.get(i));
                Message msg = new Message(schoolTopic,schoolTag, JSON.toJSONString(schoolDTO).getBytes(StandardCharsets.UTF_8));
                SendResult sendResult = defaultMQProducer.send(msg, new MessageQueueSelector() {
                    @Override
                    public MessageQueue select(List mqs, Message msg, Object arg) {
                        Integer param = (Integer) arg;
                        int queueIndex = param % mqs.size();
                        return mqs.get(queueIndex);
                    }
                }, uUID.hashCode());
                logger.info("发送第{}批学校消息成功,stage={}",i,stage);
            }
            logger.info("发送学校消息成功");
        }catch (Exception e){
            logger.error("发送学校消息失败,errorMessage={}",e.getMessage(),e);
        }
    }

 消费者:

public class UserAndSchoolListener implements MessageListenerOrderly {
    private static Logger logger = LoggerFactory.getLogger(UserAndSchoolListener.class);

    @Override
    public ConsumeOrderlyStatus consumeMessage(List msgs, ConsumeOrderlyContext context) {
        context.setAutoCommit(true);
        for (MessageExt msg : msgs){
            //每个queue有唯一的consume线程来消费,用户对每个queue分区有序
            System.out.println("consumeThread=" + Thread.currentThread().getName() + ", " +
                    "queueId" + msg.getQueueId() + ", content:" + new String(msg.getBody()));
        }
        try {
            //模拟处理业务逻辑
            TimeUnit.MILLISECONDS.sleep(300);
        } catch (Exception e) {
            e.printStackTrace();
            //先等一会儿,再处理这批消息,而不是放到重试队列里
            return ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT;
        }
        return ConsumeOrderlyStatus.SUCCESS;
    }
}

通过消费者的消息打印可以看到这批消息队列id都是一样的: 

consumeThread=ConsumeMessageThread_test-mq-groupname_1, queueId0, content:{"schoolIds":["000000a0","000000a1","000000a2","000000a3","000000a4"],"stage":"start"}
consumeThread=ConsumeMessageThread_test-mq-groupname_1, queueId0, content:{"schoolIds":["000000a5","000000a6","000000a7","000000a8","000000a9"]}
consumeThread=ConsumeMessageThread_test-mq-groupname_1, queueId0, content:{"schoolIds":["000000a10","000000a11","000000a12","000000a13","000000a14"]}
consumeThread=ConsumeMessageThread_test-mq-groupname_1, queueId0, content:{"schoolIds":["000000a15"],"stage":"end"}

三、延时消息

1、原理介绍:当消息写入到Broker后,在指定的时长后才可被消费处理的消息,称为延时消息。采用RocketMQ的延时消息可以实现定时任务的功能,而无需使用定时器。原理:

RocketMq(四)消息分类_第17张图片

RocketMq(四)消息分类_第18张图片

Producer将消息发送到Broker后,Broker会首先将消息写入到commitlog文件,然后需要将其分发到相应的consumequeue。不过,在分发之前,系统会先判断消息中是否带有延时等级。若没有,则直接正常分发;若有则需要经历一个复杂的过程

修改消息的Topic为SCHEDULE_TOPIC_XXXX
根据延时等级,在consumequeue目录中SCHEDULE_TOPIC_XXXX主题下创建出相应的queueId目录与consumequeue文件(如果没有这些目录与文件的话)

延迟等级delayLevel与queueId的对应关系为queueId = delayLevel -1。需要注意,在创建queueId目录时,并不是一次性地将所有延迟等级对应的目录全部创建完毕, 而是用到哪个延迟等级创建哪个目录。

修改消息索引单元内容。索引单元中的Message Tag HashCode部分原本存放的是消息的Tag的Hash值。现修改为消息的投递时间。投递时间是指该消息被重新修改为原Topic后再次被写入到commitlog中的时间。投递时间 = 消息存储时间 + 延时等级时间。消息存储时间指的是消息被发送到Broker时的时间戳。
将消息索引写入到SCHEDULE_TOPIC_XXXX主题下相应的consumequeue中。

SCHEDULE_TOPIC_XXXX目录中各个延时等级Queue中的消息是按照消息投递时间排序的。一个Broker中同一等级的所有延时消息会被写入到consumequeue目录中SCHEDULE_TOPIC_XXXX目录下相同Queue中。即一个Queue中消息投递时间的延迟等级时间是相同的。那么投递时间就取决于于消息存储时间了。即按照消息被发送到Broker的时间进行排序的。

(1)投递延时消息
Broker内部有⼀个延迟消息服务类ScheuleMessageService,其会消费SCHEDULE_TOPIC_XXXX中的消息,即按照每条消息的投递时间,将延时消息投递到⽬标Topic中。不过,在投递之前会从commitlog中将原来写入的消息再次读出,并将其原来的延时等级设置为0,即原消息变为了一条不延迟的普通消息。然后再次将消息投递到目标Topic中。

ScheuleMessageService在Broker启动时,会创建并启动一个定时器TImer,用于执行相应的定时任务。系统会根据延时等级的个数,定义相应数量的TimerTask,每个TimerTask负责一个延迟等级消息的消费与投递。每个TimerTask都会检测相应Queue队列的第一条消息是否到期。若第 一条消息未到期,则后面的所有消息更不会到期(消息是按照投递时间排序的);若第一条消 息到期了,则将该消息投递到目标Topic,即消费该消息。
(2)将消息重新写入commitlog

延迟消息服务类ScheuleMessageService将延迟消息再次发送给了commitlog,并再次形成新的消息索引条目分发到相应Queue。其实就是一次普通消息发送。只不过Producer是延迟消息服务类。

2、延时等级:延时消息的延迟时长不支持随意时长的延迟,是通过特定的延迟等级来指定的。延时等级定义在RocketMQ服务端的MessageStoreConfig类中的如下变量中

延迟等级是从1开始计数的,如指定的延时等级为3,则表示延迟时长为10s。当然,如果需要自定义的延时等级,可以通过在broker加载的配置中新增如下配置(例如下面增加了1天这个等级1d)。配置文件在RocketMQ安装目录下的conf目录中。

messageDelayLevel = 1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h 1d

3、实例:

电商平台,订单创建时会发送一条延迟消息。这条消息将会在30分钟后投递给后台业务系统(Consumer),后台业务系统收到该消息后会判断对应的订单是否已经完成支付。如果未完成,则取消订单,将商品再次放回到库存;如果完成支付,则忽略。

12306平台,车票预订成功后会发送一条延迟消息。这条消息将会在45分钟后投递给后台业务系统(Consumer),后台业务系统收到该消息后会判断对应的订单是否已经完成支付。如果未完成,则取消预订,将车票再次放回到票池;如果完成支付,则忽略。

 @RequestMapping("/sendUser")
    public void sendUser(@RequestBody UserDTO userDTO,int count){
        try{
           
           //发送延迟消息
            Message msg = new Message(userTopic,userTag, JSON.toJSONString(userDTO).getBytes(StandardCharsets.UTF_8));
            // 执行消息的延迟等级为3级,即延迟10秒
            msg.setDelayTimeLevel(3);
            SendResult sendResult = defaultMQProducer.send(msg);
            // 输出消息被发送的时间
            System.out.println(new SimpleDateFormat("hh:mm:ss").format(new Date()));
            System.out.println(" , " + sendResult);
        }catch (Exception e){
            logger.error("发送用户消息失败,errorMessage={}",e.getMessage(),e);
        }
    }

生产者控制台打印 

03:43:48
 , SendResult [sendStatus=SEND_OK, msgId=7F000001560418B4AAC2276C9A540001, offsetMsgId=AC1F070900002A9F00000000000699F6, messageQueue=MessageQueue [topic=test-mq-user-topic, brokerName=i-8847E0CB, queueId=0], queueOffset=1]
public class UserAndSchoolListener implements MessageListenerConcurrently {
    private static Logger logger = LoggerFactory.getLogger(UserAndSchoolListener.class);

    @Override
    public ConsumeConcurrentlyStatus consumeMessage(List list, ConsumeConcurrentlyContext context) {
        for (MessageExt msg : list) {
            System.out.println(new SimpleDateFormat("hh:mm:ss").format(new Date()));
            System.out.println(" , " + msg);
        }
        return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
    }
}

消费者控制台打印

03:43:58
 , MessageExt [brokerName=i-8847E0CB, queueId=0, storeSize=308, queueOffset=62, sysFlag=0, bornTimestamp=1696751028820, bornHost=/172.16.12.131:52831, storeTimestamp=1696751011679, storeHost=/172.31.7.9:10911, msgId=AC1F070900002A9F0000000000069B2B, commitLogOffset=432939, bodyCRC=356242209, reconsumeTimes=0, preparedTransactionOffset=0, toString()=Message{topic='test-mq-user-topic', flag=0, properties={MIN_OFFSET=0, REAL_TOPIC=test-mq-user-topic, MAX_OFFSET=63, CONSUME_START_TIME=1696751038868, UNIQ_KEY=7F000001560418B4AAC2276C9A540001, CLUSTER=DefaultCluster, DELAY=3, WAIT=true, TAGS=test-mq-user-tag, REAL_QID=0}, body=[123, 34, 97, 103, 101, 34, 58, 49, 44, 34, 117, 115, 101, 114, 65, 99, 99, 111, 117, 110, 116, 34, 58, 34, 122, 104, 97, 110, 103, 115, 97, 110, 34, 44, 34, 117, 115, 101, 114, 78, 97, 109, 101, 34, 58, 34, -27, -68, -96, -28, -72, -119, 34, 125], transactionId='null'}]

四、事务消息:分布式事务,是在每个微服务项目中都绕不开的问题。常见的解决分案有通过Redis、zk、mq、seata等方式处理。

1、介绍:指Producer端消息发送事件和本地事务事件,同时成功或同时失败
RocketMQ实现事务主要分为两个阶段: 正常事务的发送及提交、事务信息的补偿流程(都是针对生产者 因为事务只出现在DataBase中 有些情况需要将消息存储在数据库中 如果发生事务问题…),整体流程为

正常事务发送与提交阶段
生产者发送一个半消息给broker(半消息是指的暂时不能消费的消息)
服务端响应
开始执行本地事务
根据本地事务的执行情况执行Commit或者Rollback
事务信息的补偿流程
如果broker长时间没有收到本地事务的执行状态,会向生产者发起一个确认会查的操作请求
生产者收到确认会查请求后,检查本地事务的执行状态
根据检查后的结果执行Commit或者Rollback操作 补偿阶段主要是用于解决生产者在发送Commit或者Rollbacke操作时发生超时或失败的情况

RocketMq(四)消息分类_第19张图片

2、关键流程: 

(1)事务消息在一阶段对用户不可见:事务消息相对普通消息最大的特点就是一阶段发送的消息对用户是不可见的,也就是说消费者不能直接消费.这里RocketMQ实现方法是原消息的主题与消息消费队列,然后把主题改成RMQ_SYS_TRANS_HALF_TOPIC.这样由于消费者没有订阅这个主题,所以不会消费。

(2)处理第二阶段的发送消息:在本地事务执行完成后回向Broker发送Commit或者Rollback操作,此时如果在发送消息的时候生产者出故障了,要保证这条消息最终被消费,broker就会向服务端发送回查请求,确认本地事务的执行状态.当然RocketMQ不会无休止的发送事务状态回查请求,默认是15次,如果15次回查还是无法得知事务的状态,RocketMQ默认回滚消息(broker就会将这条半消息删除)。事务的三种状态:

TransactionStatus.CommitTransaction:提交事务消息,消费者可以消费此消息

TransactionStatus.RollbackTransaction:回滚事务,它代表该消息将被删除,不允许被消费。

TransactionStatus.Unknown :中间状态,它代表需要检查消息队列来确定状态。

3、约束: 

事务消息不支持定时和批量
为了避免一个消息被多次检查,导致半数队列消息堆积,RocketMQ限制了单个消息的默认检查次数为15次,通过修改broker配置文件中的transactionCheckMax参数进行调整
特定的时间段之后才检查事务,通过broker配置文件参数transactionTimeout或用户配置CHECK_IMMUNITY_TIME_IN_SECONDS调整时间
一个事务消息可能被检查或消费多次
提交过的消息重新放到用户目标主题可能会失败
事务消息的生产者ID不能与其他类型消息的生产者ID共享

4、demo:

(1)创建生产者:不是简单地创建DefaultMQProducer,而是RocketMQ事务专属的 TransactionMQProducer; 并且不再简单地发送消息了,而是设置一个事务监听器 setTransactionListener(new TransactionListener(){…}); 实现接口方法。 并且由于监听器需要等待本地事务的执行情况不能再生产者发送完消息后关闭。

package com.demo.config;
import org.apache.rocketmq.client.producer.LocalTransactionState;
import org.apache.rocketmq.client.producer.TransactionListener;
import org.apache.rocketmq.client.producer.TransactionMQProducer;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.common.message.MessageExt;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class TransactionMessageProducerConfig {
    private static Logger logger = LoggerFactory.getLogger(TransactionMessageProducerConfig.class);
    @Value("${mq.groupname}")
    private String groupName;

    @Value("${mq.nameserveraddress}")
    private String nameserveraddress;

    /**
     * 事务mq
     * @return
     */
    @Bean(name="transactionMessageProducer",initMethod = "start",destroyMethod = "shutdown")
    public TransactionMQProducer transactionMQProducer(){
        TransactionListener transactionListenerImpl = new TransactionListener() {

            /**
             * 在发送消息成功时执行本地事务
             * @param msg
             * @param arg producer.sendMessageInTransaction的第二个参数
             * @return 返回事务状态
             * LocalTransactionState.COMMIT_MESSAGE:提交事务,提交后broker才允许消费者使用
             * LocalTransactionState.RollbackTransaction:回滚事务,回滚后消息将被删除,并且不允许别消费
             * LocalTransactionState.Unknown:中间状态,表示MQ需要核对,以确定状态
             */
            @Override
            public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
                // TODO 开启本地事务(实际就是我们的jdbc操作)

                // TODO 执行业务代码(插入订单数据库表)
                // int i = orderDatabaseService.insert(....)
                // TODO 提交或回滚本地事务(如果用spring事务注解,这些都不需要我们手工去操作)

                // 模拟一个处理结果
                int index = 8;
                /**
                 * 模拟返回事务状态
                 */
                switch (index) {
                    case 3:
                        logger.info("本地事务回滚,回滚消息,id:{}", msg.getKeys());
                        return LocalTransactionState.ROLLBACK_MESSAGE;
                    case 5:
                    case 8:
                        return LocalTransactionState.UNKNOW;
                    default:
                        logger.info("事务提交,消息正常处理");
                        return LocalTransactionState.COMMIT_MESSAGE;
                }
            }

            /**
             * Broker端对未确定状态的消息发起回查,将消息发送到对应的Producer端(同一个Group的Producer),
             * 由Producer根据消息来检查本地事务的状态,进而执行Commit或者Rollback
             * @param msg
             * @return 返回事务状态
             */
            @Override
            public LocalTransactionState checkLocalTransaction(MessageExt msg) {
                // 根据业务,正确处理: 订单场景,只要数据库有了这条记录,消息应该被commit
                String transactionId = msg.getTransactionId();
                //这里发送消息的key为整数
                Integer key = Integer.valueOf(msg.getKeys());
                logger.info("回查事务状态 key:{} msgId:{} transactionId:{}", key, msg.getMsgId(), transactionId);
                //偶数提交、奇数回滚
                if (key % 2 == 0) { // 刚刚测试的10条消息, 把id_5这条消息提交,其他的全部回滚。
                    logger.info("回查到本地事务已提交,提交消息,id:{}", msg.getKeys());
                    return LocalTransactionState.COMMIT_MESSAGE;
                } else {
                    logger.info("未查到本地事务状态,回滚消息,id:{}", msg.getKeys());
                    return LocalTransactionState.ROLLBACK_MESSAGE;
                }
            }
        };

        // 1. 创建事务生产者对象,和普通消息生产者有所区别,这里使用的是TransactionMQProducer
        TransactionMQProducer producer = new TransactionMQProducer(groupName);
        // 2. 设置NameServer的地址,如果设置了环境变量NAMESRV_ADDR,可以省略此步
        producer.setNamesrvAddr(nameserveraddress);
        producer.setSendMsgTimeout(3000);
        // 3. 设置事务监听器
        producer.setTransactionListener(transactionListenerImpl);
        return producer;
    }
}

(2)生产者发送消息 

@Autowired
private TransactionMQProducer transactionMQProducer;

 @RequestMapping("/sendTransactionMsg")
    public void sendTransactionMsg(){
       try{
           for (int i = 0; i < 10; i++) {
               String content = "Hello transaction message " + i;
               Message msg = new Message(userTopic,userTag, content.getBytes(StandardCharsets.UTF_8));
               msg.setKeys(String.valueOf(i));
               SendResult result = transactionMQProducer.sendMessageInTransaction(msg, i);
               logger.info("发送结果:{}", result);
           }
       }catch (Exception e){
           logger.error("发送失败,errorMsg={}",e.getMessage(),e);
       }
    }

生产者控制台打印:

2023-10-08 16:47:34.875  INFO 32572 --- [nio-8888-exec-8] c.demo.controller.SendMessageController  : 发送结果:SendResult [sendStatus=SEND_OK, msgId=7F0000017F3C18B4AAC227A6FBBE0000, offsetMsgId=null, messageQueue=MessageQueue [topic=test-mq-user-topic, brokerName=i-8847E0CB, queueId=0], queueOffset=15]
2023-10-08 16:47:34.884  INFO 32572 --- [nio-8888-exec-8] c.demo.controller.SendMessageController  : 发送结果:SendResult [sendStatus=SEND_OK, msgId=7F0000017F3C18B4AAC227A6FBDC0001, offsetMsgId=null, messageQueue=MessageQueue [topic=test-mq-user-topic, brokerName=i-8847E0CB, queueId=1], queueOffset=16]
2023-10-08 16:47:34.893  INFO 32572 --- [nio-8888-exec-8] c.demo.controller.SendMessageController  : 发送结果:SendResult [sendStatus=SEND_OK, msgId=7F0000017F3C18B4AAC227A6FBE50002, offsetMsgId=null, messageQueue=MessageQueue [topic=test-mq-user-topic, brokerName=i-8847E0CB, queueId=0], queueOffset=17]
2023-10-08 16:47:34.903  INFO 32572 --- [nio-8888-exec-8] c.demo.controller.SendMessageController  : 发送结果:SendResult [sendStatus=SEND_OK, msgId=7F0000017F3C18B4AAC227A6FBED0003, offsetMsgId=null, messageQueue=MessageQueue [topic=test-mq-user-topic, brokerName=i-8847E0CB, queueId=1], queueOffset=18]
2023-10-08 16:47:34.911  INFO 32572 --- [nio-8888-exec-8] c.demo.controller.SendMessageController  : 发送结果:SendResult [sendStatus=SEND_OK, msgId=7F0000017F3C18B4AAC227A6FBF70004, offsetMsgId=null, messageQueue=MessageQueue [topic=test-mq-user-topic, brokerName=i-8847E0CB, queueId=0], queueOffset=19]
2023-10-08 16:47:34.919  INFO 32572 --- [nio-8888-exec-8] c.demo.controller.SendMessageController  : 发送结果:SendResult [sendStatus=SEND_OK, msgId=7F0000017F3C18B4AAC227A6FC000005, offsetMsgId=null, messageQueue=MessageQueue [topic=test-mq-user-topic, brokerName=i-8847E0CB, queueId=1], queueOffset=20]
2023-10-08 16:47:34.928  INFO 32572 --- [nio-8888-exec-8] c.demo.controller.SendMessageController  : 发送结果:SendResult [sendStatus=SEND_OK, msgId=7F0000017F3C18B4AAC227A6FC070006, offsetMsgId=null, messageQueue=MessageQueue [topic=test-mq-user-topic, brokerName=i-8847E0CB, queueId=0], queueOffset=21]
2023-10-08 16:47:34.937  INFO 32572 --- [nio-8888-exec-8] c.demo.controller.SendMessageController  : 发送结果:SendResult [sendStatus=SEND_OK, msgId=7F0000017F3C18B4AAC227A6FC100007, offsetMsgId=null, messageQueue=MessageQueue [topic=test-mq-user-topic, brokerName=i-8847E0CB, queueId=1], queueOffset=22]
2023-10-08 16:47:34.951  INFO 32572 --- [nio-8888-exec-8] c.demo.controller.SendMessageController  : 发送结果:SendResult [sendStatus=SEND_OK, msgId=7F0000017F3C18B4AAC227A6FC190008, offsetMsgId=null, messageQueue=MessageQueue [topic=test-mq-user-topic, brokerName=i-8847E0CB, queueId=0], queueOffset=23]
2023-10-08 16:47:34.958  INFO 32572 --- [nio-8888-exec-8] c.demo.controller.SendMessageController  : 发送结果:SendResult [sendStatus=SEND_OK, msgId=7F0000017F3C18B4AAC227A6FC270009, offsetMsgId=null, messageQueue=MessageQueue [topic=test-mq-user-topic, brokerName=i-8847E0CB, queueId=1], queueOffset=24]
2023-10-08 16:48:24.028  INFO 32572 --- [pool-1-thread-1] c.d.c.TransactionMessageProducerConfig   : 回查事务状态 key:4 msgId:AC1F070900002A9F000000000006C742 transactionId:7F0000017F3C18B4AAC227A6FBF70004
2023-10-08 16:48:24.028  INFO 32572 --- [pool-1-thread-1] c.d.c.TransactionMessageProducerConfig   : 回查到本地事务已提交,提交消息,id:4
2023-10-08 16:48:24.030  INFO 32572 --- [pool-1-thread-1] c.d.c.TransactionMessageProducerConfig   : 回查事务状态 key:1 msgId:AC1F070900002A9F000000000006C325 transactionId:7F0000017F3C18B4AAC227A6FBDC0001
2023-10-08 16:48:24.031  INFO 32572 --- [pool-1-thread-1] c.d.c.TransactionMessageProducerConfig   : 未查到本地事务状态,回滚消息,id:1
2023-10-08 16:48:24.035  INFO 32572 --- [pool-1-thread-1] c.d.c.TransactionMessageProducerConfig   : 回查事务状态 key:2 msgId:AC1F070900002A9F000000000006C484 transactionId:7F0000017F3C18B4AAC227A6FBE50002
2023-10-08 16:48:24.037  INFO 32572 --- [pool-1-thread-1] c.d.c.TransactionMessageProducerConfig   : 回查到本地事务已提交,提交消息,id:2
2023-10-08 16:48:24.041  INFO 32572 --- [pool-1-thread-1] c.d.c.TransactionMessageProducerConfig   : 回查事务状态 key:6 msgId:AC1F070900002A9F000000000006CA00 transactionId:7F0000017F3C18B4AAC227A6FC070006
2023-10-08 16:48:24.042  INFO 32572 --- [pool-1-thread-1] c.d.c.TransactionMessageProducerConfig   : 回查到本地事务已提交,提交消息,id:6
2023-10-08 16:48:24.046  INFO 32572 --- [pool-1-thread-1] c.d.c.TransactionMessageProducerConfig   : 回查事务状态 key:8 msgId:AC1F070900002A9F000000000006CCBE transactionId:7F0000017F3C18B4AAC227A6FC190008
2023-10-08 16:48:24.053  INFO 32572 --- [pool-1-thread-1] c.d.c.TransactionMessageProducerConfig   : 回查到本地事务已提交,提交消息,id:8
2023-10-08 16:49:24.018  INFO 32572 --- [pool-1-thread-1] c.d.c.TransactionMessageProducerConfig   : 回查事务状态 key:0 msgId:AC1F070900002A9F000000000006D788 transactionId:7F0000017F3C18B4AAC227A6FBBE0000
2023-10-08 16:49:24.020  INFO 32572 --- [pool-1-thread-1] c.d.c.TransactionMessageProducerConfig   : 回查到本地事务已提交,提交消息,id:0
2023-10-08 16:49:24.021  INFO 32572 --- [pool-1-thread-1] c.d.c.TransactionMessageProducerConfig   : 回查事务状态 key:5 msgId:AC1F070900002A9F000000000006DA46 transactionId:7F0000017F3C18B4AAC227A6FC000005
2023-10-08 16:49:24.021  INFO 32572 --- [pool-1-thread-1] c.d.c.TransactionMessageProducerConfig   : 未查到本地事务状态,回滚消息,id:5
2023-10-08 16:49:24.022  INFO 32572 --- [pool-1-thread-1] c.d.c.TransactionMessageProducerConfig   : 回查事务状态 key:9 msgId:AC1F070900002A9F000000000006DD04 transactionId:7F0000017F3C18B4AAC227A6FC270009
2023-10-08 16:49:24.022  INFO 32572 --- [pool-1-thread-1] c.d.c.TransactionMessageProducerConfig   : 未查到本地事务状态,回滚消息,id:9
2023-10-08 16:50:24.013  INFO 32572 --- [pool-1-thread-1] c.d.c.TransactionMessageProducerConfig   : 回查事务状态 key:7 msgId:AC1F070900002A9F000000000006E2B3 transactionId:7F0000017F3C18B4AAC227A6FC100007
2023-10-08 16:50:24.014  INFO 32572 --- [pool-1-thread-1] c.d.c.TransactionMessageProducerConfig   : 未查到本地事务状态,回滚消息,id:7
2023-10-08 16:52:24.024  INFO 32572 --- [pool-1-thread-1] c.d.c.TransactionMessageProducerConfig   : 回查事务状态 key:3 msgId:AC1F070900002A9F000000000006E5F9 transactionId:7F0000017F3C18B4AAC227A6FBED0003
2023-10-08 16:52:24.025  INFO 32572 --- [pool-1-thread-1] c.d.c.TransactionMessageProducerConfig   : 未查到本地事务状态,回滚消息,id:3

(3)消费者:整个事务消息环节与Consumer相关不大,所以不用对原来的Consumer进行修改, 正常接收消息即可. 

public class UserAndSchoolListener implements MessageListenerConcurrently {
    private static Logger logger = LoggerFactory.getLogger(UserAndSchoolListener.class);

    @Override
    public ConsumeConcurrentlyStatus consumeMessage(List list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
        try{
            logger.info("{}消息:{} ", Thread.currentThread().getName(), list);
            for(MessageExt message : list){
                String body = new String(message.getBody(), "UTF-8");
                System.out.println("消息:"+body);
            }
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        }catch (Exception e){
            logger.error("接收消息异常{}",e.getMessage(),e);
            return ConsumeConcurrentlyStatus.RECONSUME_LATER;
        }
    }

消费者控制台打印:

消息:Hello transaction message 4
2023-10-08 16:48:24.076  INFO 20060 --- [-mq-groupname_4] com.demo.listener.UserAndSchoolListener  : ConsumeMessageThread_test-mq-groupname_4消息:[MessageExt [brokerName=i-8847E0CB, queueId=0, storeSize=345, queueOffset=66, sysFlag=8, bornTimestamp=1696754854919, bornHost=/10.5.168.3:50674, storeTimestamp=1696754876823, storeHost=/172.31.7.9:10911, msgId=AC1F070900002A9F000000000006D3C6, commitLogOffset=447430, bodyCRC=706870882, reconsumeTimes=0, preparedTransactionOffset=444928, toString()=Message{topic='test-mq-user-topic', flag=0, properties={TRANSACTION_CHECK_TIMES=1, TRAN_MSG=true, CONSUME_START_TIME=1696754904076, MIN_OFFSET=0, REAL_TOPIC=test-mq-user-topic, MAX_OFFSET=67, KEYS=6, UNIQ_KEY=7F0000017F3C18B4AAC227A6FC070006, CLUSTER=DefaultCluster, PGROUP=test-mq-groupname, WAIT=true, TAGS=test-mq-user-tag, REAL_QID=0}, body=[72, 101, 108, 108, 111, 32, 116, 114, 97, 110, 115, 97, 99, 116, 105, 111, 110, 32, 109, 101, 115, 115, 97, 103, 101, 32, 54], transactionId='7F0000017F3C18B4AAC227A6FC070006'}]] 
消息:Hello transaction message 6
2023-10-08 16:48:24.076  INFO 20060 --- [-mq-groupname_3] com.demo.listener.UserAndSchoolListener  : ConsumeMessageThread_test-mq-groupname_3消息:[MessageExt [brokerName=i-8847E0CB, queueId=0, storeSize=345, queueOffset=65, sysFlag=8, bornTimestamp=1696754854885, bornHost=/10.5.168.3:50674, storeTimestamp=1696754876813, storeHost=/172.31.7.9:10911, msgId=AC1F070900002A9F000000000006D1E5, commitLogOffset=446949, bodyCRC=759970427, reconsumeTimes=0, preparedTransactionOffset=443524, toString()=Message{topic='test-mq-user-topic', flag=0, properties={TRANSACTION_CHECK_TIMES=1, TRAN_MSG=true, CONSUME_START_TIME=1696754904076, MIN_OFFSET=0, REAL_TOPIC=test-mq-user-topic, MAX_OFFSET=67, KEYS=2, UNIQ_KEY=7F0000017F3C18B4AAC227A6FBE50002, CLUSTER=DefaultCluster, PGROUP=test-mq-groupname, WAIT=true, TAGS=test-mq-user-tag, REAL_QID=0}, body=[72, 101, 108, 108, 111, 32, 116, 114, 97, 110, 115, 97, 99, 116, 105, 111, 110, 32, 109, 101, 115, 115, 97, 103, 101, 32, 50], transactionId='7F0000017F3C18B4AAC227A6FBE50002'}]] 
消息:Hello transaction message 2
2023-10-08 16:48:24.088  INFO 20060 --- [-mq-groupname_5] com.demo.listener.UserAndSchoolListener  : ConsumeMessageThread_test-mq-groupname_5消息:[MessageExt [brokerName=i-8847E0CB, queueId=0, storeSize=345, queueOffset=67, sysFlag=8, bornTimestamp=1696754854937, bornHost=/10.5.168.3:50674, storeTimestamp=1696754876829, storeHost=/172.31.7.9:10911, msgId=AC1F070900002A9F000000000006D5A7, commitLogOffset=447911, bodyCRC=1301926757, reconsumeTimes=0, preparedTransactionOffset=445630, toString()=Message{topic='test-mq-user-topic', flag=0, properties={TRANSACTION_CHECK_TIMES=1, TRAN_MSG=true, CONSUME_START_TIME=1696754904088, MIN_OFFSET=0, REAL_TOPIC=test-mq-user-topic, MAX_OFFSET=68, KEYS=8, UNIQ_KEY=7F0000017F3C18B4AAC227A6FC190008, CLUSTER=DefaultCluster, PGROUP=test-mq-groupname, WAIT=true, TAGS=test-mq-user-tag, REAL_QID=0}, body=[72, 101, 108, 108, 111, 32, 116, 114, 97, 110, 115, 97, 99, 116, 105, 111, 110, 32, 109, 101, 115, 115, 97, 103, 101, 32, 56], transactionId='7F0000017F3C18B4AAC227A6FC190008'}]] 
消息:Hello transaction message 8
2023-10-08 16:49:24.030  INFO 20060 --- [-mq-groupname_6] com.demo.listener.UserAndSchoolListener  : ConsumeMessageThread_test-mq-groupname_6消息:[MessageExt [brokerName=i-8847E0CB, queueId=0, storeSize=345, queueOffset=68, sysFlag=8, bornTimestamp=1696754854848, bornHost=/10.5.168.3:50674, storeTimestamp=1696754936788, storeHost=/172.31.7.9:10911, msgId=AC1F070900002A9F000000000006DE63, commitLogOffset=450147, bodyCRC=1128422231, reconsumeTimes=0, preparedTransactionOffset=448392, toString()=Message{topic='test-mq-user-topic', flag=0, properties={TRANSACTION_CHECK_TIMES=2, TRAN_MSG=true, CONSUME_START_TIME=1696754964030, MIN_OFFSET=0, REAL_TOPIC=test-mq-user-topic, MAX_OFFSET=69, KEYS=0, UNIQ_KEY=7F0000017F3C18B4AAC227A6FBBE0000, CLUSTER=DefaultCluster, PGROUP=test-mq-groupname, WAIT=true, TAGS=test-mq-user-tag, REAL_QID=0}, body=[72, 101, 108, 108, 111, 32, 116, 114, 97, 110, 115, 97, 99, 116, 105, 111, 110, 32, 109, 101, 115, 115, 97, 103, 101, 32, 48], transactionId='7F0000017F3C18B4AAC227A6FBBE0000'}]] 
消息:Hello transaction message 0

五、批量消息:

1、介绍:

(1)批量发送消息:能显著提高传递小消息的性能。限制是这些批量消息应该有相同的 topic,相同的 waitStoreMsgOK,而且不能是延时消息。此外,这一批消息的总大小不应超过 4MB,如果超过可以有两种方案:①可以设置Producer的maxMessageSize属性,②修改配置文件中的maxMessageSize属性。

(2)批量接收消息:能提高传递小消息的性能,同时与顺序消息配合的情况下,还能根据业务主键对顺序消息进行去重(是否可去重,需要业务来决定),减少消费者对消息的处理。监听消息接口的consumeMessage()方法第一个参数为消息列 表,默认情况下每次只能消费一条消息,可以通过consumer的pullBatchSize属性设置消息拉取数量(默认32),可以通过设置 consumeMessageBatchMaxSize 属性设置消息一次消费数量(默认1)。

注意:pullBatchSize 和 consumeMessageBatchMaxSize并不是设置越大越好,一次拉取数据量太大会导致长时间等待,性能降低。而且消息处理失败同一批消息都会失败,然后进行重试,导致消费时长增加。增加没必要的重试次数。

2、使用场景:如果消息过多,每次发送消息都和MQ建立连接,无疑是一种性能开销,批量消息可以把消息打包批量发送,批量发送消息能显著提高传递小消息的性能。

3、demo:http://localhost:8888/mqProviderTest/sendMessage/sendUser?count=6

(1)生产者:区别是在发送消息的send方法传参List,而不是Message

 @RequestMapping("/sendUser")
    public void sendUser(@RequestBody UserDTO userDTO,int count){
        try{
            String userName = userDTO.getUserName();
            //发送批量消息
            List messageList = new ArrayList<>();
            for(int i=0;i<=count;i++){
                userDTO.setUserName(userName+i);
                Message msg = new Message(userTopic,userTag, JSON.toJSONString(userDTO).getBytes(StandardCharsets.UTF_8));
                msg.setKeys("key"+i);
                messageList.add(msg);
            }
            SendResult sendResult = defaultMQProducer.send(messageList);
            System.out.println("发送结果:"+sendResult);
        }catch (Exception e){
            logger.error("发送用户消息失败,errorMessage={}",e.getMessage(),e);
        }
    }

 控制台打印

发送结果:SendResult [sendStatus=SEND_OK, msgId=7F0000017FD418B4AAC22B7E3FFD0012,7F0000017FD418B4AAC22B7E3FFD0013,7F0000017FD418B4AAC22B7E3FFD0014,7F0000017FD418B4AAC22B7E3FFD0015,7F0000017FD418B4AAC22B7E3FFD0016,7F0000017FD418B4AAC22B7E3FFD0017,7F0000017FD418B4AAC22B7E3FFD0018, offsetMsgId=AC1F070900002A9F0000000000070220,AC1F070900002A9F0000000000070338,AC1F070900002A9F0000000000070450,AC1F070900002A9F0000000000070568,AC1F070900002A9F0000000000070680,AC1F070900002A9F0000000000070798,AC1F070900002A9F00000000000708B0, messageQueue=MessageQueue [topic=test-mq-user-topic, brokerName=i-8847E0CB, queueId=0], queueOffset=75]

(2)消费者:

设置cosumer批量消费消息最大数量为2

//设置消费最大批量消息条数为2
            consumer.setConsumeMessageBatchMaxSize(2);

 监听器无需修改

 @Override
    public ConsumeConcurrentlyStatus consumeMessage(List list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
        try{
            logger.info("{}消息:{} ", Thread.currentThread().getName(), list);
            for(MessageExt message : list){
                String body = new String(message.getBody(), "UTF-8");
                System.out.println("消息:"+body);
            }
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        }catch (Exception e){
            logger.error("接收消息异常{}",e.getMessage(),e);
            return ConsumeConcurrentlyStatus.RECONSUME_LATER;
        }
    }

控制台打印:

2023-10-09 10:41:34.252  INFO 2348 --- [-mq-groupname_4] com.demo.listener.UserAndSchoolListener  : ConsumeMessageThread_test-mq-groupname_4消息条数:2 
消息:{"age":1,"userAccount":"zhangsan","userName":"张三0"}
消息:{"age":1,"userAccount":"zhangsan","userName":"张三1"}
2023-10-09 10:41:34.277  INFO 2348 --- [-mq-groupname_5] com.demo.listener.UserAndSchoolListener  : ConsumeMessageThread_test-mq-groupname_5消息条数:2 
消息:{"age":1,"userAccount":"zhangsan","userName":"张三2"}
消息:{"age":1,"userAccount":"zhangsan","userName":"张三3"}
2023-10-09 10:41:34.279  INFO 2348 --- [-mq-groupname_7] com.demo.listener.UserAndSchoolListener  : ConsumeMessageThread_test-mq-groupname_7消息条数:1 
消息:{"age":1,"userAccount":"zhangsan","userName":"张三6"}
2023-10-09 10:41:34.282  INFO 2348 --- [-mq-groupname_6] com.demo.listener.UserAndSchoolListener  : ConsumeMessageThread_test-mq-groupname_6消息条数:2 
消息:{"age":1,"userAccount":"zhangsan","userName":"张三4"}
消息:{"age":1,"userAccount":"zhangsan","userName":"张三5"}


 

你可能感兴趣的:(springBoot+消息队列,java)