文章目录
本系列文章:
RocketMQ(一)消息的发送、存储与消费
RocketMQ(二)RocketMQ实战
一、初识RocketMQ
RocketMQ是一个低延迟、高并发、高可用、高卡靠的分布式消息和流数据平台。RocketMQ设计基于主题的发布与订阅模式
,其核心功能包括消息发送、消息存储和消息消费,整体设计追求简单和性能高效。
1.1 RocketMQ的特点
- 1、架构模式
RocketMQ与大部分消息中间件一样,采用发布订阅模式,主要参与组件包括:消息发送者、消息服务器(消息存储)、消息消费和路由发现。
RocketMQ是一种基于发布/订阅的消息模型,发送者将消息发送到指定的主题,Broker负责持久化存储,订阅了该主题的消费者从服务器拉取该消息(RocketMQ的PUSH模式也是基于PULL模式实现的)。
NameServer提供路由注册与发现功能
。无状态,可集群部署,节点之间互不通信,每个节点采用“最终一致性”记录完整的路由信息。
Broker提供消息存储服务
。有Master和Slave两种角色,Master的BorkerId为0,负责消息读写,当负载过高时,默认从BrokerId为1的Slave读取消息。所有BrokerId节点与NameServer建立长连接,每30秒向NameServer发送一次心跳,该心跳内容包括该Broker的所有Topic信息。
Client包括Producer(消息发送者)和Consumer(消息消费者)
。无状态,多个实例组成Producer Cluster或Consumer Cluster。与NameServer集群中的一个节点建立长连接,每30秒从NameServer更新一次主题路由信息。与主题所在的Master Broker节点建立长连接,每30秒向该Broker节点发送一次心跳信息。 - 2、顺序消息
所谓顺序消息,就是消息消费者按照消息达到消息存储服务器的顺序消费。RocketMQ可以严格保证消息有序(RocketMQ支持一种特殊的消息类型:顺序消息)。 - 3、消息过滤
消息过滤是指在消息消费时,消息消费者可以对同一主题下的消息按照规则只消费自己感兴趣的消息。常见的过滤方式是根据Topic和tag来过滤。 - 4、消息存储
消息中间件的一个核心实现是消息的存储,对于消息存储一般有如下两个维度的考量:消息堆积能力和消息存储性能。RocketMQ追求消息存储的高性能,引入内存映射机制,所有主题的消息按顺序存储在同一个文件中。同时为了避免消息在消息存储服务器中无限地累积,引入了消息文件过期机制与文件存储空间报警机制。
RocketMQ
追求消息发送的高吞吐量,消息存储文件被设计成文件组的概念,组内单个文件大小固定,方便引入内存映射机制,所有主题的消息存储按顺序编写
,极大地提升了消息的写性能。同时为了兼顾消息消费与消息查找,引入了消息消费队列文件与索引文件。
- 5、消息高可用性
通常影响消息可靠性的有以下几种情况:
1)Broker异常崩溃。
2)操作系统崩溃。
3)机器断电,但是能立即恢复供电。
4)机器无法开机(可能是CPU、主板、内存等关键设备损坏)。
5)磁盘设备损坏。
对于前3种情况,RocketMQ在同步刷盘模式下可以确保不丢失消息,在异步刷盘模式下,会丢失少量消息。后2种情况属于单点故障,一旦发生,该节点上的消息会全部丢失。如果开启了异步复制机制,RoketMQ能保证只丢失少量消息。
- 6、消息到达(消费)低延迟
在消息不堆积情况下,消息到达Broker后,能立刻到达Consumer。RocketMQ使用长轮询Pull方式,可保证消息非常实时。 - 7、确保消息必须被消费一次
RocketMQ通过消息消费确认机制(ACK)确保消息至少被消费一次,因为ACK消息有可能出现丢失等情况,RocketMQ无法做到消息只被消费一次,所以有重复消费的可能。
RocketMQ只保证消息被消费者消费,在设计上允许消息被重复消费
(消息重复问题由消费者在消息消费时实现幂等)。
- 8、回溯消息
回溯消息是指消息消费端已经消费成功,根据业务要求,需要重新消费消息。RocketMQ支持按时间向前或向后回溯消息,时间维度可精确到毫秒。 - 9、消息堆积
消息中间件的主要功能是异步解耦,必须能应对前端的数据洪峰,提高后端系统的可用性,这必然要求消息中间件具备一定的消息堆积能力。RocketMQ使用磁盘文件存储消息(内存映射机制),并且在物理布局上为多个大小相等的文件组成逻辑文件组,可以无限循环使用。RocketMQ消息存储文件并不是永久存储在消息服务器端的,而是提供了过期机制,默认保留3天。 - 10、定时消息(延迟消息)
定时消息是指消息发送到Broker后,不能被消息消费端立即消费,而是要到特定的时间点或者等待特定的时间后才能被消费。因为如果要支持任意精度的定时消息消费,就必须在消息服务端对消息进行排序,这势必带来很大的性能损耗,所以RocketMQ不支持任意进度的定时消息,只支持特定延迟级别。 - 11、消息重试机制
RocketMQ支持消息重试机制。消息重试是指在消息消费时如果发生异常,消息中间件支持消息重新投递。
1.2 RocketMQ的使用场景
- 1、消息通道
RocketMQ常作为消息通道使用,在不同的服务之间进行消息通信。通过RocketMQ,通道服务之间的调用不再直接耦合,而是通过主题和消费组实现异步解耦。 - 2、削峰填谷
在实际工作中,公司的业务有高峰和低谷期,高峰期促销活动带来的大量突发请求很可能对系统造成压力。这类请求可以先写入RocketMQ,由RocketMQ承担压力。消费服务可以根据自身消费能力去处理请求,也可以异步通知处理结果。 - 3、顺序消费
通常说的顺序消费指的是队列内有序,而全局有序则需要主题只有一个队列。全局有序在性能、扩容方面均受限制。RocketMQ支持的顺序消费可以很好地支持按顺序消费消息的场景。 - 4、广播消费
广播模式中每个 消费者都会收到所有的消息,比如在客户聊天场景中,可以通过广播模式实现对群成员的消息触达。 - 5、事务消息
使用事务消息时,第一次发送的是“暂不可消费”的半事务消息;数据修改成功后,再发起第二次提交;提交成功后,该消息才会变成“可消费”,从而保证数据的一致性。
1.3 注册中心NameServer
消息中间件的设计思路一般是基于主题的订阅发布机制,消息生产者(Producer)发送某一主题的消息到消息服务器,消息服务器负责该消息的持久化存储,消息消费者(Consumer)订阅感兴趣的主题,消息服务器根据订阅信息(路由信息)将消息推送给消费者(推模式)或者消息消费者主动向消息服务器拉取消息(拉模式),从而实现消息生产者与消息消费者的解耦。
RocketMQ的架构:
Broker消息服务器在启动时向所有NameServer注册,消息生产者在发送消息之前先从NameServer获取Broker服务器的地址列表,然后根据负载算法从列表中选择一台消息服务器发送消息。NameServer与每台Broker服务器保持长连接,并间隔10s检测Broker是否存活,如果检测到Broker宕机,则从路由注册表中将其移除,但是路由变化不会马上通知消息生产者。这样设计是为了降低NameServer实现的复杂性,因此需要在消息发送端提供容错机制来保证消息发送的高可用性。
Name Server是一个几乎无状态节点,可集群部署,节点之间无任何信息同步。
Broker部署相对复杂,Broker分为Master与Slave,一个Master可以对应多个Slave,但是一个Slave只能对应一个Master,Master与Slave的对应关系通过指定相同的BrokerName,不同的BrokerId来定义,BrokerId为0表示Master,非0表示Slave。Master也可以部署多个。每个Broker与Name Server集群中的所有节点建立长连接,定时注册Topic信息到所有Name Server。
Producer与Name Server集群中的其中一个节点(随机选择)建立长连接,定期从Name Server取Topic路路由信息,并向提供Topic服务的Master建⽴立长连接,且定时向Master发送心跳。Producer完全无状态,可集群部署。
Consumer与Name Server集群中的其中一个节点(随机选择)建立长连接,定期从Name Server取Topic路由信息,并向提供Topic服务的Master、Slave建立长连接,且定时向Master、Slave发送心跳。Consumer既可以从Master
订阅消息,也可以从Slave订阅消息,订阅规则由Broker配置决定。
- NameServer核心架构设计
1、Broker启动时每隔30s向NameServer集群的每一台机器发送心跳包,包含自身创建的topic路由等信息。
2、消息客户端每隔30s向NameServer更新对应topic的路由信息。
3、NameServer收到Broker发送的心跳包时会记录时间戳。
4、NameServer每隔10s会扫描一次brokerLiveTable(存放心跳包的时间戳信息),如果在120s内没有收到心跳包,则认为Broker失效,更新topic的路由信息,将失效的Broker信息移除【这是删除路由信息的一种场景,还有一种场景是:Broker被正常关闭】。
5、RocketMQ路由发现是非实时的,当topic路由出现变化后,NameServer不主动推送给客户端,而是由客户端定时拉取主题最新的路由。
- 路由发现机制
通过上面的了解,会发现一种情况:NameServer需要等Broker失效至少120s才能将该Broker从路由表中移除,如果在Broker故障期间,消息生产者根据主题获取到的路由信息包含已经宕机的Broker,就会导致消息发送失败。这样情况如何处理,就要看RocketMQ的消息发送机制了。
二、消息发送(同步发送/异步发送/单向发送)
RocketMQ发送普通消息有3种实现方式:可靠同步发送、可靠异步发送和单向发送。
同步
:发送者向RocketMQ执行发送消息API时,同步等待,直到消息服务器返回发送结果。
异步
:发送者向RocketMQ执行发送消息API时,指定消息发送成功后的回调函数,调用消息发送API后,立即返回,消息发送者线程不阻塞,直到运行结束,消息发送成功或失败的回调任务在一个新的线程中执行。
单向
:消息发送者向RocketMQ执行发送消息API时,直接返回,不等待消息服务器的结果,也不注册回调函数。
2.1 消息发送的高可用设计(本地缓存topic信息/消息发送重试机制,默认两次/故障规避机制)
- Topic路由机制
消息发送者向某一个topic发送消息时,需要查询topic的路由信息。初次发送时会根据topic的名称向NameServer集群查询topic的路由信息,然后将其存储在本地内存缓存中,并且每隔30s依次遍历缓存中的topic,向NameServer查询最新的路由信息。如果成功查询到路由信息,会将这些信息更新至本地缓存,实现topic路由信息的动态感知。
RocketMQ中的路由消息是持久化在Broker中的,NameServer中的路由信息来自Broker的心跳包并存储在内存中。
- 消息发送高可用设计
发送端在自动发现主题的路由信息后,RocketMQ默认使用轮询算法进行路由的负载均衡
。RocketMQ在消息发送时支持自定义的队列负载算法。使用自定义的路由负载算法后,RocketMQ的重试机制将失效。
RocketMQ为了实现消息发送高可用,引入了两个非常重要的特性:
1、
消息发送重试机制
。RocketMQ在消息发送时如果出现失败,默认会重试两次。
2、故障规避机制
。当消息第一次发送失败时,如果下一次消息还是发送到刚刚失败的Broker上,其消息发送大概率还是会失败,因此为了保证重试的可靠性,在重试时会尽量避开刚刚接收失败的Broker,而是选择其他Broker上的队列进行发送,从而提高消息发送的成功率。
消息发送的高可用性设计:
2.2 消息Message
RocketMQ消息封装类是Message,其属性:
//主题
private String topic;
//消息的标记,RocketMQ不做任何处理
private int flag;
//扩展属性
private Map<String, String> properties;
//消息体
private byte[] body;
//事务id,仅在事务消息中使用到
private String transactionId;
其构造方法:
//topic:主题(必填)
//tags: 消息tag,用于消息过滤
//keys:消息索引键,用空格隔开,RocketMQ可以根据这些key快速检索消息
//body:消息的内容,这是一个字节数组,序列化方式由应用决定,例如你可以将一个json转为字节数组
//flag:消息的标记,完全由应用设置,RocketMQ不做任何处理(选填)
//waitStoreMsgOK:消息发送时是否等消息存储完成后再返回(默认为true)
public Message(String topic, String tags, String keys,
int flag, byte[] body, boolean waitStoreMsgOK)
2.3 消息生产者DefaultMQProducer
DefaultMQProducer是默认消息生产者实现类。
- DefaultMQProducer的核心属性
//生产者所属组,消息服务器在回查事务状态时会随机选择该组中任何一个生产者发起的事务回查请求
private String producerGroup;
//默认topicKey
private String createTopicKey = MixAll.DEFAULT_TOPIC;
//默认主题在每一个Broker队列的数量
private volatile int defaultTopicQueueNums = 4;
//发送消息的超时时间,默认为3s
private int sendMsgTimeout = 3000;
//消息体超过该值则启用压缩,默认为4KB
private int compressMsgBodyOverHowmuch = 1024 * 4;
//同步方式发送消息重试次数,默认为2,总共执行3次
private int retryTimesWhenSendFailed = 2;
//异步方式发送消息的重试次数,默认为2
private int retryTimesWhenSendAsyncFailed = 2;
//消息重试时选择另外一个Broker,是否不等待存储结果就返回,默认为false
private boolean retryAnotherBrokerWhenNotStoreOK = false;
//允许发送的最大消息长度,默认为4MB,最大值为2的32次方 - 1
private int maxMessageSize = 1024 * 1024 * 4;
- 消息发送方法
DefaultMQProducer的主要方法:
/**
* 创建主题
* key:目前无实际作用,可以与newTopic相同
* newTopic:主题名称
* queueNum:队列数量
* topicSysFlag:主题系统标签,默认为0
**/
void createTopic(String key, String newTopic, int queueNum, int topicSysFlag)
//同步发送消息,具体发送到主题中的哪个消息队列由负载算法决定
SendResult send(Message msg)
//同步发送消息,如果发送超过timeout则抛出超时异常
SendResult send(Message msg, final long timeout)
//异步发送消息,sendCallback参数是消息发送成功后的回调方法
void send(Message msg, SendCallback sendCallback)
//异步发送消息,如果发送超过timeout则抛出超时异常
void send(Message msg, SendCallback sendCallback, long timeout)
//单向消息发送,即不在乎发送结果,消息发送出去后该方法立即返回
void sendOneway(Message msg)
//同步方式发送消息,且发送到指定的消息队列
SendResult send(Message msg, MessageQueue mq, final long timeout)
//异步方式发送消息,且发送到指定的消息队列
void send(final Message msg, final MessageQueue mq, final SendCallback sendCallback, long timeout)
//单向方式发送消息,且发送到指定的消息队列
void sendOneway(Message msg, MessageQueue Selector selector, Object arg)
//同步批量消息发送
SendResult send(final Collection msgs):
可以看出:
同步和异步发送消息时,都可以设置超时时间。
当调用方法参数中有SendCallback回调接口时,表示是异步发送。
- 息发送结果SendResult
SendResult表示消息发送结果,有个枚举字段SendStatus表示消息发送的结果。
发送消息时,将获得包含SendStatus的SendResult。首先,我们假设Message的isWaitStoreMsgOK = true(默认为true)。如果没有抛出异常,我们将始终获得SEND_OK。
SendStatus有以下枚举值:
public enum SendStatus {
//消息发送成功
SEND_OK,
//消息发送成功,但服务在进行刷盘的时候超时了。消息已经进入服务器队列,
//刷盘超时会等待下一次的刷盘时机再次刷盘,如果此时服务器down机消息丢失,会返回此种状态,
//如果业务系统是可靠性消息投递,那么需要重发消息。
FLUSH_DISK_TIMEOUT,
//在主从同步的时候,同步到Slave超时了。如果此时Master节点down机,消息也会丢失。
FLUSH_SLAVE_TIMEOUT,
//消息发送成功,但Slave不可用,只有Master节点down机,消息才会丢失。
SLAVE_NOT_AVAILABLE,
}
后三种状态,如果业务系统是可靠性消息投递,那么需要考虑补偿进行可靠性的重试投递。
2.4 消息发送类别
2.4.1 同步发送(收到响应才发下一条消息)
同步发送是最常用的方式,是指消息发送方发出一条消息后,会在收到服务端同步响应之后才发下一条消息的通讯方式,可靠的同步传输被广泛应用于各种场景,如重要的通知消息、短消息通知等。
同步发送流程:
- 1、首先要创建一个producer。普通消息可以创建 DefaultMQProducer,创建时需要填写生产组的名称,生产者组是指同一类Producer的集合,这类Producer发送同一类消息且发送逻辑一致。
- 2、设置NameServer的地址。RocketMQ很多方式设置NameServer地址,这里是在代码中调用producer的API setNamesrvAddr进行设置,如果有多个NameServer,中间以分号隔开,比如"127.0.0.2:9876;127.0.0.3:9876"。
- 3、构建消息。指定topic、tag、body等信息,tag可以理解成标签,对消息进行再归类,RocketMQ可以在消费端对tag进行过滤。
- 4、调用send接口发送消息。同步发送等待结果最后返回SendResult,SendResult包含实际发送状态还包括SEND_OK(发送成功), FLUSH_DISK_TIMEOUT(刷盘超时), FLUSH_SLAVE_TIMEOUT(同步到备超时), SLAVE_NOT_AVAILABLE(备不可用),如果发送失败会抛出异常。
同步发送方式请务必捕获发送异常,并做业务侧失败兜底逻辑,如果忽略异常则可能会导致消息未成功发送的情况。
同步发送示例:
public class SyncProducer {
public static void main(String[] args) throws Exception {
// 初始化一个producer并设置Producer group name
DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
// 设置NameServer地址
producer.setNamesrvAddr("localhost:9876");
// 启动producer
producer.start();
for (int i = 0; i < 100; i++) {
// 创建一条消息,并指定topic、tag、body等信息,tag可以理解成标签,对消息进行再归类,
//RocketMQ可以在消费端对tag进行过滤
Message msg = new Message("TopicTest" ,
"TagA" ,
("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET)
);
// 利用producer进行发送,并同步等待发送结果
SendResult sendResult = producer.send(msg);
System.out.printf("%s%n", sendResult);
}
// 一旦producer不再使用,关闭producer
producer.shutdown();
}
}
- 批量消息发送
RocketMQ可以将一些消息聚成一批以后进行发送,可以增加吞吐率,并减少API和网络调用次数。批量消息发送也是同步的
。示例:
public class SimpleBatchProducer {
public static void main(String[] args) throws Exception {
DefaultMQProducer producer = new DefaultMQProducer("BatchProducerGroupName");
producer.start();
String topic = "BatchTest";
List<Message> messages = new ArrayList<>();
messages.add(new Message(topic, "Tag", "OrderID001", "Hello world 0".getBytes()));
messages.add(new Message(topic, "Tag", "OrderID002", "Hello world 1".getBytes())