rocketMQ
什么是MQ?
一种提供消息队列服务的中间件,一般用来解决应用解耦,异步消息,流量削峰等问题,实现高性能,高可用,可伸缩和最终一致性架构。
MQ的三大功能:
- 流量消峰
- 应用解耦
- 异步处理
同类产品:
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创建的模式
- 集群模式:在该集群中,所有的broker中的queue数量相同。
- broker模式:在该集群中,每个broker中的queue数量可以不同。
自动创建topic默认使用的broker模式
broker中的读写队列
读写队列的划分是逻辑上的划分。读写队列数量一般情况是相同,当读写数量不一致可能导致资源浪费
broker的主从复制策略
- 同步复制:消息写入master后,master会等待同步数据成功后才想生产者返回成功消息。
- 异步复制:消息写入master后,master立即向生产者返回成功消息,无需等待同步完成。可以提高系统的吞吐量。
broker刷盘策略
- 同步刷盘:当消息持久化到broker的磁盘才算是成功写入。
- 当消息写入到broker的内存后算是写入成功,不需要消息持久化到磁盘。
RocketMQ安装
-
下载安装包并放到服务器上
-
解压
-
修改默认的虚拟机内存大小
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工作原理
消息生产过程
- producer在发送消息之前,先向nameserver获取消息topic的路由信息和broker列表。
- nameserver返回消息topic的路由表和broker列表。
- producer根据既定的queue选择策略,从queue中选出一个队列,用于存储消息。
- 然后对即将发送的消息进行特殊处理,比如压缩等。
- producer向选择的queue发出RPC请求,将消息发送到对应的queue。
所谓路由表:一个以topic为key,该topic中所有brokername列表为value的Map结构
;
queue的选择算法(无序消息)
轮序算法(默认)
优点:保证每个queue中可以均匀的获取到消息。
缺点:如果某些broker上的queue可能投递延迟较高,会导致producer的缓存队列中有较大的消息积压,影响消息的投递性能。
最小投递延迟算法
每次统计消息投递时间延迟最小的,然后将消息投递给延迟最小的queue,如果延迟相同就用轮询。但是,极端情况下,由于投递queue不均与可能会导致消费不均匀,造成资源浪费,消息堆积。
消息的存储
cmmitlog文件
- 文件存放了很多的mappedFile文件,每个mappedFile文件最大空间位1G
- 每个文件的文件名由20位0构成,表示按顺序排列,之前n-1个文件的逻辑偏移量,就是前面所存文件的大小。
- 所有的消息不区分topic,全部按顺序存入mappedFile文件。
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的时候写到该文件中,用于全局索引。
index索引流程
-
根据传入的时间找到相应的indexFile(文件名是由时间戳生成的)
-
计算出传入时间与找到的indexFile文件名的差值
-
计算出业务key的hash值
-
通过hash值算出key在indexFile中所处的槽位【key的hashcode % 500w】。
-
计算槽位序号为n的槽位在indexFile中的起始位置【40 + (n-1) * 4,每个槽位4字节】。
-
计算indexNum为m的index在indexFile中的位置【40 + 500w*4 +(m-1)*20】
-
前面计算出的时间差和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作为目标。
解决方案:
异步发送失败策略
刷盘失败策略
消息消费重试机制
- 顺序消息的消费重试
顺序消息没有重发机制,但是消费有重试机制;必须要及时监控,处理西欧阿飞失败情况,否则可能造成永久阻塞。
范围是:100-30000
consumer1.setSuspendCurrentQueueTimeMillis(1000);
- 无序消费方式重试
只有集群消费模式提消费重试。
//修改值小于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>