RocketMQ部署及简单API应用


文章目录

  • 一、运行启动
    • Linux 启动
    • Windows 启动
  • RocketMQ集群搭建
  • Rocketmq-dashboard 搭建
  • 调整系统参数
  • RocketMQ原⽣API使⽤
    • 消息生产
      • 同步发送
      • 异步发送
      • 单向发送
    • 消费消息
    • 顺序消息
    • 广播消息
    • 延迟消息
    • 批量消息
    • 过滤消息
    • 事务消息
  • SpringBoot整合RocketMQ
    • 消息⽣产者
    • 消息消费者
  • SpringCloudStream整合RocketMQ


学习黑马的RocketMQ,根据黑马笔记与图灵百里笔记整合加深印象

一、运行启动

首先需要JAVA 环境
根据网上其他文档安装后

Linux 启动

启动Runserver

由于RocketMQ默认预设的JVM内存是4G,这是RocketMQ给我们的最佳配置。但是通常我们用虚拟机的话都是不够4G内存的,所以需要调整下JVM内存大小。修改的方式是直接修改runserver.sh。

进入安装目录/bin下

#将JAVA_OPT="${JAVA_OPT} -server -Xms4g -Xmx4g -Xmn2g 修改为 -server -Xms512m -Xmx512m -Xmn256m

vi runserver.sh

JAVA_OPT="${JAVA_OPT} -server -Xms512m -Xmx512m -Xmn256m -
XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=320m"

授权

chmod 777 /opt/rocketmq/bin/mqnamesrv

⽤静默启动的⽅式启动NameServer服务(注意设置为自己安装路径):

nohup /opt/rocketmq/bin/mqnamesrv > /opt/rocketmq/nameServerLog 2>&1 &
cat /opt/rocketmq/nameServerLog

输出如下为成功 :
在这里插入图片描述

启动Broker

Broker的默认预设内存是8G,启动前,如果内存不够,同样需要
调整下JVM内存。

vim /opt/rocketmq/bin/runbroker.sh

修改内存为

JAVA_OPT="${JAVA_OPT} -server -Xms512m -Xmx512m -Xmn256m"

然后我们需要找到$ROCKETMQ_HOME/conf/broker.conf, vim指令进⾏编辑,在最下⾯加⼊⼀个配置:

vim /opt/rocketmq/conf/broker.conf
#允许自动创建topic
autoCreateTopicEnable=true
#添加nameserver地址
namesrvAddr=localhost:9876

授权

chmod 777 /opt/rocketmq/bin/mqbroker

静默启动 :

nohup /opt/rocketmq/bin/mqbroker -c /opt/rocketmq/conf/broker.conf -n
localhost:9876 > /opt/rocketmq/brokerlog 2>&1 &

启动验证,输出下图内容即成功

cat /opt/rocketmq/brokerlog

在这里插入图片描述

jsp 查看BrokerStartup进程。brookcer、namesrv都成功启动

RocketMQ部署及简单API应用_第1张图片

命令⾏快速验证

在RocketMQ的安装包中,提供了⼀个tools.sh⼯具可以⽤来在命令⾏快速验证RocketMQ服务。

授权:

chmod 777 /opt/rocketmq/bin/tools.sh

启动消息⽣产者发送消息:默认会发1000条消息

[root@localhost bin]# export NAMESRV_ADDR='localhost:9876'
[root@localhost bin]# ./tools.sh  org.apache.rocketmq.example.quickstart.Producer

RocketMQ部署及简单API应用_第2张图片

上⾯部分就是我们发送的消息的内容。后⾯两句标识消息⽣产者正常关闭。

启动消息消费者接收消息:

[root@localhost bin]# export NAMESRV_ADDR='localhost:9876'
[root@localhost bin]# ./tools.sh org.apache.rocketmq.example.quickstart.Consumer

启动后,可以看到消费到的消息。

RocketMQ部署及简单API应用_第3张图片

这个Consume指令并不会结束,他会继续挂起,等待消费其他的消息。我们可以使⽤CTRL+C停⽌该进程。

关闭RocketMQ服务

#授权
chmod 777 /opt/rocketmq/bin/mqshutdown
#关闭NameServer
sh /opt/rocketmq/bin/mqshutdown namesrv
#关闭Broker
sh /opt/rocketmq/bin/mqshutdown broker

Windows 启动

RocketMQ集群搭建

RocketMQ的四种集群模式

  • 单Master模式

    这种方式风险较大,一旦Broker重启或者宕机时,会导致整个服务不可用。不建议线上环境使用,可以用于本地测试。
    前面我们搭建的单机版RocketMQ其实就是单Master这种模式,具体的搭建过程读者朋友可参考前面的文章。

  • 多 Master 模式
    一个集群无Slave,全是Master,例如2个Master或者3个Master,这种模式的优缺点如下:
    优点:配置简单,单个Master宕机或重启维护对应用无影响,在磁盘配置为RAID10时,即使机器宕机不可恢复情况下,由于RAID10磁盘非常可靠,消息也不会丢(异步刷盘丢失少量消息,同步刷盘一条不丢),性能最高;
    缺点:单台机器宕机期间,这台机器上未被消费的消息在机器恢复之前不可订阅,消息消费的实时性会受到影响。

  • 多Master多Slave模式-异步复制
    每个Master配置一个Slave,有多对Master-Slave,HA采用异步复制方式,主备有短暂消息延迟(毫秒级),这种模式的优缺点如下:
    优点:即使磁盘损坏,消息丢失的非常少,且消息实时性不会受影响,同时Master宕机后,消费者仍然可以从Slave消费,而且此过程对应用透明,不需要人工干预,性能同多Master模式几乎一样。
    缺点:Master宕机,磁盘损坏情况下会丢失少量消息。

  • 多Master多Slave模式-同步双写
    每个Master配置一个Slave,有多对Master-Slave,HA采用同步双写方式,即只有主备都写成功,才向应用返回成功,这种模式的优缺点如下:
    优点:数据与服务都无单点故障,Master宕机情况下,消息无延迟,服务可用性与数据可用性都非常高;
    缺点:性能比异步复制模式略低(大约低10%左右),发送单个消息的RT会略高。

Master支持读和写,Slave仅支持读,也就是 Producer只能和Master连接写入消息;Consumer可以连接 Master,也可以连接Slave来读取消息。

以Linux示例,Windows同理

准备三台虚机,并配置机器名。可以利用安装好的虚机通过克隆出另外两个机器

1、使用vi /etc/hosts命令,配置机器名,在文件末尾加上以下配置:

[root@localhost ~]# vi /etc/hosts
192.168.43.134 worker1
192.168.43.135 worker2
192.168.43.136 worker3

2.服务之间设置免密登陆,三个机器都使用ssh-keygen生成秘钥。提示录入直接回车即可

[root@localhost ~]# ssh-keygen

RocketMQ部署及简单API应用_第4张图片

3.三个机器都使用以下命令分发给其他机器,输入yes,然后输入密码;这样可以直接某个机器使用ssh或者scp到另外的机器。

[root@localhost ~]# ssh-copy-id worker1
[root@localhost ~]# ssh-copy-id worker2
[root@localhost ~]# ssh-copy-id worker3

RocketMQ部署及简单API应用_第5张图片

4.停止并禁用防火墙或者删除防火墙,我这边使用的是删除防火墙。

#检查防火墙状态
[root@localhost ~]# firewall-cmd --state
#停止并禁用防火墙
[root@localhost ~]# systemctl stop firewalld
[root@localhost ~]# systemctl disable firewalld
#删除防火墙 (慎用)
#[root@localhost ~]# yum remove firewalld
#[root@localhost ~]# firewall-cmd --state
#输出 ‘command not found’ 即成功

使用conf/2m-2s-async下的配置文件搭建一个2主2从异步刷盘的集群。设计的集群情况如下:

机器名 nemaeServer节点部署 broker节点部署
worker1 nameserver
worker2 nameserver broker-a,broker-b-s
worker3 nameserver broker-b,broker-a-s

conf目录下存在三种配置方式

  • 2m-2s-async:2主2从异步刷盘(吞吐量较大,但是消息可能丢失)
  • 2m-2s-sync:2主2从同步刷盘(吞吐量会下降,但是消息更安全)
  • 2m-noslave:2主无从(单点故障),然后还可以直接配置broker.conf,进行单点环境配置

还有一个dleger目录,而dleger就是用来实现主从切换的。集群中的节点会基于Raft协议随机选举出一个leader,其他的就都是follower。通常正式环境都会采用这种方式来搭建集群。

broker-a.properties配置

#所属集群名字,名字一样的节点就在同一个集群内
brokerClusterName=DefaultCluster
#broker名字,名字一样的节点就是一组主从节点。
brokerName=broker-a
#brokerid,0就表示是Master,>0的都是表示Slave
brokerId=0
#nameServer地址,分号分割
namesrvAddr=worker1:9876;worker2:9876;worker3: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=/app/rocketMQ/store
#commitLog存储路径
storePathCommitLog=/app/rocketMQ/store/commitlog
#消费队列存储路径存储路径
storePathConsumeQueue=/app/rocketMQ/store/consumequeue
#消息索引存储路径
storePathIndex=/app/rocketMQ/store/index
#checkpoint文件存储路径
storeCheckpoint=/app/rocketMQ/store/checkpoint
#abort文件存储路径
abortFile=/app/rocketMQ/store/abort
#限制的消息大小
maxMessageSize=65536
#flushCommitLogLeastPages=4
#flushConsumeQueueLeastPages=2
#flushCommitLogThoroughInterval=10000
#flushConsumeQueueThoroughInterval=60000
#Broker的角色
#-ASYNC_MASTER异步复制Master
#-SYNC_MASTER同步双写Master
#-SLAVE
brokerRole=ASYNC_MASTER
#刷盘方式
#-ASYNC_FLUSH异步刷盘
#-SYNC_FLUSH同步刷盘
flushDiskType=ASYNC_FLUSH
#checkTransactionMessageEnable=false
#发消息线程池数量
#sendMessageThreadPoolNums=128
#拉消息线程池数量
#pullMessageThreadPoolNums=128
#开启Sql过滤
enablePropertyFilter=true
#重试支持过滤
filterSupportRetry=true

broker-b.properties配置:

#所属集群名字,名字一样的节点就在同一个集群内
brokerClusterName=DefaultCluster
#broker名字,名字一样的节点就是一组主从节点。
brokerName=broker-b
#brokerid,0就表示是Master,>0的都是表示Slave
brokerId=0
#nameServer地址,分号分割
namesrvAddr=worker1:9876;worker2:9876;worker3: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=/app/rocketMQ/store
#commitLog存储路径
storePathCommitLog=/app/rocketMQ/store/commitlog
#消费队列存储路径存储路径
storePathConsumeQueue=/app/rocketmQ/store/consumequeue
#消息索引存储路径
storePathIndex=/app/rocketMQ/store/index
#checkpoint文件存储路径
storeCheckpoint=/app/rocketMQ/store/checkpoint
#abort文件存储路径
abortFile=/app/rocketMQ/store/abort
#限制的消息大小
maxMessageSize=65536
#flushCommitLogLeastPages=4
#flushConsumeQueueLeastPages=2
#flushCommitLogThoroughInterval=10000
#flushConsumeQueueThoroughInterval=60000
#Broker的角色
#-ASYNC_MASTER异步复制Master
#-SYNC_MASTER同步双写Master
#-SLAVE
brokerRole=ASYNC_MASTER
#刷盘方式
#-ASYNC_FLUSH异步刷盘
#-SYNC_FLUSH同步刷盘
flushDiskType=ASYNC_FLUSH
#checkTransactionMessageEnable=false
#发消息线程池数量
#sendMessageThreadPoolNums=128
#拉消息线程池数量
#pullMessageThreadPoolNums=128
#开启Sql过滤
enablePropertyFilter=true
#重试支持过滤
filterSupportRetry=true

broker-a-s.properties配置

#所属集群名字,名字一样的节点就在同一个集群内
brokerClusterName=DefaultCluster
#broker名字,名字一样的节点就是一组主从节点。
brokerName=broker-a
#brokerid,0就表示是Master,>0的都是表示Slave
brokerId=1
#nameServer地址,分号分割
namesrvAddr=worker1:9876;worker2:9876;worker3: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=/app/rocketMQ/storeSlave
#commitLog存储路径
storePathCommitLog=/app/rocketMQ/storeSlave/commitlog
#消费队列存储路径存储路径
storePathConsumeQueue=/app/rocketMQ/storeSlave/consumequeue
#消息索引存储路径
storePathIndex=/app/rocketMQ/storeSlave/index
#checkpoint文件存储路径
storeCheckpoint=/app/rocketMQ/storeSlave/checkpoint
#abort文件存储路径
abortFile=/app/rocketMQ/storeSlave/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
#开启Sql过滤
enablePropertyFilter=true
#重试支持过滤
filterSupportRetry=true

broker-b-s.properties配置

#所属集群名字,名字一样的节点就在同一个集群内
brokerClusterName=DefaultCluster
#broker名字,名字一样的节点就是一组主从节点。
brokerName=broker-b
#brokerid,0就表示是Master,>0的都是表示Slave
brokerId=1
#nameServer地址,分号分割
namesrvAddr=worker1:9876;worker2:9876;worker3: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=/app/rocketMQ/storeSlave
#commitLog存储路径
storePathCommitLog=/app/rocketMQ/storeSlave/commitlog
#消费队列存储路径存储路径
storePathConsumeQueue=/app/rocketMQ/storeSlave/consumequeue
#消息索引存储路径
storePathIndex=/app/rocketMQ/storeSlave/index
#checkpoint文件存储路径
storeCheckpoint=/app/rocketMQ/storeSlave/checkpoint
#abort文件存储路径
abortFile=/app/rocketMQ/storeSlave/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
#开启Sql过滤
enablePropertyFilter=true
#重试支持过滤
filterSupportRetry=true

注意事项

这样2主2从的集群配置基本就完成了。搭建过程中需要注意的配置项:

  • 同一机器上两个实例的store目录不能相同,否则会报错 Lock failed,MQ already started
  • 同一机器上两个实例的listenPort也不能相同。否则会报端口占用的错
  • 如果是多网卡的机器,比如云服务器,那么需要在broker.conf中增加brokerIP1属性,指定所在机器的外网网卡地址。

启动worker1、worker2、worker3的nameServer,并观察启动日志

[root@localhost 2m-2s-async]# cd ../../bin/
#启动之前使用jps命令查看下环境是否正常,有时候会出现环境变量异常,需要重新使用source ~/.bash_profile命令刷新配置
[root@localhost bin]# nohup ./mqnamesrv &
#观察日志查看是否启动成功,同样出现The Name Server boot success. serializeType=JSON 即成功启动
[root@localhost bin]# tail -f nohup.out
#也可以使用tail -f ~/logs/rocketmqlogs/namesrv.log 观察日志

日志如下启动成功

在这里插入图片描述

worker2上启动broker-a节点与broker-b-s节点

[root@localhost bin]# nohup ./mqbroker -c ../conf/2m-2s-async/broker-a.properties & 
#出现以下日志即启动成功,观察注册的nameServer服务
#The broker[broker-a, 192.168.43.135:10911] boot success. serializeType=JSON and name server is worker1:9876;worker2:9876;worker3:9876

[root@localhost bin]# nohup ./mqbroker -c ../conf/2m-2s-async/broker-b-s.properties & 
[root@localhost bin]# tail -f nohup.out 
#出现以下日志即启动成功,观察注册的nameServer服务
#The broker[broker-b, 192.168.43.135:11011] boot success. serializeType=JSON and name server is worker1:9876;worker2:9876;worker3:9876
#也可以使用tail -f ~/logs/rocketmqlogs/broker.log 观察日志

日志如下启动成功
在这里插入图片描述
worker3上启动broker-b节点与broker-a-s节点

[root@localhost bin]# nohup ./mqbroker -c ../conf/2m-2s-async/broker-b.properties & 
#出现以下日志即启动成功,观察注册的nameServer服务
#The broker[broker-b, 192.168.43.136:10911] boot success. serializeType=JSON and name server is worker1:9876;worker2:9876;worker3:9876
[root@localhost bin]# nohup ./mqbroker -c ../conf/2m-2s-async/broker-a-s.properties &
#出现以下日志即启动成功,观察注册的nameServer服务
#The broker[broker-b, 192.168.43.136:10911] boot success. serializeType=JSON and name server is worker1:9876;worker2:9876;worker3:9876

使用测试工具测试消息收发

# worker2发送消息
[root@localhost bin]# ./tools.sh org.apache.rocketmq.example.quickstart.Producer
# worker3接受消息
[root@localhost bin]# ./tools.sh org.apache.rocketmq.example.quickstart.Consumer

部署5.x版本-Local模式

待学习

Apache RocketMQ 5.0 版本完成基本消息收发,包括 NameServer、Broker、Proxy 组件。 在 5.0 版本中 Proxy 和 Broker 根据实际诉求可以分为 Local 模式和 Cluster 模式,一般情况下如果没有特殊需求,或者遵循从早期版本平滑升级的思路,可以选用Local模式。

  • 在 Local 模式下,Broker 和 Proxy 是同进程部署,只是在原有 Broker 的配置基础上新增 Proxy的简易配置就可以运行。
  • 在 Cluster 模式下,Broker 和 Proxy 分别部署,即在原有的集群基础上,额外再部署Proxy 即可。

关闭worker2,worker3的broker服务

[root@localhost bin]# sh ./mqshutdown broker
# worker2机器
[root@localhost bin]# nohup ./mqbroker -c ../conf/2m-2s-async/broker-a.properties --enable-proxy &
# worker3机器
[root@localhost bin]# nohup ./mqbroker -c ../conf/2m-2s-async/broker-b.properties --enable-proxy &

#使用tail -f ~/logs/rocketmqlogs/proxy.log查看日志
[root@localhost bin]# tail -f ~/logs/rocketmqlogs/proxy.log
# 出现以下异常即成功启动
# 2023-04-23 15:09:33 INFO main - The broker[broker-a, 192.168.43.135:10911] boot success. serializeType=JSON and name server is worker1:9876;worker2:9876;worker3:9876
# 2023-04-23 15:09:34 INFO main - grpc server start successfully.

官网还提供了其他部署模式,有兴趣的小伙伴可以自行研究,官网部署方式:https://rocketmq.apache.org/zh/docs/deploymentOperations/01deploy。

到此搭建了⼀个主从结构的RocketMQ集群,但是这种主从结构是只做数据备份,没有容灾功能的。也就是说当⼀个master节点挂了后,slave节点是⽆法切换成master节点继续提供服务的。注意这个集群⾄少要是3台,允许少于⼀半的节点发⽣故障。

Dleger⾼可⽤集群搭建

在RocketMQ 4.5之前,RocketMQ 都是采用master-slave主从架构部署,master节点负责写入消息,slave节点负责同步master节点消息。假设master-1节点有个topic-A,假设此时master节点宕机无法提供服务了,此时我们是无法写入topic-A消息的,这个时候RocketMQ 对于我们producer服务来说来说是不可用的,只有手动让slave-1节点升级生master节点,或者重启恢复master-1节点,RocketMQ 集群才能恢复正常。

如果要进⾏⾼可⽤的容灾备份,需要采⽤Dledger的⽅式来搭建⾼可⽤集群。注意,这个Dledger需要在RocketMQ4.5以后的版本才⽀持,我们使⽤的4.7.1版本已经默认集成了dledger。

这种模式是基于Raft协议的,是⼀个类似于Zookeeper的paxos协议的选举协议,也是会在集群中随机选举出⼀个leader,其他的就是follower。只是他选举的过程跟paxos有点不同。Raft协议基于随机休眠机制的,选举过程会⽐paxos相对慢⼀点。

dledger介绍

dledger集群架构原理:

  1. 一个RocketMQ集群至少需要部署三个节点,其中一个leader节点,其余两个follower节点。
  2. leader节点负责写入消息,当消息写入leader节点内存之后,leader会将消息同步到follower节点,当集群过半数(节点数/2 +1)节点都写入了消息,leader节点则提交这个消息,这样一条消息就算是写成功了。RocketMQ这种类似2pc写入方式保证了主从最终一致性。
  3. 如果leader节点挂了,RocketMQ集群会触发leader选举,重新选举一个新的leader节点负责写入数据,选举过程中整个RocketMQ集群是不可用状态。
  4. 和之前主从集群架构一样,RocketMQ dledger集群的每个节点也都会向namesrv所有的节点进行注册,没有任何区别。

RocketMQ部署及简单API应用_第6张图片

要搭建⾼可⽤的Broker集群,我们只需要配置conf/dleger下的配置⽂件就⾏。
⾸先:我们同样是需要修改runserver.sh和runbroker.sh,对JVM内存进⾏定制。
然后:我们需要修改conf/dleger下的配置⽂件。 跟dleger相关的⼏个配置项如下:

RocketMQ部署及简单API应用_第7张图片
配置完后,同样是使⽤ nohup bin/mqbroker -c $conf_name & 的⽅式指定实例⽂件。

可以使⽤ bin/mqadmin clusterList -n worker1.conf的⽅式查看集群状态。
单机状态下⼀般⼀次主从切换需要⼤概10S的时间。

Rocketmq-dashboard 搭建

RocketMQ源代码中并没有提供控制台,但是有⼀个Rocket的社区扩展项⽬中提供了⼀个控制台
官⽹地址:https://github.com/apache/rocketmq-externals
gitCode地址:https://gitcode.net/java_wxid/rocketmq-externals-master

需要提前安装MAVEN

下载下来后,进⼊其中的rocket-console⽬录,使⽤maven进⾏编译

这里可以使用idea编译完成后拷贝至Linux环境

 mvn clean package -Dmaven.test.skip=true

编译完成后,获取target下的jar包,就可以直接执⾏。但是这个时候要注意,在这个项⽬的
application.properties中需要指定nameserver的地址。默认这个属性是空的。
那我们可以在jar包的当前⽬录下增加⼀个application.properties⽂件,覆盖jar包中默认的⼀个属性:

 rocketmq.config.namesrvAddr=worker1:9876;worker2:9876;worker3:9876

如果不是集群,只需要配置⼀个namesrvAddr即可

maven打包完成后运行Jar包

java -jar rocketmq-console-ng-1.0.1.jar

启动完成后,可以访问 http://ip:端⼝看到管理⻚⾯

docker镜像运⾏

vim /opt/rocketmq/conf/broker.conf

增加两⾏配置(填写自己namesrvAddr和brokerIP地址):

namesrvAddr=139.224.233.121:9876
brokerIP1=139.224.233.121

docker进⾏直接拉取镜像运⾏

docker pull styletang/rocketmq-console-ng

运行

docker run --name rocketmq -d -e "JAVA_OPTS=-
Drocketmq.namesrv.addr=139.224.233.121:9876 -
Dcom.rocketmq.sendMessageWithVIPChannel=false -
Duser.timezone='Asia/Shanghai'" -v /etc/localtime:/etc/localtime -p
8080:8080 -t styletang/rocketmq-console-ng

调整系统参数

在实际使⽤时,我们说RocketMQ的吞吐量、性能都很⾼,那要发挥RocketMQ的⾼性能,还需要对RocketMQ以及服务器的性能进⾏定制

在runserver.sh中需要定制nameserver的内存⼤⼩,在runbroker.sh中需要定制broker的
内存⼤⼩。默认的配置可以认为都是经过检验的最优化配置,但是在实际情况中都还需要根据服务器的实际情况进⾏调整。

以runbroker.sh中对G1GC的配置举例,在runbroker.sh中的关键配置:

JAVA_OPT="${JAVA_OPT} -XX:+UseG1GC -XX:G1HeapRegionSize=16m -
XX:G1ReservePercent=25 -XX:InitiatingHeapOccupancyPercent=30 -
XX:SoftRefLRUPolicyMSPerMB=0"
JAVA_OPT="${JAVA_OPT} -verbose:gc -
Xloggc:${GC_LOG_DIR}/rmq_broker_gc_%p_%t.log -XX:+PrintGCDetails -
XX:+PrintGCDateStamps -XX:+PrintGCApplicationStoppedTime -
XX:+PrintAdaptiveSizePolicy"
JAVA_OPT="${JAVA_OPT} -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -
XX:GCLogFileSize=30m"

-XX:+UseG1GC: 使⽤G1垃圾回收器, -XX:G1HeapRegionSize=16m 将G1的region块⼤⼩设为16M,-XX:G1ReservePercent:在G1的⽼年代中预留25%空闲内存,这个默认值是10%,RocketMQ把这个参数调⼤了。-XX:InitiatingHeapOccupancyPercent=30:当堆内存的使⽤率达到30%之后就会启动G1垃圾回收器尝试回收垃圾,默认值是45%,RocketMQ把这个参数调⼩了,也就是提⾼了GC的频率,但是避免了垃圾对象过多,⼀次垃圾回收时间太⻓的问题。然后,后⾯定制了GC的⽇志⽂件,确定GC⽇志⽂件的地址、打印的内容以及控制每个⽇志⽂件的⼤⼩为30M并且只保留5个⽂件。这些在进⾏性能检验时,是相当重要的参考内容。

其他⼀些核⼼参数
例如在conf/dleger/broker-n0.conf中有⼀个参数:sendMessageThreadPoolNums=16。这⼀个参数是表明RocketMQ内部⽤来发送消息的线程池的线程数量是16个,其实这个参数可以根据机器的CPU核⼼数进⾏适当调整,例如如果你的机器核⼼数超过16个,就可以把这个参数适当调⼤。

Linux内核参数定制

  • ulimit:需要进⾏⼤量的⽹络通信和磁盘IO。
  • vm.extra_free_kbytes:告诉VM在后台回收(kswapd)启动的阈值与直接回收(通过分配进程)的阈值之间保留额外的可⽤内存。RocketMQ使⽤此参数来避免内存分配中的⻓延迟。(与具体内核版本相关)
  • vm.min_free_kbytes:如果将其设置为低于1024KB,将会巧妙的将系统破坏,并且系统在⾼负载下容易出现死锁。
  • vm.max_map_count:限制⼀个进程可能具有的最⼤内存映射区域数。RocketMQ将使⽤mmap加载CommitLog和ConsumeQueue,因此建议将为此参数设置较⼤的值。
  • vm.swappiness,定义内核交换内存⻚⾯的积极程度。较⾼的值会增加攻击性,较低的值会减少交换量。建议将值设置为10来避免交换延迟。
  • File descriptor limits:RocketMQ需要为⽂件(CommitLog和ConsumeQueue)和⽹络连接打开⽂件描述符。我们建议设置⽂件描述符的值为655350。

RocketMQ原⽣API使⽤

⾸先创建⼀个基于Maven的SpringBoot⼯程,引⼊如下依赖:

<dependency>
	<groupId>org.apache.rocketmqgroupId>
	<artifactId>rocketmq-clientartifactId>
	<version>4.7.1version>
dependency>

另外还与⼀些依赖,例如openmessage、acl等扩展功能还需要添加对应的依赖。具体可以参⻅RocketMQ源码中的example模块。在RocketMQ源码包中的example模块提供了⾮常详尽的测试代码,也可以拿来直接调试。我们这⾥就⽤源码包中的示例来连接我们⾃⼰搭建的RocketMQ集群来进⾏演示。

RocketMQ的官⽹上有很多经典的测试代码

这些测试代码中的⽣产者和消费者都需要依赖NameServer才能运⾏,只需要NameServer指向我们⾃⼰搭建的RocketMQ集群,⽽不需要管Broker在哪⾥,就可以连接我们⾃⼰的⾃⼰的RocketMQ集群。⽽RocketMQ提供的⽣产者和消费者寻找NameServer的⽅式有两种:

  1. 在代码中指定namesrvAddr属性。例如consumer.setNamesrvAddr(“127.0.0.1:9876”);
  2. 通过NAMESRV_ADDR环境变量来指定。多个NameServer之间⽤分号连接。

RocketMQ的⽣产者和消费者的编程模型都是有个⽐较固定的步骤的,掌握这个固定的步骤,对于我们学习源码以及以后使⽤都是很有帮助的。

消息发送者的固定步骤

  1. 创建消息⽣产者producer,并制定⽣产者组名
  2. 指定Nameserver地址
  3. 启动producer
  4. 创建消息对象,指定主题Topic、Tag和消息体
  5. 发送消息
  6. 关闭⽣产者producer

消息消费者的固定步骤

  1. 创建消费者Consumer,制定消费者组名
  2. 指定Nameserver地址
  3. 订阅主题Topic和Tag
  4. 设置回调函数,处理消息
  5. 启动消费者consumer

消息生产

消息生产者分别通过三种方式发送消息:

  • 同步发送:等待消息返回后再继续进行下面的操作。
  • 异步发送:不等待消息返回直接进入后续流程。broker将结果返回后调用callback函数,并使用CountDownLatch计数。
  • 单向发送:只负责发送,不管消息是否发送成功。

同步发送

发送完消息之后,等待消息返回后再继续进⾏下⾯的操作。(消息发送最慢)

package Simple;

import org.apache.rocketmq.client.exception.MQBrokerException;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.remoting.exception.RemotingException;

import java.nio.charset.StandardCharsets;

/**
* 同步发送
* Created by BaiLi
*/
public class SyncProducer {
    public static void main(String[] args) throws MQClientException, MQBrokerException, RemotingException, InterruptedException {
        DefaultMQProducer producer = new DefaultMQProducer("SyncProducer");
        producer.setNamesrvAddr("192.168.43.137:9876");
        producer.start();
        for (int i = 0; i < 2; i++) {
            Message msg = new Message("Simple", //主题
                                      "TagA",  //设置消息Tag,用于消费端根据指定Tag过滤消息。
                                      "Simple-Sync".getBytes(StandardCharsets.UTF_8) //消息体。
                                     );
            SendResult send = producer.send(msg);
            System.out.printf(i + ".发送消息成功:%s%n", send);
        }
        producer.shutdown();
    }
}

异步发送

发完消息之后就去做⾃⼰的事情了,但是会给客户端⼀个回调⽅法,把消息发送的结果给到客户端。这⾥引⼊了⼀个countDownLatch来保证所有消息回调⽅法都执⾏完了再关闭Producer。 所以从这⾥可以看出,RocketMQ的Producer也是⼀个服务端,在往Broker发送消息的时候也要作为服务端提供服务。

package Simple;

import org.apache.rocketmq.client.exception.MQBrokerException;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendCallback;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.remoting.exception.RemotingException;

import java.nio.charset.StandardCharsets;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

/**
* 异步发送
* Created by BaiLi
*/
public class AsyncProducer {
    public static void main(String[] args) throws MQClientException, MQBrokerException, RemotingException, InterruptedException {
        DefaultMQProducer producer = new DefaultMQProducer("AsyncProducer");
        producer.setNamesrvAddr("192.168.43.137:9876");
        producer.start();
        CountDownLatch countDownLatch = new CountDownLatch(100);//计数
        for (int i = 0; i < 100; i++) {
            Message message = new Message("Simple", "TagA", "Simple-Async".getBytes(StandardCharsets.UTF_8));
            final int index = i;
            producer.send(message, new SendCallback() {
                @Override
                public void onSuccess(SendResult sendResult) {
                    countDownLatch.countDown();
                    System.out.printf("%d 消息发送成功%s%n", index, sendResult);
                }

                @Override
                public void onException(Throwable throwable) {
                    countDownLatch.countDown();
                    System.out.printf("%d 消息失败%s%n", index, throwable);
                    throwable.printStackTrace();
                }
            }
                         );
        }
        countDownLatch.await(5, TimeUnit.SECONDS);
        producer.shutdown();
    }
}

单向发送

package Simple;

import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.remoting.exception.RemotingException;

import java.nio.charset.StandardCharsets;

/**
* 单向发送
* Created by BaiLi
*/
public class OnewayProducer {
    public static void main(String[] args) throws MQClientException, RemotingException, InterruptedException {
        DefaultMQProducer producer = new DefaultMQProducer("AsyncProducer");
        producer.setNamesrvAddr("192.168.43.137:9876");
        producer.start();
        for (int i = 0; i < 10; i++) {
            Message message = new Message("Simple","TagA", "Simple-Oneway".getBytes(StandardCharsets.UTF_8));
            producer.sendOneway(message);
            System.out.printf("%d 消息发送完成 %n" , i);
        }
        Thread.sleep(5000);
        producer.shutdown();
    }
}

消费消息

消费者消费消息分两种:

  • 拉模式:消费者主动去Broker上拉取消息。
  • 推模式:消费者等待Broker把消息推送过来。

拉模式

package Simple;

import org.apache.rocketmq.client.consumer.DefaultMQPullConsumer;
import org.apache.rocketmq.client.consumer.PullResult;
import org.apache.rocketmq.client.consumer.store.ReadOffsetType;
import org.apache.rocketmq.client.exception.MQBrokerException;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.common.message.MessageQueue;
import org.apache.rocketmq.remoting.exception.RemotingException;

import java.util.HashSet;
import java.util.Set;

/**
* 拉模式
* Created by BaiLi
*/
public class PullConsumer {
    public static void main(String[] args) throws MQClientException {
        DefaultMQPullConsumer pullConsumer = new DefaultMQPullConsumer("SimplePullConsumer");
        pullConsumer.setNamesrvAddr("192.168.43.137:9876");//执行nameserver地址
        Set<String> topics = new HashSet<>();
        topics.add("Simple");//添加Topic
        topics.add("TopicTest");
        pullConsumer.setRegisterTopics(topics);
        pullConsumer.start();
        while (true) { //循环拉取消息
            pullConsumer.getRegisterTopics().forEach(n -> {
                try {
                    Set<MessageQueue> messageQueues = pullConsumer.fetchSubscribeMessageQueues(n);//获取主题中的Queue
                    messageQueues.forEach(l -> {
                        try {
                            //获取Queue中的偏移量
                            long offset = pullConsumer.getOffsetStore().readOffset(l, ReadOffsetType.READ_FROM_MEMORY);
                            if (offset < 0) {
                                offset = pullConsumer.getOffsetStore().readOffset(l, ReadOffsetType.READ_FROM_STORE);
                            }
                            if (offset < 0) {
                                offset = pullConsumer.maxOffset(l);
                            }
                            if (offset < 0) {
                                offset = 0;
                            }
                            //拉取Queue中的消息。每次获取32条
                            PullResult pullResult = pullConsumer.pull(l, "*", offset, 32);
                            System.out.printf("循环拉取消息ing %s%n",pullResult);
                            switch (pullResult.getPullStatus()) {
                                case FOUND:
                                    pullResult.getMsgFoundList().forEach(p -> {
                                        System.out.printf("拉取消息成功%s%n", p);
                                    });
                                    //更新偏移量
                                    pullConsumer.updateConsumeOffset(l, pullResult.getNextBeginOffset());
                            }
                        } catch (MQClientException e) {
                            e.printStackTrace();
                        } catch (RemotingException e) {
                            e.printStackTrace();
                        } catch (MQBrokerException e) {
                            e.printStackTrace();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        });
                        } catch (MQClientException e) {
                            e.printStackTrace();
                        }
                        });
                        }
                        }
                        }

推模式:

package Simple;

import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.common.message.MessageExt;

import java.util.List;

/**
* 推模式
* Created by BaiLi
*/
public class Consumer {
    public static void main(String[] args) throws MQClientException {
        DefaultMQPushConsumer pushConsumer = new DefaultMQPushConsumer("SimplePushConsumer");
        pushConsumer.setNamesrvAddr("192.168.43.137:9876");
        pushConsumer.subscribe("Simple","*");
        pushConsumer.setMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
                list.forEach( n->{
                    System.out.printf("收到消息: %s%n" , n);
                });
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        pushConsumer.start();
        System.out.printf("Consumer Started.%n");
    }
}

通常情况下,用推模式比较简单。需要注意DefaultMQPullConsumerImpl这个消费者类已标记为过期,但是还是可以使用的。替换的类是DefaultLitePullConsumerImpl。

  • LitePullConsumerSubscribe:随机获取一个queue消息
  • LitePullConsumerAssign:指定一个queue消息

拉模式-随机获取一个queue

package Simple;

import org.apache.rocketmq.client.consumer.DefaultLitePullConsumer;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.common.message.MessageExt;

import java.util.List;

/**
* Created by BaiLi
*/
public class PullLiteConsumer {
    public static void main(String[] args) throws MQClientException {
        DefaultLitePullConsumer litePullConsumer = new DefaultLitePullConsumer("SimpleLitePullConsumer");
        litePullConsumer.setNamesrvAddr("192.168.43.137:9876");
        litePullConsumer.subscribe("Simple");//随机获取一个queue消息
        litePullConsumer.start();
        while (true) {
            List<MessageExt> poll = litePullConsumer.poll();
            System.out.printf("消息拉取成功 %s%n" , poll);
        }
    }
}

拉模式-指定获取一个queue

package Simple;

import org.apache.rocketmq.client.consumer.DefaultLitePullConsumer;
import org.apache.rocketmq.common.message.MessageExt;
import org.apache.rocketmq.common.message.MessageQueue;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

/**
* 指定获取messageQueue消息
* Created by BaiLi
*/
public class PullLiteConsumerAssign {
    public static void main(String[] args) throws Exception {
        DefaultLitePullConsumer litePullConsumer = new DefaultLitePullConsumer("SimpleLitePullConsumer");
        litePullConsumer.setNamesrvAddr("192.168.43.137:9876");
        litePullConsumer.start();
        Collection<MessageQueue> messageQueues = litePullConsumer.fetchMessageQueues("TopicTest");
        List<MessageQueue> list = new ArrayList<>(messageQueues);
        litePullConsumer.assign(list);
        litePullConsumer.seek(list.get(0), 10);//指定获取一个queue
        try {
            while (true) {
                List<MessageExt> messageExts = litePullConsumer.poll();
                System.out.printf("%s %n", messageExts);
            }
        } finally {
            litePullConsumer.shutdown();
        }
    }
}

上面DefaultMQPushConsumer拉模式中需要自己手动维护偏移量,不够友好,DefaultLitePullConsumerImpl⽅式改进了,自动维护偏移量

package org.apache.rocketmq.example.simple;

import org.apache.rocketmq.client.consumer.DefaultLitePullConsumer;
import org.apache.rocketmq.common.consumer.ConsumeFromWhere;
import org.apache.rocketmq.common.message.MessageExt;
import java.util.List;

public class LitePullConsumerSubscribe {
    public static volatile boolean running = true;
    public static void main(String[] args) throws Exception {
        DefaultLitePullConsumer litePullConsumer = new
                DefaultLitePullConsumer("lite_pull_consumer_test");
        litePullConsumer.setNamesrvAddr("139.224.233.121:9876");
        litePullConsumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_
                OFFSET);
        litePullConsumer.subscribe("TopicTest", "*");
        litePullConsumer.start();
        try {
            while (running) {
				//直接去拉就可以了
                List<MessageExt> messageExts = litePullConsumer.poll();
                System.out.printf("%s%n", messageExts);
            }
        } finally {
            litePullConsumer.shutdown();
        }
    }
}

上⾯那种就是傻⽠式的⼀次拉取32条,⽆法定制化拉取指定某⼀个区间的消息,所以下⾯这种⼜进⾏了定制化调整

package org.apache.rocketmq.example.simple;

import org.apache.rocketmq.client.consumer.DefaultLitePullConsumer;
import org.apache.rocketmq.common.message.MessageExt;
import org.apache.rocketmq.common.message.MessageQueue;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
        
public class LitePullConsumerAssign {
    public static volatile boolean running = true;
    public static void main(String[] args) throws Exception {
        DefaultLitePullConsumer litePullConsumer = new
                DefaultLitePullConsumer("PullConsumer_1");
        litePullConsumer.setAutoCommit(false);
        litePullConsumer.start();
        //拉取这个主题⾥⾯的队列
        Collection<MessageQueue> mqSet =
                litePullConsumer.fetchMessageQueues("Topic_1");
        List<MessageQueue> list = new ArrayList<>(mqSet);
        //这⼀步就是过滤⼀部分队列,取其中⼀部分的队列
        List<MessageQueue> assignList = new ArrayList<>();
        for (int i = 0; i < list.size() / 2; i++) {
            assignList.add(list.get(i));
        }
        //把队列分配给这个消费者客户端
        litePullConsumer.assign(assignList);
        //取第⼀个队列,从偏移量为10的起点开始消费消息
        litePullConsumer.seek(assignList.get(0), 10);
        try {
            while (running) {
                //默认拉取32条
                List<MessageExt> messageExts = litePullConsumer.poll();
                System.out.printf("%s %n", messageExts);
                litePullConsumer.commitSync();
            }
        } finally {
            litePullConsumer.shutdown();
        }
    }
}

官方API样例

生产者:
   同步发送:org.apache.rocketmq.example.simple.Producer
   异步发送:org.apache.rocketmq.example.simple.AsyncProducer
   单向发送:org.apache.rocketmq.example.simple.OnewayProducer
消费者:
   拉模式:org.apache.rocketmq.example.simple.PullConsumer
   推模式:org.apache.rocketmq.example.simple.PushConsumer
   拉模式(随机获取一个queue)org.apache.rocketmq.example.simple.LitePullConsumerSubscribe.java
   拉模式(指定获取一个queue):org.apache.rocketmq.example.simple.LitePullConsumerAssign.java

顺序消息

顺序消息⽣产者

顺序消息⽣产者样例⻅:org.apache.rocketmq.example.ordermessage.Producer

package org.apache.rocketmq.example.ordermessage;

import org.apache.rocketmq.client.exception.MQBrokerException;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.MQProducer;
import org.apache.rocketmq.client.producer.MessageQueueSelector;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.common.message.MessageQueue;
import org.apache.rocketmq.remoting.common.RemotingHelper;
import org.apache.rocketmq.remoting.exception.RemotingException;
import java.io.UnsupportedEncodingException;
import java.util.List;

public class Producer {
    public static void main(String[] args) throws UnsupportedEncodingException {
        try {
            DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
            producer.setNamesrvAddr("139.224.233.121:9876");
            producer.start();
            //业务场景:我有⼗个订单,每个订单有六个步骤,现在需要按照固定的步骤⼀条条的发送消息
            for (int i = 0; i < 10; i++) {
                int orderId = i;
                for(int j = 0 ; j <= 5 ; j ++){
                    Message msg = new Message("OrderTopicTest", "order_"+orderId, "KEY" + orderId,
                                    ("order_"+orderId+" step " + j).getBytes(RemotingHelper.DEFAULT_CHARSET));
                    //消息队列的选择器
                    SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
                    //第⼀个参数:所有的消息,第⼆个参数:发送的消息,第三个参数:根据什么发送,这⾥⾯传的是orderId
                                @Override
                                public MessageQueue select(List<MessageQueue> mqs, essage msg, Object arg) {
                                    Integer id = (Integer) arg;
                                    int index = id % mqs.size();
                                    //获取订单id进⾏取模,取其中⼀个消息
                                    return mqs.get(index);
                                }
                            //同⼀个订单id可以放到同⼀个队列⾥⾯去
                            }, orderId);
                    System.out.printf("%s%n", sendResult);
                }
            }
            producer.shutdown();
        } catch (MQClientException | RemotingException | MQBrokerException
                | InterruptedException e) {
            e.printStackTrace();
        }
    }
}

顺序消息消费者


package org.apache.rocketmq.example.ordermessage;

import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.*;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.common.consumer.ConsumeFromWhere;
import org.apache.rocketmq.common.message.MessageExt;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
        
public class Consumer {
    public static void main(String[] args) throws MQClientException {
        DefaultMQPushConsumer consumer = new
                DefaultMQPushConsumer("please_rename_unique_group_name_3");
        consumer.setNamesrvAddr("139.224.233.121:9876");
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);
        consumer.subscribe("OrderTopicTest", "*");
        //MessageListenerOrderly是可以保证消息顺序消费的,因为它是⼀个队列⼀个队列  的去拿消息的
        consumer.registerMessageListener(new MessageListenerOrderly() {
            @Override
            public ConsumeOrderlyStatus consumeMessage(List<MessageExt>  msgs, ConsumeOrderlyContext context) {
                //⾃动提交
                context.setAutoCommit(true);
                for (MessageExt msg : msgs) {
                    System.out.println("收到消息内容 " + new String(msg.getBody()));
                }
                return ConsumeOrderlyStatus.SUCCESS;
            }
        });
    }
}

MessageListenerOrderly是可以保证最终消费顺序的,它是⼀个队列⼀个队列的去拿消息的
RocketMQ部署及简单API应用_第8张图片
MessageListenerConcurrently是保证不了最终消费顺序的,不保证全局有序,只保证局部有序

RocketMQ部署及简单API应用_第9张图片

实际上,RocketMQ也只保证了每个OrderID的所有消息有序(发到了同⼀个queue),⽽并不能保证所有消息都有序。所以这就涉及到了RocketMQ消息有序的原理。要保证最终消费到的消息是有序的,需要从Producer、Broker、Consumer三个步骤都保证消息有序才⾏。

⾸先在发送者端:在默认情况下,消息发送者会采取Round Robin轮询⽅式把消息发送到不同的MessageQueue(分区队列),⽽消费者消费的时候也从多个MessageQueue上拉取消息,这种情况下消息是不能保证顺序的。⽽只有当⼀组有序的消息发送到同⼀个MessageQueue上时,才能利⽤MessageQueue先进先出的特性保证这⼀组消息有序。⽽Broker中⼀个队列内的消息是可以保证有序的。

然后在消费者端:消费者会从多个消息队列上去拿消息。这时虽然每个消息队列上的消息是有序的,但是多个队列之间的消息仍然是乱序的。消费者端要保证消息有序,就需要按队列⼀个⼀个来取消息,即取完⼀个队列的消息后,再去取下⼀个队列的消息。⽽给consumer注⼊的MessageListenerOrderly对象,在RocketMQ内部就会通过锁队列的⽅式保证消息是⼀个⼀个队列来取的。MessageListenerConcurrently这个消息监听器则不会锁队列,每次都是从多个Message中取⼀批数据(默认不超过32条)。因此也⽆法保证消息有序。

RocketMQ 在默认情况下不保证顺序,要保证全局顺序,需要把 Topic 的读写队列数设置为 1,然后⽣产者和消费者的并发设置也是1,不能使⽤多线程。所以这样的话 ⾼并发,⾼吞吐量的功能完全⽤不上。

全局顺序消息 对于指定的⼀个 Topic,所有消息按照严格的先⼊先出(FIFO)的顺序进⾏发布 和消费。 分区顺序消息 对于指定的⼀个 Topic,所有消息根据 Sharding Key 进⾏区块分区。同⼀个分区内的消息按照严格的 FIFO 顺序进⾏发布和消费。Sharding Key 是顺序消息中⽤ 来区分不同分区的关键字段,和普通消息的 Message Key 是完全不同的概念。

  • 全局有序就是⽆论发的是不是同⼀个分区,我都可以按照你⽣产的顺序来消费
  • 分区有序就只针对发到同⼀个分区的消息可以顺序消费

顺序消息官方API样例
生产者: org.apache.rocketmq.example.order.Producer
消费者: org.apache.rocketmq.example.order.Consumer

广播消息

广播消息并没有特定的消息消费者样例,这是因为这涉及到消费者的集群消费模式。

  • MessageModel.BROADCASTING:广播消息。一条消息会发给所有订阅了对应主题的消费者,不管消费者是不是同一个消费者组。
  • MessageModel.CLUSTERING:集群消息。每一条消息只会被同一个消费者组中的一个实例消费。
package broadcast;

import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.common.message.MessageExt;
import org.apache.rocketmq.remoting.protocol.heartbeat.MessageModel;

import java.util.List;

/**
 * 广播消息消费模式
 * Created by BaiLi
 */
public class BroadcastConsumer {
    public static void main(String[] args) throws MQClientException {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("BroadCastConsumer");
        consumer.setNamesrvAddr("192.168.43.137:9876");
        consumer.subscribe("simple","*");
        consumer.setMessageModel(MessageModel.BROADCASTING); //广播模式
//        consumer.setMessageModel(MessageModel.CLUSTERING);//集群模式
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
                list.forEach(n->{
                    System.out.println("QueueId:"+n.getQueueId() + "收到消息内容 "+new String(n.getBody()));
                });
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        consumer.start();
        System.out.printf("Broadcast Consumer Started.%n");
    }
}

延迟消息

延迟消息实现的效果就是在调用producer.send方法后,消息并不会立即发送出去,而是会等一段时间再发送出去。这是RocketMQ特有的一个功能。

  • message.setDelayTimeLevel(3):预定日常定时发送。1到18分别对messageDelayLevel=1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h;可以在dashboard中broker配置查看。
  • msg.setDelayTimeMs(10L):指定时间定时发送。默认支持最大延迟时间为3天,可以根据broker配置:timerMaxDelaySec修改。(需要使用5.0.0以上的客户端)

预定日程生产者

package schedule;

import org.apache.rocketmq.client.exception.MQBrokerException;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.remoting.exception.RemotingException;

import java.nio.charset.StandardCharsets;
import java.time.LocalTime;

/**
 * 预定日程定时发送
 * Created by BaiLi
 */
public class ScheduleProducer {
    public static void main(String[] args) throws MQClientException, MQBrokerException, RemotingException, InterruptedException {
        DefaultMQProducer producer = new DefaultMQProducer("ScheduleProducer");
        producer.setNamesrvAddr("192.168.43.137:9876");
        producer.start();
        for (int i = 0; i < 2; i++) {
            Message msg = new Message("Schedule", //主题
                    "TagA",  //设置消息Tag,用于消费端根据指定Tag过滤消息。
                    "ScheduleProducer".getBytes(StandardCharsets.UTF_8) //消息体。
            );
            //1到18分别对应messageDelayLevel=1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
            msg.setDelayTimeLevel(3);
            producer.send(msg);
            System.out.printf(i + ".发送消息成功:%s%n", LocalTime.now());
        }
        producer.shutdown();
    }
}

预定日程消费者

package schedule;

import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.common.message.MessageExt;

import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.List;

/**
 * 预定日程消费者
 * Created by BaiLi
 */
public class ScheduleConsumer {
    public static void main(String[] args) throws MQClientException {
        DefaultMQPushConsumer pushConsumer = new DefaultMQPushConsumer("SimplePushConsumer");
        pushConsumer.setNamesrvAddr("192.168.43.137:9876");
        pushConsumer.subscribe("Schedule","*");
        pushConsumer.setMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
                list.forEach( n->{
                    System.out.printf("接收时间:%s %n", LocalTime.now());
                });
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        pushConsumer.start();
        System.out.printf("Simple Consumer Started.%n");
    }
}

指定时间生产者

package schedule;

import org.apache.rocketmq.client.exception.MQBrokerException;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.remoting.exception.RemotingException;

import java.nio.charset.StandardCharsets;
import java.time.LocalTime;

/**
 * 指定时间发送
 * 默认支持最大延迟时间为3天,可以根据broker配置:timerMaxDelaySec 修改
 * Created by BaiLi
 */
public class TimeProducer {
    public static void main(String[] args) throws MQClientException, MQBrokerException, RemotingException, InterruptedException {
        DefaultMQProducer producer = new DefaultMQProducer("TimeProducer");
        producer.setNamesrvAddr("192.168.43.137:9876");
        producer.start();
        for (int i = 0; i < 2; i++) {
            Message msg = new Message("Schedule", //主题
                    "TagA",  //设置消息Tag,用于消费端根据指定Tag过滤消息。
                    "TimeProducer".getBytes(StandardCharsets.UTF_8) //消息体。
            );
            // 相对时间:延时消息。此消息将在 10 秒后传递给消费者。
            msg.setDelayTimeMs(10000L);
            // 绝对时间:定时消息。设置一个具体的时间,然后在这个时间之后多久在进行发送消息
//            msg.setDeliverTimeMs(System.currentTimeMillis() + 10000L);
            producer.send(msg);
            System.out.printf(i + ".发送消息成功:%s%n", LocalTime.now());
        }
        producer.shutdown();
    }
}

加粗样式

package schedule;

import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.common.message.MessageExt;

import java.time.LocalTime;
import java.util.List;

/**
 * 定时发送消费者
 * Created by BaiLi
 */
public class TimeConsumer {
    public static void main(String[] args) throws MQClientException {
        DefaultMQPushConsumer pushConsumer = new DefaultMQPushConsumer("TimeConsumer");
        pushConsumer.setNamesrvAddr("192.168.43.137:9876");
        pushConsumer.subscribe("Schedule","*");
        pushConsumer.setMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
                list.forEach( n->{
                    System.out.printf("接收时间:%s %n", LocalTime.now());
                });
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        pushConsumer.start();
        System.out.printf("Simple Consumer Started.%n");
    }

延迟消息官方API样例

生产者:
预定日期发送:org.apache.rocketmq.example.schedule.ScheduledMessageProducer.java
指定时间发送:org.apache.rocketmq.example.schedule.TimerMessageProducer.java
消费者:
预定日期消费:org.apache.rocketmq.example.schedule.ScheduledMessageConsumer.java
指定时间消费:org.apache.rocketmq.example.schedule.TimerMessageConsumer.java

批量消息

批量消息是指将多条消息合并成⼀个批量消息,⼀次发送出去。这样的好处是可以减少⽹络IO,提升吞吐量。

批量消息的使用限制:

  • 消息大小不能超过4M,虽然源码注释不能超1M,但是实际使用不超过4M即可。平衡整体的性能,建议保持1M左右。
  • 相同的Topic,
  • 相同的waitStoreMsgOK
  • 不能是延迟消息、事务消息等

批量消息

package batch;

import org.apache.rocketmq.client.exception.MQBrokerException;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.remoting.exception.RemotingException;

import java.nio.charset.StandardCharsets;
import java.util.ArrayList;

/**
 * 批量发送消息
 * Created by BaiLi
 */
public class BatchProducer {
    public static void main(String[] args) throws MQClientException, MQBrokerException, RemotingException, InterruptedException {
        DefaultMQProducer producer = new DefaultMQProducer("BatchProducer");
        producer.setNamesrvAddr("192.168.43.137:9876");
        producer.start();
        ArrayList<Message> messages = new ArrayList<>();
        messages.add(new Message("simple","TagA", "BatchProducer0".getBytes(StandardCharsets.UTF_8)));
        messages.add(new Message("simple","TagA", "BatchProducer1".getBytes(StandardCharsets.UTF_8)));
        messages.add(new Message("simple","TagA", "BatchProducer2".getBytes(StandardCharsets.UTF_8)));
        SendResult send = producer.send(messages);
        System.out.printf(".发送消息成功:%s%n", send);
        producer.shutdown();
    }
}

分批发送批量消息

package batch;

import org.apache.rocketmq.client.exception.MQBrokerException;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.remoting.exception.RemotingException;

import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

/**
 * 分批批量发送消息
 * 注意修改SIZE_LIMIT为 = 10 * 1000,不然发送消息时会提示消息体积过大
 * Created by BaiLi
 */
public class SplitBatchProducer {
    public static void main(String[] args) throws MQClientException, MQBrokerException, RemotingException, InterruptedException {
        DefaultMQProducer producer = new DefaultMQProducer("SplitBatchProducer");
        producer.setNamesrvAddr("192.168.43.137:9876");
        producer.start();
        ArrayList<Message> messages = new ArrayList<>();
        for (int i = 0; i < 10000; i++) {
            messages.add(new Message("simple","TagA", ("SplitBatchProducer"+i).getBytes(StandardCharsets.UTF_8)));
        }
        ListSplitter splitter = new ListSplitter(messages);
        while (splitter.hasNext()) {
            List<Message> listItem = splitter.next();
            SendResult sendResult = producer.send(listItem);
            System.out.printf(".发送消息成功:%s%n", sendResult);
        }
        producer.shutdown();
    }
}

class ListSplitter implements Iterator<List<Message>> {
    private static final int SIZE_LIMIT = 10 * 1000;  // 每个消息批次的最大大小
    private final List<Message> messages;	// 待发送的消息列表
    private int currIndex; // 当前拆分到的位置

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

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

    @Override
    public List<Message> next() {
        int nextIndex = currIndex;
        int totalSize = 0;
        for (; nextIndex < messages.size(); nextIndex++) {
            Message message = messages.get(nextIndex);
            int tmpSize = message.getTopic().length() + message.getBody().length;
            Map<String, String> properties = message.getProperties();
            for (Map.Entry<String, String> entry : properties.entrySet()) {
                tmpSize += entry.getKey().length() + entry.getValue().length();
            }
            tmpSize = tmpSize + 20;
            // 如果超过了单个批次所允许的大小,就将此消息之前的消息作为下一个子列表返回
            if (tmpSize > SIZE_LIMIT) {
                // 如果是第一条消息就超出大小限制,就跳过这条消息再继续扫描
                if (nextIndex - currIndex == 0) {
                    nextIndex++;
                }
                break;
            }
            // 如果当前子列表大小已经超出所允许的单个批次大小,那么就暂停添加消息
            if (tmpSize + totalSize > SIZE_LIMIT) {
                break;
            } else {
                totalSize += tmpSize;
            }
        }
        // 返回从currIndex到nextIndex之间的所有消息
        List<Message> subList = messages.subList(currIndex, nextIndex);
        currIndex = nextIndex;
        return subList;
    }
}

生产者:
org.apache.rocketmq.example.batch.SimpleBatchProducer
org.apache.rocketmq.example.batch.SplitBatchProducer


过滤消息

使用Tag方式过滤

通过consumer.subscribe(“TagFilterTest”, “TagA || TagC”)实现

过滤消息-tag过滤生产者

package filter;

import org.apache.rocketmq.client.exception.MQBrokerException;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.remoting.exception.RemotingException;

import java.nio.charset.StandardCharsets;

/**
 * 过滤消息-tag过滤生产者
 * Created by BaiLi
 */
public class TagFilterProducer {
    public static void main(String[] args) throws MQClientException, MQBrokerException, RemotingException, InterruptedException {
        DefaultMQProducer producer = new DefaultMQProducer("SyncProducer");
        producer.setNamesrvAddr("192.168.43.137:9876");
        producer.start();
        String[] tags = new String[] {"TagA","TagB","TagC"};
        for (int i = 0; i < 15; i++) {
            Message msg = new Message("FilterTopic", //主题
                    tags[i % tags.length],  //设置消息Tag,用于消费端根据指定Tag过滤消息。
                    ("TagFilterProducer_"+tags[i % tags.length]).getBytes(StandardCharsets.UTF_8) //消息体。
            );
            SendResult send = producer.send(msg);
            System.out.printf(i + ".发送消息成功:%s%n", send);
        }
        producer.shutdown();
    }
}

过滤消息-tag过滤消费者

package filter;

import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.common.message.MessageExt;

import java.util.List;

/**
 * 过滤消息-tag过滤消费者
 * Created by BaiLi
 */
public class TagFilterConsumer {
    public static void main(String[] args) throws MQClientException {
        DefaultMQPushConsumer pushConsumer = new DefaultMQPushConsumer("SimplePushConsumer");
        pushConsumer.setNamesrvAddr("192.168.43.137:9876");
        pushConsumer.subscribe("FilterTopic","TagA || TagC");
        pushConsumer.setMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
                list.forEach( n->{
                    System.out.printf("收到消息: %s%n" , new String(n.getBody()));
                });
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        pushConsumer.start();
        System.out.printf("TagFilter Consumer Started.%n");
    }
}

Tag是RocketMQ中特有的一个消息属性。

RocketMQ的最佳实践中就建议使用RocketMQ时,一个应用可以就用一个Topic,而应用中的不同业务就用Tag来区分。

Tag方式有一个很大的限制,就是一个消息只能有一个Tag,这在一些比较复杂的场景就有点不足了。 这时候可以使用SQL表达式来对消息进行过滤。

使用Sql方式过滤

通过MessageSelector.bySql(String sql)参数实现

过滤消息-SQL过滤生产者

package filter;

import org.apache.rocketmq.client.exception.MQBrokerException;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.remoting.exception.RemotingException;

import java.nio.charset.StandardCharsets;

/**
* 过滤消息-SQL过滤生产者
* Created by BaiLi
*/
public class SqlFilterProducer {
    public static void main(String[] args) throws MQClientException, MQBrokerException, RemotingException, InterruptedException {
        DefaultMQProducer producer = new DefaultMQProducer("SyncProducer");
        producer.setNamesrvAddr("192.168.43.137:9876");
        producer.start();
        String[] tags = new String[] {"TagA","TagB","TagC"};
        for (int i = 0; i < 15; i++) {
            Message msg = new Message("FilterTopic", //主题
                                      tags[i % tags.length],  //设置消息Tag,用于消费端根据指定Tag过滤消息。
                                      ("TagFilterProducer_"+tags[i % tags.length] +  "_i_" + i).getBytes(StandardCharsets.UTF_8) //消息体。
                                     );
            msg.putUserProperty("baiLi", String.valueOf(i));
            SendResult send = producer.send(msg);
            System.out.printf(i + ".发送消息成功:%s%n", send);
        }
        producer.shutdown();
    }
}

过滤消息-SQL过滤消费者

package filter;

import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.MessageSelector;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.common.message.MessageExt;

import java.util.List;

/**
 * 过滤消息-SQL过滤消费者
 * Created by BaiLi
 */
public class SqlFilterConsumer {
    public static void main(String[] args) throws MQClientException {
        DefaultMQPushConsumer pushConsumer = new DefaultMQPushConsumer("SimplePushConsumer");
        pushConsumer.setNamesrvAddr("192.168.43.137:9876");
        pushConsumer.subscribe("FilterTopic", MessageSelector.bySql("(TAGS is not null And TAGS IN ('TagA','TagC'))"
        + "and (baiLi is not null and baiLi between 0 and 3)"));
        pushConsumer.setMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
                list.forEach( n->{
                    System.out.printf("收到消息: %s%n" , new String(n.getBody()));
                });
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        pushConsumer.start();
        System.out.printf("SqlFilter Consumer Started.%n");
    }
}

SQL92语法:

RocketMQ只定义了一些基本语法来支持这个特性。我们可以很容易地扩展它。

  • 数值比较,比如:>,>=,<,<=,BETWEEN,=;
  • 字符比较,比如:=,<>,IN;
  • IS NULL ,IS NOT NULL;
  • 逻辑符号 AND,OR,NOT;

常量支持类型为:

  • 数值,比如:123,3.1415;
  • 字符,比如:‘abc’,必须用单引号包裹起来;
  • NULL,特殊的常量
  • 布尔值,TRUE 或 FALSE

使用注意:

  • 只有推模式的消费者可以使用SQL过滤。拉模式是用不了的;
  • 另外消息过滤是在Broker端进行的,提升网络传输性能,但是broker服务会比较繁忙。(consumer将过滤条件推送给broker端)

事务消息

事务消息是RocketMQ提供的一个非常有特色的功能,需要着重理解。

事务消息是在分布式系统中保证最终一致性的两阶段提交的消息实现。他可以保证本地事务执行与消息发送两个操作的原子性,也就是这两个操作一起成功或者一起失败。

事务消息的实现机制:
RocketMQ部署及简单API应用_第10张图片
事务消息机制的关键是在发送消息时会将消息转为一个half半消息,并存入RocketMQ内部的一个Topic(RMQ_SYS_TRANS_HALF_TOPIC),这个Topic对消费者是不可见的。再经过一系列事务检查通过后,再将消息转存到目标Topic,这样对消费者就可见了,在 4.3.0 版中已经支持分布式事务消息。


事务消息只保证消息发送者的本地事务与发消息这两个操作的原⼦性,因此,事务消息的示例只涉及到消息发送者,对于消息消费者来说,并没有什么特别的。

事务消息的关键是在TransactionMQProducer中指定了一个TransactionListener事务监听器,这个事务监听器就是事务消息的关键控制器。

事务消息生产者

package transaction;

import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.TransactionListener;
import org.apache.rocketmq.client.producer.TransactionMQProducer;
import org.apache.rocketmq.client.producer.TransactionSendResult;
import org.apache.rocketmq.common.message.Message;

import java.nio.charset.StandardCharsets;
import java.util.concurrent.*;

/**
 * 事务消息生产者
 * Created by BaiLi
 */
public class TransactionProducer {
    public static void main(String[] args) throws MQClientException, InterruptedException {
        TransactionMQProducer producer = new TransactionMQProducer("TransProducer");
        producer.setNamesrvAddr("192.168.43.137:9876");
        //使用executorService异步提交事务状态,从而提高系统的性能和可靠性
        ExecutorService executorService = new ThreadPoolExecutor(2, 5, 100, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(2000), new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                Thread thread = new Thread(r);
                thread.setName("client-transaction-msg-check-thread");
                return thread;
            }
        });
        producer.setExecutorService(executorService);

        //本地事务监听器
        TransactionListener transactionListener = new TransactionListenerImpl();
        producer.setTransactionListener(transactionListener);

        producer.start();
        String[] tags = new String[] {"TagA","TagB","TagC","TagD","TagE"};
        for (int i = 0; i < 10; i++) {
            Message message = new Message("TransactionTopic",
                    tags[ i % tags.length],
                    ("Transaction_"+ tags[ i % tags.length]).getBytes(StandardCharsets.UTF_8));
            TransactionSendResult transactionSendResult = producer.sendMessageInTransaction(message, null);
            System.out.printf("%s%n", transactionSendResult);

            Thread.sleep(10); //延迟10毫秒
        }

        Thread.sleep(100000);//等待broker端回调
        producer.shutdown();
    }
}

本地事务监听器

package transaction;

import org.apache.commons.lang3.StringUtils;
import org.apache.rocketmq.client.producer.LocalTransactionState;
import org.apache.rocketmq.client.producer.TransactionListener;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.common.message.MessageExt;

/**
 * 本地事务监听器
 * Created by BaiLi
 */
public class TransactionListenerImpl implements TransactionListener {

    @Override
    /**
     * 在提交完事务消息后执行。
     * 返回COMMIT_MESSAGE状态的消息会立即被消费者消费到。
     * 返回ROLLBACK_MESSAGE状态的消息会被丢弃。
     * 返回UNKNOWN状态的消息会由Broker过一段时间再来回查事务的状态。
     */
    public LocalTransactionState executeLocalTransaction(Message message, Object o) {
        String tags = message.getTags();
        //TagA的消息会立即被消费者消费到
        if(StringUtils.contains(tags,"TagA")){
            return LocalTransactionState.COMMIT_MESSAGE;
            //TagB的消息会被丢弃
        }else if(StringUtils.contains(tags,"TagB")){
            return LocalTransactionState.ROLLBACK_MESSAGE;
            //其他消息会等待Broker进行事务状态回查。
        }else{
            return LocalTransactionState.UNKNOW;
        }
    }

    @Override
    /**
     * 在对UNKNOWN状态的消息进行状态回查时执行。
     * 返回COMMIT_MESSAGE状态的消息会立即被消费者消费到。
     * 返回ROLLBACK_MESSAGE状态的消息会被丢弃。
     * 返回UNKNOWN状态的消息会由Broker过一段时间再来回查事务的状态。
     */
    public LocalTransactionState checkLocalTransaction(MessageExt messageExt) {
        String tags = messageExt.getTags();
        //TagC的消息过一段时间会被消费者消费到
        if(StringUtils.contains(tags,"TagC")){
            return LocalTransactionState.COMMIT_MESSAGE;
            //TagD的消息也会在状态回查时被丢弃掉
        }else if(StringUtils.contains(tags,"TagD")){
            return LocalTransactionState.ROLLBACK_MESSAGE;
            //剩下TagE的消息会在多次状态回查后最终丢弃
        }else{
            return LocalTransactionState.UNKNOW;
        }
    }
}

事务消息的使用限制

  • 事务消息不支持延迟消息和批量消息。
  • 为了避免单个消息被检查太多次而导致半队列消息累积,我们默认将单个消息的检查次数限制为 15 次,但是用户可以通过 Broker 配置文件的transactionCheckMax参数来修改此限制。如果已经检查某条消息超过N次的话(N = transactionCheckMax)则 Broker 将丢弃此消息,并在默认情况下同时打印错误日志。可以通过重AbstractTransactionCheckListener类来修改这个行为。
  • 事务性消息可能不止一次被检查或消费。

SpringBoot整合RocketMQ

代码地址:https://gitcode.net/java_wxid/springboot-rocketmq

在使⽤SpringBoot的starter集成包时,要特别注意版本。因为SpringBoot集成RocketMQ的starter依赖是由Spring社区提供的,⽬前正在快速迭代的过程当中,不同版本之间的差距⾮常⼤,甚⾄基础的底层对象都会经常有改动。例如如果使⽤rocketmq-spring-boot-starter:2.0.4版本开发的代码,升级到⽬前最新的rocketmq-spring-boot-starter:2.1.1后,基本就⽤不了了。

创建⼀个maven⼯程,引⼊关键依赖:

<dependencies>
	<dependency>
		<groupId>org.apache.rocketmqgroupId>
		<artifactId>rocketmq-spring-boot-starterartifactId>
		<version>2.1.1version>
			<exclusions>
				<exclusion>
					<groupId>org.springframework.bootgroupId>
					<artifactId>spring-boot-starterartifactId>
				exclusion>
				<exclusion>
					<groupId>org.springframeworkgroupId>
					<artifactId>spring-coreartifactId>
				exclusion>
				<exclusion>
					<groupId>org.springframeworkgroupId>
					<artifactId>spring-webmvcartifactId>
				exclusion>
			exclusions>
	dependency>
	<dependency>
		<groupId>org.springframework.bootgroupId>
		<artifactId>spring-boot-starter-webartifactId>
		<version>2.1.6.RELEASEversion>
	dependency>
	<dependency>
		<groupId>io.springfoxgroupId>
		<artifactId>springfox-swagger-uiartifactId>
		<version>2.9.2version>
	dependency>
	<dependency>
		<groupId>io.springfoxgroupId>
		<artifactId>springfox-swagger2artifactId>
		<version>2.9.2version>
	dependency>
dependencies>

以SpringBoot的⽅式,快速创建⼀个简单的Demo项目

修改配置⽂件 application.properties

#NameServer地址
rocketmq.name-server=139.224.233.121:9876
#默认的消息⽣产者组
rocketmq.producer.group=springBootGroup

消息⽣产者

package com.roy.rocket.basic;

import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.spring.annotation.RocketMQTransactionListener;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.io.UnsupportedEncodingException;
/**
 * @author :zhiwei liao
 * @date :Created in 2022/01/09
 * @description:
 **/
@Component
public class SpringProducer {
    @Resource
    private RocketMQTemplate rocketMQTemplate;
    //发送普通消息的示例
    public void sendMessage(String topic,String msg){
        this.rocketMQTemplate.convertAndSend(topic,msg);
    }
    //发送事务消息的示例
    public void sendMessageInTransaction(String topic,String msg) throws
            InterruptedException {
        String[] tags = new String[] {"TagA", "TagB", "TagC", "TagD",
                "TagE"};
        for (int i = 0; i < 10; i++) {
            Message<String> message =
                    MessageBuilder.withPayload(msg).build();
            String destination =topic+":"+tags[i % tags.length];
            SendResult sendResult =
                    rocketMQTemplate.sendMessageInTransaction(destination,
                            message,destination);
            System.out.printf("%s%n", sendResult);
            Thread.sleep(10);
        }
    }
}

消息消费者

package com.roy.rocket.basic;

import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.stereotype.Component;
/**
 * @author :zhiwei liao
 * @date :Created in 2022/01/09
 * @description:
 **/
@Component
@RocketMQMessageListener(consumerGroup = "MyConsumerGroup", topic = "TestTopic")
public class SpringConsumer implements RocketMQListener<String> {
    @Override
    public void onMessage(String message) {
        System.out.println("Received message : "+ message);
    }
}

SpringBoot集成RocketMQ,消费者部分的核⼼就在这个@RocketMQMessageListener注解上。所有消费者的核⼼功能也都会集成到这个注解中。所以我们还要注意下这个注解⾥⾯的属性:例如:消息过滤可以由⾥⾯的selectorType属性和selectorExpression来定制消息有序消费还是并发消费则由consumeMode属性定制。消费者是集群部署还是⼴播部署由messageModel属性定制。

关于事务消息,还需要配置⼀个事务消息监听器:

package com.roy.rocket.config;

import org.apache.commons.lang3.StringUtils;
import org.apache.rocketmq.client.producer.LocalTransactionState;
import org.apache.rocketmq.spring.annotation.RocketMQTransactionListener;
import org.apache.rocketmq.spring.core.RocketMQLocalTransactionListener;
import org.apache.rocketmq.spring.core.RocketMQLocalTransactionState;
import org.apache.rocketmq.spring.support.RocketMQUtil;
import org.springframework.messaging.Message;
import org.springframework.messaging.converter.StringMessageConverter;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author :zhiwei liao
 * @date :Created in 2022/01/09
 * @description:
 **/
@RocketMQTransactionListener(rocketMQTemplateBeanName = "rocketMQTemplate")
public class MyTransactionImpl implements RocketMQLocalTransactionListener {
    
    private ConcurrentHashMap<Object, String> localTrans = new ConcurrentHashMap<>();
    
    @Override
    public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
        
        Object id = msg.getHeaders().get("id");
        String destination = arg.toString();
        localTrans.put(id,destination);
        org.apache.rocketmq.common.message.Message message =
                RocketMQUtil.convertToRocketMessage(new StringMessageConverter(),"UTF-8",destination, msg);
        String tags = message.getTags();
        if(StringUtils.contains(tags,"TagA")){
            return RocketMQLocalTransactionState.COMMIT;
        }else if(StringUtils.contains(tags,"TagB")){
            return RocketMQLocalTransactionState.ROLLBACK;
        }else{
            return RocketMQLocalTransactionState.UNKNOWN;
        }
        
    }
    
    @Override
    public RocketMQLocalTransactionState checkLocalTransaction(Message  msg) {
         //SpringBoot的消息对象中,并没有transactionId这个属性。跟原⽣API不⼀样。
         //String destination = localTrans.get(msg.getTransactionId());
         return RocketMQLocalTransactionState.COMMIT;
    }
    
}

启动应⽤后,就能够通过访问 http://localhost:8080/MQTest/sendMessage?message=123
接⼝来发送⼀条简单消息。并在SpringConsumer中消费到。

也可以通过访问http://localhost:8080/MQTest/sendTransactionMessage?message=123,来发送⼀条事务消息。

SpringBoot 引⼊org.apache.rocketmq:rocketmq-spring-boot-starter依赖后,就可以通过内置的RocketMQTemplate来与RocketMQ交互。相关属性都以rockemq.开头。具体所有的配置信息可以参⻅org.apache.rocketmq.spring.autoconfigure.RocketMQProperties这个类。

SpringBoot依赖中的Message对象和RocketMQ-client中的Message对象是两个不同的对象,这在使⽤的时候要⾮常容易弄错。例如RocketMQ-client中的Message⾥的TAG属性,在SpringBoot依赖中的Message中就没有。Tag属性被移到了发送⽬标中,与Topic⼀起,以Topic:Tag的⽅式指定。

最后强调⼀次,⼀定要注意版本。rocketmq-spring-boot-starter的更新进度⼀般都会略慢于RocketMQ的版本更新,并且版本不同会引发很多奇怪的问题。

apache有⼀个官⽅的rocketmq-spring示例,地址https://github.com/apache/rocketmq-spring.git以后如果版本更新了,可以参考下这个示例代码

设置每次拉去数量

@Service
@RocketMQMessageListener(topic = RocketConstant.Topic.PRAISE_TOPIC, consumerGroup = RocketConstant.ConsumerGroup.PRAISE_CONSUMER)
@Slf4j
public class PraiseListener implements RocketMQListener<PraiseRecordVO>, RocketMQPushConsumerLifecycleListener {

    @Resource
    private PraiseRecordService praiseRecordService;

    @Override
    public void onMessage(PraiseRecordVO vo) {
        praiseRecordService.insert(vo.copyProperties(PraiseRecord::new));
    }

    @Override
    public void prepareStart(DefaultMQPushConsumer consumer) {
        // 每次拉取的间隔,单位为毫秒
        consumer.setPullInterval(2000);
        // 设置每次从队列中拉取的消息数为16
        consumer.setPullBatchSize(16);
    }

}

次pull消息的最大数目受broker存储的MessageStoreConfig.maxTransferCountOnMessageInMemory(默认为32)值限制,即若想要消费者从队列拉取的消息数大于32有效(pullBatchSize>32)则需更改Broker的启动参数maxTransferCountOnMessageInMemory值。

在MQ削峰的配置参数里,以下几个DefaultMQPushConsumer的参数是需要注意一下的:

  1. pullInterval:每次从Broker拉取消息的间隔,单位为毫秒
  2. pullBatchSize:每次从Broker队列拉取到的消息数,实质上是从每个队列的拉取数,即Consume每次拉取的消息总数如下:EachPullTotal=所有Broker上的写队列数和(writeQueueNums=readQueueNums) * pullBatchSize
  3. consumeMessageBatchMaxSize:每次消费(即将多条消息合并为List消费)的最大消息数目,默认值为1,rocketmq-spring-boot-starter 目前不支持批量消费(2.1.0版本)

在消费者开始消息消费时会先从各队列中拉取一条消息进行消费,消费成功后再以每次pullBatchSize的数目进行拉取。

PraiseListener中设置了每次拉取的间隔为2s,每次从队列拉取的消息数为16,在搭建了2master broker且broker上writeQueueNums=readQueueNums=4的环境下每次拉取的消息理论数值为16 * 2 * 4 = 128,在第一次从各队列拉取1条消息(即共8条)后消费成功后会每次就会拉取最多128条消息进行消费

如何使用RocketMQ批量消费

rocketmq-spring-boot-starter并没有提供批量消费的功能,所以要批量消费消息需要自定义DefaultMQPushConsumer并配置其consumeMessageBatchMaxSize属性。consumeMessageBatchMaxSize属性默认值为1,即每次只消费一条消息,需要注意的是该属性也会受pullBatchSize影响,如果consumeMessageBatchMaxSize为32但pullBatchSize只为12,那么每次批量消费的最大消息数也就只有12。

@Bean
   public DefaultMQPushConsumer userMQPushConsumer() throws MQClientException {

       DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(RocketConstant.ConsumerGroup.SPRING_BOOT_USER_CONSUMER);
       consumer.setNamesrvAddr(nameServer);
       consumer.subscribe(RocketConstant.Topic.SPRING_BOOT_USER_TOPIC, "*");
       // 设置每次消息拉取的时间间隔,单位毫秒
       consumer.setPullInterval(1000);
       // 设置每个队列每次拉取的最大消息数
       consumer.setPullBatchSize(24);
       // 设置消费者单次批量消费的消息数目上限
       consumer.setConsumeMessageBatchMaxSize(12);
       
       consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
           
           List<UserInfo> userInfos = new ArrayList<>(msgs.size());
           Map<Integer, Integer> queueMsgMap = new HashMap<>(8);
           
           msgs.forEach(msg -> {
               userInfos.add(JSONObject.parseObject(msg.getBody(), UserInfo.class));
               queueMsgMap.compute(msg.getQueueId(), (key, val) -> val == null ? 1 : ++val);
           });
           
           log.info("userInfo size: {}, content: {}", userInfos.size(), userInfos);
           //处理批量消息,如批量插入:userInfoMapper.insertBatch(userInfos);
           return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
           
       });
       consumer.start();
       return consumer;
   }

流量削峰用法


SpringCloudStream整合RocketMQ

代码地址:https://gitcode.net/java_wxid/springcloudstream-rocketmq
SpringCloudStream是Spring社区提供的⼀个统⼀的消息驱动框架,⽬的是想要以⼀个统⼀的编程模型来对接所有的MQ消息中间件产品。我们还是来看看SpringCloudStream如何来集成RocketMQ。

使用较少,直接看上传的文档即可

图灵雀语笔记

你可能感兴趣的:(MQ,RocketMQ,java-rocketmq,rocketmq)