RocketMQ

RocketMQ

    • MQ介绍
      • 为什么要使用MQ?
      • MQ的优缺点
      • 各种MQ产品的比较
    • RockeetMQ快速入门
      • 准备工作
        • 下载RocketMQ
      • 安装RocketMQ
      • 启动RocketMQ
      • 测试RocketMQ
        • 发送消息
        • 接收消息
    • RocketMQ集群搭建
      • 角色介绍
      • 集群搭建方式
        • 集群特点
        • 集群模式
          • 1、单Master模式
          • 2、多Master模式
          • 3、多Master多Slave模式(异步)
          • 3、多Master多Slave模式(同步)
      • 集群工作流程
          • Host添加信息
          • 关闭防火墙或开放特定端口号
          • 配置环境变量
          • 创建消息存储路径
          • broker配置文件
      • 修改启动脚本文件
        • runbroker.sh
      • 服务启动
        • 启动NameServe集群
        • 启动Broker集群
        • 查看端口占用状态/进程状态
      • 集群监控平台搭建(RocketMQConsole)
    • 消息发送样栗
      • 基本样栗
        • 消息发送
          • 发送同步消息
          • 发送异步消息
          • 发送单向消息
        • 消费消息
        • 负载均衡模式(不指定默认负载均衡)
        • 广播模式
      • 顺序消息
        • 顺序消息生产
      • 延迟消息
        • 生产延迟消息(消费者无变化)
      • 批量消息
        • 批量发送消息
      • 事务消息
        • 1)事务消息发送及提交
        • 2)事务补偿
        • 3)事务消息状态
        • 使用限制
    • RocketMQ高级
      • 消息存储
        • 存储介质
        • 性能对比
      • 消息的存储和发送
          • 消息存储
          • 消息发送
        • 消息存储结构
        • 刷盘机制
      • 高可用机制
        • 消息消费高可用
        • 消息发送高可用
        • 主从复制
          • 同步复制
          • 异步复制
          • 配置
          • 总结
      • 负载均衡
        • Producer负载均衡
        • Consumer负载均衡
      • 消息重试
        • 顺序消息的重试
        • 无序消息的重试
          • 重试次数
          • 配置方式
      • 死信队列
        • 死信特性
        • 查看死信信息
        • 消费幂等
        • 消费幂等的必要性
        • 处理方式

MQ介绍

为什么要使用MQ?

消息队列是一种“先进先出”的数据结构。
RocketMQ_第1张图片
主要应用场景包括一下3个方面:
1、应用解耦:
系统的耦合性越高,容错性越低。以电商应用为例,用户创建订单后,如果耦合调用库存系统、物流系统、支付系统, 任何一个子系统处理故障或者因为升级等原因暂时不可用,都会造成下单操作异常,影响用户体验。
RocketMQ_第2张图片
使用消息队列解耦和,系统的耦合性就会降低。如物流系统发生故障,需要几分钟才能来修复,在这段时间内,物流系统要处理的数据被缓存到消息队列中。用户的下单操作正常完成。当物流系统恢复后,补充处理存在消息队列中的定单消息既可,终端系统感知不到物流系统发生过几分钟故障。
RocketMQ_第3张图片
2、流量削峰
RocketMQ_第4张图片
应用系统如果遇到系统请求流量瞬间猛增,有可能会将系统压垮。有了消息队列可以将大量的请求缓存起来,分散到很长一段时间去处理,这样可以大大提高系统稳定性和用户体验。
RocketMQ_第5张图片
一般情况,为了保证系统的稳定性,如果系统负载超过阈值,就会阻止用户请求,这会影响用户体验,而如果使用消息队列将请求缓存起来,等待系统处理完毕后通知用户下单完毕,这样总比不能下单号。

处于经济考量目的:
业务系统正常时段的QPS如果是1000,流量最高峰事10000,为了应对流量高峰配置高性能的服务器显然不划算,这时可以使用消息队列对峰值流量削峰。

  • 数据分发
    RocketMQ_第6张图片RocketMQ_第7张图片
    通过消息队列可以让数据在多个系统更加直接进行沟通。数据的产生方不需要关系谁来使用数据,是需要将数据发送到消息队列,数据使用方直接在消息队列中直接获取数据即可。

MQ的优缺点

优点:
解耦、削峰、数据分发。
缺点:

  • 系统可用性降低
    系统引入外部依赖越多,系统稳定性越差。一旦MQ宕机,就会对业务造成影响。

  • 系统复杂度提高
    MQ的加入大大增家了系统复杂度,以前系统间是同步的远程调用,现在是通过MQ进行异步调用。
    如何保证消息重复消费?怎么处理消息丢失的情况?怎么保证消息传递的顺序性?

  • 一致性问题
    A系统处理完业务,通过MQ给B、C、D三个系统发送消息数据,如果B新系统,C系统处理成功,D系统处理失败。
    如何保证数据处理的一致性?

各种MQ产品的比较

RocketMQ_第8张图片

RockeetMQ快速入门

准备工作

下载RocketMQ

Rocketmq-all-4.7.0
下载地址:http://rocketmq.apache.org/release_notes/

安装RocketMQ

1、解压安装包
2、进入安装目录
RocketMQ_第9张图片

启动RocketMQ

#1、启动NameServe // 后台启动
    nohup sh bin/mqnamesrv &
    查看启动日志
    tail -100f ~/logs/rocketmqlogs/namesrv.log
#2、启动Broker // 后台启动
    nohup sh bin/mqbroker -n localhost:9876 &
    查看启动日志
    tail -100f ~/logs/rocketmqlogs/broker.log
#3、关闭NameServer
    sh bin/mqshutdown namesrv
#4、关闭Broker
    sh bin/mqshutdown broker
  • 可能遇到问题的描述
    RocketMQ默认的虚拟机内存较大,启动Broker如果因为内存不足失败,需要编辑如下两个配置文件,修改JVM内存大小。
编辑runbroker.sh和runsever.sh修改默认JVM大小
vim ./bin/runbroker.sh
vim ./bin/runserver.sh
  • 参考设置:
    JAVA_OPT="${JAVA_OPT} -sever -Xms256m -Xmx256m -Xmn128m XX:MeatspacesSize=128M -XX:MaxMetaspacesSize=320m "

测试RocketMQ

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

RocketMQ集群搭建

角色介绍

  • Producer:消息的发送者;栗如:发信者
  • Consumer:消息的接收者;栗如:收信者
  • Broker:暂存和传输消息;栗如:邮局
  • NameServer:管理Broker;栗如:各个邮局管理机构
  • Topic;区分消息的种类;一个发送者可以发送一个或多个Topic;一个消息接收者可以订阅一个或多个Topic消息;
  • MessageQueue:相当于Topic的一个分区;用于并行发送和接收消息;
    RocketMQ_第10张图片

集群搭建方式

集群特点
  • NameServer事一个几乎无状态节点,可集群部署,节点之间无任何信息同步。
  • Broker部署相对复杂,Broker分为Master与Slave,一个Master可以对应多个Slave,但以Slave只能对应一个Master。Master与Slave的对应关系通过指定相同BrokerName,不同的BrokerId来定义,BrokerId为0表示Master,非0表示Slave。Master也可以部署多个。每个Broker与NameServer集群中所有节点建立长链接,定时注册Topic信息到所有NameServer。
  • Producer与NameServer集群中的其中一个节点(随机选择)建立长链接,定期从NameServer取Topic路由信息,并向提供Topic服务的Master建立长链接,且定时向Master发送心跳。Producer完全无状态,可集群部署。
  • Consumer与ManeServer集群中的其中一个节点(随机选择)建立长链接,定期从NameServer取Topic路由信息,并向提供Topic服务的Master、Slave建立长连接,且定时向Master、Slave发送心跳。Consumer既可以从Master订阅消息,也可以从Slave订阅消息,订阅规则由Broker配置决定。
集群模式
1、单Master模式
这种风险较大,一旦Broker重启或宕机时,会导致整个服务不可用。不建议先上环境使用,可以用于本地测试。
2、多Master模式
一个集群无Slave,全是Master,栗如:2个Master或者3个Master,这种模式的缺点如下:
优点:配置简单,单个Master宕机或重启维护对应应用无影响,在磁盘设置PAID10时,即使机器宕机不可恢复情况下,由于RAID10磁盘非常可口,消息也回不回丢(异步刷盘丢失少量消息,同步刷盘一条不丢),性能最高。
缺点:单台机器宕机事件,这台机器上未被消费的消息在机器恢复之前不可订阅,消息实时性会收到影响。
3、多Master多Slave模式(异步)
每个Master配置一个Slave,有多对Master-Slave,HA采用异步复制方式,主备用、有短暂消息延迟(毫秒级),这种模式优缺点如下:
优点:即使磁盘损害,消息丢失的非常少,且消息实时性不会受影响,同时Master宕机后,消费者仍然可以从Slave消费,而且此过程应用透明,不需要人工干预,性能同多Master模式几乎一样。
缺点:Master宕机,磁盘损坏情况下会丢失少量消息。
3、多Master多Slave模式(同步)
每个Master配置一个Slave,有多对Master-Slave,HA采用同步双写方式,即只有贮备都写成功,才像应用返回成功,这种模式优缺点如下:
优点:数据与服务都无单节点故障,Master宕机情况下,消息无延迟,服务可用性与数据库可用性都非常高。
缺点:性能比义务复制模式略低(大约第10%左右),发送单个消息的RT会略高,且目前版本在煮节点宕机后,从节点不能自动切换为主机。

集群工作流程

RocketMQ_第11张图片
1、启动NameServer,NameServer起来后监听端口,等待Broker、Producer、Consumer连上来,相当于一个路由器控制中心。
2、Broker启动,跟所有的NameServer保持长链接,定时发送心跳包。心跳包含当前Broker信息(IP+端口等)以及存储所有Topic信息。注册成功后,NameServer集群中就有Topic 跟Broker的映射关系。
3、收消息钱,先创建Topic,创建Topic时需要指定该Topic要存储在哪些Broker上,也可以在发送消息时自动创建Tpoic。
4、Producer发送消息,启动时想嗯NameServer集群中的其中一台建立长链接,并从NameServer中获取当前发送的Topic存在哪些Broker上,轮询从队列列表中选择一个队列,然后与队列所在的Broker建立长链接从而向Broker发消息。
5、Consumer跟Producer类似,跟其中一台NameServer建立长链接,获取当前订阅Topic存在哪些Broker上,然后直接跟Broker建立连接通道,开始消费消息。

Host添加信息
#修改hosts配置文件
vim /etc/hosts
#nameserver
192.xxx.xxx rocketmq-nameserver1
192.xxx.xxx rocketmq-nameserver2
#broker
192.xxx.xxx rocketmq-master1
192.xxx.xxx rocketmq-master2
192.xxx.xxx rocketmq-slave1
192.xxx.xxx rocketmq-slave2
#重启网卡
systemctl restart network
关闭防火墙或开放特定端口号
#关闭防火墙(远程访问)
systemctl stop firewalld.service
#查看防火墙状态
firewall -cmd --state
#禁止firewall开机启动
systemctl disable firewalld.service

#开放name server默认端口号
firewall -cmd -remove-port=9876/tcp --permanent
#开放master默认端口号
firewall -cmd -remove-port=10911/tcp --permanent
#开放slave默认端口号
firewall -cmd -remove-port=11011/tcp --permanent
#重启防火墙
firewall -cmd --reload
配置环境变量
vim /etc/profile
#set rocketmq
ROCKETMQ_HOME=/usr/local/tools/rocketmq/
PATH=$PATH:$ROCKETMQ_HOME/bin
exprot ROCKET_MQ PATH
#重启配置文件
source /etc/profile
创建消息存储路径
mkdir /usr/local/tools/rocketmq/store
mkdir /usr/local/tools/rocketmq/store/commitlog
mkdir /usr/local/tools/rocketmq/store/counsumequeue
mkdir /usr/local/tools/rocketmq/index
broker配置文件
  1. master1
    修改服务器配置:
    vim /usr/local/tools/rocketmq/rocketmq-4.7.0-1/conf/2m-2s-sync/broker-a.properties
#所属集群名字
brokerClusterName=rocketmq-cluster
#broker 名字,注意此处不同的配置文件填写的不一样
brokerName=broker-a
#0 表示 Master, >0 表示 Slave
brokerId=0
#nameServer 地址,分号分割
namesrvAddr=rocketmq-nameserver1:9876;rocketmq-nameserver2:9875
#在发送消息时,自动创建服务器不存在的 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=/usr/local/tools/rocketmq/store/master1
#commitLog 存储路径
storePathCommitLog=/usr/local/tools/rocketmq/store/master1/commitlog
#消费队列存储路径存储路径
storePathConsumeQueue=/usr/local/tools/rocketmq/store/master1/consumequeue
#消息索引存储路径
storePathIndex=/usr/local/tools/rocketmq/store/master1/index
#checkpoint 文件存储路径
storeCheckpoint=/usr/local/tools/rocketmq/store/master1/checkpoint
#abort 文件存储路径
abortFile=/usr/local/tools/rocketmq/store/master1/abort
#限制的消息大小
maxMessageSize=65536
#flushCommitLogLeastPages=4
#flushConsumeQueueLeastPages=2
#flushCommitLogThoroughInterval=10000
#flushConsumeQueueThoroughInterval=60000
#Broker 的角色
#- ASYNC_MASTER 异步复制 Master
#- SYNC_MASTER 同步双写 Master
#- SLAVE
brokerRole=SYNC_FLUSH
#刷盘方式
#- ASYNC_FLUSH 异步刷盘
#- SYNC_FLUSH 同步刷盘
flushDiskType=ASYNC_FLUSH
#checkTransactionMessageEnable=false
#发消息线程池数量
#sendMessageThreadPoolNums=128
#拉消息线程池数量
#pullMessageThreadPoolNums=128

2.slave1
修改服务器配置:
vim /usr/local/tools/rocketmq/rocketmq-4.7.0-2/conf/2m-2s-sync/broker-a-s.properties
修改配置如下:

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

修改启动脚本文件

runbroker.sh
vi /usr/local/rocketmq/bin/runbroker.sh

需要根据内存大小进行适当的对JVM参数进行调整:

#===================================================
# 开发环境配置 JVM Configuration
JAVA_OPT="${JAVA_OPT} -server -Xms256m -Xmx256m -Xmn128m"

####2)runserver.sh

vim /usr/local/rocketmq/bin/runserver.sh
JAVA_OPT="${JAVA_OPT} -server -Xms256m -Xmx256m -Xmn128m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=320m"

服务启动

启动NameServe集群

启动NameServer
NameServer默认端口9876,因单台阿里云服务器搭建2主2从,需要修改NameServer默认端口号,所有rocketmq实例都新增加内容为:listenPort=9875的namesrv.properties的配置文件。(文件名任意,我使用namesrv.txt也一样验证通过了)
RocketMQ_第12张图片
RocketMQ_第13张图片

# 第一台实例默认端口号9876 
cd /usr/local/tools/rocketmq/rocketmq-4.7.0-1/bin
nohup sh mqnamesrv &

# -c configfile 指定配置文件
cd /usr/local/tools/rocketmq/rocketmq-4.7.0-2/bin
nohup sh mqnamesrv -c /usr/local/tools/rocketmq/rocketmq-4.7.0-2/namesrv.properties &
启动Broker集群
  • 启动master1和slave1

master1:

cd /usr/local/tools/rocketmq/rocketmq-4.7.0-1/bin
nohup sh mqbroker -c /usr/local/tools/rocketmq/rocketmq-4.7.0-1/conf/2m-2s-sync/broker-a.properties &

slave1:

cd /usr/local/tools/rocketmq/rocketmq-4.7.0-2/bin
nohup sh mqbroker -c /usr/local/tools/rocketmq/rocketmq-4.7.0-2/conf/2m-2s-sync/broker-a-s.properties &
  • 启动master2和slave2

master2

cd /usr/local/tools/rocketmq/rocketmq-4.7.0-2/bin
nohup sh mqbroker -c /usr/local/tools/rocketmq/rocketmq-4.7.0-2/conf/2m-2s-sync/broker-b.properties &

slave2

cd /usr/local/tools/rocketmq/rocketmq-4.7.0-1/bin
nohup sh mqbroker -c /usr/local/tools/rocketmq/rocketmq-4.7.0-1/conf/2m-2s-sync/broker-b-s.properties &
查看端口占用状态/进程状态

RocketMQ_第14张图片

集群监控平台搭建(RocketMQConsole)

RocketMQ有一个对其扩展的开源项目incubator-rocketmq-externals,这个项目中有一个子模块叫rocketmq-console,这个便是管理控制台项目了,先将incubator-rocketmq-externals拉到本地,因为我们需要自己对rocketmq-console进行编译打包运行。

git clone https://github.com/apache/rocketmq-externals
cd rocketmq-console
# 忽略测试
mvn clean package -Dmaven.test.skip=true

注意:打包前在rocketmq-console中配置namesrv集群地址:

# 修改application.properties配置
rocketmq.config.namesrvAddr=192.xxx.xxx.xxx:9876;192.xxx.xxx.xxx:9875
server.port=9999

启动rocketmq-console:

nohup java -jar rocketmq-console-ng-1.0.0.jar &

RocketMQ_第15张图片

消息发送样栗

基本样栗

  • 导入MQ客户端依赖
<dependency>
    <groupId>org.apache.rocketmqgroupId>
    <artifactId>rocketmq-clientartifactId>
    <version>4.7.0version>
dependency>
  • 消息发送者步骤分析
1.创建消息生产者producer,并制定生产者组名
2.指定Nameserver地址
3.启动producer
4.创建消息对象,指定主题Topic、Tag和消息体
5.发送消息
6.关闭生产者producer
  • 消息消费者步骤分析
1.创建消费者Consumer,制定消费者组名
2.指定Nameserver地址
3.订阅主题Topic和Tag
4.设置回调函数,处理消息
5.启动消费者consumer
消息发送
发送同步消息

这种可靠性同步的发送方式使用的比较广泛,栗如:重要消息通知、短信通知

package com.example.demo.producer;

import lombok.extern.slf4j.Slf4j;
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.util.concurrent.TimeUnit;

/**
 * 发送同步消息
 *
 * @author Xiaolei Shi
 * @date 2020-06-13 12:15 AM
 */
@Slf4j
public class SycProducer {
    public static void main(String[] args) throws MQClientException, RemotingException, InterruptedException, MQBrokerException {
        // 1、创建消息生产者Producer,并制定生产者组名
        DefaultMQProducer defaultMQProducer = new DefaultMQProducer("group1");
        defaultMQProducer.setSendMsgTimeout(20000);
        // 2、指定Nameserver地址
        // connect to 172.17.190.187:10711 failed
        // brokerIP1=服务器地址(broker部署在虚拟机,并且虚拟双网卡,client无法正常连接服务端)
        defaultMQProducer.setNamesrvAddr("182.xx.xx.xx:9876;182.xx.xx.xx:9875");
        // 3、启动Producer
        defaultMQProducer.start();
        // 4、创建消息对象,制定主题Topic、Tag和消息体
        for (int i = 0; i < 10; i++) {
            // 4.1、指定主题(topic)、标签(tag)、内容(content)
            Message message = new Message();
            message.setTopic("base");
            message.setTags("tag1");
            message.setBody(("HelloWord: " + i).getBytes());
            // 5、发送消息
            SendResult result = defaultMQProducer.send(message);
            log.info("MsgId={}|MessageQueueId={}|result={}", result.getMsgId(), result.getMessageQueue().getQueueId(), result);
            TimeUnit.SECONDS.sleep(1);
        }
        // 6、关闭生产者Producer
        defaultMQProducer.shutdown();
    }

}

客户端连接时有可能会报错:
RemotingConnectException: connect to<172.17.190.187:10711> failed
这个错误看代码是发送消息后出的问题,说明和代码没有关系
原因是:broker部署在虚拟机,并且虚拟双网卡,client无法正常连接服务端

# 解决 在config/broker.conf配置文件中添加:
brokerIP1=服务器地址
namesrvAddr =服务器地址:9876
# 指定为本机的IP
# 然后重启rocketmq就可以了。

发送异步消息

异步消息通常用在对响应时间敏感的业务场景,即发送端不能容忍长时间的等待Broker的响应

package com.example.demo.producer;
import lombok.extern.slf4j.Slf4j;
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.util.concurrent.TimeUnit;

/**
 * 发送异步消息
 *
 * @author Xiaolei Shi
 * @date 2020-06-13 1:41 AM
 */
@Slf4j
public class AsyncProducer {
    public static void main(String[] args) throws MQClientException, RemotingException, InterruptedException, MQBrokerException {
        // 1、创建消息生产者Producer,并制定生产者组名
        DefaultMQProducer defaultMQProducer = new DefaultMQProducer("group1");
        defaultMQProducer.setSendMsgTimeout(20000);
        // 2、指定Nameserver地址
        // connect to 172.17.190.187:10711 failed
        // brokerIP1=服务器地址(broker部署在虚拟机,并且虚拟双网卡,client无法正常连接服务端)
        defaultMQProducer.setNamesrvAddr("182.xxx.xxx.xxx:9876;182.xxx.xxx.xxx:9875");
        // 3、启动Producer
        defaultMQProducer.start();
        // 4、创建消息对象,制定主题Topic、Tag和消息体
        for (int i = 0; i < 10; i++) {
            // 4.1、指定主题(topic)、标签(tag)、内容(content)
            Message message = new Message();
            message.setTopic("base");
            message.setTags("tag2");
            message.setBody(("HelloWord: " + i).getBytes());
            // 5、发送异步消息
            defaultMQProducer.send(message, new SendCallback() {
                /**
                 * 发送成功回调函数
                 * @param sendResult
                 */
                @Override
                public void onSuccess(SendResult sendResult) {
                    log.info("发送结果sendResult={}", sendResult);
                }
                /**
                 * 发送失败回调函数
                 * @param throwable
                 */
                @Override
                public void onException(Throwable throwable) {
                    log.info("发送异常throwable={}", throwable);

                }
            });
            TimeUnit.SECONDS.sleep(1);
        }
        // 6、关闭生产者Producer
        defaultMQProducer.shutdown();
    }
}
发送单向消息

主要用在不是特别关心发送结果的场景

package com.example.demo.producer;

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.util.concurrent.TimeUnit;

/**
 * 单向发送消息,无返回
 *
 * @author Xiaolei Shi
 * @date 2020-06-13 1:49 AM
 */
public class OneWayProducer {
    public static void main(String[] args) throws MQClientException, RemotingException, InterruptedException {
        /*
          1.创建消息生产者producer,并制定生产者组名
          2.指定Nameserver地址
          3.启动producer
          4.创建消息对象,指定主题Topic、Tag和消息体
          5.发送消息
          6.关闭生产者producer
         */
        DefaultMQProducer defaultMQProducer = new DefaultMQProducer("group1");
        defaultMQProducer.setNamesrvAddr("182.xxx.xxx.xxx:9876;182.xxx.xxx.xxx:9875");
        defaultMQProducer.start();
        for (int i = 0; i < 10; i++) {
            Message message = new Message();
            message.setTopic("base");
            message.setTags("tags3");
            message.setBody(("HelloWord: " + i).getBytes());
            defaultMQProducer.sendOneway(message);
            TimeUnit.SECONDS.sleep(1);
        }
        defaultMQProducer.shutdown();
    }
}

消费消息
负载均衡模式(不指定默认负载均衡)

消费者采用负载均衡消费消息,多个消费者共同消费队列消息,每个消费者处理的消息不同

package com.example.demo.consumer;

import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
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.protocol.heartbeat.MessageModel;

/**
 * 负载均衡消息接收
 *
 * @author Xiaolei Shi
 * @date 2020-06-13 1:29 PM
 */
public class Consumer {
    public static void main(String[] args) throws MQClientException {
        // 1.创建消费者Consumer,制定消费者组名
        DefaultMQPushConsumer defaultMQPushConsumer = new DefaultMQPushConsumer("group1");
        // 2.指定Nameserver地址
        defaultMQPushConsumer.setNamesrvAddr("182.xxx.xxx.xxxx:9876;182.xxx.xxx.xxxx:9875");
        // 3.订阅主题Topic和Tag
        defaultMQPushConsumer.subscribe("base", "tag3");
        // 4、负载均衡消费模式
        defaultMQPushConsumer.setMessageModel(MessageModel.CLUSTERING);
        // 5.设置回调函数,处理消息
        defaultMQPushConsumer.registerMessageListener((MessageListenerConcurrently) (list, consumeConcurrentlyContext) -> {
            list.forEach(messageExt -> {
                System.out.println(new String(messageExt.getBody()));
            });
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        });
        // 6.启动消费者consumer
        defaultMQPushConsumer.start();
    }
}
广播模式

消费者采用广播的方式消费消息,每个消费者的消息都是相同的

package com.example.demo.consumer;

import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
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.protocol.heartbeat.MessageModel;
/**
 * 广播消息接收
 *
 * @author Xiaolei Shi
 * @date 2020-06-13 1:29 PM
 */
public class Consumer {
    public static void main(String[] args) throws MQClientException {
        // 1.创建消费者Consumer,制定消费者组名
        DefaultMQPushConsumer defaultMQPushConsumer = new DefaultMQPushConsumer("group1");
        // 2.指定Nameserver地址
        defaultMQPushConsumer.setNamesrvAddr("182.xxx.xxx.xxx:9876;182.xxx.xxx.xxx:9875");
        // 3.订阅主题Topic和Tag
        defaultMQPushConsumer.subscribe("base", "tag3");
        // 4、广播消费模式
        defaultMQPushConsumer.setMessageModel(MessageModel.BROADCASTING);
        // 5.设置回调函数,处理消息
        defaultMQPushConsumer.registerMessageListener((MessageListenerConcurrently) (list, consumeConcurrentlyContext) -> {
            list.forEach(messageExt -> {
                System.out.println(new String(messageExt.getBody()));
            });
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        });
        // 6.启动消费者consumer
        defaultMQPushConsumer.start();
    }
}

顺序消息

消息有序指可以按照消息发送的顺序来消费(FIFO)。RocketMQ可以严格的保证消息的有序可可以分为分区有序或全局有序。
顺序消费的原理解析,在默认情况下消息发送会采取RoundRobin轮询的方式把消息发送到不同的queue(分区队列);而消费消息的时候从多个queue上拉取消息这种情况发送和消费是不能保证顺序。但如果控制发送的顺序消息只能一次发送到同一个queue中,消费的时候只从这个queue上一次拉取,则就能保证了顺序。当发送和消费参与的queue只有一个,则是全局有序;如果多个queue参与,则为分区有序,即相对每个queue,消息都是有序的
下面用订单进行分区有序的示栗:一个订单的顺序流程是:创建、付款、推送、完成。
RocketMQ_第16张图片

顺序消息生产
package com.example.demo.order;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

/**
 * 订单构建POJO
 *
 * @author Xiaolei Shi
 * @date 2020-06-13 7:26 PM
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
@Slf4j
public class OrderStep implements Serializable {
    private static final long serialVersionUID = -1601131722943653924L;
    private long orderId;
    private String desc;
    public static List<OrderStep> buildOrders() {
        List<OrderStep> list = new ArrayList<>();
        list.add(new OrderStep(1, "创建1"));
        list.add(new OrderStep(2, "创建2"));
        list.add(new OrderStep(1, "付款1"));
        list.add(new OrderStep(3, "创建3"));
        list.add(new OrderStep(2, "付款2"));
        list.add(new OrderStep(3, "付款3"));
        list.add(new OrderStep(2, "完成2"));
        list.add(new OrderStep(1, "推送1"));
        list.add(new OrderStep(3, "完成3"));
        list.add(new OrderStep(1, "完成1"));
        log.info("list size={}", list.size());
        return list;
    }

}
package com.example.demo.order;

import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
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.util.List;
/**
 * 订单生产者(顺序生产)
 *
 * @author Xiaolei Shi
 * @date 2020-06-13 7:38 PM
 */
@Slf4j
public class OrderProducer {
    public static void main(String[] args) throws MQClientException {
        // 1、创建生产者并指定生产者组名
        DefaultMQProducer defaultMQProducer = new DefaultMQProducer("group1");
        // 2、关联指定NameServeri之
        defaultMQProducer.setNamesrvAddr("182.xxx.xxx.xxx:9876;182.xxx.xxx.xxx:9875");
        // 3、启动生产者Producer
        defaultMQProducer.start();
        // 4、获取订单消息
        List<OrderStep> orderSteps = OrderStep.buildOrders();
        // 5、发送消息
        orderSteps.forEach(orderStep -> {
            // 6、组装消息
            Message message = new Message();
            message.setTopic("OrderTopic");
            message.setTags("Order");
            message.setBody(JSON.toJSONBytes(orderStep));
            try {
                // send 方法参数
                // 参数一:消息对象
                // 参数二:消息队列的选择器
                // 参数三:选择队列的业务标识(订单ID)
                SendResult result = defaultMQProducer.send(message, (list, msg, arg) -> {
                    // MessageQueueSelector.select()方法参数
                    // 参数一:队列集合
                    // 参数二:消息对象
                    // 参数三:业务标识的参数(订单ID)
                    long orderId = (long) arg;
                        /*
                          以订单ID向队列大小取模获取队列队列位置,存入消息。
                         */
                    int delivery = (int) (orderId % list.size());
                    return list.get(delivery);
                }, orderStep.getOrderId());
                log.info("订单生产者发送顺序队列:result={}", result);
            } catch (MQClientException | RemotingException | MQBrokerException | InterruptedException e) {
                log.info("订单生产者发送顺序队列异常:e={}", e.getMessage());
            }
        });
        defaultMQProducer.shutdown();
    }
}
package com.example.demo.order;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerOrderly;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.common.protocol.heartbeat.MessageModel;

/**
 * 订单消费者(顺序消费)
 *
 * @author Xiaolei Shi
 * @date 2020-06-13 8:32 PM
 */
@Slf4j
public class OrderConsumer {
    public static void main(String[] args) throws MQClientException {
        // 1.创建消费者Consumer,制定消费者组名
        DefaultMQPushConsumer defaultMQPushConsumer = new DefaultMQPushConsumer("group1");
        // 2.指定Nameserver地址
        defaultMQPushConsumer.setNamesrvAddr("182.xxx.xxx.xxx:9876;182.xxx.xxx.xxx:9875");
        // 3.订阅主题Topic和Tag
        defaultMQPushConsumer.subscribe("OrderTopic", "*");
        // 4.设置负载均衡接收
        defaultMQPushConsumer.setMessageModel(MessageModel.CLUSTERING);
        // 4. 设置回调函数,处理消息
        defaultMQPushConsumer.registerMessageListener((MessageListenerOrderly) (list, consumeOrderlyContext) -> {
            list.forEach(messageExt -> log.info("订单顺序消费者消息:线程号={}|message={}", Thread.currentThread().getName(), JSON.parse(messageExt.getBody())));
            return ConsumeOrderlyStatus.SUCCESS;
        });
        // 5. 启动消费者consumer
        defaultMQPushConsumer.start();
    }
}

延迟消息

比如电商,提交了一个订单就可以发送一个延迟消息,1h后去检查这个订单状态,如果还是未付款就取消订单释放库存

生产延迟消息(消费者无变化)
// 现在RocketMq并不支持任意时间的延时,需要设置几个固定的延时等级,从1s到2h分别对应着等级1到18
// 个人猜想:1、延迟消息与顺序读写冲突。2、延迟与性能因素折中。
// org/apache/rocketmq/store/config/MessageStoreConfig.java
private String messageDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h";
package com.example.demo.producer;
import lombok.extern.slf4j.Slf4j;
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;

/**
 * 延迟消息发送
 *
 * @author Xiaolei Shi
 * @date 2020-06-13 10:14 PM
 */
@Slf4j
public class DelayProducer {
    public static void main(String[] args) throws MQClientException, RemotingException, InterruptedException, MQBrokerException {
        // 1、创建消息生产者Producer,并制定生产者组名
        DefaultMQProducer defaultMQProducer = new DefaultMQProducer("group1");
        defaultMQProducer.setSendMsgTimeout(20000);
        // 2、指定Nameserver地址
        // connect to 172.17.190.187:10711 failed
        // brokerIP1=服务器地址(broker部署在虚拟机,并且虚拟双网卡,client无法正常连接服务端)
        defaultMQProducer.setNamesrvAddr("182.xxx.xxx.xxx:9876;182.xxx.xxx.xxx:9875");
        // 3、启动Producer
        defaultMQProducer.start();
        // 4、创建消息对象,制定主题Topic、Tag和消息体
        for (int i = 0; i < 20; i++) {
            // 4.1、指定主题(topic)、标签(tag)、内容(content)
            Message message = new Message();
            message.setTopic("base");
            message.setTags("tag1");
            // 设置延时等级3,这个消息将在10s之后发送(现在只支持固定的几个时间,详看delayTimeLevel)
            message.setDelayTimeLevel(3);
            message.setBody(("同步helloWord: " + i).getBytes());
            // 5、发送消息
            SendResult result = defaultMQProducer.send(message);
            log.info("result={}", result);
            //    TimeUnit.SECONDS.sleep(1);
        }
        // 6、关闭生产者Producer
        defaultMQProducer.shutdown();
    }
}

批量消息

批量发送消息能显著提高消息的性能。限制是这些批量消息应该有相同的topic,相同的waitStoreMsgOK,而且不鞥是延迟消息。这一批消息总大小不应超过4MB

批量发送消息
package com.example.demo.producer;

import lombok.extern.slf4j.Slf4j;
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.common.message.Message;
import org.apache.rocketmq.remoting.exception.RemotingException;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * 批量发送消息
 *
 * @author Xiaolei Shi
 * @date 2020-06-13 10:35 PM
 */
@Slf4j
public class BatchProducer {
    private static final String TOPIC = "Batch";
    private static final String TAGS = "Tag1";

    public static void main(String[] args) throws MQClientException, RemotingException, InterruptedException, MQBrokerException {
        // 1、创建消息生产者Producer,并制定生产者组名
        DefaultMQProducer defaultMQProducer = new DefaultMQProducer("group1");
        // 2、指定Nameserver地址
        // connect to 172.17.190.187:10711 failed
        // brokerIP1=服务器地址(broker部署在虚拟机,并且虚拟双网卡,client无法正常连接服务端)
        defaultMQProducer.setNamesrvAddr("182.xxx.xxx.xxx:9876;182.xxx.xxx.xxx:9875");
        // 3、启动Producer
        defaultMQProducer.start();
        // 4、创建消息对象,制定主题Topic、Tag和消息体
        List<Message> messageList = new ArrayList<>();
        messageList.add(new Message(TOPIC, TAGS, ("hello word " + 1).getBytes()));
        messageList.add(new Message(TOPIC, TAGS, ("hello word " + 2).getBytes()));
        messageList.add(new Message(TOPIC, TAGS, ("hello word " + 3).getBytes()));

        //把大的消息分裂成若干个小的消息
        ListSplitter splitter = new ListSplitter(messageList);
        while (splitter.hasNext()) {
            try {
                List<Message> listItem = splitter.next();
                defaultMQProducer.send(listItem);
            } catch (Exception e) {
                //处理error
                log.info("批量发送消息异常:e={}",e.getMessage());
            }
        }
        TimeUnit.SECONDS.sleep(1);
        // 6、关闭生产者Producer
        defaultMQProducer.shutdown();
    }
   /**
    * 切割消息为4MB以内
    */
    static class ListSplitter implements Iterator<List<Message>> {
        private final int SIZE_LIMIT = 1024 * 1024 * 4;
        private final List<Message> messages;
        private int currIndex;

        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; // 增加日志的开销20字节
                if (tmpSize > SIZE_LIMIT) {
                    //单个消息超过了最大的限制
                    //忽略,否则会阻塞分裂的进程
                    if (nextIndex - currIndex == 0) {
                        //假如下一个子列表没有元素,则添加这个子列表然后退出循环,否则只是退出循环
                        nextIndex++;
                    }
                    break;
                }
                if (tmpSize + totalSize > SIZE_LIMIT) {
                    break;
                } else {
                    totalSize += tmpSize;
                }
            }
            List<Message> subList = messages.subList(currIndex, nextIndex);
            currIndex = nextIndex;
            return subList;
        }
    }
}

事务消息

下图说明了事务消息的大致方案,其中分为两个流程:正常事务消息的发送及提交、事务消息的补偿流程。
RocketMQ_第17张图片

1)事务消息发送及提交

(1) 发送消息(half消息)。
(2) 服务端响应消息写入结果。
(3) 根据发送结果执行本地事务(如果写入失败,此时half消息对业务不可见,本地逻辑不执行)。
(4) 根据本地事务状态执行Commit或者Rollback(Commit操作生成消息索引,消息对消费者可见)

2)事务补偿

(1) 对没有Commit/Rollback的事务消息(pending状态的消息),从服务端发起一次“回查”
(2) Producer收到回查消息,检查回查消息对应的本地事务的状态
(3) 根据本地事务状态,重新Commit或者Rollback
其中,补偿阶段用于解决消息Commit或者Rollback发生超时或者失败的情况。

3)事务消息状态

事务消息共有三种状态,提交状态、回滚状态、中间状态:

  • TransactionStatus.CommitTransaction: 提交事务,它允许消费者消费此消息。
  • TransactionStatus.RollbackTransaction: 回滚事务,它代表该消息将被删除,不允许被消费。
  • TransactionStatus.Unknown: 中间状态,它代表需要检查消息队列来确定状态。
package com.example.demo.transaction;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.LocalTransactionState;
import org.apache.rocketmq.client.producer.TransactionListener;
import org.apache.rocketmq.client.producer.TransactionMQProducer;
import org.apache.rocketmq.client.producer.TransactionSendResult;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.common.message.MessageExt;
/**
 * 事务消息生产者
 *
 * @author Xiaolei Shi
 * @date 2020-06-13 11:08 PM
 */
@Slf4j
public class TransactionProducer {
    public static void main(String[] args) throws MQClientException {
        // 1.创建消息事务生产者producer,并制定生产者组名
        TransactionMQProducer transactionMQProducer = new TransactionMQProducer("transactiongroup");
        // 2.指定Nameserver地址
        transactionMQProducer.setNamesrvAddr("182.xxx.xxx.xxx:9876;182.xxx.xxx.xxx:9875");
        // 3.设置tags便于测试,成功,本地事务执行失败回滚,不作处理。
        String[] tags = new String[]{"TagA", "TagB", "TagC"};
        // 3.设定生产者监听器
        transactionMQProducer.setTransactionListener(new TransactionListener() {
            /**
             * 该方法中执行本地事务
             * @param message
             * @param o
             * @return
             */
            @Override
            public LocalTransactionState executeLocalTransaction(Message message, Object o) {
                // 根据Tags类型选择返回结果
                switch (message.getTags()) {
                    case "TagA":
                        return LocalTransactionState.COMMIT_MESSAGE;
                    case "TagB":
                        return LocalTransactionState.ROLLBACK_MESSAGE;
                    default:
                        return LocalTransactionState.UNKNOW;
                }
            }
            /**
             * 该方法是MQ进行消息事务状态回查
             * @param messageExt
             * @return
             */
            @Override
            public LocalTransactionState checkLocalTransaction(MessageExt messageExt) {
                log.info("MQ进行消息事务状态回查:tags={}", messageExt.getTags());
                return LocalTransactionState.COMMIT_MESSAGE;
            }
        });
        // 4.启动producer
        transactionMQProducer.start();
        // 5.创建消息对象,指定主题Topic、Tag和消息体
        for (String tag : tags) {
            Message message = new Message();
            message.setTopic("Transaction");
            message.setTags(tag);
            message.setBody(("hell " + tag + " transaction").getBytes());
            // 6.发送消息
            TransactionSendResult transactionSendResult = transactionMQProducer.sendMessageInTransaction(message, null);
            log.info("发送事务消息:transactionSendResult={}", transactionSendResult);
        }
        // 7.关闭生产者producer
        //  transactionMQProducer.shutdown();
    }
}
使用限制
  1. 事务消息不支持延时消息和批量消息。
  2. 为了避免单个消息被检查太多次而导致半队列消息累积,我们默认将单个消息的检查次数限制为 15 次,但是用户可以通过 Broker 配置文件的 transactionCheckMax参数来修改此限制。如果已经检查某条消息超过 N 次的话( N = transactionCheckMax ) 则 Broker 将丢弃此消息,并在默认情况下同时打印错误日志。用户可以通过重写 AbstractTransactionCheckListener 类来修改这个行为。
  3. 事务消息将在 Broker 配置文件中的参数 transactionMsgTimeout 这样的特定时间长度之后被检查。当发送事务消息时,用户还可以通过设置用户属性 CHECK_IMMUNITY_TIME_IN_SECONDS 来改变这个限制,该参数优先于 transactionMsgTimeout 参数。
  4. 事务性消息可能不止一次被检查或消费。
  5. 提交给用户的目标主题消息可能会失败,目前这依日志的记录而定。它的高可用性通过 RocketMQ 本身的高可用性机制来保证,如果希望确保事务消息不丢失、并且事务完整性得到保证,建议使用同步的双重写入机制。
  6. 事务消息的生产者 ID 不能与其他类型消息的生产者 ID 共享。与其他类型的消息不同,事务消息允许反向查询、MQ服务器能通过它们的生产者 ID 查询到消费者。

RocketMQ高级

消息存储

分布式队列因为有高可靠性的要求,所以数据要进行持久化存储。
RocketMQ_第18张图片1. 消息生成者发送消息
2. MQ收到消息,将消息进行持久化,在存储中新增一条记录
3. 返回ACK给生产者
4. MQ push 消息给对应的消费者,然后等待消费者返回ACK
5. 如果消息消费者在指定时间内成功返回ACK,那么MQ认为消息消费成功,在存储中删除消息,即执行第6步,如果MQ在执行时间内没有收到ACK,则认为消息消费失败,会重新push消息,重复执行4、5、6、步骤。
6. MQ删除消息

存储介质

关系型数据库DB
Apache下开源的另一款MQ-ActiveMQ(默认采用的KahaDb做消息存储)可选用JDBC的方式来做消息持久化,通过简单的xml配置信息即可实现JDBC消息春初。由于,普通关系型数据库(如:MySQL)在单表数据量达到千万级别的情况下,其IO读写性能往往会出现瓶颈。在可靠性方面,该种方案非常依赖DB,如果一旦DB出现故障,则MQ的消息就会无法落盘存粗会导致线上故障。

文件系统
目前业界较为常用的几款产品(RocketMQ/Kafa/RabbitMQ)均采用的是消息刷配至所部属虚拟机/物理机的文件系统来做持久化(刷盘一般可以分为异步刷盘和同步刷盘两种模式)。消息刷盘为消息存储提供了一种高效率高可靠性和高性能的数据持久化方式。除非部署MQ机器本身或是本地磁盘挂了,非则一般是不会出现无法持久化的故障问题。

性能对比

文件系统>关系型DB

消息的存储和发送

消息存储

磁盘如果使用得当,磁盘的速度完全可以匹配上网络的数据参数速度。目前的高性能磁盘,顺序读写的速度可达到600MB/s,超过了一般网卡的传输速度,但是磁盘随机写的书读只有大概100KB/s,和顺序写的性能相差6000倍。因为有如此巨大的速度差别,好的消息队列系统会比普通的消息队列系统速度快多个数量级。RocketMQ的消息用顺序读写,保证了消息存储的速度。

消息发送

Linux操作系统分为【用户态】和【内核态】,文件操作、网络操作需要设计这两种形态的切换,免不了进行数据复制。一台服务器把本季磁盘文件的内容发送到客户端,一般分为两个步骤:
reda:读取本地文件内容;
write:将读取的内容通过网络发送出去;
这两个操作实际进行了4次数据复制,分别是:
1、从磁盘复制数据到内核态内存。
2、从内核态内存复制到用户态内存。
3、然后从用户态内存复制到网络驱动的内核态内存。
4、最后是从网络驱动的内核态内存复制到网卡中进行传输。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xwx1BILB-1592129114797)(evernotecid://9E63F4F3-CC2A-4DCD-A7C3-25AA4F636457/appyinxiangcom/14212861/ENResource/p952)]
通过使用mmap的方式,可以省去向用户态内存复制,提高速度。这种机制在Java中是通过MappedByteBuffer实现的RocketMQ充分利用了上述特性,也就是所谓的“零拷贝”技术,提高消息存盘和网络发送的速度。
这里需要注意的是,采用MappedByteBuffer这种内存映射的方式有几个限制,其中之一是一次只能映射1.5~2G的文件只用户态的虚拟内存,这也是为何RocketMQ默认设置耽搁CommitLog日志数据文件为1G的原因。

消息存储结构

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

  • CommitLog:存储消息的元数据
  • ConsumerQueue:存储消息在CommitLog的索引
  • IndexFile:为两消息查询提供了一种通过key或时间区间来查询消息的方法,这种通过娜IndexFile来查找消息的方法不影响发送与消费消息的主流程。
刷盘机制

RocketMQ的消息是存储到磁盘上的,这样既能保证断电后恢复 ,又可以让存储的消息量超出内存限制。rocketMQ为了提高性能,回尽可能低保证磁盘的顺序写。消息通过Producer写入RocketMQ时,有两种写入方式,分布式同步刷盘和异步刷盘。
RocketMQ_第20张图片

  1. 同步刷盘
    在返回写成功状态时,消息一句被写入磁盘。具体流程是:消息写入内存的PAGECACHE后,立即通知刷盘线程刷盘,然后等待刷盘完成,刷盘线程执行完成后换行等待的线程,返回消息写成功状态。
  2. 异步刷盘
    在返回写成功状态时,消息可能只是被写入了内存的PAGECACHE,写操作的返回快,吞吐量达,当内存里的消息积累到一定程度时,统一出发写磁盘动作,快速写入。
  3. 配置
    同步刷盘还是异步刷盘,都是通过Broker配置文件里的flushDiskType参数设置的,这个参数被配置成SYNC_FLUSH、ASYNC_FLUSH中的一个

高可用机制

消息消费高可用

在Consumer的配置文件中,并不需要设置从Master读还是从Slave读,当Master不可用活繁忙的时候,COnsumer会被自动切换到Slave读。有了自动切换Consumer这种机制,当一个Master角色的机器出现故障后,Consumer仍然可以从Slave读消息,不影响Consumer程序。这就达到了消费的高可用

消息发送高可用

在创建Topic的时候,把Topic的多个Message Queue创建在多个Broker组上(相同Broker名称,不同Broker的机器组成一个Broker组)这样当一个Broker组的Master不可用后,其他组的Master仍然可用,Producer仍然可以发送消息。PocketMQ目前还不支持吧Slave自动转换成Master,如果机器资源不足,需要吧Slave转换成Master,则需要手动停止Slave角色的Broker文件,用新的配置文件启动Broker
RocketMQ_第21张图片

主从复制

如果一个Broker组有Master和Slave,消费需要从Master复制到Slave上,有同步和异步两种复制方式

同步复制
  • 同步复制时等Master和Slave均写成功后才反馈给客户端写成功状态
  • 在同步复制方式下,如果Master出故障,Slave上有全部的备份数据,容易恢复,但是同步复制会增大护具写入延迟,降低系统吞吐量。
异步复制
  • 异步复制方式只要Master写成功即可反馈给客户端写成功状态。
  • 在异步复制方式下,系统拥有较低的延迟和较高的吞吐量,但如果Master处理故障,有些数据因为没哟不诶写入Slave,有可能会丢失。
配置

同步复制和异步复制是通过Broker配置文件里的brokerRole参数进行实质的,这个参数可以被设置成ASYNC_MASTER、SYNC_MASTER、SLAVE三种。

总结

RocketMQ_第22张图片
实际应用中要结合业务场景,合理设置刷盘方式和主从复制方式,尤其是同步刷盘,由于频繁地出发磁盘写动作,会明显降低性能。通常情况下,应该把Master和Save配置成ASYNC_FLUS的刷盘方式,主从之间配置成SYNC_MASTER的复制方式,这样即使有一台机器出现故障,仍然能够保证数据不丢。

负载均衡

Producer负载均衡

每个实例在发消息的时候,默认会轮询所有的message queue发送,以达到让消息平均落在不同的queue上。而由于queue可以散落在不同的broker,所以消息就发送到不同的broker下,如下图:
RocketMQ_第23张图片

Consumer负载均衡
  1. 集群模式
    在集群消费模式下, 每条消息只需要投递到订阅者Topic的COnsumerGropu下的一个实例即可。RocketMQ采用主动拉去的方式拉去并消费消息,在拉取的时候需要明确指定拉取那一条message queue。
    而每当实例的数量有变更,都会触发一次所有的负载均衡,这时会按照queue的数量平均肥胖queue给每个实例。
    默认分配算法时AllocateMessageQueueAveragely,如下图:
    RocketMQ_第24张图片
    还有另一种平均的算法时AllocateMessageQueueAveragelyByCircle,也是平均分摊每一条queue,只是以环状轮流分queue的形式,如下图:
    RocketMQ_第25张图片
  2. 广播模式
    由于广播模式下要求消息需要投递到一个消费者下面所有的消费者实例,所以也就没有消息被分摊消费的说法。
    在实现上,其中一个不同就是consumer分配queue的时候,所有consumer都分到所有的queueu。
    RocketMQ_第26张图片

消息重试

顺序消息的重试

对于顺序消息,当消费者消费消息失败后,消息队列RocketMQ会自动不断进行消息重试(每次间隔时间为1秒),这时应用会出现消息消费被阻塞的情况。因此,在使用顺序消息时,务必保证,应用能够及时监控并处理消费失败的情况,避免阻塞现象发生。

无序消息的重试

对于无序消息(普通、定时、延迟、事务消息),当消费者消费消息失败时,可以通过设置返回状态达到消息重试的结果。无序消息的重试只针对对轮询消费方式生效,广播方式不提供消息失败重试特性,即消费失败后,失败消息不在重试,继续消费新消息。

重试次数

RocketMQ默认允许每条消息最多重试16次,每次重试时间间隔如下:
RocketMQ_第27张图片
如果消息重试16次仍然失败,消息将不在投递。一条消息无论重试多少次,MessageID不会改变。

配置方式

消费失败后,重试配置方式
集群消费方式下,消息消费失败后期望消息重试,需要在消息监听器接口的实现中明确进行配置(三种方式任选一种):

  • 返回 Action.ReconsumeLater (推荐)
  • 返回 Null
  • 抛出异常
public class MessageListenerImpl implements MessageListener {
    @Override
    public Action consume(Message message, ConsumeContext context) {
        //处理消息
        doConsumeMessage(message);
        //方式1:返回 Action.ReconsumeLater,消息将重试
        return Action.ReconsumeLater;
        //方式2:返回 null,消息将重试
        return null;
        //方式3:直接抛出异常, 消息将重试
        throw new RuntimeException("Consumer Message exceotion");
    }
}

消费失败后,不重试配置方式
集群消费方式下,消息失败后期望消息不重试,需要捕获消费逻辑中可能抛出的异常,最终返回 Action.CommitMessage,此后这条消息将不会再重试。

public class MessageListenerImpl implements MessageListener {
    @Override
    public Action consume(Message message, ConsumeContext context) {
        try {
            doConsumeMessage(message);
        } catch (Throwable e) {
            //捕获消费逻辑中的所有异常,并返回 Action.CommitMessage;
            return Action.CommitMessage;
        }
        //消息处理正常,直接返回 Action.CommitMessage;
        return Action.CommitMessage;
    }
}

自定义消息最大重试次数
消息队列 RocketMQ 允许 Consumer 启动的时候设置最大重试次数,重试时间间隔将按照如下策略:

  • 最大重试次数小于等于 16 次,则重试时间间隔同上表描述。
  • 最大重试次数大于 16 次,超过 16 次的重试时间间隔均为每次 2 小时。
Properties properties = new Properties();
//配置对应 Group ID 的最大消息重试次数为 20 次
properties.put(PropertyKeyConst.MaxReconsumeTimes,"20");
Consumer consumer =ONSFactory.createConsumer(properties);

注意:

  • 消息最大重试次数的设置对相同 Group ID 下的所有 Consumer 实例有效。
  • 如果只对相同 Group ID 下两个 Consumer 实例中的其中一个设置了 MaxReconsumeTimes,那么该配置对两个 Consumer 实例均生效。
  • 配置采用覆盖的方式生效,即最后启动的 Consumer 实例会覆盖之前的启动实例的配置
    获取消息重试次数
    消费者收到消息后,可按照如下方式获取消息的重试次数:
public class MessageListenerImpl implements MessageListener {
    @Override
    public Action consume(Message message, ConsumeContext context) {
        //获取消息的重试次数
        System.out.println(message.getReconsumeTimes());
        return Action.CommitMessage;
    }
}

死信队列

当一条消息初次消费失败,消息队列 RocketMQ 会自动进行消息重试;达到最大重试次数后,若消费依然失败,则表明消费者在正常情况下无法正确地消费该消息,此时,消息队列 RocketMQ 不会立刻将消息丢弃,而是将其发送到该消费者对应的特殊队列中。
在消息队列 RocketMQ 中,这种正常情况下无法被消费的消息称为死信消息(Dead-Letter Message),存储死信消息的特殊队列称为死信队列(Dead-Letter Queue)。

死信特性

死信消息具有以下特性

  • 不会再被消费者正常消费。
  • 有效期与正常消息相同,均为 3 天,3 天后会被自动删除。因此,请在死信消息产生后的 3 天内及时处理。
    死信队列具有以下特性:
  • 一个死信队列对应一个 Group ID, 而不是对应单个消费者实例。
  • 如果一个 Group ID 未产生死信消息,消息队列 RocketMQ 不会为其创建相应的死信队列。
  • 一个死信队列包含了对应 Group ID 产生的所有死信消息,不论该消息属于哪个 Topic。
查看死信信息
  1. 在控制台查询出现死信队列的主题信息
    RocketMQ_第28张图片
  2. 在消息界面根据主题查询死信消息
    RocketMQ_第29张图片
  3. 选择重新发送消息
    一条消息进入死信队列,意味着某些因素导致消费者无法正常消费该消息,因此,通常需要您对其进行特殊处理。排查可疑因素并解决问题后,可以在消息队列 RocketMQ 控制台重新发送该消息,让消费者重新消费一次。
消费幂等

消息队列 RocketMQ 消费者在接收到消息以后,有必要根据业务上的唯一 Key 对消息做幂等处理的必要性。

消费幂等的必要性

在互联网应用中,尤其在网络不稳定的情况下,消息队列 RocketMQ 的消息有可能会出现重复,这个重复简单可以概括为以下情况:

  • 发送时消息重复
    当一条消息已被成功发送到服务端并完成持久化,此时出现了网络闪断或者客户端宕机,导致服务端对客户端应答失败。 如果此时生产者意识到消息发送失败并尝试再次发送消息,消费者后续会收到两条内容相同并且 Message ID 也相同的消息。
  • 投递时消息重复
    消息消费的场景下,消息已投递到消费者并完成业务处理,当客户端给服务端反馈应答的时候网络闪断。 为了保证消息至少被消费一次,消息队列 RocketMQ 的服务端将在网络恢复后再次尝试投递之前已被处理过的消息,消费者后续会收到两条内容相同并且 Message ID 也相同的消息。
  • 负载均衡时消息重复(包括但不限于网络抖动、Broker 重启以及订阅方应用重启)
    当消息队列 RocketMQ 的 Broker 或客户端重启、扩容或缩容时,会触发 Rebalance,此时消费者可能会收到重复消息。
处理方式

因为 Message ID 有可能出现冲突(重复)的情况,所以真正安全的幂等处理,不建议以 Message ID 作为处理依据。 最好的方式是以业务唯一标识作为幂等处理的关键依据,而业务的唯一标识可以通过消息 Key 进行设置:

Message message = new Message();
message.setKey("ORDERID_100");
SendResult sendResult = producer.send(message);

订阅方收到消息时可以根据消息的 Key 进行幂等处理:

consumer.subscribe("ons_test", "*", new MessageListener() {
    public Action consume(Message message, ConsumeContext context) {
        String key = message.getKey()
        // 根据业务唯一标识的 key 做幂等处理
    }
});

你可能感兴趣的:(微服务,分布式事务,java,队列,分布式,java)