RocketMQ(1)-架构原理
RocketMQ是阿里开源的分布式消息中间件,跟其它中间件相比,RocketMQ的特点是纯JAVA实现;集群和HA实现相对简单;在发生宕机和其它故障时消息丢失率更低。
一、RocketMQ专业术语
先讲专业术语的含义,后面会画流程图来更好的去理解它们。
Producer
消息生产者,位于用户的进程内,Producer通过NameServer获取所有Broker的路由信息
,根据负载均衡策略选择将消息发到哪个Broker,然后调用Broker接口提交消息。
Producer Group
生产者组,简单来说就是多个发送同一类消息的生产者称之为一个生产者组。
Consumer
消息消费者,位于用户进程内。Consumer通过NameServer获取所有broker的路由信息后,向Broker发送Pull请求来获取消息数据。Consumer可以以两种模式启动,广播(Broadcast)和集群(Cluster),广播模式下,一条消息会发送给所有Consumer,集群模式下消息只会发送给一个Consumer。
Consumer Group
消费者组,和生产者类似,消费同一类消息的多个 Consumer 实例组成一个消费者组。
Topic
Topic用于将消息按主题做划分,Producer将消息发往指定的Topic,Consumer订阅该Topic就可以收到这条消息。Topic跟发送方和消费方都没有强关联关系,发送方可以同时往多个Topic投放消息,消费方也可以订阅多个Topic的消息。在RocketMQ中,Topic是一个上逻辑概念。消息存储不会按Topic分开。
Message
代表一条消息,使用MessageId
唯一识别,用户在发送时可以设置messageKey,便于之后查询和跟踪。一个 Message 必须指定 Topic,相当于寄信的地址。Message 还有一个可选的 Tag 设置,以便消费端可以基于 Tag 进行过滤消息。也可以添加额外的键值对,例如你需要一个业务 key 来查找 Broker 上的消息,方便在开发过程中诊断问题。
Tag
标签可以被认为是对 Topic 进一步细化。一般在相同业务模块中通过引入标签来标记不同用途的消息。
Broker
Broker是RocketMQ的核心模块,负责接收并存储消息
,同时提供Push/Pull接口来将消息发送给Consumer。Consumer可选择从Master或者Slave读取数据。多个主/从组成Broker集群,集群内的Master节点之间不做数据交互。Broker同时提供消息查询的功能,可以通过MessageID和MessageKey来查询消息。Borker会将自己的Topic配置信息实时同步到NameServer。
Queue
Topic和Queue是1对多的关系,一个Topic下可以包含多个Queue,主要用于负载均衡。发送消息时,用户只指定Topic,Producer会根据Topic的路由信息选择具体发到哪个Queue上。Consumer订阅消息时,会根据负载均衡策略决定订阅哪些Queue的消息。
Offset
RocketMQ在存储消息时会为每个Topic下的每个Queue生成一个消息的索引文件,每个Queue都对应一个Offset记录当前Queue中消息条数。
NameServer
NameServer可以看作是RocketMQ的注册中心,它管理两部分数据:集群的Topic-Queue的路由配置;Broker的实时配置信息。其它模块通过Nameserv提供的接口获取最新的Topic配置和路由信息。
Producer/Consumer
:通过查询接口获取Topic对应的Broker的地址信息Broker
: 注册配置信息到NameServer, 实时更新Topic信息到NameServer
二、流程图
我们由简单到复杂的来理解,它的一些核心概念
这个图很好理解,消息先发到Topic,然后消费者去Topic拿消息。只是Topic在这里只是个概念,那它到底是怎么存储消息数据的呢,这里就要引入Broker概念。
2、Topic的存储
Topic是一个逻辑上的概念,实际上Message是在每个Broker上以Queue的形式记录。
从上面的图片可以总结下几条结论。
1、消费者发送的Message会在Broker中的Queue队列中记录。
2、一个Topic的数据可能会存在多个Broker中。
3、一个Broker存在多个Queue。
4、单个的Queue也可能存储多个Topic的消息。
也就是说每个Topic在Broker上会划分成几个逻辑队列,每个逻辑队列保存一部分消息数据,但是保存的消息数据实际上不是真正的消息数据,而是指向commit log的消息索引。
Queue不是真正存储Message的地方,真正存储Message的地方是在CommitLog
。
如图(盗图)
左边的是CommitLog。这个是真正存储消息的地方。RocketMQ所有生产者的消息都是往这一个地方存的。
右边是ConsumeQueue。这是一个逻辑队列。和上文中Topic下的Queue是一一对应的。消费者是直接和ConsumeQueue打交道。ConsumeQueue记录了消费位点,这个消费位点关联了commitlog的位置。所以即使ConsumeQueue出问题,只要commitlog还在,消息就没丢,可以恢复出来。还可以通过修改消费位点来重放或跳过一些消息。
3、部署模型
在部署RocketMQ时,会部署两种角色。NameServer和Broker。如图(盗图)
针对这张图做个说明
1、Product和consumer集群部署,是你开发的项目进行集群部署。
2、Broker 集群部署是为了高可用,因为Broker是真正存储Message的地方,集群部署是为了避免一台挂掉,导致整个项目KO.
那Name SerVer是做什么用呢,它和Product、Consumer、Broker之前存在怎样的关系呢?
先简单概括Name Server的特点
1、Name Server是一个几乎无状态节点,可集群部署,节点之间无任何信息同步。
2、每个Broker与Name Server集群中的所有节点建立长连接,定时注册Topic信息到所有Name Server。
3、Producer与Name Server集群中的其中一个节点(随机选择)建立长连接,定期从Name Server取Topic路由信息。
4、Consumer与Name Server集群中的其中一个节点(随机选择)建立长连接,定期从Name Server取Topic路由信息。
这里面最核心的是每个Broker与Name Server集群中的所有节点建立长连接
这样做好处多多。
1、这样可以使Name Server之间可以没有任何关联,因为它们绑定的Broker是一致的。
2、作为Producer或者Consumer可以绑定任何一个Name Server 因为它们都是一样的。
三、详解Broker
1、Broker与Name Server关系
1)连接 单个Broker和所有Name Server保持长连接。
2)心跳
心跳间隔:每隔30秒向所有NameServer发送心跳,心跳包含了自身的Topic配置信息。
心跳超时:NameServer每隔10秒,扫描所有还存活的Broker连接,若某个连接2分钟内没有发送心跳数据,则断开连接。
3)断开:当Broker挂掉;NameServer会根据心跳超时主动关闭连接,一旦连接断开,会更新Topic与队列的对应关系,但不会通知生产者和消费者。
2、 负载均衡
一个Topic分布在多个Broker上,一个Broker可以配置多个Topic,它们是多对多的关系。
如果某个Topic消息量很大,应该给它多配置几个Queue,并且尽量多分布在不同Broker上,减轻某个Broker的压力。
3 、可用性
由于消息分布在各个Broker上,一旦某个Broker宕机,则该Broker上的消息读写都会受到影响。
所以RocketMQ提供了Master/Slave的结构,Salve定时从Master同步数据,如果Master宕机,则Slave提供消费服务,但是不能写入消息,此过程对应用透明,由RocketMQ内部解决。
有两个关键点:思考1
一旦某个broker master宕机,生产者和消费者多久才能发现?
受限于Rocketmq的网络连接机制,默认情况下最多需要30秒,因为消费者每隔30秒从nameserver获取所有topic的最新队列情况,这意味着某个broker如果宕机,客户端最多要30秒才能感知。
思考2
master恢复恢复后,消息能否恢复。
消费者得到Master宕机通知后,转向Slave消费,但是Slave不能保证Master的消息100%都同步过来了,因此会有少量的消息丢失。但是消息最终不会丢的,一旦Master恢复,未同步过去的消息会被消费掉。
四 Consumer (消费者)
1 、Consumer与Name Server关系
1)连接 : 单个Consumer和一台NameServer保持长连接,如果该NameServer挂掉,消费者会自动连接下一个NameServer,直到有可用连接为止,并能自动重连。
2)心跳: 与NameServer没有心跳
3)轮询时间 : 默认情况下,消费者每隔30秒从NameServer获取所有Topic的最新队列情况,这意味着某个Broker如果宕机,客户端最多要30秒才能感知。
2、 Consumer与Broker关系
1)连接 :单个消费者和该消费者关联的所有broker保持长连接。
3、 负载均衡
集群消费模式下,一个消费者集群多台机器共同消费一个Topic的多个队列,一个队列只会被一个消费者消费。如果某个消费者挂掉,分组内其它消费者会接替挂掉的消费者继续消费。
五、 Producer(生产者)
1、 Producer与Name Server关系
1)连接 单个Producer和一台NameServer保持长连接,如果该NameServer挂掉,生产者会自动连接下一个NameServer,直到有可用连接为止,并能自动重连。
2)轮询时间 默认情况下,生产者每隔30秒从NameServer获取所有Topic的最新队列情况,这意味着某个Broker如果宕机,生产者最多要30秒才能感知,在此期间,
发往该broker的消息发送失败。
3)心跳 与nameserver没有心跳
2、 与broker关系
连接 单个生产者和该生产者关联的所有broker保持长连接。
=前言=
1、因为自己只买了一台阿里云服务器,所以RocketMQ集群都部署在单台服务器上只是端口不同,如果实际开发,可以分别部署在多台服务器上。
2、这里有关 Broker 和 NameServer 分别都做了了集群部署(各部署两个),且BroKer是按两主进行部署。
之所以选用Docker部署主要还是考虑 :通过Docker部署RocketMQ集群更快速,而且对系统的资源利用更好!
之前有写过Liunx如何部署Docker的博客:【Docker】(3)---linux部署Docker、Docker常用命令
之前有关RocketMQ概念做了介绍的博客:RocketMQ(1)-架构原理
下面先写好所需配置文件,在运行配置文件,最终看运行结果!
一、写配置文件
1、创建所需文件夹
mkdir -p /opt/rocketmq/logs/nameserver-a
mkdir -p /opt/rocketmq/logs/nameserver-b
mkdir -p /opt/rocketmq/store/nameserver-a
mkdir -p /opt/rocketmq/store/nameserver-b
mkdir -p /opt/rocketmq/logs/broker-a
mkdir -p /opt/rocketmq/logs/broker-b
mkdir -p /opt/rocketmq/store/broker-a
mkdir -p /opt/rocketmq/store/broker-b
mkdir -p /home/rocketmq/broker-a/
mkdir -p /home/rocketmq/broker-b/
2、创建broker.conf
broker.conf是Broker的配置文件
,因为此时RocketMQ镜像还没有拉取,所以还没有默认的broker.conf。所以这里直接写好,到时候通过命令替换默认的broker.conf。
因为是双主模式部署,所以会有两个broker.conf,这里暂且命名 broker-a.conf 和 broker-b.conf
1) broker-a.conf
brokerClusterName = rocketmq-cluster
brokerName = broker-a
brokerId = 0
#这个很有讲究 如果是正式环境 这里一定要填写内网地址(安全)
#如果是用于测试或者本地这里建议要填外网地址,因为你的本地代码是无法连接到阿里云内网,只能连接外网。
brokerIP1 = xxxxx
deleteWhen = 04
fileReservedTime = 48
brokerRole = ASYNC_MASTER
flushDiskType = ASYNC_FLUSH
# 内网的(阿里云有内网IP和外网IP)
namesrvAddr=172.18.0.5:9876;172.18.0.5:9877
autoCreateTopicEnable=true
#Broker 对外服务的监听端口,
listenPort = 10911
#Broker角色
#- ASYNC_MASTER 异步复制Master
#- SYNC_MASTER 同步双写Master
#- SLAVE
brokerRole=ASYNC_MASTER
#刷盘方式
#- ASYNC_FLUSH 异步刷盘
#- SYNC_FLUSH 同步刷盘
flushDiskType=ASYNC_FLUSH
这里配置的主要信息有:
1、当前Broker对外暴露的端口号
2、注册到NameServer的地址,看到这里有两个地址,说明NameServer也是集群部署。
3、当前Broker的角色,是主还是从,这里表示是主。
2)broker-b.conf
brokerClusterName = rocketmq-cluster
brokerName = broker-b
brokerId = 0
brokerIP1 = xxxxx
deleteWhen = 04
fileReservedTime = 48
brokerRole = ASYNC_MASTER
flushDiskType = ASYNC_FLUSH
# 内网的(阿里云有内网IP和外网IP)
namesrvAddr=172.18.0.5:9876;172.18.0.5:9877
autoCreateTopicEnable=true
#Broker 对外服务的监听端口,
listenPort = 10909
#Broker角色
#- ASYNC_MASTER 异步复制Master
#- SYNC_MASTER 同步双写Master
#- SLAVE
brokerRole=ASYNC_MASTER
#刷盘方式
#- ASYNC_FLUSH 异步刷盘
#- SYNC_FLUSH 同步刷盘
flushDiskType=ASYNC_FLUSH
这个上面并无差别,只是当前Brocker对外暴露的端口不一样。
3、编写 docker-compose.yml
我们可以简单把docker-compose.yml理解成一个类似Shell的脚本,这个脚本定义了运行多容器应用程序的信息。
version: '3.5'
services:
rmqnamesrv-a:
image: rocketmqinc/rocketmq:4.3.0
container_name: rmqnamesrv-a
ports:
- 9876:9876
volumes:
- /opt/rocketmq/logs/nameserver-a:/opt/logs
- /opt/rocketmq/store/nameserver-a:/opt/store
command: sh mqnamesrv
networks:
rmq:
aliases:
- rmqnamesrv-a
rmqnamesrv-b:
image: rocketmqinc/rocketmq:4.3.0
container_name: rmqnamesrv-b
ports:
- 9877:9877
volumes:
- /opt/rocketmq/logs/nameserver-b:/opt/logs
- /opt/rocketmq/store/nameserver-b:/opt/store
command: sh mqnamesrv
networks:
rmq:
aliases:
- rmqnamesrv-b
rmqbroker-a:
image: rocketmqinc/rocketmq:4.3.0
container_name: rmqbroker-a
ports:
- 10911:10911
volumes:
- /opt/rocketmq/logs/broker-a:/opt/logs
- /opt/rocketmq/store/broker-a:/opt/store
- /home/rocketmq/broker-a/broker-a.conf:/opt/rocketmq-4.3.0/conf/broker.conf
environment:
TZ: Asia/Shanghai
NAMESRV_ADDR: "rmqnamesrv-a:9876"
JAVA_OPTS: " -Duser.home=/opt"
JAVA_OPT_EXT: "-server -Xms256m -Xmx256m -Xmn256m"
command: sh mqbroker -c /opt/rocketmq-4.3.0/conf/broker.conf autoCreateTopicEnable=true &
links:
- rmqnamesrv-a:rmqnamesrv-a
- rmqnamesrv-b:rmqnamesrv-b
networks:
rmq:
aliases:
- rmqbroker-a
rmqbroker-b:
image: rocketmqinc/rocketmq:4.3.0
container_name: rmqbroker-b
ports:
- 10909:10909
volumes:
- /opt/rocketmq/logs/broker-b:/opt/logs
- /opt/rocketmq/store/broker-b:/opt/store
- /home/rocketmq/broker-b/broker-b.conf:/opt/rocketmq-4.3.0/conf/broker.conf
environment:
TZ: Asia/Shanghai
NAMESRV_ADDR: "rmqnamesrv-b:9876"
JAVA_OPTS: " -Duser.home=/opt"
JAVA_OPT_EXT: "-server -Xms256m -Xmx256m -Xmn256m"
command: sh mqbroker -c /opt/rocketmq-4.3.0/conf/broker.conf autoCreateTopicEnable=true &
links:
- rmqnamesrv-a:rmqnamesrv-a
- rmqnamesrv-b:rmqnamesrv-b
networks:
rmq:
aliases:
- rmqbroker-b
rmqconsole:
image: styletang/rocketmq-console-ng
container_name: rmqconsole
ports:
- 9001:9001
environment:
JAVA_OPTS: -Drocketmq.namesrv.addr=rmqnamesrv-a:9876;rmqnamesrv-b:9877 -Dcom.rocketmq.sendMessageWithVIPChannel=false
networks:
rmq:
aliases:
- rmqconsole
networks:
rmq:
name: rmq
driver: bridge
从配置文件可以大致看出几点:
1、通过Docker总共拉取了5条镜像记录。rmqnamesrv-a
、rmqnamesrv-b
、rmqbroker-a
、rmqbroker-b
、rmqconsole
。
2、rmqconsole是一个可视化的工具
,可以通过页面来查看RocketMQ相关信息。
3、可以看出对于broker的配置文件broker.conf已经被broker-a/b.conf替换。
二、环境部署
上面仅仅是把配置文件写好,但启动RocketMQ还有很多前提条件
1、所需环境
1. 建议使用64位操作系统,Linux / Unix / Mac;
2. 64位JDK 1.8+;
3. Maven 3.2.x;
4. Git的;
5. 4g +免费磁盘用于Broker服务器
同时需要运行docker-compose.yml还需要安装docker-compose
2、安装docker-compose
有关的介绍可以看官方GitHub : https://github.com/docker/compose
安装docker-compose
curl -L https://github.com/docker/compose/releases/download/1.24.1/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose
查看是否安装成功
[root@izbp13196wp34obmnd4avdz ~]# docker-compose --version
docker-compose version 1.24.1, build 4667896b
3、关键一步
前面所有的操作,都是为这一步的铺垫,通过docker-compose执行docker-compose.yml
配置文件
docker-compose -f docker-compose.yml up -d
如果你看到,那说明你大功告成!
查看可视化界面
完美!
注意
这里面涉及相关端口记得到阿里云后台开启!说明
因为我这边是单机部署集群,我发现集群部署NameServer没有问题,集群部署Broker的会因为端口的问题只能使用一台。
上篇博客讲解了服务器集群部署RocketMQ 博客地址:RocketMQ(2)---Docker部署RocketMQ集群
这篇在上篇搭建好的基础上,将SpringBoot整合RocketMQ实现生产消费。
GitHub地址
: https://github.com/yudiandemingzi/spring-boot-study
一、搭建步骤
先说下技术大致架构
SpringBoot2.1.6 + Maven3.5.4 + rocketmq4.3.0 + JDK1.8 +Lombok(插件)
1、添加rocketmq包
<!--注意: 这里的版本,要和部署在服务器上的版本号一致-->
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
<version>4.3.0</version>
</dependency>
2、JmsConfig(配置类)
连接RocketMQ服务器配置类,这里为了方便直接写成常量。
/**
* @Description: 安装实际开发这里的信息 都是应该写在配置里,来读取,这里为了方便所以写成常量
*/
public class JmsConfig {
/**
* Name Server 地址,因为是集群部署 所以有多个用 分号 隔开
*/
public static final String NAME_SERVER = "127.12.15.6:9876;127.12.15.6:9877";
/**
* 主题名称 主题一般是服务器设置好 而不能在代码里去新建topic( 如果没有创建好,生产者往该主题发送消息 会报找不到topic错误)
*/
public static final String TOPIC = "topic_family";
}
3、Producer (生产者)
@Slf4j
@Component
public class Producer {
private String producerGroup = "test_producer";
private DefaultMQProducer producer;
public Producer(){
//示例生产者
producer = new DefaultMQProducer(producerGroup);
//不开启vip通道 开通口端口会减2
producer.setVipChannelEnabled(false);
//绑定name server
producer.setNamesrvAddr(JmsConfig.NAME_SERVER);
start();
}
/**
* 对象在使用之前必须要调用一次,只能初始化一次
*/
public void start(){
try {
this.producer.start();
} catch (MQClientException e) {
e.printStackTrace();
}
}
public DefaultMQProducer getProducer(){
return this.producer;
}
/**
* 一般在应用上下文,使用上下文监听器,进行关闭
*/
public void shutdown(){
this.producer.shutdown();
}
}
4、Consumer (消费者)
@Slf4j
@Component
public class Consumer {
/**
* 消费者实体对象
*/
private DefaultMQPushConsumer consumer;
/**
* 消费者组
*/
public static final String CONSUMER_GROUP = "test_consumer";
/**
* 通过构造函数 实例化对象
*/
public Consumer() throws MQClientException {
consumer = new DefaultMQPushConsumer(CONSUMER_GROUP);
consumer.setNamesrvAddr(JmsConfig.NAME_SERVER);
//消费模式:一个新的订阅组第一次启动从队列的最后位置开始消费 后续再启动接着上次消费的进度开始消费
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);
//订阅主题和 标签( * 代表所有标签)下信息
consumer.subscribe(JmsConfig.TOPIC, "*");
// //注册消费的监听 并在此监听中消费信息,并返回消费的状态信息
consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
// msgs中只收集同一个topic,同一个tag,并且key相同的message
// 会把不同的消息分别放置到不同的队列中
try {
for (Message msg : msgs) {
//消费者获取消息 这里只输出 不做后面逻辑处理
String body = new String(msg.getBody(), "utf-8");
log.info("Consumer-获取消息-主题topic为={}, 消费消息为={}", msg.getTopic(), body);
}
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
return ConsumeConcurrentlyStatus.RECONSUME_LATER;
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});
consumer.start();
System.out.println("消费者 启动成功=======");
}
}
大致就是这边简单,下面就是测试。
二、测试
先写个测试接口进行测试。
1、Controller
@Slf4j
@RestController
public class Controller {
@Autowired
private Producer producer;
private List<String> mesList;
/**
* 初始化消息
*/
public Controller() {
mesList = new ArrayList<>();
mesList.add("小小");
mesList.add("爸爸");
mesList.add("妈妈");
mesList.add("爷爷");
mesList.add("奶奶");
}
@RequestMapping("/text/rocketmq")
public Object callback() throws Exception {
//总共发送五次消息
for (String s : mesList) {
//创建生产信息
Message message = new Message(JmsConfig.TOPIC, "testtag", ("小小一家人的称谓:" + s).getBytes());
//发送
SendResult sendResult = producer.getProducer().send(message);
log.info("输出生产者信息={}",sendResult);
}
return "成功";
}
}
2、测试结果
很明显生产发送消息已经成功,二消费者也成功接收了消息!
另外我们再来看下RocketMQ控制台是否也有消费记录
很明显在控制台这边也会有消费记录!
总结
这边只是简单的整合,后面会通过RocketMQ实现分布式事务,可以用于线上实际环境中,到时候会深入讲解下源码。
RocketMQ的核心配置在broker.conf配置文件里,下面我们来分析下它。
一、broker.conf配置
下面只列举一些常用的核心配置讲解。
1、broker.conf核心配置讲解
# nameServer地址,如果nameserver是多台集群的话,就用分号分割
namesrvAddr=172.1.21.29:9876;143.13.262.43:9876
# 所属集群名字(同一主从下:Master和slave名称要一致)
brokerClusterName=rocketmq-cluster
# broker名字,注意此处不同的配置文件填写的不一样 例如:在a.properties 文件中写 broker-a 在b.properties 文件中写 broker-b
brokerName=broker-a
# 0 表示 Master,>0 表示 Slave
brokerId=0
# Broker 对外服务的监听端口
listenPort=10911
# 在发送消息时,自动创建服务器不存在的topic,默认创建的队列数。由于是4个broker节点,所以设置为4
# defaultTopicQueueNums=4
# 是否允许 Broker 自动创建Topic,建议线下开启,线上关闭
autoCreateTopicEnable=true
# 是否允许 Broker 自动创建订阅组,建议线下开启,线上关闭
autoCreateSubscriptionGroup=true
# commitLog每个文件的大小默认1G
mapedFileSizeCommitLog=1073741824
# ConsumeQueue每个文件默认存30W条,根据业务情况调整
mapedFileSizeConsumeQueue=300000
# 检测可用的磁盘空间大小,当磁盘被占用超过90%,消息写入会直接报错
diskMaxUsedSpaceRatio=90
# Broker 的角色: ASYNC_MASTER 异步复制Master ; SYNC_MASTER 同步双写Master; SLAVE
brokerRole=SYNC_MASTER
# 刷盘方式 ASYNC_FLUSH 异步刷盘; SYNC_FLUSH 同步刷盘
flushDiskType=ASYNC_FLUSH
2、同步刷盘 or 异步刷盘
同步刷盘和异步刷盘指的是 内存和磁盘 的关系。
RocketMQ的消息最终是是存储到磁盘上的,这样既能保证断电后恢复,又可以让存储的消息量超出内存的限制。
从客户端发送消息,一开始先写到内存,再写到磁盘上。如下图所示
两种策略
同步刷盘
:当数据成功写到内存中之后立刻刷盘(同步),在保证消息写到磁盘也成功的前提下返回写成功状态。
异步刷盘
:数据写入内存后,直接返回成功状态。异步将内存中的数据持久化到磁盘上。
同步刷盘和异步输盘的优劣
同步刷盘
优点
:保证了数据的可靠性,保证数据不会丢失。
缺点
:同步刷盘效率较低,因为需要内存将消息写入磁盘后才返回成功状态。
异步刷盘
优点
:异步刷盘可以提高系统的吞吐量。因为它仅仅是写入内存成功后,就返回成功状态。
缺点
:异步刷盘不能保证数据的可靠性。因为写入内存成功,但写入磁盘的时候因为某种原因写入失败,那就会丢失该条消息。
3、同步复制 or 异步复制
同步复制和异步复制指的是 Master节点和slave节点 的关系。
如果一个Broker组有Master和Slave,消息需要从Master复制到Slave上
两种策略
同步复制
: 当数据成功写到内存中Master节点之后立刻同步到Slave中,当Slave也成功的前提下返回写成功状态。
异步复制
: 当数据成功写到内存中Master节点之后,直接返回成功状态,异步将Master数据存入Slave节点。
同步复制和异步复制的优劣:
同步复制
: 数据安全性高,性能低一点。
异步复制
: 数据可能丢失,性能高一点。
建议
线上采用 同步复制 + 异步刷盘;
消息重试分为两种:Producer发送消息的重试和 Consumer消息消费的重试。
一、Producer端重试
Producer端重试是指: Producer往MQ上发消息没有发送成功,比如网络原因导致生产者发送消息到MQ失败。
看一下代码:
@Slf4j
public class RocketMQTest {
/**
* 生产者组
*/
private static String PRODUCE_RGROUP = "test_producer";
public static void main(String[] args) throws Exception {
//1、创建生产者对象
DefaultMQProducer producer = new DefaultMQProducer(PRODUCE_RGROUP);
//设置重试次数(默认2次)
producer.setRetryTimesWhenSendFailed(3000);
//绑定name server
producer.setNamesrvAddr("74.49.203.55:9876");
producer.start();
//创建消息
Message message = new Message("topic_family", ("小小今年3岁" ).getBytes());
//发送 这里填写超时时间是5毫秒 所以每次都会发送失败
SendResult sendResult = producer.send(message,5);
log.info("输出生产者信息={}",sendResult);
}
}
超时重试
针对网上说的超时异常会重试的说法都是错误的,想想都觉得可怕,我查的所以文章都说超时异常都会重试,难道这么多人都没有去测试一下 或者去看个源码。
我发现这个问题,是因为我上面超时时间设置为5毫秒 ,按照正常肯定会报超时异常,但我设置1次重试和3000次的重试,虽然最终都会报下面异常,但输出错误时间报
显然不应该是一个级别。但测试发现无论我设置的多少次的重试次数,报异常的时间都差不多。
org.apache.rocketmq.remoting.exception.RemotingTooMuchRequestException: sendDefaultImpl call timeout
针对这个疑惑,我去看了源码之后,才恍然大悟。
/**
* 说明 抽取部分代码
*/
private SendResult sendDefaultImpl(Message msg, final CommunicationMode communicationMode, final SendCallback sendCallback, final long timeout) {
//1、获取当前时间
long beginTimestampFirst = System.currentTimeMillis();
long beginTimestampPrev ;
//2、去服务器看下有没有主题消息
TopicPublishInfo topicPublishInfo = this.tryToFindTopicPublishInfo(msg.getTopic());
if (topicPublishInfo != null && topicPublishInfo.ok()) {
boolean callTimeout = false;
//3、通过这里可以很明显看出 如果不是同步发送消息 那么消息重试只有1次
int timesTotal = communicationMode == CommunicationMode.SYNC ? 1 + this.defaultMQProducer.getRetryTimesWhenSendFailed() : 1;
//4、根据设置的重试次数,循环再去获取服务器主题消息
for (times = 0; times < timesTotal; times++) {
MessageQueue mqSelected = this.selectOneMessageQueue(topicPublishInfo, lastBrokerName);
beginTimestampPrev = System.currentTimeMillis();
long costTime = beginTimestampPrev - beginTimestampFirst;
//5、前后时间对比 如果前后时间差 大于 设置的等待时间 那么直接跳出for循环了 这就说明连接超时是不进行多次连接重试的
if (timeout < costTime) {
callTimeout = true;
break;
}
//6、如果超时直接报错
if (callTimeout) {
throw new RemotingTooMuchRequestException("sendDefaultImpl call timeout");
}
}
}
通过这段源码很明显可以看出以下几点
- 如果是
异步发送
那么重试次数只有1次 - 对于同步而言,
超时异常也是不会再去重试
。 - 如果发生重试是在一个for 循环里去重试,所以它是立即重试而不是隔一段时间去重试。
真是实践出真知!!!
二、 Consumer端重试
消费端比较有意思,而且在实际开发过程中,我们也更应该考虑的是消费端的重试。
消费者端的失败主要分为2种情况,Exception
和 Timeout
。
1、Exception
@Slf4j
@Component
public class Consumer {
/**
* 消费者实体对象
*/
private DefaultMQPushConsumer consumer;
/**
* 消费者组
*/
public static final String CONSUMER_GROUP = "test_consumer";
/**
* 通过构造函数 实例化对象
*/
public Consumer() throws MQClientException {
consumer = new DefaultMQPushConsumer(CONSUMER_GROUP);
consumer.setNamesrvAddr("47.99.203.55:9876;47.99.203.55:9877");
//订阅topic和 tags( * 代表所有标签)下信息
consumer.subscribe("topic_family", "*");
//注册消费的监听 并在此监听中消费信息,并返回消费的状态信息
consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
//1、获取消息
Message msg = msgs.get(0);
try {
//2、消费者获取消息
String body = new String(msg.getBody(), "utf-8");
//3、获取重试次数
int count = ((MessageExt) msg).getReconsumeTimes();
log.info("当前消费重试次数为 = {}", count);
//4、这里设置重试大于3次 那么通过保存数据库 人工来兜底
if (count >= 2) {
log.info("该消息已经重试3次,保存数据库。topic={},keys={},msg={}", msg.getTopic(), msg.getKeys(), body);
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
//直接抛出异常
throw new Exception("=======这里出错了============");
//return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
} catch (Exception e) {
e.printStackTrace();
return ConsumeConcurrentlyStatus.RECONSUME_LATER;
}
});
//启动监听
consumer.start();
}
}
这里的代码意思很明显: 主动抛出一个异常,然后如果超过3次,那么就不继续重试下去,而是将该条记录保存到数据库由人工来兜底。
看下运行结果
注意
消费者和生产者的重试还是有区别的,主要有两点
1、默认重试次数:Product默认是2次,而Consumer默认是16次。
2、重试时间间隔:Product是立刻重试,而Consumer是有一定时间间隔的。它照1S,5S,10S,30S,1M,2M····2H
进行重试。
3、Product在异步情况重试失效,而对于Consumer在广播情况下重试失效。
2、Timeout
说明
这里的超时异常并非真正意义上的超时,它指的是指获取消息后,因为某种原因没有给RocketMQ返回消费的状态,即没有return ConsumeConcurrentlyStatus.CONSUME_SUCCESS
或 return ConsumeConcurrentlyStatus.RECONSUME_LATER
。
那么 RocketMQ会认为该消息没有发送,会一直发送。因为它会认为该消息根本就没有发送给消费者,所以肯定没消费。
做这个测试很简单。
//1、消费者获得消息
String body = new String(msg.getBody(), "utf-8");
//2、获取重试次数
int count = ((MessageExt) msg).getReconsumeTimes();
log.info("当前消费重试次数为 = {}", count);
//3、这里睡眠60秒
Thread.sleep(60000);
log.info("休眠60秒 看还能不能走到这里。topic={},keys={},msg={}", msg.getTopic(), msg.getKeys(), body);
//返回成功
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
当获得 当前消费重试次数为 = 0 后 , 关掉该进程。再重新启动该进程,那么依然能够获取该条消息
consumer消费者 当前消费重试次数为 = 0
休眠60秒 看还能不能走到这里。topic=topic_family,keys=1a2b3c4d5f,msg=小小今年3岁