分布式消息队列RocketMQ
一 RocketMQ概述
概述
1、MQ简介
MQ,Message Queue,是一种提供消息队列服务的中间件,是一套提供了消息生产、存储、消费全过程API的软件系统。
2、MQ用途
限流削峰
MQ可以将系统的超量请求暂存其中,以便系统后期可以慢慢进行处理,从而避免了请求的丢失或系统被压垮。
异步解耦
上游系统对下游系统的调用若为同步调用,则会大大降低系统的吞吐量和并发度,且系统耦合度太高。而异步调用则会解决这些问题,所以两层之间若要实现由同步到异步的转化,一般性做法就是,在这两层之间添加一个MQ层
数据搜集
分布式系统会产生海量级数据流,如:业务日志、监控数据、用户行为等。针对这些数据流进行实时或批量采集汇总,然后对这些数据流进行大数据分析,这是当前互联网平台的必备技术。通过MQ完成此类数据收集是最好的选择。
3、常见的MQ产品
ActiveMQ
ActiveMQ是使用Java语言开发一款MQ产品。早期很多公司与项目中都在使用。但现在的社区活跃度已经很低。现在的项目中已经很少使用了。
RabbitMQ
RabbitMQ是使用Erlang语言开发的一款MQ产品,其吞吐量较Kafka与RocketMQ要低,且由于其不是Java语言开发,所以公司内部对其实现定制化开发难度较大。
Kafaka
Kafka是使用Scala/Java语言开发的一款MQ产品。其最大的特点就是高吞吐量,常用于大数据领域的实时计算、日志采集等场景。其没有遵循任何常见的MQ协议,而是使用自研协议。对于Spring Cloud Netflix,其仅支持RabbitMQ与Kafka
RocketMQ
RocketMQ是使用Java语言开发的一款MQ产品。经过数年阿里双十一的考验,性能与稳定性非常高。其没有遵循任何常见的MQ协议,而是使用自研协议。Spring Cloud Alibaba,其支持RabbitMQ、Kafka,但提倡使用RocketMQ。
关键词 | ActiveMQ | RabbitMQ | Kafka | RocketMQ |
开发语言 | Java | Erlang | Java | Java |
单机吞吐量 | 万级 | 万级 | 十万级 | 十万级 |
Topic | 百级Topic时会影响系统吞吐量 | 千级Topic时会影响系统吞吐量 | ||
社区活跃度 | 低 | 高 | 高 | 高 |
4、MQ常见的协议
一般情况下MQ的实现是要遵循一些常规性协议的。常见的协议如下:
JMS
JMS,Java Messaging Service (Java消息服务)。是Java平台上有关MOM(Message Oriented Middleware,面向消息的中间件 PO/OO/AO)的技术规范,它便于消息系统中的Java应用程序进行消息交换,并且通过提供标准的产生、发送、接收消息的接口,简化企业应用的开发。ActiveMQ是该协议的典型实现。
STOMP
STOMP,Streaming Text Orientated Message Protocol(面向流文本的消息协议),是一种MOM设计的简单文本协议。STOMP提供一个可互操作的连接格式,允许客户端与人意STOMP消息代理(Broker)进行交互。ActiveMQ是该协议的典型实现,RabbitMQ通过插件可以支持该协议。
AMQP
AMQP,Advanced Message Queuing Protocol(高级消息队列协议),一个提供统一消息服务的应用层标准,是应用层协议的一个开放标准,是一种MOM设计。基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件不同产品,不同开发语言等条件的限制。RabbitMQ是该协议的典型实现。
MQTT
MQTT,Message Queuing Telemetry Transport(消息队列遥测传输),是IBM开发的一个即时通信协议,是一种二进制协议,主要用于服务器和低功耗IoT设备间的通信。该协议支持所以平台,几乎可以把所有联网物品和外部连接起来,,被用于当作传感器和制动器的通信吸引。RabbitMQ通过插件可以支持该协议。
二、RocketMQ概述
1、RocketMQ简介
RocketMQ是一个统一消息引擎、轻量级数据处理平台
RocketMQ是一款阿里巴巴开源的消息中间件。
2、RocketMQ发展历程
2007年,阿里开始五彩石项目,Notify作为项目中交易核心消息流转系统,应运而生。Notify系统是RocketMQ的雏形。
2010年,B2B大规模使用ActiveMQ作为阿里的消息内核,阿里急需一个具有‘海量堆积能力’的消息系统。
2011年,Kafka开源。淘宝中间件团队在对Kafka进行了深入研究后,开发了一款新的MQ,MetaQ。
2012年,MetaQ发展到了v3.0,在它基础上进行了进一步的抽象,形成了RocketMQ,然后就将其进行了开源。
2015年,阿里在RocketMQ的基础上,又推出了一款专门针对阿里云上用户的消息系统AliwareMQ。
2016年双十一,RocketMQ承载了万亿级消息的流转,跨越了一个新的里程碑。11月28日阿里巴巴向Apache软件基金会捐赠RocketMQ,成为Apache孵化项目。
2017年9月25日,Apache宣布RocketMQ孵化成为Apache顶级项目(TLP),成为国内首个互联网中间件在Apache上的顶级项目。
二 RocketMQ的安装与启动
一、基本概念
1 消息(Message)
消息是指,消息系统所传输信息的物理载体,生产和消费数据的最小单位,每条消息必须属于一个主题。
2 主题(Topic)
Topic表示一类消息的集合,每个主题包含若干条消息,每条消息只能属于一个主题,是RocketMQ进行消息订阅的基本单位。 Topic:Message 1:n | Message:Topic 1:1
一个生产者可以同时发送多种Topic的消息;而一个消费者只对某种特定的Topic感兴趣,即只可以订阅和消费一种Topic的消息。 Producer:Topic 1:n | Consumer:Topic 1:1
3 标签(Tag)
为消息设置的标签,用于同一主题下区分不同类型的消息。来自同一业务单元的消息,可以根据不同业务目的在同一主题下设置不同标签。标签能够有效的保持代码的清晰度和连贯性,并优化RocketMQ提供的查询系统。消费者可以根据Tag实现对不同子主题的不同消费逻辑,实现更好的扩展性。
4 队列(Queue)
存储消息的物理实体。一个Topic中可以包含多个Queue,每个Queue中存放的就是该Topic的消息。一个Topic的Queue也被称为一个Topic中消息的分区(Partition)
一个Topic的Queue中的消息,只能被一个消费组中的一个消费者消费。一个Queue中的消息不允许同一个消费者组中的多个消费者同时消费。
在学习参考其他相关资料时,还会看到一个概念:分片(Sharding)。分片不同于分区。在RocketMQ中,分片指的是存放相应Topic的Broker的数量。每个分片中会创建出相应数量的分区,即Queue,每个Queue的大小都是相同的。
5 消息标识(MessageId/Key)
RocketMQ中的每个消息拥有唯一的MessageId,且可以携带具有业务标识的Key,以便对消息的查询。不过需要注意的是,MessageId有两个:在生产者send()消息时会自动生成一个MessageId(msgId),当消息到达Broker后,Broker也会自动生成一个MessageId(offsetMsgId)。msgId、OffsetMsgId与key都被称为消息标识。
·msgId:由producer端生成,其生成规则为:
ProducerIp+进程pid+MessageClientIDSetter类的ClassLoader的hashCode+当前是时间+AutomicInteger自增计数器
·offetMsgId:由broker端生成,其生成规则为:brokerIp+物理分区的offset
·key:由用户指定的业务相关的唯一标识
二、系统架构
RocketMQ架构上主要分为四个部分:
1 Producer
消息生产者,负责生产消息。Producer通过MQ的负载均衡模块选择相应的Broker集群队列进行消息投递,投递的过程支持快速失败并且低延迟。
例如,业务系统产生的日志写入到MQ的过程,就是消息生产的过程
再如,电商平台中用户提交的秒杀请求写入到MQ的过程,就是消息生产的过程
RocketMQ中的消息生产者都是以生产者组(Producer Group)的形式出现的。生产者组都是同一类生产者的集合,这类Producer发送相同Topic类型的消息。一个生产者组可以同时发送多个主题的消息。
2 Consumer
消息消费者,负责消费消息。一个消息消费者会从Broker服务器中获取到消息,并对消息进行相关业务员处理。
例如,QoS系统从MQ中读取日志,并对日志进行解析处理的过程就是消息消费的过程
再如,电商平台的业务系统从MQ中读取到秒杀请求,并对请求进行处理的过程就是消息消费的过程。
RocketMQ中的消息消费者都是以消费者组(Consumer Group)的形式出现的。消费者组是同一类消费者的集合,这类Consumer消费的是同一个Topic类型的消息。消费者组使得在消息消费方面,实现负载均衡(将一个Topic中的不同的Queue平均分配给同一个Consumer组的不同的Consumer,注意,并不是将消息负载均衡)和容错(一个Consumer挂了,该Consumer Group中的其他Consumer可以接着进行消费原Consumer消费的Queue)的目标变得非常容易。
消费者组中Consumer的数量应该小于等于订阅Topic的Queue数量。如果超出Queue数量,则多出的Consumer将不能消费消息
不过,一个Topic类型的消息可以被多个消费者组同时消费。
注意,
1、消费者组只能消费一个Topic消息,不能消费多个Topic消息
2、一个消费者组中的消费者必须订阅完全相同的Topic
3 Name Server
功能介绍
NameServer是一个Broker与Topic路由的注册中心,支持Broker的动态注册和发现。
RocketMQ的思想来自于Kafka,而Kafka是依赖了Zookeeper的。所以在RocketMQ的早期版本,即在MetaQ v1.0与v2.0版本中,也是依赖于Zookeeper。从MetaQ v3.0,即RocketMQ开始去掉了Zookeeper依赖,使用了自己的NameServer。
主要包括两个功能:
·Broker管理:接受Broker集群的注册信息并且保存下来作为路由信息的基本数据;提供心跳检测机制,检查Broker是否还存活。
·路由信息管理:每个NameServer中都保存着Broker集群的整个路由信息和用于客户端查询的队列信息。Producer和Consumer通过NameServer可以获取整个Broker集群的路由信息,从而进行信息对的投递和消费。
路由注册
NameServer通常也是以集群的方式部署,不过,NameServer是无状态的,即NameServer集群中的各个节点间是无差异的,各节点间相互不进行信息通讯。那各节点中的数据是如何进行数据同步的呢?在Broker节点启动时,轮询NameServer列表,与每个NameServer节点建立长连接,发起注册请求。在NameServer内部维护着一个Broker列表,用来动态存储Broker的信息。
注意,这是与其他像zk、Eureka、Nacos等注册中心不同的地方
这种NameServer的无状态方式,有什么优缺点:
优点:NameServer集群搭建简单。
缺点:对于Broker,必须明确指出所以NameServer地址,否则未指出的将不会去注册。也正因为如此,NameServer并不能随便扩容。因为,若Broker不重新配置,新增的NameServer对于Broker来说是不可见的,其不会向这个NameServer进行注册。
Broker节点为了证明自己是活着的,为了维护与NameServer间的长连接,会将最新的消息以心跳包的方式上报给NameServer,每30s发送一次心跳。心跳包中包含BrokerId、Broker地址(IP+Port)、Broker名称、Broker所属集群名称等待。NameServer在接收到心跳包后,会更新心跳时间戳,记录这个Broker的最新存活时间。
路由剔除
由于Broker关机、宕机或网络都懂等原因,NameServer没有收到Broker的心跳,NameServer可能会将其从Broker列表中剔除。
扩展:对于RocketMQ日常运维工作,例如Broker升级,需要停掉Broker的工作。OP需要怎么做?
OP需要将Broker的读写权限禁掉。一旦client(Consumer或Producer)向Broker发送请求,都会收到Broker的NO_PERMISSION响应,然后Client会进行对其他Broker的重试
当OP观察到这个Broker没有流量会,再关闭它,实现Broker从NameServer的移除
OP:运维工程师
SRE:Site Reliability Engineer 现场可靠性工程师
NameServer中有一个定时任务,每隔10s就会扫描一次Broker表,查看每一个Broker的最新心跳时间戳距离当前时间是否超过120s,如果超过,则会判定Broker失效,然后将其从Broker列表中剔除。
路由发现
RocketMQ的路由发现采用的是Pull模型。当Topic路由信息出现变化时,NameServer不会主动推送给客户端,而是客户端定时拉取主题最新的路由。默认客户端每30s会拉取一次最新的路由。
扩展
1、Push模型:推送模型。其实时性较好,是一个“发布-订阅”模型,其需要维护一个长连接。而长连接的维护是需要资源成本的。该模型适合于的场景:
·实时性要求较高
·Client数量不多,Server数据变化较频繁。
2、Pull模型:拉取模型。存在问题:实时性较差。
3、Long Polling模型:长轮询模型。其实际上是对Push与Pull模型的整合,充分利用了这两种模型的优势。
客户端NameServer选择策略
这里的客户端指的是Producer与Consumer
客户端在配置时必须要写上NameServer集群的地址,那么客户端到底连接的是哪个NameServer节点呢?
客户端首先会生成一个随机数,然后再与NameServer节点数量取模,此时得到的就是所要连接的节点索引,然后就会进行连接。如果连接失败,则会采用round-robin策略,逐个尝试着去连接其他节点。
首先采用的是随机策略进行的选择,失败后采用的是轮询策略。、
扩展:Zookeeper Client是如何选择Zookeeper Server的?
简单来说,经过两次Shuffle,然后选择第一台Zookeeper Server。
详细来说就是,将配置文件中的zk server地址进行第一次shuffle,然后随机选择一个。这个选择出的一般都是一个hostname。然后获取到该hostname对应的所有ip,再对这些ip进行第二次shuffle,shuffle过的结果中取第一个server地址进行连接。
4 Broker
功能介绍
Broker充当着消息中转角色,负责存储消息、转发消息。Broker在RocketMQ系统中负责接收并存储从生产者发送来的消息,同时为消费者的拉取请求作准备。Broker同时也存储者消息相关的元数据,包括消费者组消费进度偏移offset、主题、队列等。
Kafka 0.8版本之后,offset是存放在Broker中的,之前版本是存放在Zookeeper中的。
模块构成
下图为Broker Server的功能模块示意图。
Remoting Module:整个Broker的实体,负责处理来自Client端的请求。而这个Broker实体则由以下模块构成。
Client Manager:客户端管理器。负责接收、解析客户端(Producer/Consumer)请求,管理客户端。例如,维护Consumer的Topic订阅信息
Store Service:存储服务。提供方便简单的API接口,处理消息存储到物理硬盘和消息查询功能。
HA Service:高可用服务,提供Master Broker和Slave Broker之间的数据同步功能。
Index Service:索引服务。根据特定的Message Key,对投递到Broker的消息进行索引服务,同时也提供根据Message Key对消息进行快速查询的功能。
集群部署
为了增强Broker性能和吞吐量,Broker一般都是以集群形式出现的。各集群节点中可能存放着相同的topic的不同Queue。不过,这里有一个问题,如果某Broker节点宕机,如何保证数据不丢失呢?其解决方案是,将每个Broker集群节点进行横向扩展,即将Broker节点再建为一个HA集群,解决单点问题。
Broker节点集群是一个主从集群,即集群中具有Master和Slave两种角色。Master负责处理读写操作请求,而Slave仅负责读操作请求。当Master挂掉了,SLave则会自动切换为Master去工作。所以这个Broker集群是主备集群。一个Master可以包含多个Slave,但一个Slave只能隶属于一个Master。Master与Slave的对应关系是通过指定相同的BrokerName、不同的BrokerId来确定的。BrokerId为0表示Master,非0表示Slave。每个Broker与NameServer集群中的所有节点建立长连接,定点注册Topic信息到所有的NameServer。
5 工作流程
具体流程
1)启动NameServer,NameServer启动后开始监听端口,等待Broker、Producer、Consumer连接。
2)启动Broker时,Broker会与所有的NameServer建立并保持长连接,然后每30秒向NameServer定时发送心跳包。
3)收发消息前,可以先创建Topic,创建Topic时需要指定该Topic要存储在哪些Broker上,当然,在创建Topic时也会将Topic与Broker的关系写入到NameServer中。不过,这步时可选的,也可以在发送消息时自动创建topic。
4)Producer发送消息,启动时先跟NameServer集群中的其中一台建立长连接,并从NameServer中获取路由信息,即当前发送的Topic的Queue与Broker地址(IP+Port)的映射关系。然后根据算法策略从队选择一个Queue,与队列所在的Broker建立长连接从而向Broker发消息。当然,在获取到路由消息之后,Producer会首先将路由信息缓存到本地,再每30s从NamerServer更新一次路由信息。
5)Consumer跟Producer类似,跟其中一台NameServer建立长连接,获取其所订阅Topic的路由信息,然后根据算法策略从路由信息中获取到其所要消费的Queue,然后直接跟Broker建立长连接,开始消费其中的消息。Consumer在获取到路由消息之后,同样也会每30s从NameServer更新一次路由信息。不过不同于Producer的是,Consumer还会向Broker发送心跳,以确保Broker的存活状态。
Topic的创建模式
Topic的创建有两种模式:
·集群模式:该模式下创建的Topic在该集群中,所有Broker中的Queue数量时相同的。
·Broker模式:该模式下创建的Topic在该集群中,每个Broker中的Queue数量可以不同。
自动创建Topic时,默认采用的是Broker模式,会为每个Broker默认创建4个Queue。
读/写队列
从物理上来讲,读/写队列是同一个队列。所以,不存在读/写队列数据同步问题。读/写队列是逻辑上进行区分的概念。一般情况下,读/写队列数量是相同的。
例如,创建Topic时设置的的写队列数量为8,读队列数量为4,此时系统会创建8个Queue,分别是0 1 2 3 4 5 6 7。Producer会将消息写入到这8个队列,到哪Consumer只会消费0 1 2 3这4个队列中的消息,4 5 6 7中的消息是不会被消费到的。
再如,创建Topic时设置的写队列数量为4,读队列数量为8,此时系统会创建8个Queue,分别是0 1 2 3 4 5 6 7。Producer会将消息写入到0 1 2 3这四个队列,但Consumer只会消费0 1 2 3 4 5 6 7这8个队列中的消息,但是4 5 6 7中是没有消息的。此时假设Consumer Group中包含两个Consuer,Consumer1消费0 1 2 3,而Consumer2消费4 5 6 7.但实际情况是。Consumer2是没有消息消费的。
也就是说,当读/写队列数量设置不同时,总是有问题的。那么,为什么要这样设计呢?
其这样设计的目的是为了,方便Topic中的Queue缩容。
例如,原来创建的Topic中包含16个Queue,如何能够使其Queue缩容为8个,还不会丢失消息?可以动态修改写队列数量为8,读队列数量不变。此时新的消息只能写入到前8个队列,而消费者消费的却是16个队列中的数据。当发现后8个Queue中的消息消费完毕后,就可以再将读队列数量动态设置为8.整个缩容过程,没有丢失任何消息。
perm用于设置对当前创建Topic的操作权限:2表示只写,4表示只读,6表示读写
三、单机安装与启动(docker)
1.创建namesrv服务
docker pull rocketmqinc/rocketmq
创建namesrv数据存储路径
mkdir -p /docker/rocketmq/data/namesrv/logs /docker/rocketmq/data/namesrv/store
构建namesrv容器
docker run -d \
--restart=always \
--name rmqnamesrv \
-p 9876:9876 \
-v /docker/rocketmq/data/namesrv/logs:/root/logs \
-v /docker/rocketmq/data/namesrv/store:/root/store \
-e "MAX_POSSIBLE_HEAP=100000000" \
rocketmqinc/rocketmq \
sh mqnamesrv
2.创建broker节点
创建broker数据存储路径
mkdir -p /docker/rocketmq/data/broker/logs /docker/rocketmq/data/broker/store /docker/rocketmq/conf
创建配置文件
vi /docker/rocketmq/conf/broker.conf
# 所属集群名称,如果节点较多可以配置多个
brokerClusterName = DefaultCluster
#broker名称,master和slave使用相同的名称,表明他们的主从关系
brokerName = broker-a
#0表示Master,大于0表示不同的slave
brokerId = 0
#表示几点做消息删除动作,默认是凌晨4点
deleteWhen = 04
#在磁盘上保留消息的时长,单位是小时
fileReservedTime = 48
#有三个值:SYNC_MASTER,ASYNC_MASTER,SLAVE;同步和异步表示Master和Slave之间同步数据的机制;
brokerRole = ASYNC_MASTER
#刷盘策略,取值为:ASYNC_FLUSH,SYNC_FLUSH表示同步刷盘和异步刷盘;SYNC_FLUSH消息写入磁盘后才返回成功状态,ASYNC_FLUSH不需要;
flushDiskType = ASYNC_FLUSH
# 设置broker节点所在服务器的ip地址
brokerIP1 = 192.168.2.10
构建broker容器
docker run -d \
--restart=always \
--name rmqbroker \
--link rmqnamesrv:namesrv \
-p 10911:10911 \
-p 10909:10909 \
-v /docker/rocketmq/data/broker/logs:/root/logs \
-v /docker/rocketmq/data/broker/store:/root/store \
-v /docker/rocketmq/conf/broker.conf:/opt/rocketmq-4.4.0/conf/broker.conf \
-e "NAMESRV_ADDR=namesrv:9876" \
-e "MAX_POSSIBLE_HEAP=200000000" \
rocketmqinc/rocketmq \
sh mqbroker -c /opt/rocketmq-4.4.0/conf/broker.conf
.png)]
3.创建rockermq-console服务
拉取镜像
docker pull pangliang/rocketmq-console-ng
构建rockermq-console容器
docker run -d \
--restart=always \
--name rmqadmin \
-e "JAVA_OPTS=-Drocketmq.namesrv.addr=192.168.2.10:9876 \
-Dcom.rocketmq.sendMessageWithVIPChannel=false" \
-p 9999:8080 \
pangliang/rocketmq-console-ng
问题解决
需要关闭防火墙或者开放namesrv和broker端口
异常信息:
org.apache.rocketmq.remoting.exception.RemotingConnectException: connect to failed
关闭防火墙
systemctl stop firewalld.service
开放端口
firewall-cmd --permanent --zone=public --add-port=9876/tcp
firewall-cmd --permanent --zone=public --add-port=10911/tcp
# 即可生效
firewall-cmd --reload
三 RocketMQ普通消息的发送
同步发送
同步发送:producer.send()后,需要等待发送返回结果,才能进行下一条消息的发送。会阻塞发送消息的线程一般适用于需要确保消息发送成功的场景(重要的消息通知、短信通知、物流信息通知等)
可靠
/**
* 同步发送
*/
public class SyncProducer {
public static void main(String[] args) throws MQClientException, InterruptedException {
// 创建一个Producer实例
DefaultMQProducer producer = new DefaultMQProducer("group_test");
// 设置NameServer地址
producer.setNamesrvAddr("127.0.0.1:9876");
// 启动Producer实例
producer.start();
for (int i = 0; i < 10; i++) {
try {
// 创建消息:指定topic,tag,消息体
Message msg = new Message("TopicTest", // Topic (衣服)
"TagA", // Tag 相当于二级目录 (男装/女装)
("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) // 消息体 发送的消息都是字节数组
);
// 发送消息(同步方式)
SendResult sendResult = producer.send(msg);
System.out.printf("%s%n", sendResult);
} catch (Exception e) {
e.printStackTrace();
Thread.sleep(1000);
}
}
// 如果不再发送消息,关闭Producer实例
producer.shutdown();
}
}
追溯 producer.send(msg)方法
异步发送
异步发送:producer.send(msg, new SendCallback(){ … }) ,SendCallback接收异步返回结果的回调。不会阻塞发送消息的线程一般适用于消息量大,对响应时间比较敏感的场景。不能容忍长时间阻塞等待broker的响应
可靠
for (int i = 0; i < 10; i++) {
// 创建消息:指定topic,tag,消息体
final int index = i;
Message msg = new Message("TopicTest", "TagA", ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET));
// 发送消息(异步方式 SendCallback接收异步返回结果的回调)
producer.send(msg, new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
System.out.printf("%s%n", sendResult);
}
@Override
public void onException(Throwable e) {
System.out.printf("%-10d Exception %s %n", index, e);
}
});
}
单向发送
只发送消息,不需要得到mq的确认,不关心是否发送成功,不需要获取发送后的响应。这种发送方式是不可靠的,但是速度是最快的。适合一些耗时短,对可靠性要求不是很高的场景(日志消息的记录)
不可靠
// 发送消息(单向发送)
producer.sendOneway(msg);
四 RocketMQ普通消息的消费
集群消费
消费组中的consumer均摊消费消息,每条消息只会被消费组中一个实例消费
集群消费也是一般场景下默认的消费模式,消息只会被消费一次
消息的消费进度,是在mq服务端维护的,可靠性比较高
public class BalanceConsumer {
public static void main(String[] args) throws MQClientException {
// 实例化消息消费者,指定组名
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group_consumer");
// 指定NameServer地址
consumer.setNamesrvAddr("127.0.0.1:9876");
// 订阅topic
consumer.subscribe("TopicTest", "*");
// 设置消费模式 => 集群消费 负载均衡模式
consumer.setMessageModel(MessageModel.CLUSTERING);
// 注册回调函数,处理消息
consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs);
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});
// 启动消费者实例
consumer.start();
System.out.printf("Consumer Started.%n");
}
}
广播消费
即消费组中每个实例都会拿到每一条消息,进行消费。消息会重复消费
消息消费进度的维护不在mq服务端,在consumer消费组端。不能处理消息的顺序消费
// 设置消费模式 => 广播消费
consumer.setMessageModel(MessageModel.BROADCASTING);
五 特殊消息的发送和消费
顺序消息的生产和消费
全局顺序消息
一个生产者,一个消费组,rocketmq的topic中只定义一个messageQueue
部分顺序消息
topic中有多个messageQueue,将顺序发送的消息进行标记,将标记同种颜色的消息顺序放入到对应的队列中,然后指定的消费者去订阅对应的队列,那么获取到的消息也是顺序的
生产消息时:
根据不同的消息id对消息队列数目进行取余运算。实现根据消息id选择投送消息的queueproducer.send(msg, new MessageQueueSelector{ … }),用到了消息队列选择器
package com.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.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.exception.RemotingException;
import java.util.ArrayList;
import java.util.List;
public class ProducerInOrder {
public static void main(String[] args) throws MQClientException, RemotingException, InterruptedException, MQBrokerException {
DefaultMQProducer producer = new DefaultMQProducer("OrderProducer");
producer.setNamesrvAddr("127.0.0.1:9876");
producer.start();
// 订单列表
List<Order> orderList = new ProducerInOrder().buildOrders();
for (int i = 0; i < orderList.size(); i++) {
String body = orderList.get(i).toString();
Message msg = new Message("PartOrder", null, "KEY" + i, body.getBytes());
SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
int id = (int) arg; // 根据订单id选择发送的queue
long index = id % mqs.size();
return mqs.get((int) index);
}
}, orderList.get(i).getOrderId()); // 订单id
System.out.println(String.format("SendResult status:%s, queueId:%d, body:%s",
sendResult.getSendStatus(),
sendResult.getMessageQueue().getQueueId(),
body));
}
producer.shutdown();
}
class Order{
private int orderId;
private String desc;
public int getOrderId() {
return orderId;
}
public void setOrderId(int orderId) {
this.orderId = orderId;
}
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
@Override
public String toString() {
return "Order{" +
"orderId=" + orderId +
", desc='" + desc + '\'' +
'}';
}
}
// 模拟生成订单数据 3个订单,每个订单4个状态
// 每个订单 创建->付款->推送->完成
private List<Order> buildOrders(){
List<Order> orderList = new ArrayList<>();
Order orderDemo = new Order();
orderDemo.setOrderId(001);
orderDemo.setDesc("创建");
orderList.add(orderDemo);
orderDemo = new Order();
orderDemo.setOrderId(002);
orderDemo.setDesc("创建");
orderList.add(orderDemo);
orderDemo = new Order();
orderDemo.setOrderId(001);
orderDemo.setDesc("付款");
orderList.add(orderDemo);
orderDemo = new Order();
orderDemo.setOrderId(003);
orderDemo.setDesc("创建");
orderList.add(orderDemo);
orderDemo = new Order();
orderDemo.setOrderId(002);
orderDemo.setDesc("付款");
orderList.add(orderDemo);
orderDemo = new Order();
orderDemo.setOrderId(003);
orderDemo.setDesc("付款");
orderList.add(orderDemo);
orderDemo = new Order();
orderDemo.setOrderId(002);
orderDemo.setDesc("推送");
orderList.add(orderDemo);
orderDemo = new Order();
orderDemo.setOrderId(003);
orderDemo.setDesc("推送");
orderList.add(orderDemo);
orderDemo = new Order();
orderDemo.setOrderId(002);
orderDemo.setDesc("完成");
orderList.add(orderDemo);
orderDemo = new Order();
orderDemo.setOrderId(001);
orderDemo.setDesc("推送");
orderList.add(orderDemo);
orderDemo = new Order();
orderDemo.setOrderId(001);
orderDemo.setDesc("完成");
orderList.add(orderDemo);
orderDemo = new Order();
orderDemo.setOrderId(003);
orderDemo.setDesc("完成");
orderList.add(orderDemo);
return orderList;
}
}
消费消息时
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET); // 设置从最后的偏移位置消费
consumer.registerMessageListener(new MessageListenerOrderly() { … } ) ,使用到了顺序消息监听器,实现顺序接收消息,一个queue对应一个线程进行操作
A MessageListenerOrderly object is used to receive messages orderly. One queue by one thread
package com.ordermessage;
import org.apache.commons.lang3.RandomUtils;
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.Random;
import java.util.concurrent.TimeUnit;
public class ConsumerInOrder {
public static void main(String[] args) throws MQClientException {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("OrderConsumer");
consumer.setNamesrvAddr("119.23.143.89:9876");
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);
consumer.subscribe("PartOrder", "*");
consumer.registerMessageListener(new MessageListenerOrderly() {
Random random = new Random();
@Override
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
context.setAutoCommit(true);
for(MessageExt msg: msgs){
// 可以看到每个queue有唯一的consume线程来消费,订单对每个queue(分区)有序
System.out.println("consumeThread="+Thread.currentThread().getName()+" , queueId="+msg.getQueueId()
+ " , content="+new String(msg.getBody()));
}
try{
// 模拟业务逻辑处理中...
TimeUnit.MILLISECONDS.sleep(random.nextInt(300));
}catch (Exception e){
e.printStackTrace();
// 这里要注意:意思是先等一会儿,一会儿再处理这批消息,而不是放到重试队列中。
// 直接放入重试队列,会导致消息的顺序性被破坏
return ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT;
}
return ConsumeOrderlyStatus.SUCCESS;
}
});
consumer.start();
System.out.println("Consumer Started.");
}
}
生产端打印:
消费端打印:
延时消息的生产与消费
延时消息:指生产者将消息投递给rocketmq,并不期望消息立马投递给消费者。而是希望延时一段时间,再投递给消费者
延时消息有很多应用场景(比如:购买电影票,选好座位后,需要发送一个延时通知。避免过长时间,用户选了座位,但是未支付。就需要通知用户进行支付处理。如果用户已经支付了,就可以清除消息;电商交易系统的订单超时未支付,自动取消订单)
生产端:
包装好消息后,通过message.setDelayTimeLevel(4); 给消息设置延时等级
delayTimeLevel: (1-18个等级) “ 1s 5s 10s 30s 1min 2min 3min 4min 5min 6min 7min 8min 9min 10min 20min 30min 1h 2h ”
level有以下三种情况:
level == 0,消息为非延迟消息
1<=level<=maxLevel,消息延迟特定时间,例如level==1,延迟1s
level > maxLevel,则level== maxLevel,例如level==20,延迟2h
生产端:
package com.scheduled;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.common.message.Message;
public class ScheduledMessageProducer {
public static void main(String[] args) throws Exception {
// 初始化producer实例
DefaultMQProducer producer = new DefaultMQProducer("ScheduledProducer");
// 设置namserver地址
producer.setNamesrvAddr("119.23.143.89:9876");
// 启动producer实例
producer.start();
int totalMessageToSend = 10;
for (int i = 0; i < totalMessageToSend ; i++) {
// 包装消息
Message message = new Message("ScheduledTopic", ("Hello scheduled message " + i).getBytes());
// 设置延时等级为4,这个消息将在30s之后投递给消费者
// delayTimeLevel: (1-18个等级) “ 1s 5s 10s 30s 1min 2min 3min 4min 5min 6min 7min 8min 9min 10min 20min 30min 1h 2h ”
message.setDelayTimeLevel(4);
// 发送消息
producer.send(message);
}
// 关闭producer实例
producer.shutdown();
}
}
消费端:
package com.scheduled;
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;
public class ScheduledMessageConsumer {
public static void main(String[] args) throws MQClientException {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("ScheduledConsumer");
consumer.setNamesrvAddr("119.23.143.89:9876");
consumer.subscribe("ScheduledTopic","*");
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> messages, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
for (MessageExt message:messages) {
System.out.println("Receive message[msgId="+message.getMsgId()+"]"+
(message.getStoreTimestamp()-message.getBornTimestamp())+"ms later");
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
}
}
批量消息的生产与消费
批量消息的出现是因为,原来发送消息都是一条一条的发送,那么在大量消息发送的场景下,就容易出现性能瓶颈。所以,可以将一批消息打成一个包,做批量发送,可以显著提升发送消息的性能
批量消息的生产:
生产者:
package com.batch;
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.common.message.Message;
import java.util.ArrayList;
import java.util.List;
public class BatchProducer {
public static void main(String[] args) throws MQClientException {
DefaultMQProducer producer = new DefaultMQProducer("BantchProducer");
producer.setNamesrvAddr("119.23.143.89:9876");
producer.start();
String topic = "BatchTest";
List<Message> messages = new ArrayList<>();
messages.add(new Message(topic,"Tag","OrderID001","Hello World 1".getBytes()));
messages.add(new Message(topic,"Tag","OrderID001","Hello World 2".getBytes()));
messages.add(new Message(topic,"Tag","OrderID001","Hello World 3".getBytes()));
messages.add(new Message(topic,"Tag","OrderID001","Hello World 4".getBytes()));
messages.add(new Message(topic,"Tag","OrderID001","Hello World 5".getBytes()));
messages.add(new Message(topic,"Tag","OrderID001","Hello World 6".getBytes()));
try{
producer.send(messages);
}
catch (Exception e)
{
producer.shutdown();
e.printStackTrace();
}
// 如果不再发送消息,关闭Producer实例。
producer.shutdown();
}
}
消费者:
package com.batch;
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.common.protocol.heartbeat.MessageModel;
import java.util.List;
public class BatchConsumer {
public static void main(String[] args) throws MQClientException {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("BatchConsumer");
consumer.setNamesrvAddr("119.23.143.89:9876");
consumer.subscribe("BatchTest","*");
consumer.setMessageModel(MessageModel.CLUSTERING);
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
System.out.printf("%s Receive New Message : %s %n",Thread.currentThread().getName(),msgs);
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
// 启动消费者
consumer.start();
System.out.println("Consumer Started.%n");
}
}
ps:
单批次的消息,不能超过4MB。如果超过了4MB,rocketmq就会出现性能瓶颈
那么当出现超过4MB的大消息时,需要进行切分。切割成不超过4MB的块,再进行批量发送
工具类:
package com.batch;
import org.apache.rocketmq.common.message.Message;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
public class ListSplitter implements Iterable<List<Message>> {
private int sizeLimit=1000*1000;
private final List<Message> messages;
private int currIndex;
public ListSplitter(List<Message> messages)
{
this.messages=messages;
}
public boolean hasNext()
{
return currIndex<messages.size();
}
@Override
public Iterator<List<Message>> iterator() {
return null;
}
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>sizeLimit)
{
if (nextIndex-currIndex==0) //单个消息超过了最大限制(1M),否则会阻塞线程
{
nextIndex++; // 假如下一个子列表没有元素,则添加这个子列表然后推出循环,否则退出循环
}
break;
}
if (tmpSize+totalSize>sizeLimit)
{
break;
}
if (tmpSize+totalSize>sizeLimit)
{
break;
}
else {totalSize+=tmpSize;}
}
List<Message> subList= messages.subList(currIndex,nextIndex);
currIndex = nextIndex;
return subList;
}
}
Producer:
package com.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.common.message.Message;
import org.apache.rocketmq.remoting.exception.RemotingException;
import java.util.ArrayList;
import java.util.List;
public class SplitBatchProducer {
public static void main(String[] args) throws MQBrokerException, RemotingException, InterruptedException, MQClientException {
DefaultMQProducer producer = new DefaultMQProducer("BatchProducer1");
producer.setNamesrvAddr("119.23.143.89:9876");
producer.start();
String topic = "BatchTest1";
//使用List组装
List<Message> messages = new ArrayList<>(100*1000);
// 10万元素的数组
for (int i = 0; i < 10*10000; i++) {
messages.add(new Message(topic,"Tag","OrderID"+i,("Hello World"+i).getBytes()));
}
// 把大的消息分裂成若干个小的消息(1M左右)
ListSplitter splitter = new ListSplitter(messages);
while (splitter.hasNext())
{
List<Message> listItem = splitter.next();
producer.send(listItem);
Thread.sleep(100);
}
// 如果不再发送消息,关闭Producer实例
producer.shutdown();
System.out.println("Consumer Started.%n");
}
}
过滤消息的生产与消费
1. Tag过滤
producer创建消息的时候,里面有一个tag的参数
2. SQL过滤
producer通过 msg.putUserProperty(“a”, String.valueOf(i)); 给消息设置用于sql过滤的属性
ps:如果涉及到sql 则需要在broker.conf里添加
enablePropertyFilter=true
package com.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.common.RemotingHelper;
import org.apache.rocketmq.remoting.exception.RemotingException;
import java.io.UnsupportedEncodingException;
public class TagFilterProducer {
public static void main(String[] args) throws UnsupportedEncodingException, MQClientException, MQBrokerException, RemotingException, InterruptedException {
DefaultMQProducer producer = new DefaultMQProducer("TagFilterProducer");
producer.setNamesrvAddr("119.23.143.89:9876");
producer.start();
// todo 设定三种标签
String[] tags = new String[]{"TagA","TagB","TagC"};
for (int i = 0; i < 3; i++) {
Message msg = new Message("TagFilterTest",
tags[i%tags.length],
"hello,world".getBytes(RemotingHelper.DEFAULT_CHARSET));
SendResult sendResult = producer.send(msg);
System.out.printf("%s%n",sendResult);
}
producer.shutdown();
}
}
consumer通过 MessageSelector.bySql(“(TAGS is not null and TAGS in (‘TagA’, ‘TagB’)) and (a is not null and a between 0 and 3)”) ,以sql的方式进行消息的过滤筛选
package com.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 org.apache.rocketmq.common.protocol.heartbeat.MessageModel;
import org.apache.rocketmq.remoting.common.RemotingHelper;
import java.util.List;
public class TagFilterConsumer {
public static void main(String[] args) throws MQClientException {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("TagFilterConsumer");
consumer.setNamesrvAddr("119.23.143.89:9876");
consumer.subscribe("TagFilterTest","TagA||TagB");
consumer.setMessageModel(MessageModel.CLUSTERING);
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
ConsumeConcurrentlyContext consumeConcurrentlyContext) {
try{
for (MessageExt msg :msgs) {
String topic = msg.getTopic();
String msgBody = new String(msg.getBody(),"utf-8");
String msgPro = msg.getProperty("a");
String tags = msg.getTags();
System.out.println("收到消息:"+"topic:"+topic+",tags:"+tags+",a:"+
msgPro+",msg:"+msgBody);
}
}
catch (Exception e)
{
e.printStackTrace();
return ConsumeConcurrentlyStatus.RECONSUME_LATER;
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
System.out.printf("Consumer Started.%n");
}
}
六消息发送时的重要方法属性
七、消息消费时的重要方法和属性
消息消费失败,就放入到重试队列中。可能会导致消费顺序性乱掉
为了确保消息的顺序消费,可以使用MessageListenerOrderly(顺序事件监听器),消费失败的时候,设置mq等一会儿,再处理。并不直接放入重试队列。
八、RocketMQ的高可用机制
mq服务器端为保证流入的消息不丢失,会将消息进行持久化。为防止broker节点宕机,保证高可用,会采用集群的方式部署
一般场景会采用 多master多slave模式 同步复制 异步刷盘 的模式。【综合可靠性和性能】
集群部署模式
·单master模式
·多master模式
·多master多slave模式(同步)
·多master多slave模式(异步)
在这里插入图片描述
刷盘与主从同步
同步刷盘与异步刷盘
同步复制与异步复制
消息存储设计
RocketMQ因为有高可用的要求(宕机不丢失数据),所以要进行持久化存储,RocketMQ采用文件的方式进行消息数据的存储
commitlog:消息存储目录(单文件大小默认1GB,如果存放的消息超过了1GB,就会再创建一个1GB的新文件)
consumequeue:消息消费队列存储目录。consumequeue的一级目录为Topic,二级目录为Topic的消息队列。主要是针对每一个Topic建立的索引,方便consumer消费某个topic下的消息
index:消息索引文件存储目录。存消息的hash值
config:运行期间一些配置信息,通过json文件存储
abort:如果存在该文件,则broker非正常关闭
checkpoint:文件检查点,存储commitlog文件最后一次刷盘时间戳、consumequeue最后一次刷盘时间戳,index索引文件最后一次刷盘时间戳
设计思路:
当有一条消息从producer端发送到commitLog中,会有一个异步线程监听到,然后生成一个消息对应的索引,存入comsumequeue目录指定文件中。由于消息中包含(topic,tag,消息体字节数组),由此构造出每条消息对应的索引数据(每条消息的索引为20个字节,包括:8字节的commitLog offset偏移量,可以看作顺序放入到commitLog中的消息的位置下标;4字节的消息长度;8字节的tag的hashcode,这个值用于对消息进行二级过滤)
commitLog
indexFile
config
rocketmq的存储文件设计,保证消息查找的时间复杂度为O(1),消息的消费速度很快。
当consumer消费一条消息的时候,例如:消费:TopicA Q1的消息(消费第2条消息)
查找消息的逻辑:
首先找到comsumequeue文件目录,找到对应的topic子文件目录,找到Q1子文件目录,取出里面的索引文件。
直接从第20个字节开始找,找到第20-40字节的数据。因为每条消息的索引都是20个字节,第2条消息就是从20开始。 查找时间复杂度O(1)
取出第20-40字节的数据,然后取出 commitLog offset 和 size。
按照commitLog offset 和 size ,去commitLog文件中查找对应位置的消息本体。查找时间复杂度O(1)
因此:整体去查询一条需要消费的消息,时间复杂度为O(1),查找效率非常快,所以消费速度也很快。
IndexFile文件中存每一条消息的hash值(消息key的hash值),方便进行消息的查找。
过期文件删除机制
零拷贝与MMP
什么是零拷贝
零拷贝技术是指计算机执行操作时,CPU不需要先将数据从某处内存复制到另一个特定区域。这种技术通常用于通过网络传输文件时节省CPU周期和内存带宽
零拷贝技术可以减少数据拷贝和共享总线操作的次数,消除传输数据再存储器之间不必要的中间拷贝次数,从而有效地提高数据传输效率
零拷贝技术减少了用户进程地址空间和内核地址空间之间因为上下文切换而带来的开销
可以看出没有说不需要拷贝,只是说减少冗余【不必要】的拷贝
下面这些组件、框架中均使用了零拷贝技术:Kafka、Netty、RocketMQ、Nginx、Apache
传统数据传输机制
比如:读取文件,再用socket发送出去,实际经过四次copy
伪代码实现如下:
buffer=File.read()
Socket.send(buffer)
1、第一次:将磁盘文件,读取到操作系统内核缓冲区 (DMA拷贝)
2、第二次:将内核缓冲区的数据,copy到应用程序的buffer(CPU拷贝)
3、第三次:将application应用程序buffer中的数据,copy到socket网络发送缓冲区(属于操作系统内核的缓冲区);(CPU拷贝)
4、第四次:将socket buffer 的数据,copy到网卡,由网卡进行网络传输。(DMA拷贝)
MMAP 内存映射
硬盘上文件的位置和应用程序缓冲区进行映射(建立一种一一对应关系),由于MMAP()将文件直接映射到用户空间,所以时间文件读取时根据这个映射关系,直接将文件从硬盘拷贝到用户空间,只进行了一次数据拷贝,不再有文件内容从硬盘拷贝到内核空间的一个缓冲区。
MMAP内存映射将会经历:3次拷贝:1次cpu copy 2次DMAcopy
打个比喻:200M的数据,读取文件再用socket发送出去,如果是使用MMAP实际经过三次copy(1次cpu拷贝每次100ms,2次DMS拷贝每次10ms)合计只需要120ms
从数据拷贝的角度上看,就比传统的网络传输,性能提升了接近一倍
mmap()是<sys/mman.h>
消息生产的高可用机制
如果超过重试次数(默认为2,即总共三次机会)还是发送失败,就进行默认的规避策略(即认为之前选择的brokerA节点不可用,下次选择队列会去选择brokerB上的)
消息生产的故障延迟机制策略(非默认的规避策略)
故障延迟机制策略更适合网络状况不是很好,网络波动比较大的场景
RocketMQ中的负载均衡策略
九、分布式事务
rocketMQ中的解决方案
这两种情况都会出现问题。所以rocketMQ进行了优化
两阶段提交(2pc)
在生产者侧,需要处理 半事务,本地事务,以及事务回查(执行本地事务比较耗时,返回unknow,然后事务回查会间隔一定时间定时回查消息发送是否成功)
在消费者侧,需要确保消息幂等性,处理重试消费,消息重复的问题
十、源码亮点分析
1. 提升文件读写性能的MMAP零拷贝技术
result = result && this.commitLog.load();
// ==> 追入load()方法。会调用一个this.mappedFileQueue.load()方法
public boolean load() {
boolean result = this.mappedFileQueue.load();
log.info("load commit log " + (result ? "OK" : "Failed"));
return result;
}
// ==> 追入load()方法。MappedFile mappedFile = new MappedFile(file.getPath(), mappedFileSize);
public boolean load() {
File dir = new File(this.storePath);
File[] files = dir.listFiles();
if (files != null) {
// ascending order
Arrays.sort(files);
for (File file : files) {
if (file.length() != this.mappedFileSize) {
log.warn(file + "\t" + file.length()
+ " length not matched message store config value, please check it manually");
return false;
}
try {
MappedFile mappedFile = new MappedFile(file.getPath(), mappedFileSize);
mappedFile.setWrotePosition(this.mappedFileSize);
mappedFile.setFlushedPosition(this.mappedFileSize);
mappedFile.setCommittedPosition(this.mappedFileSize);
this.mappedFiles.add(mappedFile);
log.info("load " + file.getPath() + " OK");
} catch (IOException e) {
log.error("load file " + file + " error", e);
return false;
}
}
}
return true;
}
// 追入new MappedFile(file.getPath(), mappedFileSize)。里面会调用一个init(fileName, fileSize)方法
public MappedFile(final String fileName, final int fileSize) throws IOException {
init(fileName, fileSize);
}
// 追入init(fileName, fileSize)中
private void init(final String fileName, final int fileSize) throws IOException {
this.fileName = fileName;
this.fileSize = fileSize;
this.file = new File(fileName);
this.fileFromOffset = Long.parseLong(this.file.getName());
boolean ok = false;
ensureDirOK(this.file.getParent());
try {
// 文件通道 fileChannel
this.fileChannel = new RandomAccessFile(this.file, "rw").getChannel();
// FileChannel配合着ByteBuffer,将读写的数据缓存到内存中(操作大文件时可以显著提升效率)
// MappedByteBuffer(零拷贝之内存映射:mmap)
// FileChannel定义了一个map()方法,它可以把一个文件从position位置开始,size大小的区域映射为内存
this.mappedByteBuffer = this.fileChannel.map(MapMode.READ_WRITE, 0, fileSize);
// 原子操作类 -- CAS的原子操作类 -- 多线程效率(加锁)
TOTAL_MAPPED_VIRTUAL_MEMORY.addAndGet(fileSize);
TOTAL_MAPPED_FILES.incrementAndGet();
ok = true;
} catch (FileNotFoundException e) {
log.error("Failed to create file " + this.fileName, e);
throw e;
} catch (IOException e) {
log.error("Failed to map file " + this.fileName, e);
throw e;
} finally {
if (!ok && this.fileChannel != null) {
this.fileChannel.close();
}
}
}
MMAP零拷贝核心代码
map()方法 – 内存映射
mappedByteBuffer其实是磁盘上的一块区域,rocketmq把它当作内存使用。类似虚拟内存的概念。
mmap零拷贝技术
数据的拷贝过程(接收网络数据,并落盘的过程):
·消息数据通过网络socket传输到mq服务器;
·首先到达网卡(网络设备缓冲区)
·然后经过两次拷贝(DMA拷贝,CPU拷贝)才能到达rocketmq。
·因为能够与网络设备打交道的只能是操作系统或内核,所以数据首先通过一次DMA拷贝,到达内核缓冲区。【操作系统与应用层是隔离的,不能共享数据。所以内存是单独隔离的】
·然后在应用层经过一次CPU拷贝,到达rocketmq进行业务逻辑处理。处理后的数据需要存到文件系统或磁盘中
·如果不使用零拷贝技术,那么rocketmq中的数据需要经过两次拷贝(一次CPU拷贝,一次DMA拷贝)才能到达磁盘。先通过CPU拷贝将数据拷贝到内存缓冲区中,然后通过DMA拷贝将内存缓冲区中的数据拷贝到磁盘中
·使用mmap方式的零拷贝技术,可以减少第三次CPU拷贝。从而提升数据读写的效率
一般CPU拷贝比较慢,DMA拷贝比较快
sendFile零拷贝技术(mmap与sendFile的区别?)
mmap只能减少第三次CPU拷贝,提升写入效率
sendFile
优势:可以减少前后两次的CPU拷贝,在拷贝性能上是优于mmap的
劣势:减少了两次CPU拷贝。数据的传输在操作系统层完成。应用层只能读取到文件描述符,也就是说拿不到完整的数据。如果在应用层需要获取完整数据,并对数据进行业务处理,这种场景下,sendFile的方式就不太适用。
2. 提升同步双写性能的CompletableFuture
public CompletableFuture<PutMessageResult> asyncPutMessage(final MessageExtBrokerInner msg) {
// Set the storage time
...
}
这个CompletableFuture是在rocketmq-4.7.0之后才有的。
同步的方式:
producer -->生产消息,发送到 --> Broker --> Broker会启动一个线程来处理消息(可能有多个业务处理步骤 1,2,3,4,5)。那么中间就需要阻塞,等待处理完的结果 --> 回复给 producer
rocketmq-4.7.0之后,采用CompletableFuture异步的方式:
启动一个线程来处理消息(主线程),不会阻塞。会启动一个子线程,拿到处理返回的结果后,就会响应(通过CompletableFuture.completedFuture()方法)–> 回复给 producer
使用CompletableFuture的好处?
在rocketmq集群架构下,有一种保证消息数据不丢失的机制 – 同步双写(2主2从)
从节点进行数据备份,同步复制主节点的消息数据。
返回之前还需要将Memory中的数据同步刷入磁盘中。
这个过程中,用到了很多CompletableFuture,来提升同步双写的性能。
3. Commitlog写入时使用可重入锁还是自旋锁?
异步刷盘建议使用自旋锁,同步刷盘建议使用重入锁
由于rocketmq消息写入CommitLog中,是单文件多队列的存储设计。那么同时有多个生产者往多个topic的队列中写入消息,对应的都是写入到同一个commitLog文件中。就会存在线程安全的问题。
因此commitLog采取锁的机制,来保证多线程并发写入的线程安全。
// CommitLog.java
public CompletableFuture<PutMessageResult> asyncPutMessage(final MessageExtBrokerInner msg) {
protected final PutMessageLock putMessageLock;
// 构造函数
public CommitLog(final DefaultMessageStore defaultMessageStore) {
...
// 默认使用new PutMessageSpinLock(),自旋锁 (乐观锁)
// 也可设置成 可重入锁(悲观锁)
// UseReentrantLockWhenPutMessage参数默认值是false,使用自旋锁。异步刷盘建议使用自旋锁,同步刷盘建议使用重入锁
this.putMessageLock = defaultMessageStore.getMessageStoreConfig().isUseReentrantLockWhenPutMessage() ? new PutMessageReentrantLock() : new PutMessageSpinLock();
}
...
// 会有多个线程并行处理,需要上锁
putMessageLock.lock(); //spin or ReentrantLock ,depending on store config
try{
...
}finally{
putMessageLock.unlock(); // 解锁。标准的lock锁的方式
}
}
// PutMessageSpinLock.java
public class PutMessageSpinLock implements PutMessageLock {
//true: Can lock, false : in lock.
private AtomicBoolean putMessageSpinLock = new AtomicBoolean(true);
@Override
public void lock() {
boolean flag;
do {
flag = this.putMessageSpinLock.compareAndSet(true, false);
}
while (!flag);
}
@Override
public void unlock() {
this.putMessageSpinLock.compareAndSet(false, true);
}
}
核心代码片段:
this.putMessageLock = defaultMessageStore.getMessageStoreConfig().isUseReentrantLockWhenPutMessage() ? new PutMessageReentrantLock() : new PutMessageSpinLock();
rocketmq中:CommitLog初始化的时候,默认使用的是PutMessageSpinLock(自旋锁)。当然,也可配置成使用PutMessageReentrantLock(可重入锁)
那么 自旋锁 和 可重入锁 的差别在哪里?
这个需要结合场景。
自旋锁:不会有上下文的切换,获取不到锁资源时,会采用消耗cpu空转的方式等待。
可重入锁:有可能阻塞线程。发生上下文的切换。
同步刷盘建议使用重入锁。
因为同步刷盘下,多线程对锁资源的竞争很激烈,如果使用自旋,那么CAS失败的机率很高。CAS失败会自旋,导致对CPU的消耗过大。
异步刷盘建议使用自旋锁。
因为异步刷盘下,锁资源竞争小,使用自旋锁,可以减少上下文的切换,提高刷盘的效率。
4. 数据读写分离之堆外内存机制
// CommitLog.java
public CompletableFuture<PutMessageResult> asyncPutMessage(final MessageExtBrokerInner msg) {
...
result = mappedFile.appendMessage(msg, this.appendMessageCallback);
}
// 追入,MappedFile.java
public AppendMessageResult appendMessage(final MessageExtBrokerInner msg, final AppendMessageCallback cb) {
return appendMessagesInner(msg, cb);
}
// 追入
public AppendMessageResult appendMessagesInner(final MessageExt messageExt, final AppendMessageCallback cb) {
assert messageExt != null;
assert cb != null;
// 当前这个MappedFile的写入位置
int currentPos = this.wrotePosition.get();
if (currentPos < this.fileSize) {
// 异步刷盘时,还有两种刷盘模式可以选择
// 如果writeBuffer != null 即开启了堆外内存缓冲,使用writeBuffer,否则使用mappedByteBuffer(也是继承的ByteBuffer)
// slice() 方法,创建一个新的字节缓冲区
ByteBuffer byteBuffer = writeBuffer != null ? writeBuffer.slice() : this.mappedByteBuffer.slice();
byteBuffer.position(currentPos);
AppendMessageResult result;
if (messageExt instanceof MessageExtBrokerInner) {
// 写入具体的数据 commitLog中的数据格式
result = cb.doAppend(this.getFileFromOffset(), byteBuffer, this.fileSize - currentPos, (MessageExtBrokerInner) messageExt);
} else if (messageExt instanceof MessageExtBatch) {
result = cb.doAppend(this.getFileFromOffset(), byteBuffer, this.fileSize - currentPos, (MessageExtBatch) messageExt);
} else {
return new AppendMessageResult(AppendMessageStatus.UNKNOWN_ERROR);
}
this.wrotePosition.addAndGet(result.getWroteBytes());
this.storeTimestamp = result.getStoreTimestamp();
return result;
}
log.error("MappedFile.appendMessage return null, wrotePosition: {} fileSize: {}", currentPos, this.fileSize);
return new AppendMessageResult(AppendMessageStatus.UNKNOWN_ERROR);
}
为什么要用堆外内存?
因为读写数据本身就是一个频次很高的操作。
堆内存:读写过程中,会产生大量数据 -> new 对象 -> GC -> 垃圾回收 -> 停顿 -> 效率低
堆外内存:使用的是本地内存 -> 手动的GC -> 没有停顿 -> 效率高
堆外内存也有一些缺点:
内存需求比较大
数据写入需要两步,写入不够及时
如果业务场景对消息写入的时效性要求很高,那么最好选择默认的写入模式
入模式
常见面试题
1.1 为什么使用消息队列
面试官问你这个问题,期望的一个回答是说,你们公司有个什么业务场景,这个业务场景又个什么技术挑战,如果不用MQ可能会很麻烦,但是你现在用了MQ之后带给你很多的好处。消息队列的常见使用场景,其实场景有很多,但是比较核心的有3个:
解耦
A系统发送个数据到BCD三个系统,接口调用发送,那如果E系统也要这个数据呢?那如果C系统现在不需要了呢?现在A系统又要发送第二种数据了呢?而且A系统要时时刻刻考虑BCDE四个系统如果挂了怎么办?要不要重发?我要不要把消息给存起来?
你需要去考虑一下你负责的系统中是否有类似的场景,就是一个系统或者一个模块,调用了多个系统或模块,互相之间的调用很复杂,维护起来很麻烦。但是其实这个调用时不需要直接同步调用接口的,如果用MQ给他异步化解耦,也是可以的,你就需要去考虑在你的项目中,是不是可以运用这个MQ去进行系统的解耦。
异步
A系统接收一个请求,需要在自己本地写库,还需要在BCD三个系统写库,自己本地写库要30ms,BCD三个系统分别写库要300ms、450ms、200ms。最终请求总延时时30+300+450+200=980ms,接近1s,异步后,BCD三个系统分别写库的时间,A系统就不再考虑了。
削峰
1.2 消息队列有什么优缺点
优点上面已经说了,就是在特殊场景下有其对应的好处,解耦、异步、削峰
缺点呢?
系统可用性降低
系统引入的外部依赖越多,越容易挂掉,本来你就是A系统调用BCD三个系统的接口就好了,ABCD四个系统好好的,没啥问题,你偏偏加一个MQ进来,万一MQ挂了怎么办?MQ挂了,整套系统崩溃了,业务也就停顿了。
系统复杂性提高
硬生生加个MQ进来,怎么保证消息没有重复消费?怎么处理消息丢失的情况?怎么保证消息传递的顺序性?
一致性问题
A系统处理完了直接返回成功了,人都以为你这个请求就成功了;但问题是,要是BCD三个系统那里,BD两个系统写库成功了,结果C系统写库失败了,你在数据就不一致了。
所以消息队列实际上是一种非常复杂的架构,你引入它有很多好处,但是也得真对它带来的坏处做各种额外的技术方案和架构来规避掉。
RocketMQ
为什么选择RocketMQ
性能:阿里支撑,禁受住淘宝,天猫双十一重重考验;性能高;可靠性好;可用性高;易扩展
功能:功能完善,我们需要的功能,基本都能够满足,如:事务消息,消息重试,私信队列,定时消息等;
易用,跨平台:跨语言,多协议接入(支持HTTP、MQTT、TCP等协议,支持Restful风格HTTP收发消息)
钱能解决的问题,一般都不是问题,所以免费服务不能满足的,适当的花钱购买所需服务是值得的,引进的就是RocketMQ的阿里云和VIP服务;