1 同步发送 可以拿到结果,等拿到发送的结果才能进行下一步
2 异步发送 可以拿到结果但不需要等拿到结果也能进行下一步,有回调方法可以直到是否发送成功
3 单向发送 拿不到结果,与下一步间隔极小 没有回调方法,不关心消息是否发送成功,也是异步的。
rocketMq 主要有nameServer 和broker
nameServer 和微服务注册中心类似,它记录了topic 和broker 的路由规则,并通过长连接监听broker 是否存活
broker 实际存储消息的地方 一般是一主多从的结构,通过选举与raft 算法防止单点故障 内部有多个MessageQueue ,每个MessageQueue 都是有序的,broker 为MessageQueue 提供了负载均衡策略,broker 会按消费者topic的需求分配对应topic的消息
消费者一开始会从masterBroker 拉取,第二次会听取第一次masterBroker的建议 是从主拉还是从从节点拉取
Topic 与Tag都是对消息的分类 Topic 相当于一级目录,Tag相当于二级目录 ,在实际的使用场景中,Topic-order\Topic-pay,Topic-order 下又分为product-tag、amount-tag 等
MessageQueue 与Topic Tag 的关系
MessageQueue 是broker 的物理分区,便于实现负载均衡
Topic 和Tag是消息的物理属性 ,只方便消费者过滤数据使用
一个 Topic下面有多个MessageQuegue ,但每个MessageQueue 可能含有相同的tag,也可能含有不同的Tag ,对消费者过滤数据没有影响。
Tag 内部是由索引实现的,效率比较高
perperty 可以用类sql过滤,但是放在消息体上,效率较低;
官方建议:一般消费者做接口幂等时,取perperty 固定的key keys 设置唯一标识,setKeys()
顺序消费通过绑定传参hash固定来控制发送的消息在一个固定的topic 的MessageQueue 实现顺序消费
消费模式分为两种 一种是广播 另一种是集群
集群模式是默认的 在同一个消费者组,一条消息只能被消费者组里的一个节点消费
广播模式 指的是一个消费者组的多个节点可以同时消费一条消息
重试策略 如果消息发送失败抛出异常默认会进行16此重试,重试的间隔由1s,5s,10s依次底层 直到4个半小时,可以通过循环计数+sleep 优化为在5s内重试
延迟策略与重试策略类似 也是进行18次延时,时间间隔依次递增,花钱买阿里云的rocketMq可以指定固定的时间间隔
在设置消息监听器时,消费模式ConsumeMode 分为两种 CONCURRENTLY 并发、ORDERLY 严格顺序。
并发模式下 线程不会顺序执行 topic 不会按照放入MessageQueue的顺序取 而是随机取
严格顺序 只会放在一个MessageQueue中 且发生异常时后面的消息不会被消费,且重试的次数间隔不像普通消息,很频繁间隔很短。
事务性消息是为了解决生产者无法感知消息在传输过程中是否发送成功而存在的
如果在接收结果时发送异常,如果回滚 consuer已经可以消费了
如果在发送消息时发送异常,如果提交 broker还没拿到消息
以上两种情况都会造成数据不一致
如何解决,完整流程
1 生产者会发送一个半事务消息到broker 这个消息对consumer 是不可见的
2 生产者不会异步执行本地事务,而是等半事务消息返回成功,才开始执行本地事务,这样做是确保broker 存在了半事务消息。如果本地事务执行成功 会向broker 发送comit 命令 将半事务消息可见,执行失败会发送rollback 命令 删除掉boker的半事务消息
3 如果本地事务一直未发送命令,boroker 会回查本地事务是否已经执行成功,成功或失败重复2步骤
幂等 由于 Mq 的重试机制无法保证只发一次,在消费者者端设置接口幂等,保证数据安全
消息丢失 1同步发送 拿到返回值进行下一步,效率低
2 (1) broker数据刷盘 (2) 搭建Delagte 基于Raft 的高可用架构,添加Controller 组件
3 消费者手动ack 拿到数据先处理消费者逻辑,最后返回消费成功。4 消费者端手动提交Offset偏移量
重复消费 消费者端接口幂等,消费者是集群的话双重检查锁
顺序 MessageQueue 一个队列保证,如果消费者是集群,并发下生产者与消费者速率不匹配
拆分数据条数,每个数据只发到一个队列 保证有序
1 解耦:
远程调用 一个服务同时调用多个服务 先放到mq 异步处理,减少代码逻辑
服务a 掉服务b,c 又要满足a,b,c的顺序 如何保证顺序
1 abc 三个服务消息都发到一个队列里 (串行)
2 发送事务消息,消费者端控制abc 顺序 未满足顺序 直接抛异常,利用事务补偿机制,生产者重新分别发送重新发送 (并发)
3 链式调用 a->b->c ->发消息
2 异步 让消费者去主动等待消费,实现异步
3 削峰 通过积压的方式,消费者根据自身的消费能力只消费队列中的部分数据。
消费者注册的监听器如果是consumerLater,会出发重试,与延迟类似,但延迟是18次,重试是16 次
底层实现是创建一个对指定消费者组失败的队列,进行重试,如果重试到最后依然没有成功,会放到死信队列里,默认保存三天,默认配置是不可读不可写,在处理或手动补偿时要重新设置。
在整合springBoot,过程中一般都使用XXXTemplate ,,事务消息,会调用对应事务消息的api,同时,为了满足spring的风格,需要我们自行配置一个bean作为消息监听器,如果有多个事务消息,可以用ExtRocketMqTemplate 注解自定义多种template,,在每个事务监听器指定ExtRocketMqTemplate Bean 的名字
版本号与spring 版本号不兼容时如何处理? 默认配置需要看对应源码的参数
例如,Spring 在封装RocketMqTemplate 时 通过BeanPostProcess 将producer\consumer放到spring容器中,又通过实现initliceBean 接口完成初始化,在处理逻辑时,会扫描含有指定注解的类,并提取参数,通过查看是哪些参数,我们就可以通过修改配置文件修改对应参数来使版本兼容。
rocketMq consumer producer 分别利用命令行判断是否存在c,p 实现对应的功能,例如
当命令行存在 -c 命令会读取指定路径的配置文件的配置信息
存在-p 命令时会打印所有配置参数,但不启动实例
如何阅读源码?
1 带着问题去读 某个点不理解,或者想印证某个实现方式
2 第一次读只求初步理解,愈往后再度理解愈深
3 加TODO 注释帮我们快速定位 过去的理解
orderMessageEnable 是配置是否启用全局顺序消息,全局顺序消息一个topic 下只有一个MessageQueue 性能较低且吞吐量少,一般默认值为false ,可以手动开启。
分局顺序消息更为常用,区别是一个topic 对应多个MessageQueue ,在一个分区(一个MessageQueue)是有序的,不同分区(不同MessageQueue)是无序的。
三个config: nettyServerConnfig nettyClientConfig nameServerConfig
核心组件: 服务端接收客户端请求 nameServerController \远程客户端组件、远程服务端组件、路由组件、状态组件
ControllerManager 组件 管理高可用,主从模式主节点宕掉后完成主从切换
nameServer 作为注册中心,为生产者、消费者,寻找并确定数据在broker 的位置
所以nameServer 一般作为服务端存储路由,而自身的客户端是嵌入到生产者、消费者和broker中
broker 实际数据的存储位置,起到接收生产者发送的消息,向消费者推送消息的作用。自身只作为服务端,没有实际的客户端概念,生产者、消费者作为客户端
采用的是pushConsumeer(伪pull模式)
内部封装了pull模式的实现,基于pull模式,实现的长轮询
nettyRemoteServer 作为nettey 长连接的服务端,用于处理生产者推送的消息和消费者拉取的消息
nettyRemoteClient 作为netty 长连接的客户端,周期性向nemeServer 发送心跳
RemoteServer 是一般通道,默认端口号是10911 ,可以自行配置改变端口号
FastRemoteServer 是vip通道 比RemoteServer 处理更快,不能自定义端口号,端口号取RemoteServer的端口号-2
通过netty 传输,每个次长连接都会封装成RemotingCommad
rocketMq 封装的netty协议的框架分为客户端和服务端,客户端与服务端均采用相同的模式
当收到请求是会根据请求对应的code 将请求交给对应的process 处理,如果是同步的将处理结果直接返回
如果是异步的会将处理结果缓存到客户端 requestId,responseFuture 的 responseTable中,等待异步请求返回对应的请求id ,从缓存表中取出回调Fulture处理结果。
定时任务每1s 扫一次
检查是否过期逻辑: 更新时间+允许间隔>当前时间
过期后1 清除responseTable 2 触发回调回调通知客户端连接已断 3关闭channel
1 程序启动时broker 向nameServer 注册broker
broker 会拿到所有nameServer 信息,遍历的发送请求到指定的nameServe 去注册broker ,注册broker 会生成brokerLiveTable 存活的broker信息 brokerAddrTable broker 的地址信息
2 broker 发送心跳告知nameSerer 是否存活
心跳发送后会更新brokerLiveTable 更新时间,nameServer 内部有定时任务,会判断更新时间+间隔时间是否>当前时间,如果不是判断不存活,同时删除brokerLiveTable\brokerAddrTable 对应的broker 信息,并通知客户端broker下线
1 producerClientApi 负责发送消息、事务消息的回查,作为client 既能发送请求,也能处理请求。
2 producer 会存储一个生产者组,以及对应的组内的index ,方便事务消息回查时找到指定的producer
3 producer 有broker、topic 对应的缓存地址,每次启动拉取缓存,本地有从本地取,本地没有再从远程拉取,后台有定时任务定时更新缓存,如果长时间无法更新缓存,producer也不可用
4 producer 同步发送消息失败,进行重试,在for循环中判断结果是否为空,不为空才跳出循环,同时,for循环的最大值取配置文件的配置参数设置
核心问题
producer 是如何找到指定topic 下对应的MessageQueue 的?
启动时会拿到topic 的缓存信息,内部是List
开始遍历
1 通过共享原子类每次+1 对集合长度取模 实现每个消息都会发送到下一个MessageQueue 实现均匀发送
2 内部错误检查机制:
设置变量broker 如果上一次没返回,下次循环会拿到上次循环的broker,也就是上次发送失败的broker
下次循环会判断下次的MessageQueue 是否是属于上次失败的broker ,如果是则过滤掉不返回,不是则返回对应的MessageQueue,最大程度地保证消息能发送成功
1 广播与集群 广播模式是存在客户端本地offset,集群模式offset是存储在broker 服务端 offset.json
2 消费者负载均衡 拿到broker 所有MessageQueue 和consumer id 集合 选择分配策略,如平均分配,轮询分配,环形数组分配,内部应用策略模式,一个接口,注入对应实现类调用allocate。
3 顺序模式与并发模式
顺序模式,生产者通过指定messgaeSelect 都发往一个MessageQueue,消费者通过加锁,保证只有一个线程消费一个MessgaeQueue 实现顺序
并发模式 不指定MessageSelect 轮询分配,跳过上次失败的broker,最大程度地负载均衡,消费者指定分配策略 平均、轮询、环形等
判断依据是根据监听器的类型order/currently 调用对用的Service 处理
持久化文件 commitLog 生产者每次法消息都会存到该文件中
consumerQueue commitLog的索引文件 记录了消息在commitLog的偏移量,消息的具体大小,Tag, 加快检索速度,只读某个位置后续多少量的消息数据确定某条消息
indexFile 文件 消费者端持久化文件,主要保存了commitLog的偏移量,下个消息的偏移量,基于时间戳构建,消息发送的时间戳、sql92 的key
rocketmq每次发消息都放到一个文件commitLog 文件中
Kafuka 每次发消息都要放到对应的partitionl 文件中,先找到对应的 partition ,在找对应partition文件名的日志文件,再放进入,文件越多查找效率越慢。
顺序写:用一个变量去接收当前文件数据写到的位置,只在后面追加,不用查找写入位置,
在循环中,每次写入后保存写入后的位置,下次循环从上次保存过的位置继续向后追加,直至不需要写入数据
刷盘 用户态调用FileChannel.force() 将缓存页刷入磁盘
同步刷盘 内部有两个队列 读队列与写队列 每次写入数据加入写队列,每过10毫秒 交换读队列与写队列的内容
刷盘时从写队列取,10毫秒内存在数据丢失的风险,但实践中发现不怎么丢失,写一个消息刷一次盘
异步刷盘 间隔由配置文件配置 两种 一种粗犷,使用堆外内存,一次刷一个文件,一种一部分一部分地刷,使用堆内内存
consumeQueue 采用顺序写,通过偏移量X固定单元大小,顺序写,有Tag 索引 速度快
indexFile 通过put
两者都是程序起后台线程定时扫描CommitLog,与commitog 存在不同步的情况 ,有about会补全不同步的数据索引。
日志文件都是固定大小 ->总有超出大小的一天
为什么设计成固定大小 ,方便映射 根据偏移量 计算位置,补全
过期文件删除策略怎样做?
两个定时任务 1 commitLog 文件删除策略 2comsumeQueue 和indexFile 文件删除策略
重点 commitLog 文件删除策略 条件 1 是否每天凌晨4点 2是否磁盘已满 3 是否开始手动删除
判断是否过期 默认保留3天 即72 小时 超过即过期
删除 进行二次删除,防止第一次删除失败的未删除
正常发送消息 提交到commitLog
判断消息有无延迟标识,有的话根据级别,偷偷地改topic, 原Topic 放到 property中 扔给对应的系统队列
后台线程池每100毫秒扫描队列中是否达到指定延迟时间,达到后根据偏移量在从commitLog 找到原消息,更改回原topic ,再次调MessageStore 的方法转移给指定队列,等待消费者不停地拉取。
固定时间点的是通过时间轮实现的
通过放入不同的MessgageQueue 对消息进行过滤 最后放入一个LocalBuffer 中时间轮每个slot 存放着LocalBuffer 的偏移量,扫描到了在取出数据放到对应的MessageQueue 在放回原来的队列
rocketmq 主要采用推模式和拉模式,但本质都是消费者pull 模式,这样建立的连接,频繁连接损失性能,不频繁连接又丢失消息
设计思路优化:
broker 端先缓存consumer 的拉请求,同时阻塞住,缓存里有Topic 信息,当消息发送时,会从commitLog取出对应的Topic与对应的缓存匹配相同的topic,如果匹配到了,就用长连接chanel 执行一次写入write
方法a 匹配不到加入缓存 调方法b 方法b 调a(false) 再判断 ,如果还不配就不加入缓存,等待下次匹配
1 顺序写
2 刷盘
3 零拷贝
1 通过提前声明映射一块连续的内存空间,来保证每次写文件都会到固定区域,数据不分散
2 通过往后追加的形式,保证数据集中,同时维护往后追加结束的位置,减少内核态寻址操作,在用户态就可以定位到下次写的位置。
常见的刷盘机制了两种
1 原始文件io 刷盘 stream.write,steream.flush
write 写 循环中每次写write 写完后立即关闭文件
2 用户态 fileChannel.force
先存所有写入内容,调一次write ,不会立即close ,与close 之间调一此fsync 把内核态文件的状态通知到用户态文件存储
类比同类型Mq 产品的刷盘方式
KafuKa 批量+定时 (默认值) 批量 消息达到多少数据量,刷一次盘,数据量是Long.Max
可以自己设置 定时 某个时间段到了 刷一次盘,时间段null 交给操作系统
RabitMq 无法做到同步刷盘 不刷盘 只能配合客户端做幂等解决
都不如RocketMq 刷盘方式稳定
内核态数据拷贝到内存或者磁盘需要cpu 但cpu 资源非常珍贵不适合做简单的复制搬运操作,通过DMA 搬运,总线复制,解决了内核态的拷贝问题,但用户态没有DMA 解决不了拷贝问题
用户态解决思路: 存储内核态数据索引,操作内核态数据的索引来实现数据修改,还是通过cpu拷贝,传输数据的操作,在内核态执行操作
零拷贝 减少cpu 拷贝的拷贝
DMA 拷贝: 只搬运数据 通常发生在内核态
零拷贝的两种实现方式
mmap:通过在用户态拿到内核态数据的索引,长度等关键信息,进行拷贝,从而减少内核态与用户态之间的拷贝,但内核态与内存的拷贝还是依靠cpu 拷贝
sendFile:用户态发送拷贝命令 拷贝完整文件,全用DMA 拷贝 忽视了数据细节,但性能高
三种节点的圈
一种没边界 Flower
一种虚线边界 Candicate
一种实线边界Leader
一开始集群都是没边界的节点Flower,后来根据过期时间不同,
最短的过期时间的节点会生成带虚线的节点Condicate,这个节点会先给自己投再请求接收其他节点发送的投票,投完后这个节点会升级成带实线的节点Leader,其他节点投出后会重置过期时间
Leader 会向其他节点发送心跳,其他节点收到心跳后.为主会重置过期时间,保证不有其他主节点可能产生,并接收主节点的日志放到entry里 日志的有序entry
如果有4个节点,恰好有两个节点同时到过期时间 都到Candicate 状态,收到的投票数又相同,会重新分配过期时间重新选举投票,直到只有一个节点升级为Candicate状态,期间无法接收命令,保证集群内数据一致,是CP 模型
Leader
数据同步-》日志文件
发送心跳 其他节点收到心跳 重置过期时间
每个节点 分为两部分
第一部分 公共部分 包括 节点的任期 、记录投票投给了谁
日志entry 有序 包括 偏移量 id 含有两个指针 一个记录日志写入已经提交到哪
一个记录命令提交到状态机执行到哪
第二部分 StateMachine 含有两部分
第一部分是实际确定的已经执行的Map 命令
第二部分是已经提交到其他节点同步日志的Map 命令
delegage 无法切换为普通集群,因为底层数据结构不一样,CommitLog 与日志entry都存有二进制数据,但entry 还有偏移量任期等,切换后无法找到具体记录位置。
心跳与日志同步很相似,都是Leader 向其他节点发送RPC请求,其他的Rpc请求还有选举等
心跳与日志同步封装到一起,根据实体里是否含有entry 判断是心跳还是日志同步,公共请求参数偏移量,节点id,任期等参数是相同的
心跳 会保证其他节点更新任期,从而不会产生新选举的可能
日志同步会同步日志数据,也会更新任期,同时主节点收到从节点返回的响应后更新已发送请求的Map
order监听思路 出现异常是返回MessageQueue 阻塞消费的状态,从而保证顺序性
current并发监听思路 出现异常返回重试状态,将异常消息单独存放到重试队列中,之后的消息正常消费,
这样异常的消息与异常之后的消息无法保证顺序性
KafuKa: 1 消费者端单线程,采用锁锁住队列Patial与RocketMq 类似
2 出现异常会把异常消息放阻塞队列中 继而不能保证顺序性
RabitMq 1 必须设置一个消费者来消费
2 出现异常会把异常消息放到队尾,等待再次消费
结论 :实际场景中如果必须保证消息的顺序性,一定不能用KafuKa,RabitMq
消息幂等 生产者发消息幂等 消费者收消息幂等
生产者发消息:
RcoketMq 内部封装了从producer 到broker 的逻辑,如果broker 服务端已经收到过该消息,消息体会增加属性,消息id,如果发生重试只需判断幂等有无相同消息id
Kafuka 每个partial 都分配递增的序列号,broker 存一个写入的序列号,如果当前写入seq
RobitMq :与RocketMq 类似 只不过属性唯一标识的名字叫MessageTag
消费者接收消息:
RocketMq 可以根据MessageId 作为唯一标识判断幂等性,但一般不这么做,为了保证系统稳定不应该交由三方Mq产品来维持稳定,一般采用唯一业务id,如订单id放入自定义属性property "key" 来判断。
Kafuka:
RobitMq: 也可以用业务id 自定义属性的方式
RocketMq Kafuka 一定程度的积压是没问题的,如果积压一直变多,那么需要增加消费者消费能力
如RocketMq 同一个MessageQueue 只能被同一个消费者组的Consumer 消费,最大的消费能力就是consumer 数量=MessageQueue的数量
所以如果线上出现积压问题,可以采取临时方案,开启一个线程,消费旧的tpoic 发消息到新topic 新topic 增加MessageQueue 数量和consumer 数量 ,消费完成后要放回原topic 同时做好消息幂等。
传统的配置中心,如果是集群,要保持强一致,会定期发送心跳,然后同步数据
nameServer 因为允许客户端producer consumer 不必要拿到所有broker,只需找制定Topic 下的一个broker
所以nameServer 集群之间可以不同步。
常见的使用方式 集群模式 同一个topic 中的某条消息 只能被同一个消费者组的消费者所消费
这样设计是为了 messageQueue 与消费者端都记录了消息消费的偏移量,如果一个MessageQueue被多个comsumer 消费会产生锁竞争,offset 会不准,所以在单个MessageQueue,保证只有一个comsumer 线程拿到消息。
广播模式就是 解除 MessgaeQueue 与commsumer 的一对多的绑定关系,让topic 直接与comsumer 建立完全一对多的绑定关系,所以将锁转移到消费者端,让他们各自记录各自的offset,直接与topic绑定。