超详细的RocketMq知识点讲解以及实战

大家好我是魔笑,下面是对RocketMq知识点的一些讲解,以及代码实战,如果有讲的不对的地方,请多指教。,前期介绍了,RockeMq部署架构,以及角色以及相关术语进行讲解。后面主干是从上产消息,存储消息,消费消息,消息流控这条线去梳理知识点的,希望能对你学习RockeMq能更好的理解。

如果想知道lunix怎么安装RocketMq,请看这篇文章:
https://blog.csdn.net/weixin_44291453/article/details/119494263

目录

二,Rocketmq的应用场景

三,RocketMq的部署架构

四,RocketMq的模型

五,RocketMq生产消息重要的知识点以及实战

六,RocketMQ存储消息

七,RocketMQ消费消息重要知识点以及实战

八,高可用和负载均衡

九,Sentinel对RocketMQ进行限流实战


一,什么是Rocketmq?

rocketmq是阿里借鉴kafaka改造和优化而来的,用的是java语言写的。支持了阿里历年的双十一,系统的稳定性是很可靠的。

二,Rocketmq的应用场景

*应用解耦

可以解耦出一些系统,用RocketMq进行解耦,例如将订单发到RocketMQ中,然后订单系统对其消费

*流量削峰

如果系统有大量的请求过来,很可能导致系统奔溃,我们可以将消息发到RocketMQ中进行消息缓存,然后分散处理

*数据分发

我们可以将信息发到rocketMq中,由下游系统去选择性的消费

三,RocketMq的部署架构

从服务的角色分

Producer:消息的生产者,Producer与NameServer集群中的其中一个节点(随机选择)建立长连接,定期从NameServer获取Topic路由信息,并向提供Topic 服务的Master建立长连接,且定时向
Master发送心跳。

Consumer:消息的消费者,Consumer与NameServer集群中的其中一个节点(随机选择)建立长连接,定期从NameServer获取Topic路由信息,并向提供Topic服务的Master、Slave建立长连接,且定时向Master、Slave发送心跳

Broker:暂存和传输消息,Topic就是存放在Broker中的,可搭建主从架构,一个Master和多个slaver

Nameserver:管理Broker,Broker的元数据都要注册给NameServer。每次消费者或者生产者从nameServer获取到Broker的元数据(包括Broker的ip和端口,和所有Topic信息),Nameserver可集群部署,节点之间无任何信息同步

Topic:区分消息的种类;一个发送者可以发送消息给一个或者多个Topic;一个消息的接收者
可以订阅一个或者多个Topic消息

Message Queue:相当于是Topic的分区;用于并行发送和接收消息

注,一个Topic默认有四个Message Queue

四,RocketMq的模型

1)消息模型(Message Model)
RocketMQ主要由 Producer、Broker、Consumer 三部分组成,其中Producer 负责生产消息,
Consumer 负责消费消息,Broker 负责存储消息。Broker 在实际部署过程中对应一台服务器,每个Broker 可以存储多个Topic的消息,每个Topic的消息也可以分片存储于不同的 Broker。MessageQueue 用于存储消息的物理地址,每个Topic中的消息地址存储于多个 Message Queue 中。ConsumerGroup 由多个Consumer 实例构成。ProducerGroup(生产组)由多个Producer组成

producerGroup:同一类Producer的集合,这类Producer发送同一类消息且发送逻辑一致

        //生产组为producer_01
        DefaultMQProducer producer = new DefaultMQProducer("producer_01");

ConsumerGroup:同一类Consumer的集合,这类Consumer通常消费同一类消息且消费逻辑一至,消费者组的消费者实例必须订阅完全相同的Topic

            //消费组为consumer_01,
            DefaultMQPullConsumer consumer = new DefaultMQPullConsumer("consumer_01");

五,RocketMq生产消息重要的知识点以及实战

首先介绍消息的发送

生产者向消息队列里写入消息,不同的业务场景需要生产者采用不同的写入策略。比如同步发送、
异步发送、Oneway发送、延迟发送、发送事务消息等。 默认使用的是DefaultMQProducer类。

什么是同步发送?什么是异步发送?我们先看看下面的两个知识点,同步方式和刷盘机制

1,当Broker是主从架构时,master和slave同步方式

同步复制和异步复制的设置:在/conf/broker.conf,中设置参数brokerRole,默认是ASYNC_MASTER(异步复制),可设置为SYNC_MASTER(同步复制)

当我们在发消息时,如果是异步发送,那么我们将消息发送给broker,我们就返回状态,broker再去异步的刷盘和同步给slave,如果Broker挂了,这样容易导致信息丢失。

如果我们是同步发送,那么我们将信息发送给broker,等broker刷盘,并且同步给slave后我们再返回成功,一般不会发生消息丢失。

2,Broker的刷盘机制:

同步刷盘和异步刷盘的设置:在/conf/broker.conf ,中设置参数flushDiskType,默认的是ASYNC_FLUSH(异步刷盘),可设置为SYNC_FLUSH(同步刷盘)

同步刷盘:当生产者在将消息发到Broker时,会等待消息刷盘后才返回成功

异步刷盘:当生产者将消息发到Broker时,不会等待消息刷盘就返回状态

同步刷盘流程发:

(1). 写入 PageCache后,线程等待,通知刷盘线程刷盘。
(2). 刷盘线程刷盘后,唤醒前端等待线程,可能是一批线程。
(3). 前端等待线程向用户返回成功

同步刷盘和异步刷盘区别:同步刷盘与异步刷盘的唯一区别是异步刷盘写完 PageCache直接返回,而同步刷盘需要等待刷盘完成才返回

理解了上面的两个知识点接下来看看,我们发送消息都有哪些状态

  • FLUSH_DISK_TIMEOUT:表示没有在规定时间内完成刷盘(需要Broker的刷盘策略被设置成SYNC_FLUSH才会报这个错误)。
  • FLUSH_SLAVE_TIMEOUT:表示在主备方式下,并且Broker被设置成SYNC_MASTER(同步复制)方式,没有在设定时间内完成主从同步。
  • SLAVE_NOT_AVAILABLE:这个状态产生的场景和FLUSH_SLAVE_TIMEOUT类似,表示在主备方式下,并且Broker被设置成SYNC_MASTER(同步复制),但是没有找到被配置成Slave的Broker。
  • SEND_OK:表示发送成功,发送成功的具体含义,比如消息是否已经被存储到磁盘?消息是

否被同步到了Slave上?消息在Slave上是否被写入磁盘?需要结合所配置的刷盘策略、主从策
略来定。这个状态还可以简单理解为,没有发生上面列出的三个问题状态就是SEND_OK。

从上面我们就可以知道我们发送消息,然后根据返回结果,就可以对应的处理了。那么也就解释了什么是同步消息,异步消息了,接下来我们说说其他几种消息

3,定时消息:

定时消息(延迟队列)是指消息发送到broker后,不会立即被消费,等待特定时间投递给真正的
topic。

broker有配置项messageDelayLevel,默认值为“1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m
9m 10m 20m 30m 1h 2h”,18个level。

  • level == 0,消息为非延迟消息
  • 1<=level<=maxLevel,消息延迟特定时间,例如level==1,延迟1s
  • level > maxLevel,则level== maxLevel,例如level==20,延迟2h

原理:定时消息会暂存在名为SCHEDULE_TOPIC_XXXX的topic中,并根据delayTimeLevel存入特定的queue,即一个queue只存相同延迟的消息,这16个级别,也就有16个queue,保证具有相同发送延迟的消息能够顺序消费,当时间到了,broker会调度地消费SCHEDULE_TOPIC_XXXX,将消息写入真实的topic

在java代码中设置定时的是:

Message message=new Message("tp-1","*",("hell word").getBytes());
message.setDelayTimeLevel(i);

4,事务消息:

RocketMQ事务消息(Transactional Message)是指应用本地事务和发送消息操作可以被定义到
全局事务中,要么同时成功,要么同时失败。RocketMQ的事务消息提供类似 X/Open XA 的分布事务功能,通过事务消息能达到分布式事务的最终一致性。

RocketMQ事务消息是二阶段提交方式实现事务消息的。我们用一个例子说明一下流程,比如银行转账,A银行的某账户要转一万元到B银行的某账户。A银行发送“B银行账户增加一万元”这个消息,要和“从A银行账户扣除一万元”这个操作同时成功或者同时失败

流程如下
1)发送待确认信息(B银行账户增加一万元)到RocketMq。

2)RocketMq收到信息将信息持久化,向发送方回复消息已经发送成功。

3)发送方执行本地事务(A银行减一万元),

4)发送方根据本地事件执行结果向RocketMq发送二次确认(Commit或是Rollback)信息,RocketMQ收到Commit状态则将第一阶段消息标记为可投递,订阅方将能够收到消息,收到
Rollback状态则删除第一阶段的消息,订阅方接收不到该消息。

5)如果出现异常情况,步骤4)提交的二次确认最终未到达RocketMQ,服务器在经过固定时间段
后将对“待确认”消息发起回查请求。

6)如果发送一阶段消息的Producer不能工作,就访问同生产组的其他produrce,通过检查对应消息的本地事件执行结果返回Commit或Roolback状态。

7)RocketMQ收到回查请求后,按照步骤4)的逻辑处理。

下面是RocketMq流程图:

1,MQ Producer发送Send HalfMsg到MQ Server

2,MQ Server持久化信息,返回 Half msg Send OK

3,执行本地事务,Local Transaction

4,本地事务执行完,返回4 commit or RollBack MQserver根据返回结果,commit Send Msg 或者 Rollback Delete msg

5,如果本地事务没有返回结果,MQ Server 执行Check Back

6,检查本地事务(Check the state of Local)

7,根据检查本地事务,返回Commit或者Rollback

下面是执行事务的实现类:

第一个类是LocalTransaction-Executer,用来实例化步骤3)的逻辑,根据情况返回LocalTransactionState.ROLLBACK_MESSAGE或者LocalTransactionState.COMMIT_MESSAGE状态。

第二个类是TransactionMQProducer,它的用法和DefaultMQProducer类似,要通过它启动一个Producer并发消息,但是比DefaultMQProducer多设置本地事务处理函数和回查状态函数。

第三个类是TransactionCheckListener,实现步骤5)中MQ服务器的回查请求,返回LocalTransactionState.ROLLBACK_MESSAGE或者LocalTransactionState.COMMIT_MESSAGE 

 实现代码:

        TransactionListener listener = new TransactionListener() {
            //执行本地事务
            public LocalTransactionState executeLocalTransaction(Message message, Object o) {
                return null;
            }

            //当本地事务,没有返回结果事,执行返回检查
            public LocalTransactionState checkLocalTransaction(MessageExt messageExt) {
                return null;
            }
        };
        TransactionMQProducer producer = new TransactionMQProducer("tx_producer_grp_08");
        producer.setTransactionListener(listener);

5,消息顺序:

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

顺序消息分为全局顺序消息和部分顺序消息

1. 全局顺序消息指某个Topic下的所有消息都要保证顺序;
2. 部分顺序消息只要保证每一组消息被顺序消费即可。比如订单的生成、付款、发货,这3个消息必须按顺序处理才行

RocketMQ在默认情况下不保证顺序,比如创建一个Topic,默认八个写队列,八个读队列。这时
候一条消息可能被写入任意一个队列里;在数据的读取过程中,可能有多个Consumer,每个
Consumer也可能启动多个线程并行处理,所以消息被哪个Consumer消费,被消费的顺序和写入的顺序是否一致是不确定的。

要保证全局顺序消息,需要先把Topic的读写队列数设置为一,然后Producer和Consumer的并发
设置也要是一。简单来说,为了保证整个Topic的全局消息有序,只能消除所有的并发处理,各部分都设置成单线程处理。

要保证部分消息有序,需要发送端和消费端配合处理。在发送端,要做到把同一业务ID的消息发送
到同一个Message Queue;在消费过程中,要做到从同一个Message Queue读取的消息不被并发处理,这样才能达到部分有序。消费端通过使用MessageListenerOrderly类来解决单Message Queue的消息被并发处理的问题。

实现全局有效

              //推送消息  
DefaultMQPushConsumer defaultMQPushConsumer = new DefaultMQPushConsumer("consumer_grp_02");
        //设置消费者最小线程数   
        defaultMQPushConsumer.setConsumeThreadMin(1);
        //设置消费者最大线程数   
        defaultMQPushConsumer.setConsumeThreadMax(1);
        //从broker拉取的消息数量
        defaultMQPushConsumer.setPullBatchSize(1);
        //MessageListener处理的数量
        defaultMQPushConsumer.setConsumeMessageBatchMaxSize(1);

        defaultMQPushConsumer.setMessageListener(new MessageListenerOrderly() {
            public ConsumeOrderlyStatus consumeMessage(List<MessageExt> list, ConsumeOrderlyContext consumeOrderlyContext) {
                return null;
            }
        });

实现部分有效生产者

        DefaultMQProducer producer = new DefaultMQProducer("demo_produce");
        //订阅主题
        List<MessageQueue> queues = producer.fetchPublishMessageQueues("tp_demo_07");
        producer.setNamesrvAddr("NameServer的ip:port");
        producer.start();
        Message message = null;
        MessageQueue queue = null;
        for (int i = 0; i < 100; i++) {
            queue = queues.get(i % 8);
            //指定同一个MessageQueue存储,这一个流程,
            message = new Message("tp_demo_07", ("生成" + i).getBytes());
            producer.send(message, queue);
            message = new Message("tp_demo_07", ("付款" + i).getBytes());
            producer.send(message, queue);
            message = new Message("tp_demo_07", ("发货" + i).getBytes());
            producer.send(message, queue);
        }

实现部分有效的消费者
 

        DefaultMQPullConsumer consumer = new DefaultMQPullConsumer("consumer_grp_07_01");
        consumer.setNamesrvAddr("node1:9876");
        consumer.start();
//拉取和生产者相同的主题
        Set<MessageQueue> messageQueues = consumer.fetchSubscribeMessageQueues("tp_demo_07");

        for (MessageQueue messageQueue : messageQueues) {
            long nextBeginOffset = 0;
            do {
                //拉取指定主题,偏移量为0,数量为1的消息
                PullResult pullResult = consumer.pull(messageQueue, "*", nextBeginOffset, 1);
                //如果消息队列空了,就退出循环
                if (pullResult == null || pullResult.getMsgFoundList() == null) break;
                //获取下一个消息
                nextBeginOffset = pullResult.getNextBeginOffset();
                List<MessageExt> msgFoundList = pullResult.getMsgFoundList();
                //打印消息
                for (MessageExt messageExt : msgFoundList) {
                    System.out.println(
                            messageExt.getTopic() + "\t" +
                                    messageExt.getQueueId() + "\t" +
                                    messageExt.getMsgId() + "\t" +
                                    new String(messageExt.getBody())
                    );
                }
            } while (true);
        }
        consumer.shutdown();

 6,选择oneway形式发送

  • 通常消息的发送是这样一个过程:
  • 客户端发送请求到服务器
  • 服务器处理请求
  • 服务器向客户端返回应答

所以,一次消息发送的耗时时间是上述三个步骤的总和,而某些场景要求耗时非常短,但是对可靠
性要求并不高,例如日志收集类应用,此类应用可以采用oneway形式调用,oneway形式只发送请求不等待应答,而发送请求在客户端实现层面仅仅是一个操作系统系统调用的开销,即将数据写入客户端的socket缓冲区,此过程耗时通常在微秒级。

        //生产者
        DefaultMQProducer producer=new DefaultMQProducer("demo_produce");
        //指定nameServer地址,端口
        producer.setNamesrvAddr("192.168.112.129:9876");
        //启动producer
        producer.start();
        //组装消息
        Message message=new Message("demo1_tp","hell word".getBytes("utf8"));

        正常发送
        //SendResult send = producer.send(message);
        //oneWay发送
        producer.sendOneway(message);

7,消息过滤:

Tag过滤方式:RocketMQ的消费者可以根据Tag(为消息设置的标志,用于同一主题下区分不同类型的消息)进行消息过滤,也支持自定义属性过滤,消费过滤是在broker端实现的。

SQL92的过滤方式:SQL表达式可以更加灵活的支持复杂过滤逻辑,这种方式的大致做法和上面的Tag过滤方式一样

RocketMQ仅定义了几种基本的语法,用户可以扩展:
1). 数字比较: >, >=, <, <=, BETWEEN, =
2). 字符串比较: =, <>, IN; IS NULL或者IS NOT NULL;
3). 逻辑比较: AND, OR, NOT;
4). Constant types are: 数字如:123, 3.1415; 字符串如:'abc',必须是单引号引起来 NULL,特
殊常量 布尔型如:TRUE or FALSE;

生产者生产消息:

            //指定消息,表示Tag事 tag-0和tage-1,tag-2
            Message message=new Message("tp_demo_06","tag-0","Hello word".getBytes());
            Message message=new Message("tp_demo_06","tag-1","Hello word".getBytes());
            Message message=new Message("tp_demo_06","tag-2","Hello word".getBytes());

生产者消费消息:

          //Tage过滤消息
         //消费者指定消费消息tag-1和tag-0
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumer_grp_06_02");
        consumer.setNamesrvAddr("ip:端口");
         //消费者指定消费消息tag-1
         consumer.subscribe("tp_demo_06", "tag-1");
         //消费者指定消费消息tag-1和tag-0  
         consumer.subscribe("tp_demo_06", "tag-1||tag-0");
        
         //sql92过滤方式,就消费tag-1和tag-0
        consumer.subscribe("tp_demo_06", MessageSelector.bySql("mykey in ('tag-1', 'tag-0')"));

8,消息重投:

生产者在发送消息时:

  • 同步消息失败会重投
  • 异步消息有重试
  • oneway没有任何保证。

*设置retryTimesWhenSendFailed:同步发送失败重投次数,默认为2

*设置retryTimesWhenSendAsyncFailed:异步发送失败重试次数,异步重试不会选择其他broker,仅在同一个broker上做重试,不保证消息不丢

*设置retryAnotherBrokerWhenNotStoreOK:消息刷盘(主或备)超时或slave不可用(返回状态非SEND_OK),是否尝试发送到其他broker,默认false。十分重要消息可以开启。

        DefaultMQProducer producer = new DefaultMQProducer("demo_produce");
        //同步发送失败重试其他broker
        producer.setRetryAnotherBrokerWhenNotStoreOK(true);
        //同步发送失败重试次数
        producer.setRetryTimesWhenSendFailed(2);
        //异步发送失败重试次数
        producer.setRetryTimesWhenSendAsyncFailed(2);

9,消息返回状态:

当我们在发送消息时,记得要打印日志或者记录日志,这样我们就能知道我们发送的消息是否成功或者失败

不同状态在不同的刷盘策略和同步策略的下含义是不同的:
1). FLUSH_DISK_TIMEOUT:表示没有在规定时间内完成刷盘(需要Broker的刷盘策略被设置成SYNC_FLUSH才会报这个错误)。
2). FLUSH_SLAVE_TIMEOUT:表示在主备方式下,并且Broker被设置成SYNC_MASTER(同步)方式,没有在设定时间内完成主从同步。
3). SLAVE_NOT_AVAILABLE:这个状态产生的场景和FLUSH_SLAVE_TIMEOUT类似,表示在主备方式下,并且Broker被设置成SYNC_MASTER(同步),但是没有找到被配置成Slave的Broker。
4). SEND_OK:表示发送成功,发送成功的具体含义,比如消息是否已经被存储到磁盘?消息是
否被同步到了Slave上?消息在Slave上是否被写入磁盘?需要结合所配置的刷盘策略、主从策
略来定。这个状态还可以简单理解为,没有发生上面列出的三个问题状态就是SEND_OK。

        DefaultMQProducer producer = new DefaultMQProducer("demo_produce");
        producer.setRetryAnotherBrokerWhenNotStoreOK(true);
        producer.setRetryTimesWhenSendAsyncFailed(2);
        producer.setRetryTimesWhenSendFailed(2);
        //订阅主题
        List<MessageQueue> queues = producer.fetchPublishMessageQueues("tp_demo_07");
        producer.setNamesrvAddr("NameServer的ip:port");
        producer.start();
        Message message=new Message("tp-1","*",("hell word").getBytes());
        SendResult send = producer.send(message);
        SendStatus sendStatus = send.getSendStatus();
我们看看SendStatus这个类:
public enum SendStatus {
    SEND_OK,
    FLUSH_DISK_TIMEOUT,
    FLUSH_SLAVE_TIMEOUT,
    SLAVE_NOT_AVAILABLE;

    private SendStatus() {
    }
}

了解完了消息发送到broker的方式,以及返回状态,还有发送失败后的重试策略,

10,生产者流控

当生产的消息过大,会超出broker的处理能力,所以会对生产者流空

  • commitLog文件被锁时间超过osPageCacheBusyTimeOutMills时,参数默认为1000ms,发生流控。
  • 如果在broker.config中将transientStorePoolEnable=true。且broker为异步刷盘的主机,transientStorePool中资源不足,拒绝当前send请求,发生流控。
  • broker每隔10ms检查send请求队列头部请求的等待时间,如果超过waitTimeMillsInSendQueue,默认200ms,拒绝当前send请求,发生流控。broker通过拒绝send 请求方式实现流量控制。

上面我们讲了生产消息的一些特性,下面我们说说消息存储

六,RocketMQ存储消息

1,消息存储:

目前的高性能磁盘,顺序写速度可以达到600MB/s, 超过了一般网卡的传输速度。RocketMQ的消息用顺序写,保证了消息存储的速度

2,存储结构:

RocketMQ消息的存储是由ConsumeQueue和CommitLog配合完成 的,消息真正的物理存储文件
是CommitLog,ConsumeQueue是消息的逻辑队列,类似数据库的索引文件,存储的是指向物理存储的地址。每 个Topic下的每个Message Queue都有一个对应的ConsumeQueue文件。

 如图:信息是存储在commitLog文件里的,而ConsumeQueue是存储消息在commitLog中的偏移量,消息大小,还有Tag过滤标识的哈希值TagsCode。当消费者再消费消息时,首先会从comumerQueue里获取到这些值,然后再去CommitLog获取到消息。

(1) CommitLog:消息主体以及元数据的存储主体,存储Producer端写入的消息主体内容,消
息内容不是定长的。单个文件大小默认1G ,文件名长度为20位,左边补零,剩余为起始偏移
量,比如00000000000000000000代表了第一个文件,起始偏移量为0,文件大小为
1G=1073741824;当第一个文件写满了,第二个文件为00000000001073741824,起始偏
移量为1073741824,以此类推。消息主要是顺序写入日志文件,当文件满了,写入下一个文
件;

(2) ConsumeQueue:消息消费队列,引入的目的主要是提高消息消费的性能

如图是consumeQueue的存储结构:

 ConsumeQueue的存储位置:
$HOME/store/consumequeue/{topic}/{queueId}/{fileName}

如图:

七,RocketMQ消费消息重要知识点以及实战

1,获取消息的方式

 RocketMq订阅消息,有两钟模式,一种是拉取消息(PullConsumer模式),一种是推送消息(PushConsumer模式),推送消息实质就是消费者和broker建立一个长连接,当有消息时就会拉去到消息,触发消费者的监听,来处理消息,被感觉是消息推送过来,实质也是拉取消息。

以pull为例,是怎么消费消息呢?

Pull方式里,取消息的过程需要用户自己主动调用,首先通过消费的Topic拿到
MessageQueue的集合,遍历MessageQueue集合,然后针对每个MessageQueue批量取消息,一次取完后,记录该队列下一次要取的开始offset,直到取完了,再换另一个MessageQueue。

        //拉取消息
        DefaultMQPullConsumer defaultMQPullConsumer = new DefaultMQPullConsumer("consumer_grp_01");
        //推送消息
        DefaultMQPushConsumer defaultMQPushConsumer = new DefaultMQPushConsumer("consumer_grp_02");

        //拉取代码的示例
        DefaultMQPullConsumer defaultMQPullConsumer = new DefaultMQPullConsumer("consumer_grp_07_01");
        //指定nameserver的ip和地址
        defaultMQPullConsumer.setNamesrvAddr("192.168.112.129:9876");
        defaultMQPullConsumer.start();
        //获取指定主题的消息
        Set<MessageQueue> messageQueues = defaultMQPullConsumer.fetchSubscribeMessageQueues("demo1_tp");
        //遍历每个MessageQueue(消息队列)
        for (MessageQueue messageQueue : messageQueues) {
            System.err.print(messageQueue.getBrokerName() + "," + messageQueue.getTopic() + "," + messageQueue.getQueueId());
            //处理每个MessageQueue(消息队列)
            PullResult pull = defaultMQPullConsumer.pull(messageQueue, "*", 0, 10);
            List<MessageExt> msgFoundList = pull.getMsgFoundList();
            if (msgFoundList == null) continue;
            for (MessageExt messageExt : msgFoundList) {
                System.err.print(new String(messageExt.getBody(), "utf-8"));
            }

        }
        defaultMQPullConsumer.shutdown();

2,消息者怎么分配消息

广播消费:

一条消息被多个 Consumer 消费,即使这些 Consumer 属于同一个 Consumer Group,消息也会
被 Consumer Group 中的每个 Consumer 都消费一次

集群消费:

一个 Consumer Group 中的 Consumer 实例平均分摊消费消息

怎么设置广播模式和集群模式:

        DefaultMQPushConsumer consumer = new
                DefaultMQPushConsumer("consumer_grp_04_01");
        //广播模式
        consumer.setMessageModel(MessageModel.BROADCASTING);
        //集群模式
        consumer.setMessageModel(MessageModel.CLUSTERING);

3,查询消息


按照Message Key查询消息,当我们发送消息时,我们设置一个唯一的key,然后再消费消息时,再用这个唯一的key查询出来

生产者指定key

        DefaultMQProducer producer = new DefaultMQProducer("demo_produce");
        producer.setNamesrvAddr("NameServer的ip:port");
        producer.start();
        Message message=new Message("tp-1","*",("hell word").getBytes());
        String key= "0A4E00A7178878308DB150A780BB0000";
        message.setKeys(key);
        SendResult send = producer.send(message);
        producer.shutdown();

消费者查询消息

        DefaultMQPullConsumer consumer = new
                DefaultMQPullConsumer("consumer_demo_group");
        consumer.setNamesrvAddr("node1:9876");
        consumer.start();
        MessageExt message = consumer.viewMessage("tp_1",
                "0A4E00A7178878308DB150A780BB0000");
        System.out.println(message);
        System.out.println(message.getMsgId());
        consumer.shutdown();

4,消息优先级

有些场景,需要应用程序处理几种类型的消息,不同消息的优先级不同。RocketMQ是个先入先出
的队列,不支持消息级别或者Topic级别的优先级。业务中简单的优先级需求,可以通过间接的方式解决,下面列举三种优先级相关需求的具体处理方法。
第一种

  • 多个不同的消息类型使用同一个topic时,由于某一个种消息流量非常大,导致其他类型的消息无法及时消费,造成不公平,所以把流量大的类型消息在一个单独的 Topic,其他类型消息在另外一个Topic,应用程序创建两个 Consumer,分别订阅不同的 Topic,这样就可以了。

第二种

  • 情况和第一种情况类似,但是不用创建大量的 Topic。举个实际应用场景: 一个订单处理系统,接收从 100家快递门店过来的请求,把这些请求通过 Producer 写入RocketMQ;订单处理程序通过Consumer 从队列里读取消 息并处理,每天最多处理 1 万单 。 如果这 100 个快递门店中某几个门店订单量 大增,比如门店一接了个大客户,一个上午就发出 2万单消息请求,这样其他 的 99 家门店可能被迫等待门店一的 2 万单处理完,也就是两天后订单才能被处 理,显然很不公平 。
  • 这时可以创建 一 个 Topic, 设置 Topic 的 MessageQueue 数 量 超过 100 个,Producer根据订单的门店号,把每个门店的订单写人 一 个 MessageQueue。 DefaultMQPushConsumer默认是采用循环的方式逐个读取一个 Topic 的所有 MessageQueue,这样如果某家门店订单量大增,这家门店对应的 MessageQueue 消息数增多,等待时间增长,但不会造成其他家门店等待时间增长。DefaultMQPushConsumer 默认的 pullBatchSize 是 32,也就是每次从某个 MessageQueue 读取消息的时候,最多可以读 32 个 。 在上面的场景中,为了更 加公平,可以把 pullBatchSize 设置成1。

第三种
强制优先级

  • TypeA、 TypeB、 TypeC 三类消息 。 TypeA 处于第一优先级,要确保只要有TypeA消息,必须优先处理; TypeB处于第二优先 级; TypeC 处于第三优先级 。 对这种要求,或者逻辑更复杂的要求,就要用 户自己编码实现优先级控制,如果上述的 三 类消息在一个 Topic 里,可以使 用 PullConsumer,自主控制 MessageQueue 的遍历,以及消息的读取;如果上述三类消息在三个 Topic下,需要启动三个Consumer, 实现逻辑控制三个 Consumer 的消费 。

5,回溯消费

我们再消费消息时,有可能需要二次消费,那么RocketMq提供了时间回溯消费,broker可以按照时间维度回退消息,时间精确到毫秒

    //推送消息
    DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("topic-consumer-group", true);
    consumer.setNamesrvAddr(nameServer);
    consumer.subscribe("trace-topic", "*");
    consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
    // 必须是按照下面格式   年月日时分秒 如:20200722110701
    consumer.setConsumeTimestamp("20200722110701");
    consumer.setMessageListener(new MessageListenerConcurrently() {
        @Override
        public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
            if (!CollectionUtils.isEmpty(list)) {
                try {
                    for (MessageExt messageExt : list) {
                        log.info("回溯消息:{}", messageExt);
                    }
                } catch (Exception e) {
                    log.error("处理异常:{}", e);
                    return ConsumeConcurrentlyStatus.RECONSUME_LATER;
                }
            }
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        }
    });
    consumer.start();

6,消息重试:

Consumer消费消息失败后,要提供一种重试机制,令消息再消费一次,因为我们在消费时可能会因为信息不可用,或者,在处理消息时因为服务问题,导致失败。

注意: 一条消息无论重试多少次,这些重试消息的 Message ID 不会改变

无序的消费消息

无序消息的重试只针对集群消费方式生效;广播方式不提供失败重试特性,即消费失败后,失败消
息不再重试,继续消费新的消息

消息重试的最多重试 16 次,每次重试的间隔时间如下:

 消费消息重试实战,可以自定义重试次数,获取重试次数,返回状态是否重试

        DefaultMQPushConsumer consumer = new
                DefaultMQPushConsumer("consumer_grp_04_01");
        consumer.setNamesrvAddr("node1:9876");
        //消息订阅
        consumer.subscribe("tp_demo_04", "*");
        //自定义重试级别,共16个级别,大于16的一律按照2小时重试
        consumer.setMaxReconsumeTimes(20);
        // 并发消费
        consumer.setMessageListener(new MessageListenerConcurrently() {
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
                   //获取重试次数
                   for (MessageExt msg : msgs) {
                       //打印重试次数
                      System.out.println(msg.getReconsumeTimes());
                   }
                     //方式1:返回 ConsumeConcurrentlyStatus.RECONSUME_LATER,消息将重试
                    return ConsumeConcurrentlyStatus.RECONSUME_LATER;
                    //方式2:返回 null,消息将重试
                    return null;
                    //方式3:直接抛出异常, 消息将重试
                    throw new RuntimeException("Consumer Message exceotion");
                    //如下返回,消息不重试
                    return ConsumeConcurrentlyStatus.CONSUME_SUCCESS
            }
        });
        consumer.start();

自定义重试级别,共16个级别,大于16的一律按照2小时重试

  • 消息最大重试次数的设置对相同 Group ID 下的所有 Consumer 实例有效。
  • 如果只对相同 Group ID 下两个 Consumer 实例中的其中一个设置了MaxReconsumeTimes,那么该配置对两个 Consumer 实例均生效。
  • 配置采用覆盖的方式生效,即最后启动的 Consumer 实例会覆盖之前的启动实例的配置

顺序消费消息

消息队列 RocketMQ 会自动不断进行消息重试(每次间隔时间为 1 秒)

        DefaultMQPushConsumer consumer = new
                DefaultMQPushConsumer("consumer_grp_04_01");
        consumer.setNamesrvAddr("node1:9876");

        consumer.setConsumeMessageBatchMaxSize(1);
        consumer.setConsumeThreadMin(1);
        consumer.setConsumeThreadMax(1);
        //消息订阅
        consumer.subscribe("tp_demo_04", "*");
        // 顺序消费
        consumer.setMessageListener(new MessageListenerOrderly() {
            public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs,
                                                       ConsumeOrderlyContext context) {
                for (MessageExt msg : msgs) {
                    System.out.println(msg.getMsgId() + "\t" + msg.getQueueId() +
                            "\t" + new String(msg.getBody()));
                }

            }
        });
        consumer.start();

7,死性队列

RocketMQ中消息重试超过重试次数(默认16次)就会被放到死信队列中,在消息队列
RocketMQ 中,这种正常情况下无法被消费的消息称为死信消息(Dead-Letter Message)

注:当消息没有被正常消费,消费消息有一个重试机制,当达到了设置的最大重试次数,这条消息就会被放入死性队列,

死信消息具有以下特性

  • 不会再被消费者正常消费。
  • 有效期与正常消息相同,均为 3 天,3 天后会被自动删除。因此,请在死信消息产生后的 3天内及时处理。

死信队列具有以下特性:

  • 一个死信队列对应一个 Group ID, 而不是对应单个消费者实例。
  • 如果一个 Group ID 未产生死信消息,消息队列 RocketMQ 不会为其创建相应的死信队列。
  • 一个死信队列包含了对应 Group ID 产生的所有死信消息,不论该消息属于哪个 Topic。

如果想查看死信队列,可以使用可视化工具:rocketmq-console

怎么处理死信队列:

查看死信队列,然后让RocketMq重新发送消息,然后消费者消费消息,查看问题所在,然后解决问题

8,消费者流空

当消费信息过大,会超出消费者的处理能力,所以会对消费者流空

  • 消费者本地缓存消息数超过pullThresholdForQueue时,默认1000。
  • 消费者本地缓存消息大小超过pullThresholdSizeForQueue时,默认100MB。
  • 消费者本地缓存消息跨度超过consumeConcurrentlyMaxSpan时,默认2000。
  • 消费者流控的结果是降低拉取频率。

接下来我们说说,消费者和生产者的高可用和负载均很

八,高可用和负载均衡

1,高可用机制

1)NameServer可以部署多台机器,机器之间不相互同步信息。NameServer达到高可用

2)Broker可以主从建构部署,主从之间互相信息同步,Broker达到高可用

broker的master和slaver的区别:

  • 在Broker的Broker.conf的配置文件中,参数brokerId的值为0表明这个Broker是Master,
  • 大于0表明这个Broker是Slave,
  • brokerRole参数也说明这个Broker是Master还是Slave。(SYNC_MASTER/ASYNC_MASTER/SALVE)
  • Master角色的Broker支持读和写,Slave角色的Broker仅支持读。
  • Consumer可以连接Master角色的Broker,也可以连接Slave角色的Broker来读取消息。

3)生产者可以多节点,生产消息

4)消费者可以多节点,消费消息

架构图:

2,生产者高可用

生产者消息都是发送到master的,当一个master宕机了,那么可以发送给别的master,

当只是部署了一组master,slave,当master宕机了,手动将slave转化成master

1)手动停止Slave角色的Broker。
2)更改配置文件。
3)用新的配置文件启动Broker。

为了解决这个问题,RocketMQ引入了Dledger使用新的复制方式

Dledger 在写入消息的时候,要求至少消息复制到半数以上的节点之后,才给客户端返回写入成
功,并且它是支持通过选举来动态切换主节点的。

例如3台机器,信息到来时,至少下入两台,才返回信息成功

Dledger的不足:

1) 比如,选举过程中不能提供服务。
2.)最少需要 3 个节点才能保证数据一致性,3 节点时,只能保证 1 个节点宕机时可用,如果 2
个节点同时宕机,即使还有 1 个节点存活也无法提供服务,资源的利用率比较低。
3.)另外,由于至少要复制到半数以上的节点才返回写入成功,性能上也不如主从异步复制的方式
快。

3,消费者高可用

在Consumer的配置文件中,并不需要设置是从Master读还是从Slave 读,当Master不可用或者繁
忙的时候,Consumer会被自动切换到从Slave 读。

九,Sentinel对RocketMQ进行限流实战

Sentinel 专门为这种场景提供了匀速器的特性,可以把突然到来的大量请求以匀速的形式均摊,以
固定的间隔时间让请求通过,以稳定的速度逐步处理这些请求,起到“削峰填谷”的效果,从而避免流量突刺造成系统负载过高。同时堆积的请求将会排队,逐步进行处理;当请求排队预计超过最大超时时长的时候则直接拒绝,而不是拒绝全部请求。

比如在 RocketMQ 的场景下配置了匀速模式下请求 QPS 为 5,则会每 200 ms 处理一条消息,多
余的处理任务将排队;同时设置了超时时间为 5 s,预计排队时长超过 5s 的处理任务将会直接被拒绝。

流控规则:

        FlowRule rule = new FlowRule();
        // 消费组名称:主题名称   字符串
        rule.setResource(KEY);
        // 根据QPS进行流控
        rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
        // 1表示QPS为1,请求间隔1000ms。
        // 如果是5,则表示每秒5个消息,请求间隔200ms
        rule.setCount(5);
        rule.setLimitApp("default");

        // 调用使用固定间隔。如果qps为1,则请求之间间隔为1s
        rule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER);
        // 如果请求太多,就将这些请求放到等待队列中
        // 该队列有超时时间。如果等待队列中请求超时,则丢弃
        // 此处设置超时时间为5s
        rule.setMaxQueueingTimeMs(5 * 1000);
        // 使用流控管理器加载流控规则
        FlowRuleManager.loadRules(Collections.singletonList(rule));

完整代码:

public class MyConsumer {

    // 消费组名称
    private static final String GROUP_NAME = "consumer_grp_13_01";
    // 主题名称
    private static final String TOPIC_NAME = "tp_demo_13";
    // consumer_grp_13_01:tp_demo_13
    private static final String KEY = String.format("%s:%s", GROUP_NAME, TOPIC_NAME);
    // 使用map存放主题每个MQ的偏移量
    private static final Map<MessageQueue, Long> OFFSET_TABLE = new HashMap<MessageQueue, Long>();

    @SuppressWarnings("PMD.ThreadPoolCreationRule")
    // 具有固定大小的线程池
    private static final ExecutorService pool = Executors.newFixedThreadPool(32);

    private static final AtomicLong SUCCESS_COUNT = new AtomicLong(0);
    private static final AtomicLong FAIL_COUNT = new AtomicLong(0);

    public static void main(String[] args) throws MQClientException {
        // 初始化哨兵的流控
        initFlowControlRule();

        DefaultMQPullConsumer consumer = new DefaultMQPullConsumer(GROUP_NAME);
        consumer.setNamesrvAddr("192.168.112.129:9876");
        consumer.start();

        Set<MessageQueue> mqs = consumer.fetchSubscribeMessageQueues(TOPIC_NAME);
        for (MessageQueue mq : mqs) {
            System.out.printf("Consuming messages from the queue: %s%n", mq);

            SINGLE_MQ:
            while (true) {
                try {
                    PullResult pullResult =
                            consumer.pullBlockIfNotFound(mq, null, getMessageQueueOffset(mq), 32);
                    if (pullResult.getMsgFoundList() != null) {
                        for (MessageExt msg : pullResult.getMsgFoundList()) {
                            doSomething(msg);
                        }
                    }

                    long nextOffset = pullResult.getNextBeginOffset();
                    // 将每个mq对应的偏移量记录在本地HashMap中
                    putMessageQueueOffset(mq, nextOffset);
                    consumer.updateConsumeOffset(mq, nextOffset);
                    switch (pullResult.getPullStatus()) {
                        case NO_NEW_MSG:
                            break SINGLE_MQ;
                        case FOUND:
                        case NO_MATCHED_MSG:
                        case OFFSET_ILLEGAL:
                        default:
                            break;
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }

        consumer.shutdown();
    }

    /**
     * 对每个收到的消息使用一个线程提交任务
     * @param message
     */
    private static void doSomething(MessageExt message) {
        pool.submit(() -> {
            Entry entry = null;
            try {
                // 应用流控规则
                ContextUtil.enter(KEY);
                entry = SphU.entry(KEY, EntryType.OUT);

                // 在这里处理业务逻辑,此处只是打印
                System.out.printf("[%d][%s][Success: %d] Receive New Messages: %s %n", System.currentTimeMillis(),
                        Thread.currentThread().getName(), SUCCESS_COUNT.addAndGet(1), new String(message.getBody()));
            } catch (BlockException ex) {
                // Blocked.
                System.out.println("Blocked: " + FAIL_COUNT.addAndGet(1));
            } finally {
                if (entry != null) {
                    entry.exit();
                }
                ContextUtil.exit();
            }
        });
    }

    private static void initFlowControlRule() {
        FlowRule rule = new FlowRule();
        // 消费组名称:主题名称   字符串
        rule.setResource(KEY);
        // 根据QPS进行流控
        rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
        // 1表示QPS为1,请求间隔1000ms。
        // 如果是5,则表示每秒5个消息,请求间隔200ms
        rule.setCount(5);
        rule.setLimitApp("default");

        // 调用使用固定间隔。如果qps为1,则请求之间间隔为1s
        rule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER);
        // 如果请求太多,就将这些请求放到等待队列中
        // 该队列有超时时间。如果等待队列中请求超时,则丢弃
        // 此处设置超时时间为5s
        rule.setMaxQueueingTimeMs(5 * 1000);
        // 使用流控管理器加载流控规则
        FlowRuleManager.loadRules(Collections.singletonList(rule));
    }

    // 获取指定MQ的偏移量
    private static long getMessageQueueOffset(MessageQueue mq) {
        Long offset = OFFSET_TABLE.get(mq);
        if (offset != null) {
            return offset;
        }

        return 0;
    }

    // 在本地HashMap中记录偏移量
    private static void putMessageQueueOffset(MessageQueue mq, long offset) {
        OFFSET_TABLE.put(mq, offset);
    }
}

如果对大家有帮助,希望给个素质三连,好人一生平安

  • 17
    点赞
  • 44
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值