消息队列是一种“先进先出”的数据结构。
主要应用场景包括一下3个方面:
1、应用解耦:
系统的耦合性越高,容错性越低。以电商应用为例,用户创建订单后,如果耦合调用库存系统、物流系统、支付系统, 任何一个子系统处理故障或者因为升级等原因暂时不可用,都会造成下单操作异常,影响用户体验。
使用消息队列解耦和,系统的耦合性就会降低。如物流系统发生故障,需要几分钟才能来修复,在这段时间内,物流系统要处理的数据被缓存到消息队列中。用户的下单操作正常完成。当物流系统恢复后,补充处理存在消息队列中的定单消息既可,终端系统感知不到物流系统发生过几分钟故障。
2、流量削峰
应用系统如果遇到系统请求流量瞬间猛增,有可能会将系统压垮。有了消息队列可以将大量的请求缓存起来,分散到很长一段时间去处理,这样可以大大提高系统稳定性和用户体验。
一般情况,为了保证系统的稳定性,如果系统负载超过阈值,就会阻止用户请求,这会影响用户体验,而如果使用消息队列将请求缓存起来,等待系统处理完毕后通知用户下单完毕,这样总比不能下单号。
处于经济考量目的:
业务系统正常时段的QPS如果是1000,流量最高峰事10000,为了应对流量高峰配置高性能的服务器显然不划算,这时可以使用消息队列对峰值流量削峰。
优点:
解耦、削峰、数据分发。
缺点:
系统可用性降低
系统引入外部依赖越多,系统稳定性越差。一旦MQ宕机,就会对业务造成影响。
系统复杂度提高
MQ的加入大大增家了系统复杂度,以前系统间是同步的远程调用,现在是通过MQ进行异步调用。
如何保证消息重复消费?怎么处理消息丢失的情况?怎么保证消息传递的顺序性?
一致性问题
A系统处理完业务,通过MQ给B、C、D三个系统发送消息数据,如果B新系统,C系统处理成功,D系统处理失败。
如何保证数据处理的一致性?
Rocketmq-all-4.7.0
下载地址:http://rocketmq.apache.org/release_notes/
#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
编辑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 "
#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
这种风险较大,一旦Broker重启或宕机时,会导致整个服务不可用。不建议先上环境使用,可以用于本地测试。
一个集群无Slave,全是Master,栗如:2个Master或者3个Master,这种模式的缺点如下:
优点:配置简单,单个Master宕机或重启维护对应应用无影响,在磁盘设置PAID10时,即使机器宕机不可恢复情况下,由于RAID10磁盘非常可口,消息也回不回丢(异步刷盘丢失少量消息,同步刷盘一条不丢),性能最高。
缺点:单台机器宕机事件,这台机器上未被消费的消息在机器恢复之前不可订阅,消息实时性会收到影响。
每个Master配置一个Slave,有多对Master-Slave,HA采用异步复制方式,主备用、有短暂消息延迟(毫秒级),这种模式优缺点如下:
优点:即使磁盘损害,消息丢失的非常少,且消息实时性不会受影响,同时Master宕机后,消费者仍然可以从Slave消费,而且此过程应用透明,不需要人工干预,性能同多Master模式几乎一样。
缺点:Master宕机,磁盘损坏情况下会丢失少量消息。
每个Master配置一个Slave,有多对Master-Slave,HA采用同步双写方式,即只有贮备都写成功,才像应用返回成功,这种模式优缺点如下:
优点:数据与服务都无单节点故障,Master宕机情况下,消息无延迟,服务可用性与数据库可用性都非常高。
缺点:性能比义务复制模式略低(大约第10%左右),发送单个消息的RT会略高,且目前版本在煮节点宕机后,从节点不能自动切换为主机。
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建立连接通道,开始消费消息。
#修改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
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
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
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
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"
启动NameServer
NameServer默认端口9876,因单台阿里云服务器搭建2主2从,需要修改NameServer默认端口号,所有rocketmq实例都新增加内容为:listenPort=9875的namesrv.properties的配置文件。(文件名任意,我使用namesrv.txt也一样验证通过了)
# 第一台实例默认端口号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 &
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
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
有一个对其扩展的开源项目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 &
<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,消息都是有序的
下面用订单进行分区有序的示栗:一个订单的顺序流程是:创建、付款、推送、完成。
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;
}
}
}
下图说明了事务消息的大致方案,其中分为两个流程:正常事务消息的发送及提交、事务消息的补偿流程。
(1) 发送消息(half消息)。
(2) 服务端响应消息写入结果。
(3) 根据发送结果执行本地事务(如果写入失败,此时half消息对业务不可见,本地逻辑不执行)。
(4) 根据本地事务状态执行Commit或者Rollback(Commit操作生成消息索引,消息对消费者可见)
(1) 对没有Commit/Rollback的事务消息(pending状态的消息),从服务端发起一次“回查”
(2) Producer收到回查消息,检查回查消息对应的本地事务的状态
(3) 根据本地事务状态,重新Commit或者Rollback
其中,补偿阶段用于解决消息Commit或者Rollback发生超时或者失败的情况。
事务消息共有三种状态,提交状态、回滚状态、中间状态:
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();
}
}
transactionCheckMax
参数来修改此限制。如果已经检查某条消息超过 N 次的话( N = transactionCheckMax
) 则 Broker 将丢弃此消息,并在默认情况下同时打印错误日志。用户可以通过重写 AbstractTransactionCheckListener
类来修改这个行为。transactionMsgTimeout
参数。分布式队列因为有高可靠性的要求,所以数据要进行持久化存储。
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、最后是从网络驱动的内核态内存复制到网卡中进行传输。
通过使用mmap的方式,可以省去向用户态内存复制,提高速度。这种机制在Java中是通过MappedByteBuffer实现的RocketMQ充分利用了上述特性,也就是所谓的“零拷贝”技术,提高消息存盘和网络发送的速度。
这里需要注意的是,采用MappedByteBuffer这种内存映射的方式有几个限制,其中之一是一次只能映射1.5~2G的文件只用户态的虚拟内存,这也是为何RocketMQ默认设置耽搁CommitLog日志数据文件为1G的原因。
RocketMQ消息的存储是由ConsumeQueue和CommitLog配合完成的,消息真正的物理存储文件是CommitLog,Consumequeue是消息的逻辑队列,类似数据库的索引文件,存储的是指向物理存储的地址。每个Topic下的每个MessageQueue都有一个对应的ComsumerQueue文件。
RocketMQ的消息是存储到磁盘上的,这样既能保证断电后恢复 ,又可以让存储的消息量超出内存限制。rocketMQ为了提高性能,回尽可能低保证磁盘的顺序写。消息通过Producer写入RocketMQ时,有两种写入方式,分布式同步刷盘和异步刷盘。
在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
如果一个Broker组有Master和Slave,消费需要从Master复制到Slave上,有同步和异步两种复制方式
同步复制和异步复制是通过Broker配置文件里的brokerRole参数进行实质的,这个参数可以被设置成ASYNC_MASTER、SYNC_MASTER、SLAVE三种。
实际应用中要结合业务场景,合理设置刷盘方式和主从复制方式,尤其是同步刷盘,由于频繁地出发磁盘写动作,会明显降低性能。通常情况下,应该把Master和Save配置成ASYNC_FLUS的刷盘方式,主从之间配置成SYNC_MASTER的复制方式,这样即使有一台机器出现故障,仍然能够保证数据不丢。
每个实例在发消息的时候,默认会轮询所有的message queue发送,以达到让消息平均落在不同的queue上。而由于queue可以散落在不同的broker,所以消息就发送到不同的broker下,如下图:
对于顺序消息,当消费者消费消息失败后,消息队列RocketMQ会自动不断进行消息重试(每次间隔时间为1秒),这时应用会出现消息消费被阻塞的情况。因此,在使用顺序消息时,务必保证,应用能够及时监控并处理消费失败的情况,避免阻塞现象发生。
对于无序消息(普通、定时、延迟、事务消息),当消费者消费消息失败时,可以通过设置返回状态达到消息重试的结果。无序消息的重试只针对对轮询消费方式生效,广播方式不提供消息失败重试特性,即消费失败后,失败消息不在重试,继续消费新消息。
RocketMQ默认允许每条消息最多重试16次,每次重试时间间隔如下:
如果消息重试16次仍然失败,消息将不在投递。一条消息无论重试多少次,MessageID不会改变。
消费失败后,重试配置方式
集群消费方式下,消息消费失败后期望消息重试,需要在消息监听器接口的实现中明确进行配置(三种方式任选一种):
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 启动的时候设置最大重试次数,重试时间间隔将按照如下策略:
Properties properties = new Properties();
//配置对应 Group ID 的最大消息重试次数为 20 次
properties.put(PropertyKeyConst.MaxReconsumeTimes,"20");
Consumer consumer =ONSFactory.createConsumer(properties);
注意:
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)。
死信消息具有以下特性
消息队列 RocketMQ 消费者在接收到消息以后,有必要根据业务上的唯一 Key 对消息做幂等处理的必要性。
在互联网应用中,尤其在网络不稳定的情况下,消息队列 RocketMQ 的消息有可能会出现重复,这个重复简单可以概括为以下情况:
因为 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 做幂等处理
}
});