【学习笔记】我想进阿里,我要学rocketmq(库存)

114 阅读20分钟

rocketMQ

什么是MQ?

一种提供消息队列服务的中间件,一般用来解决应用解耦,异步消息,流量削峰等问题,实现高性能,高可用,可伸缩和最终一致性架构。

MQ的三大功能:

  1. 流量消峰
  2. 应用解耦
  3. 异步处理

同类产品:

ActiveMQ:年代久远,用的比较少了;

RabbitMQ:使用Erlang语言开发,吞吐量较kafka和rocketmq来说比较小。

Kafka:java/scala语言开发,高吞吐量,适用于大数据。

名词解释

topic

就是一类消息的集合。

tag:

用来进一步区分某个 Topic 下的消息分类,可以说是topic的二级分类。

queue:

一个topic中可以有多个queue,每个queue只能被一个消费组中的一个消费者消费,通过负载均衡将消息分发给每个queue。

消息标识:

rocketmq中每条消息都有一个messageid,且可以携带具有业务标识的key。messageID有两个,一个是send()的时候产生,另外一个是到达broker后产生。用来标识消息的我们都叫做消息标识

producer:

通常是以生产者组出现的,有一个生产者组可以同时生产多个topic的消息。

consumer:

consumer也是以消费者组出现的,一个消费者组的消费者应该小于等于一个topic中queue的个数,每个消费者组只能消费一个topic,一个topic可以被多个消费者组消费。消费者组中消费topic中queue的数量也是通过负载均衡实现的。

NameServer:

是一个broker和topic的路由注册中心,支持broker的动态注册和发现。

broker的管理:接收broker集群的注册信息并且保存用于路由机制,提供心跳检测机制来检查broker是否存活。

路由信息管理: NameServer保存着整个broker集群的路由信息,生产者和消费者可以通过nameserver获取整个broker路由信息,进行消费传递和消费。

broker:

处于一个中转环节,负责存储生产者生产的消息并且转发这些消息,存储着消费偏移offset,队列,主题等等。

nameserver原理

路由注册原理

NameServer通常也是以集群的方式部署,broker在向nameserver请求注册的时候,会向集群中每个nameserver申请注册,与每个nameserver建立长连接,这样每个nameserver中都维护这一个broker列表,动态存储broker的信息,nameserver之间是不会有消息的同步,是无状态的。这种策略的缺点是不易动态扩容。

路由剔除

路由心跳是由broker主动提交的(包含ip,port,name,所属集群name),nameserver维护着一个心跳时间戳,记录broker最新存活时间,每次nameserver只需检测这个时间,如果距离当前超过120s就会判定broker失效,将其剔除。默认是10s检测一次。

路由发现

rocketmq的路由发现采用的是pull模型,当topic路由信息发生变化的时候,nameserver不会主动推送,给客户端,而是客户端定时拉取最新路由信息,默认客户端30s拉取一次

push模型:保证实时性,需要维护长连接,资源占用率高。

pull模型:不能保证实时性,不用维护长连接,资源占用率低。

long polling模型:长轮询模型,对前两种模型的综合。对资源路由信息更新的时候采用push模型,但是长连接值保持一定的时间。

客户端选择nameserver策略

客户端在连接nameserver集群中的节点时,用的是随机获取的策略:首先产生一个随机数,然后与nameserver节点数取模,就能产生一个节点的索引进行连接。如果连接失败采用轮询策略。

broker原理

broker工作原理

broker以主备集群方式出现,master节点负责读写,slave节点负责备份,当master节点挂了,slave节点就会上升为master。主从关系对应关系是制定相同的brokername,不同的brokerID确定,0表示master,非0表示slave。主从节点都需要和nameserver中每个几点建立长连接。

topic创建的模式

  1. 集群模式:在该集群中,所有的broker中的queue数量相同。
  2. broker模式:在该集群中,每个broker中的queue数量可以不同。

自动创建topic默认使用的broker模式

broker中的读写队列

读写队列的划分是逻辑上的划分。读写队列数量一般情况是相同,当读写数量不一致可能导致资源浪费

broker的主从复制策略

  1. 同步复制:消息写入master后,master会等待同步数据成功后才想生产者返回成功消息。
  2. 异步复制:消息写入master后,master立即向生产者返回成功消息,无需等待同步完成。可以提高系统的吞吐量。

broker刷盘策略

  1. 同步刷盘:当消息持久化到broker的磁盘才算是成功写入。
  2. 当消息写入到broker的内存后算是写入成功,不需要消息持久化到磁盘。

RocketMQ安装

  1. 下载安装包并放到服务器上

  2. 解压

  3. 修改默认的虚拟机内存大小

    vim runbroker.sh 256m 256m 128m vim runserver.sh 256m 256m 128m

启动nameserver

#启动nameserver
nohup sh mqnamesrv &
#查看启动日志
tail -f ~/logs/rocketmqlogs/namesrv.log

#关闭
sh bin/mqshutdown namesrv

启动broker

nohup sh mqbroker -n localhost:9876  autoCreateTopicEnable=true -c ../conf/broker.conf &

#关闭
sh bin/mqshutdown broker

测试rocketmq

发送消息

#设置环境变量
export NAMESRV_ADDR=localhost:9876
#使用安装包的demo发送消息
sh bin/tools.sh org.apache.rocketmq.example.quickstart.Producer

接收消息

export NAMESRV_ADDR=localhost:9876
sh bin/tools.sh org.apache.rocketmq.example.quickstart.Consumer

rocketmq集群

单个master

一旦Broker重启或者宕机时,会导致整个服务不可用。

多master

配置简单,单个Master宕机或重启维护对应用无影响,在磁盘配置为RAID10时,即使机器宕机不可恢复情况下,由于RAID10磁盘非常可靠,消息也不会丢(异步刷盘丢失少量消息,同步刷盘一条不丢),性能最高。但是未被消费的消息在机器恢复之前不可订阅,消息实时性会受到影响。

多master多slave模式-异步复制

即使磁盘损坏,消息丢失的非常少,且消息实时性不会受影响,同时Master宕机后,消费者仍然可以从Slave消费,但是一旦Master宕机,磁盘损坏情况下会丢失少量消息,而且执行效率较多master模式低。

多master多slave模式-同步双写

同步双写:master和slave都写入成功才返回消息

数据与服务都无单点故障,Master宕机情况下,消息无延迟,服务可用性与数据可用性都非常高。性能比异步复制模式略低,且目前版本在主节点宕机后,备机不能自动切换为主机。

添加host信息

vim /etc/hosts

#配置如下
192.168.11.30 rocketmq-nameserver1
192.168.11.20 rocketmq-nameserver2
192.168.11.30 rocketmq-master1
192.168.11.30 rocketmq-slave2
192.168.11.20 rocketmq-master2
192.168.11.20 rocketmq-slave1

#重启网络服务
systemctl restart network

关闭防火墙或者开启端口

systemctl stop firewalld.service

配置环境变量

vim /etc/profile

ROCKETMQ_HOME=/opt/rocketmq-all-4.5.0-bin-release
PATH=$PATH:$ROCKETMQ_HOME/bin
export ROCKETMQ_HOME PATH

刷新配置

source /etc/profile

创建消息存储路径

mkdir /opt/rocketmq-all-4.5.0-bin-release/rocketmq/store
mkdir /opt/rocketmq-all-4.5.0-bin-release/rocketmq/store/commitlog
mkdir /opt/rocketmq-all-4.5.0-bin-release/rocketmq/store/consumequeue
mkdir /opt/rocketmq-all-4.5.0-bin-release/rocketmq/store/index

配置master1

vim /opt/xxx/conf/broker-a.properties

#暴露的外网IP
brokerIP1=192.168.11.30
brokerIP2=192.168.11.30
#所属集群名字
brokerClusterName=rocketmq-cluster
#broker名字,注意此处不同的配置文件填写的不一样
brokerName=broker-a
#0 表示 Master,>0 表示 Slave
brokerId=0
#nameServer地址,分号分割
namesrvAddr=rocketmq-nameserver1:9876;rocketmq-nameserver2:9876
#在发送消息时,自动创建服务器不存在的topic,默认创建的队列数
defaultTopicQueueNums=4
#是否允许 Broker 自动创建Topic,建议线下开启,线上关闭
autoCreateTopicEnable=true
#是否允许 Broker 自动创建订阅组,建议线下开启,线上关闭
autoCreateSubscriptionGroup=true
#Broker 对外服务的监听端口
listenPort=10911
#删除文件时间点,默认凌晨 4点
deleteWhen=04
#文件保留时间,默认 48 小时
fileReservedTime=120
#commitLog每个文件的大小默认1G
mapedFileSizeCommitLog=1073741824
#ConsumeQueue每个文件默认存30W条,根据业务情况调整
mapedFileSizeConsumeQueue=300000
#destroyMapedFileIntervalForcibly=120000
#redeleteHangedFileInterval=120000
#检测物理文件磁盘空间
diskMaxUsedSpaceRatio=88
#存储路径
storePathRootDir=/opt/rocketmq-all-4.5.0-bin-release/rocketmq/store
#commitLog 存储路径
storePathCommitLog=/opt/rocketmq-all-4.5.0-bin-release/rocketmq/store/commitlog
#消费队列存储路径存储路径
storePathConsumeQueue=/opt/rocketmq-all-4.5.0-bin-release/rocketmq/store/consumequeue                                               
#消息索引存储路径
storePathIndex=/opt/rocketmq-all-4.5.0-bin-release/rocketmq/store/index
#checkpoint 文件存储路径
storeCheckpoint=/opt/rocketmq-all-4.5.0-bin-release/rocketmq/store/checkpointt
#abort 文件存储路径
abortFile=/opt/rocketmq-all-4.5.0-bin-release/rocketmq/store/abort
#限制的消息大小
maxMessageSize=65536
#flushCommitLogLeastPages=4
#flushConsumeQueueLeastPages=2
#flushCommitLogThoroughInterval=10000
#flushConsumeQueueThoroughInterval=60000
#Broker 的角色
#- ASYNC_MASTER 异步复制Master
#- SYNC_MASTER 同步双写Master
#- SLAVE
brokerRole=SYNC_MASTER
#刷盘方式
#- ASYNC_FLUSH 异步刷盘
#- SYNC_FLUSH 同步刷盘
flushDiskType=SYNC_FLUSH
#checkTransactionMessageEnable=false
#发消息线程池数量
#sendMessageThreadPoolNums=128
#拉消息线程池数量
#pullMessageThreadPoolNums=128

配置slave2:a-s.properties

#暴露的外网IP
brokerIP1=192.168.11.30
brokerIP2=192.168.11.30
#所属集群名字
brokerClusterName=rocketmq-cluster
#broker名字,注意此处不同的配置文件填写的不一样
brokerName=broker-b
#0 表示 Master,>0 表示 Slave
brokerId=1
#nameServer地址,分号分割
namesrvAddr=rocketmq-nameserver1:9876;rocketmq-nameserver2:9876
#在发送消息时,自动创建服务器不存在的topic,默认创建的队列数
defaultTopicQueueNums=4
#是否允许 Broker 自动创建Topic,建议线下开启,线上关闭
autoCreateTopicEnable=true
#是否允许 Broker 自动创建订阅组,建议线下开启,线上关闭
autoCreateSubscriptionGroup=true
#Broker 对外服务的监听端口
listenPort=11011
#删除文件时间点,默认凌晨 4点
deleteWhen=04
#文件保留时间,默认 48 小时
fileReservedTime=120
#commitLog每个文件的大小默认1G
mapedFileSizeCommitLog=1073741824
#ConsumeQueue每个文件默认存30W条,根据业务情况调整
mapedFileSizeConsumeQueue=300000
#destroyMapedFileIntervalForcibly=120000
#redeleteHangedFileInterval=120000
#检测物理文件磁盘空间
diskMaxUsedSpaceRatio=88
#存储路径
storePathRootDir=/opt/rocketmq-all-4.5.0-bin-release/rocketmq/store/broker-b-s
#commitLog 存储路径
storePathCommitLog=/opt/rocketmq-all-4.5.0-bin-release/rocketmq/store/broker-b-s/commitlog
#消费队列存储路径存储路径
storePathConsumeQueue=/opt/rocketmq-all-4.5.0-bin-release/rocketmq/store/broker-b-s/consumequeue
#消息索引存储路径
storePathIndex=/opt/rocketmq-all-4.5.0-bin-release/rocketmq/store/broker-b-s/index
#checkpoint 文件存储路径
storeCheckpoint=/opt/rocketmq-all-4.5.0-bin-release/rocketmq/store/broker-b-s/checkpoint
#abort 文件存储路径
abortFile=/opt/rocketmq-all-4.5.0-bin-release/rocketmq/store/broker-b-s/abort
#限制的消息大小
maxMessageSize=65536
#flushCommitLogLeastPages=4
#flushConsumeQueueLeastPages=2
#flushCommitLogThoroughInterval=10000
#flushConsumeQueueThoroughInterval=60000
#Broker 的角色
#- ASYNC_MASTER 异步复制Master
#- SYNC_MASTER 同步双写Master
#- SLAVE
brokerRole=SLAVE
#刷盘方式
#- ASYNC_FLUSH 异步刷盘
#- SYNC_FLUSH 同步刷盘
flushDiskType=ASYNC_FLUSH
#checkTransactionMessageEnable=false
#发消息线程池数量
#sendMessageThreadPoolNums=128
#拉消息线程池数量
#pullMessageThreadPoolNums=128

同理:2号服务器也是如此配置,太累了!。。。。。

启动集群

1.启动nameserve集群

cd /opt/xx/bin
nohup sh mqnamesrv &

2.启动broker集群

  • 启动master1和slave2

    master1:

    cd /opt/rocketmq-all-4.5.0-bin-release/bin
    nohup sh mqbroker -c /opt/rocketmq-all-4.5.0-bin-release/conf/2m-2s-syncbroker-a.properties &
    

    slave2:

    cd /opt/rocketmq-all-4.5.0-bin-release/bin
    nohup sh mqbroker -c /opt/rocketmq-all-4.5.0-bin-release/conf/2m-2s-sync/broker-b-s.properties &
    
  • 二号服务器

    master2

    cd /opt/rocketmq-all-4.5.0-bin-release/bin
    nohup sh mqbroker -c /opt/rocketmq-all-4.5.0-bin-release/conf/2m-2s-syncbroker-a.properties &
    

    slave1

    cd /opt/rocketmq-all-4.5.0-bin-release/bin
    nohup sh mqbroker -c /opt/rocketmq-all-4.5.0-bin-release/conf/2m-2s-sync/broker-b-s.properties &
    

可视化界面

https://github.com/apache/rocketmq-externals

  • 修改配置文件(server的地址)
  • 打包:mvn clean package -Dmaven .test.skip=true

rocketmq工作原理

消息生产过程

  1. producer在发送消息之前,先向nameserver获取消息topic的路由信息和broker列表。
  2. nameserver返回消息topic的路由表和broker列表。
  3. producer根据既定的queue选择策略,从queue中选出一个队列,用于存储消息。
  4. 然后对即将发送的消息进行特殊处理,比如压缩等。
  5. producer向选择的queue发出RPC请求,将消息发送到对应的queue。

所谓路由表:一个以topic为key,该topic中所有brokername列表为value的Map结构

queue的选择算法(无序消息)

轮序算法(默认)

优点:保证每个queue中可以均匀的获取到消息。

缺点:如果某些broker上的queue可能投递延迟较高,会导致producer的缓存队列中有较大的消息积压,影响消息的投递性能。

最小投递延迟算法

每次统计消息投递时间延迟最小的,然后将消息投递给延迟最小的queue,如果延迟相同就用轮询。但是,极端情况下,由于投递queue不均与可能会导致消费不均匀,造成资源浪费,消息堆积。

消息的存储

img

cmmitlog文件

  • 文件存放了很多的mappedFile文件,每个mappedFile文件最大空间位1G
  • 每个文件的文件名由20位0构成,表示按顺序排列,之前n-1个文件的逻辑偏移量,就是前面所存文件的大小。
  • 所有的消息不区分topic,全部按顺序存入mappedFile文件。

image-20210807211400977

consumequeue

一个consumequeue文件中所有消息的topic一定相同;每个文件固定为30w*20字节。表示每个文件30w个索引条目,每个索引条目20字节,每个索引条目包括消息在mappedFile中的索引偏移量,消息长度,消息tag的hashcode值。

文件读写

消息写入

  • broker根据queueId,获取到该消息对应索引条目在consumequeue中的偏移量。
  • 将queueId、偏移量等数据与消息一起封装为消息单元。
  • 将消息单元写到commitlog。
  • 形成索引条目。

消息拉取

  • 获取到消费消息所在queue的消费偏移量(一个队列中的消费进度),计算出要消费消息的偏移量。
  • 向broker发送拉取请求,包含拉取消息的queue,消息偏移量,消息tag。
  • broker计算在该consumequeue的queue偏移量。
  • 从该queueoffset处开始查找第一个指定tag的索引条目。
  • 解析该索引条目的8个字节,即可定位到该消息在commitlog中的消息偏移量。
  • 从对应消息偏移量中读取消息单元,并发送给consumer。

读写性能

rocketmq的持久化性能是很高的;

mmap零拷贝机制:将对文件的操作直接转化为对内存的操作,大大提高读写效率。

pagecache的预读取机制:将一部分内存用作pageCache,可以让文件的顺序读写速度接近于内存的读写。他的原理是:写操作会先将数据写入到pageCache中,随后以异步的方式由pdflush内核线程将cache中的数据刷盘到磁盘中。读操作:首先会从pageCache汇总读取,若没有命中,则OS再从屋里磁盘中加载该数据到pageCache中,同时还会对其相邻数据块中数据进行预读取。

indexFile

消费者除了使用topic进行消费之外,还可以根据key进行消费。就是这些带有key的消息存放在store的indexFile中,只要消息中包含了key都会在写入broker的时候写到该文件中,用于全局索引。

image-20210808162311589

image-20210808164502260

index索引流程

  1. 根据传入的时间找到相应的indexFile(文件名是由时间戳生成的)

  2. 计算出传入时间与找到的indexFile文件名的差值

  3. 计算出业务key的hash值

  4. 通过hash值算出key在indexFile中所处的槽位【key的hashcode % 500w】。

  5. 计算槽位序号为n的槽位在indexFile中的起始位置【40 + (n-1) * 4,每个槽位4字节】。

  6. 计算indexNum为m的index在indexFile中的位置【40 + 500w*4 +(m-1)*20】

  7. 前面计算出的时间差和index中读取的timeDiff比较,大于0则找到了,读取index里面的physicaloffset定位消息,否则,读取该index单元的preindexno,作为要查找的下一个个index索引单元的indexno(相当于向前寻找)。

消息的消费

消费者的消费模式分为集群消费和广播消费两种。

拉取式消费

consumer主从从broker中拉取消息,由消费者自主控制,实时性不高。

推送式消费

具有实时性,但是需要一个长连接来维持整个系统,消费系统资源

广播消费模式

广播模式下,同一个topic所有的消息都会被推送到一个消费者组中的每个消费者。也就是说每个消费者都有同一个topic中的所有消息。

消息的进度保存:由消费者自己保存,因为每个消费者都保持全部的消息,所以消费进度自己知道,也就自己保存。

集群消费模式

同一个topic中的每条消息都会被发送到消费者组中的某个消费者进行消费。

消息的进度保存:由broker保存。由于大家均分所有消息,一条消息由一个消费者消费,所以需要broker保存目前的消费进度。

rebalance机制

在集群消费模式下,提高消费者并行消费的能力,根据消费者组中消费者的数量,将同一个topic下的所有queue进行再均衡。

在再均衡的过程中,所有的queue都会暂停等待再均衡完成,这就造成一个服务暂停;同时,由于 消费者的offset是异步提交,所以可能造成重复消费问题;再均衡时间过长会造成消息积压,造成消息突刺;

queue的分配算法

  • 平均分配策略(默认):计算平均每个consumer应该消费队列个数,然后依次分配对应数量的队列,不能整除的就依次分配;
  • 环形分配策略:直接依次将queue循环着分配给每个consumer。
  • 机房分配策略:将同机房的queue分配给同机房的consumer,然后在进行其他几种算法;
  • 一致性哈希分配策略:将consumer和queue的hash放到一个hash环上,顺时针方向上,将最接近consumer的queue分配到该consumer。(分配不均且较为复杂但是一定程度上可以防止rebalance机制)

总结:对于consumer数量变化较为频繁的情况下,我们可用一致性哈希算法分配策略,对于consumer变化不频繁,我们可以选择效率更高的两张平均分配算法。

消费幂等

消费者对某条消息重复消费,但是多次消费的结果与一次消费的结果对系统并没有什么负面影响,称之为消费幂等。

在一些特殊的场景下,可能出现消息重发等现象(比如网络闪断后重新连接),造成同一条消息多次消费。

通用解决方案:

什么是幂等令牌:生产者和消费者的既定协议,通常是具有唯一性的业务标志,一般由生产者发送过来。

什么是唯一性处理:服务端采用一定的算法策略保证同一个业务不会重复消费。

第一步:通过缓存去重,在缓存中如果已经存在了幂等令牌,就说明本机为重复操作。否则进行第二步。

第二步,唯一性处理之前先进行数据库中查询幂等令牌的索引数据是否存在,存在则说明是重复性操作。否则进行下一步。

唯一性处理后会将幂等令牌写入缓存中,并将令牌作为唯一索引存入DB中。

rocketMQ应用

同步消息

发送的时候通过选择算法,以消息key为topic,将相同key的消息放入一个队列中。

public void testOrderSend() throws Exception {
  DefaultMQProducer producer = new DefaultMQProducer("group1");
  producer.setNamesrvAddr(this.nameServer);
  producer.start();
  for (int i=0; i<10; i++) {
    Message message = new Message("topic1", "tag3", (System.currentTimeMillis() + "---" + System.nanoTime() + "hello ordered message " + i).getBytes());
    SendResult sendResult = producer.send(message, new MessageQueueSelector() {
      @Override
      public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
        int index = (int) arg;
        //奇数放一个队列,偶数放一个队列
        return mqs.get(index % mqs.size() % 2);
      }
    }, i);
    Assert.assertTrue(sendResult.getSendStatus() == SendStatus.SEND_OK);
  }
  producer.shutdown();
}
延迟消息

public class DelayProducer {

public static void main(String[] args) throws MQClientException, InterruptedException {
    DefaultMQProducer producer = new DefaultMQProducer("rmq-group");
    producer.setNamesrvAddr("localhost:9876");
    producer.start();
    try {
        for (int i = 0; i < 3; i++) {
            Message msg = new Message("TopicA-test",// topic
                    "TagA",// tag
                    (new Date() + "Hello RocketMQ ,QuickStart 11" + i)
                            .getBytes()// body
            );
            //1s,5s,10s,30s,1m,2m,3m,4m,5m,6m,7m,8m,9m,10m,20m,30m,1h,2h。
            // level=0,表示不延时。level=1,表示 1 级延时,对应延时 1s。level=2 表示 2 级延时,对应5s,以此类推
            msg.setDelayTimeLevel(2);
 
            SendResult sendResult = producer.send(msg);
            System.out.println(sendResult);
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
   producer.shutdown();
}
事务消息

public class TransactionProducer {

public static void sendTagAMessage() throws Exception {
    //定义producer
    TransactionMQProducer producer = new TransactionMQProducer("transaction_group1");

    //要设置NameServer地址,多个;分割
    producer.setNamesrvAddr("192.168.12.121:9876");

    //设置TransactionListener
    producer.setTransactionListener(new TransactionListener() {
        @Override
        public LocalTransactionState executeLocalTransaction(Message message, Object arg) {
            int num = new Random().nextInt(10);
            if (num % 3 == 1) {
                System.out.println("提交事物:" + new String(message.getBody()));
                return LocalTransactionState.COMMIT_MESSAGE;
            } else if (num % 3 == 2) {
                System.out.println("回滚事物:" + new String(message.getBody()));
                return LocalTransactionState.ROLLBACK_MESSAGE;
            } else {
                System.out.println("不做处理:" + new String(message.getBody()));
                return LocalTransactionState.UNKNOW;
            }
        }

        @Override
        public LocalTransactionState checkLocalTransaction(MessageExt messageExt) {
            //MQ回查
            System.out.println("回查:" + new String(messageExt.getBody()));
            System.out.println("提交事物:" + new String(messageExt.getBody()));
            return LocalTransactionState.COMMIT_MESSAGE;
        }
    });

    //启动
    producer.start();
    
    //发送消息
    for (int i = 0; i < 10; i++) {
        try {
            byte[] msgByte = ("Hello world TagA " + i).getBytes(RemotingHelper.DEFAULT_CHARSET);
            Message msg = new Message("TransActionTopic", "TagA", msgByte);

            SendResult sendResult = producer.sendMessageInTransaction(msg, null);
            System.out.println("发送消息--发送时间:" + new Date() + " 发送结果:" + sendResult);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    //关闭生产者
    //        producer.shutdown();
    }
public static void main(String[] args) {
    //发送消息
    try {
        sendTagAMessage();
    } catch (Exception e) {
        e.printStackTrace();
    }

}
public class TransactionConsumer {
public static void main(String[] args) throws MQClientException {
    //声明并初始化一个consumer
    DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumer_transaction_101");

    //要设置NameServer地址,多个;分割
    consumer.setNamesrvAddr("192.168.12.121:9876");

    //设置consumer所订阅的Topic和Tag,*代表全部的Tag
    consumer.subscribe("TransActionTopic", "TagA");

    //集群订阅
    consumer.setMessageModel(MessageModel.CLUSTERING);

    //这里设置的是一个consumer的消费策略
    //CONSUME_FROM_LAST_OFFSET 默认策略,从该队列最尾开始消费,即跳过历史消息
    //CONSUME_FROM_FIRST_OFFSET 从队列最开始开始消费,即历史消息(还储存在broker的)全部消费一遍
    //CONSUME_FROM_TIMESTAMP 从某个时间点开始消费,和setConsumeTimestamp()配合使用,默认是半个小时以前
    consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);
    //设置一个Listener,主要进行消息的逻辑处理
    consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
        for (MessageExt messageExt : msgs) {
            String messageBody = new String(messageExt.getBody());
            System.out.println("消费消息--消费时间:" + new Date() + " 消息内容:" + messageBody);//输出消息内容
        }

        //返回消费状态
        return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
    });

    //调用start()方法启动consumer
    consumer.start();

    System.out.println("TransactionConsumer Started.");
}
}
批量消息

批量消息必须有相同的topic

批量消息必须有相同的刷盘策略

批量消息不能是延时消息和事务消息

批量消息的大小最大是4m(可分割)

消费者默认每次最多能从broker拉取32条消息

consumer默认每次只能消费一条消息

//只处理每条消息大小不超过4m
public class MessageListSplitter implements Iterator<List<Message>> {

    private final int SIZE_LIMIT = 4 * 1024 * 1024;
    private final List<Message> messages;
    //批量消息的起始索引
    private int currentIndex;

    public MessageListSplitter(List<Message> messages) {
        this.messages = messages;
    }

    @Override
    public boolean hasNext() {
        return currentIndex < messages.size();
    }

    @Override
    public List<Message> next() {
        int nextIndex = currentIndex;
        int totalSize = 0;
        for(;nextIndex < messages.size(); nextIndex++) {
            final Message message = messages.get(nextIndex);
            int tmpSize = message.getTopic().length() + message.getBody().length;
            final Map<String, String> properties = message.getProperties();
            for (Map.Entry<String,String> entry: properties.entrySet()) {
                tmpSize += entry.getKey().length() + entry.getValue().length();
            }
            tmpSize = tmpSize + 20;
            //判断消息是否大于4m
            if(tmpSize > SIZE_LIMIT) {
                if(nextIndex - currentIndex ==0) {
                    nextIndex ++;
                }
                break;
            }
            if(tmpSize + totalSize > SIZE_LIMIT) {
                break;
            } else {
                totalSize += tmpSize;
            }
        }
        List<Message> subList = messages.subList(currentIndex,nextIndex);
        currentIndex = nextIndex;
        return subList;
    }
}
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1");
  consumer.setNamesrvAddr(this.nameServer);
  //指定批量消费的最大值,默认是1
  consumer.setConsumeMessageBatchMaxSize(5);
  //批量拉取消息的数量,默认是32
  consumer.setPullBatchSize(30);
  consumer.subscribe("topic1", "tag6");
  consumer.registerMessageListener(new MessageListenerConcurrently() {
    @Override
    public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
      System.out.println(Thread.currentThread().getName() + "一次收到" + msgs.size() + "消息");
      return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
    }
  });
//只处理每条消息大小不超过4m
public class MessageListSplitter implements Iterator<List<Message>> {

    private final int SIZE_LIMIT = 4 * 1024 * 1024;
    private final List<Message> messages;
    //批量消息的起始索引
    private int currentIndex;

    public MessageListSplitter(List<Message> messages) {
        this.messages = messages;
    }

    @Override
    public boolean hasNext() {
        return currentIndex < messages.size();
    }

    @Override
    public List<Message> next() {
        int nextIndex = currentIndex;
        int totalSize = 0;
        for(;nextIndex < messages.size(); nextIndex++) {
            final Message message = messages.get(nextIndex);
            int tmpSize = message.getTopic().length() + message.getBody().length;
            final Map<String, String> properties = message.getProperties();
            for (Map.Entry<String,String> entry: properties.entrySet()) {
                tmpSize += entry.getKey().length() + entry.getValue().length();
            }
            tmpSize = tmpSize + 20;
            //判断消息是否大于4m
            if(tmpSize > SIZE_LIMIT) {
                if(nextIndex - currentIndex ==0) {
                    nextIndex ++;
                }
                break;
            }
            if(tmpSize + totalSize > SIZE_LIMIT) {
                break;
            } else {
                totalSize += tmpSize;
            }
        }
        List<Message> subList = messages.subList(currentIndex,nextIndex);
        currentIndex = nextIndex;
        return subList;
    }
}
消息过滤

SQL过滤只能是push模式使用;

broker默认没有开启SQL过滤功能;需要在配置文件中配置enablePropertyFilter=true;

public class Producer {
    public static void main(String[] args) throws MQClientException, RemotingException, InterruptedException, MQBrokerException {
        DefaultMQProducer producer = new DefaultMQProducer("sqlFilterGroup");
        producer.setNamesrvAddr("192.168.197.126:9876;192.168.197.123:9876");
        producer.start();
        for (int i = 0; i < 3; i++) {
            Message message = new Message("sqlFilterTopic","tag",("sqlFilterMessage"+i).getBytes());
            //在消息中放入属性id
            message.putUserProperty("id",i+"");
            SendResult sendResult = producer.send(message);
            System.out.println(sendResult);
        }
        producer.shutdown();
    }
}
public class Consumer {
    public static void main(String[] args) throws MQClientException {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("sqlFilterGroup");
        consumer.setNamesrvAddr("192.168.197.126:9876;192.168.197.123:9876");
        //判断消息中的属性,id是否大于0,大于0的才消费
        consumer.subscribe("sqlFilterTopic", MessageSelector.bySql("id > 0"));
        consumer.registerMessageListener((MessageListenerConcurrently) (list, consumeConcurrentlyContext) -> {
            for (MessageExt messageExt : list) {
                System.out.println("收到消息->"+new String(messageExt.getBody()));
            }
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        });
        consumer.start();
    }
}
消息重发机制

同步或异步发送方式有重试机制,oneway消息发送没有重试机制。

普通消息具有重发机制,顺序消息没有重发机制。

重试机制

同步发送失败策略

broker还有失败隔离功能,尽量会选择未发生过发送失败的broker作为目标。

解决方案:

异步发送失败策略

刷盘失败策略

消息消费重试机制

  1. 顺序消息的消费重试

顺序消息没有重发机制,但是消费有重试机制;必须要及时监控,处理西欧阿飞失败情况,否则可能造成永久阻塞。

范围是:100-30000
consumer1.setSuspendCurrentQueueTimeMillis(1000);
  1. 无序消费方式重试

只有集群消费模式提消费重试。

//修改值小于16,按照指定间隔重试,大于16就全是2小时
consumer1.setMaxReconsumeTimes(10);
//一个消费者设置运用到组中所有消费者

重试队列

当有需要重试消费的消息时,broker会为每一个消费组都设置一个topic

只有出现重试消费的时候,就会为该组设置重试队列。如果一直失败就会放入死信队列。

broker对于重试消息的处理通过延迟消息实现的。将消息保存到延迟队列,然后到时间了就会投递到重试队列。

重试配置:

1.return ConsumeConcurrentlyStatus.RECONSUME_LATER;
2.返回null
3.抛出异常

消费不重试

不管有没有异常都返回成功;

死信队列

消息一直消费不成功就放入死信队列;

死信队列中的消息不会被消费者消费;

生命周期和正常消息相同,3天后会自动删除;

死信队列就是个特殊的topic,名称为%DLQ&consumerGroup,每个消费者组都可以有一个死信队列。没有死信消息则没有死信队列。

<!-- https://mvnrepository.com/artifact/org.apache.rocketmq/rocketmq-client -->
<dependency>
    <groupId>org.apache.rocketmq</groupId>
    <artifactId>rocketmq-client</artifactId>
    <version>4.4.0</version>
</dependency>