消息队列全文
策略
规避策略
- 选择队列策略
- Topic可以创建在多个节点的Broker上
- 发送失败,进行重试,默认是2
- 如果重试还失败,进行规避策略
选择另一个broker进行尝试发送
故障延迟机制策略
- 记录Broker发送时长,算出规避时长,在这个时间之前都会标记这个broker为故障
- 指定的broker发送失败,不会发送给故障节点,轮询选择其他节点发送
MQFaultStrategy
- 定义了故障延迟机制的规避时间
- 经验值
// 发送时长达到了 latencyMax[i]ms 规避 notAvailableDuratio[i]ms
private long[] latencyMax = new long[]{50L, 100L, 550L, 1000L, 2000L, 3000L, 15000L};
private long[] notAvailableDuration = new long[]{0L, 0L, 30000L, 60000L, 120000L, 180000L, 600000L};
- 计算发送
private long computeNotAvailableDuration(long currentLatency) {
for(int i = this.latencyMax.length - 1; i >= 0; --i) {
if (currentLatency >= this.latencyMax[i]) {
return this.notAvailableDuration[i];
}
}
return 0L;
}
负载均衡
- 生产者,轮询发送给各个broker的队列
- 消费者
- 默认,依次均分
- ByCircle,每个消费者轮一下
面试题
为什么使用消息队列
- 结合业务场景
- 解耦
- A系统发送给BCD三个系统,如果新增一个E系统,改动比较大
- 模块和模块之间是否可以不同步调用,通过消息队列实现异步解耦
- 异步,延迟减少
- 削峰,消费者固定消费消息,不会被流量突增影响
消息队列缺点
- 系统的外部依赖增多,系统可用性降低
- 系统复杂性提高,要保证消息不丢失
- 一致性问题,A系统处理完之后直接返回,但是有某个子系统消费失败,导致数据不一致
为什么选择RocketMQ
- 性能,阿里技术支撑,性能高;可靠性好;可用性高,易扩展
- 功能,功能完善
- 易用,跨平台,跨语言,多协议介入
源码
- 结构分层
- 消费者和生产者本质上都是Netty的客户端,本质上都是
MQClientInstance
- 消费者和生产者通过NRC请求功能号,向
broker
进行请求broker
启动时会注册功能号
Broker启动流程
BrokerStartup
启动类- 整体流程
流程
- 启动netty的客户端和服务端
- 客户端对应生产者和消费者
- 服务端对应broker
final NettyServerConfig nettyServerConfig = new NettyServerConfig();
final NettyClientConfig nettyClientConfig = new NettyClientConfig();
- 初始化
- 加载启动时broker/store/config文件夹下的各类主题配置
- 消费进度,订阅信息
- 加载消息存储文件
this.messageStore
,初始化commitLog/cosumequeue
文件,通过mmap
进行内存映射,文件存储在写时复制队列中- 启动
NettyRemotingServer
,也就是broker的服务this.registerProcessor()
注册功能处理器,处理客户端的请求,以功能号的形式注册,每个功能的都有唯一的功能号- 开启
schedule
定时任务this.scheduledExecutorService.scheduleAtFixedRate
controller.initialize();
- 添加钩子方法
Runtime.getRuntime().addShutdownHook
,关闭时释放资源 - 启动
controller.start();
,启动各类组件
- 消息存储/远程服务/过滤监控/对外通信
- 每隔30秒向
nameServer
发送心跳包
生产者启动流程
流程
DefaultMQProducer.start()
实际上调用this.defaultMQProducerImpl.start();
- 检查当生产者的状态,第一次进都是刚启动
CREATE_JUST
- 检查配置
this.checkConfig();
,其实就是校验分组 - 创建一个MQ的客户端实例,并通过
clientId
进行复用,注册到生产者缓存
- 从broker的角度出发,消费者和生产者都是一个客户端
clientId
,由本地IP+实例名称(一般是不变的)+unitName
(默认是null)- 一个服务只能有一个组的一个生产者
- 真正启动生产者
mQClientFactory.start();
- 启动NRC,Netty Remote Client
- 启动定时任务,获取路由地址/获取路由信息/进行主题更新/与broker进行心跳
- 启动服务,拉取消息的服务/负载均衡服务
- 启动完成后,立即向broker发心跳
并发消费流程
- 通过
start
中的定时任务this.startScheduledTask();
,获取路由地址和路由信息 - 进行负载均衡,通过
this.rebalanceService.start();
启动的守护线程,调用this.mqClientFactory.doRebalance();
获取组的消费者列表
this.rebalanceByTopic(topic, isOrder);
根据广播模式和集群模式做不同的处理- 集群模式,用主题和组名,通过
broker
向nameserver
获取所有该主题、该组的所有消费者信息
- 获取分配策略
AllocateMessageQueueStrategy strategy = this.allocateMessageQueueStrategy;
,并调用allocate
根据策略,为消费者绑定队列
- 底层通过for循环,根据策略,获取指定的队列,并返回
AllocateMessageQueueAveragely
均分AllocateMessageQueueAveragelyByCircle
取模的方式,轮流分
- 获取对应Queue的消费偏移量
this.updateProcessQueueTableInRebalance(topic, allocateResultSet, isOrder);
- 遍历每一个mq,进行`this.computePullFromWhere(mq
- 如果是广播消费模式,通过本地信息计算;如果是集群,通过远端信息计算
- 通过NRC功能号进行获取
QUERY_CONSUMER_OFFSET
- 通过
start
中的线程this.pullMessageService.start();
,调用pullMessage
,最终调用this.pullAPIWrapper.pullKernelImpl
拉取Queue消息
this.mQClientFactory.getMQClientAPIImpl().pullMessage
调用NRC的拉取方法,封装了请求头,包含topic、偏移量、数量等信息;根据不同的模式(同步、异步、oneway)去获取,默认是异步获取- 请求
PULL_MESSAGE
功能号- 推模式也是基于拉模式实现的
- 更新Queue消息消费的Offset,启动定时任务来更新偏移量,
start
中的this.startScheduledTask();
,会开启持久化消费进度的定时任务MQClientInstance.this.persistAllConsumerOffset();
,每隔五秒进行
persistConsumerOffset()
根据广播还是集群选择持久化方式- 集群,通过远程更新,调用
this.updateConsumeOffsetToBroker(mq, offset.get());
- 最终也就是通过
UPDATE_CONSUMER_OFFSET
实现更新偏移量
shutDown
,最终也就是通过NRC调用UNREGISTER_CLIENT
功能号,并关闭其他组件守护线程