RocketMQ
RocketMQ 是阿里巴巴开源的一款分布式消息中间件,后捐赠给 Apache 基金会,成为顶级项目。具有 高吞吐、低延迟、高可用 的特点,适用于金融、电商、物联网等场景。
核心组件
- NameServer:轻量级注册中心,管理 Broker 路由信息,Producer 和 Consumer 通过 NameServer 获取到具体的 Broker 地址。
- Broker:消息存储与转发节点,分布式存储消息,主从架构,保证高可用性,支持同步/异步刷盘。
- Producer:消息生产者,支持同步/异步发送。
- Consumer:消息消费者,支持 Push/Pull 模式。
- Topic:逻辑主题,一个 Topic 可对应多个队列。
- MessageQueue:一下Topic下可以有多个MessageQueue,可以进行批量消费,但是这个MessageQueue是分了多个索引,消息本体是没有分开的。
他们的架构和运行如下:
- Producer 发送消息到 Broker,消息根据 Topic 和 Queue 存储在 Broker 中。
- Consumer 从 Broker 中消费消息,支持 Push 模式(主动拉取消息)和 Pull 模式(消费者定时拉取消息)。
- NameServer 提供 Broker 路由信息,Producer 和 Consumer 通过 NameServer 获取到具体的 Broker 地址。
- Broker 支持 集群部署,分布式存储消息,保证高可用性。
消息存储
RocketMQ 使用的是基于文件的存储方式,具体来说,它使用了两种存储结构:
- CommitLog:存储所有的消息,每个消息的长度固定(默认为4M),这样可以快速定位到具体的消息位置。注意了,是所有的Topic共用一个CommitLog,查找时是用ConsumeQueue这个索引文件去查找,这是和kafka很大不同的地方。
- ConsumeQueue:为每个主题维护一个索引文件,ConsumeQueue文件里存储的是一条消息对应在CommitLog文件中的offset偏移量。因为CommitLog是一整个,所以ConsumeQueue是详细记录了每个消息的位置,是一个密集索引,和kafka的稀疏索引是不一样的,因此它磁盘IO会更高,这也是RocketMQ吞吐量不如kafka的原因之一。
生产流程
当生产者的消息发送到一个Broker上时,Broker会对这个消息做什么事情?
- Producer 从 NameServer 获取 Broker 路由信息。
- 选择队列(默认轮询),发送消息到 Broker。
- Broker会把这个消息顺序追加写入磁盘上的日志文件CommitLog的末尾。CommitLog是磁盘上的文件,每次写入磁盘效率会很低,因此Broker是采用异步刷盘的方式提升效率。
- Broker会将这条消息在这个CommitLog文件中的文件偏移量offset,写入到这条消息所属的MessageQueue对应的ConsumeQueue文件中。
如何让消息写入CommitLog文件时的性能几乎等于往内存写入消息时的性能
这里是采用了这几种方式优化:
- 顺序追加:Broker会以顺序的方式将消息写入到CommitLog文件,也就是每次写入时就是在文件末尾追加一条数据即可,对文件进行顺序写的性能要比随机写的性能高得多。
- 页缓存:数据写入CommitLog文件时,不是直接写入底层的物理磁盘文件,而是先进入OS的PageCache内存缓存,然后再由OS的后台线程选一个时间,通过异步的方式将PageCache内存缓冲中的数据刷入到CommitLog磁盘文件。
异步刷盘模式虽然可以让消息写入的吞吐量非常高,但会有数据丢失的风险。
如果使用同步刷盘的模式,强制每次消息写入都要直接刷入磁盘,那么必然会导致每条消息的写入性能急剧下降,从而导致消息写入的吞吐量急剧下降。
高可用设计:基于DLedger技术的Broker主从同步原理
那Broker接收到消息写入请求后,是如何将消息同步给其他Broker做多副本冗余的呢?
RocketMq是由DLedger来管理CommitLog,替换掉原来由Broker自己来管理CommitLog。
DLedger是啥呢,DLedger是管理主从机器CommitLog的核心组件或者机制。它是基于 Raft 协议,会确保多数节点写入成功后才返回 ACK。
DLedger的核心角色:
- Leader:处理所有客户端请求,维护日志复制流程
- Follower:被动接收 Leader 的日志,参与选举和提交投票
- Candidate:选举期间的临时状态(Raft 协议特有)
DLedger在生产消息时, 基于Raft协议进行多副本同步的工作流程:
- Leader 接收请求Producer写入消息请求:消息写入本地未提交日志,DLedger会将数据标记为uncommitted状态。注意,这时候消息是写进了CommitLog,但是没有写入ConsumeQueue,因此没有分配它的offset,消费者也感知不到它。
- Follower并行复制:Leader 的DLedger会uncommitted状态的数据发送给Followers,Followers收到后会返回ACK。
- 多数派确认:Leader 收到超过半数Follower的 ACK 后,就将数据标记为committed状态。并且通知Followers将数据标记为comitted状态。这时候才会分配offset,并写入ConsumeQueue。
- 提交后的日志对消费者可见。
DLedger如何基于Raft协议选举Leader
Raft协议确保可以选出Broker成为Leader的核心设计就是:当一轮选举选不出Leader时,就让各Broker进行随机休眠,先苏醒过来的Broker会投票给自己,其他Broker苏醒后会收到发来的投票,然后根据这些投票也把票投给那个Broker。这样,依靠这个随机休眠的机制,基本上可以快速选举出一个Leader。
简单来说,三台Broker机器启动时,都会给自己投票选自己作为Leader,然后把这个投票发送给其他Broker。
比如Broker01、Broker02、Broker03各自先投票给自己,然后再把自己的投票发送给其他Broker。在第一轮选举中,每个Broker收到所有投票后发现,每个Broker都在投票给自己,所以第一轮选举是失败的。
接着每个Broker都会进入一个随机时间的休眠,比如Broker01休眠3毫秒,Broker02休眠5毫秒,Broker03休眠4毫秒。假设Broker01先苏醒过来,那么当它苏醒过来后,就会继续尝试投票给自己,并且将自己的投票发送给其他Broker。
接着Broker03休眠4毫秒后苏醒,它发现Broker01已经发来了一个投票是投给Broker01的。此时因为它自己还没有开始投票,所以会尊重别人的选择,直接把票投给Broker01了,同时把自己的投票发送给其他Broker。
接着Broker02休眠5毫秒后苏醒,它发现Broker01投票给Broker01,Broker03也投票给Broker01,而此时它自己还没有开始投票,于是也会把票投给Broker01,并且把自己的投票发送给给其他Broker。
最后,所有Broker都会收到三张投票,都是投给Broker01的,那么Broker01就会当选成为Leader。其实只要有(3 / 2) + 1个Broker投票给某个Broker,那么就会选举该Broker为Leader,这个半数加1就是大多数的意思。
消费流程
一个Topic上的多个MessageQueue是如何让一个消费者组中的多台机器来进行消费的?可以简单理解为,它会均匀将MessageQueue分配给消费者组的多台机器来消费。
这里的一个原则是:一个MessageQueue只能被一个消费者机器去处理,但是一台消费者机器可以负责多个MessageQueue的消息处理。
Push消费模式 vs Pull消费模式
RocketMq有两种消费模式,一个是Push模式、一个是Pull模式。
这两个消费模式本质上是一样的,都是消费者机器主动发送请求到Broker机器去拉取一批消息来处理。
Push消费模式是基于Pull消费模式来实现的,只不过它的名字叫做Push而已。在Push模式下,Broker会尽可能实时把新消息交给消费者进行处理,它的消息时效性会更好。
一般我们使用RocketMQ时,消费模式通常都选择Push模式,因为Pull模式的代码写起来更加复杂和繁琐,而且Push模式底层本身就是基于Pull模式来实现的,只不过时效性更好而已。
Pull消费模式
- 由客户端控制消费节奏:自行控制拉取频率和批量大小
- 长轮询优化:Broker 无消息时挂起请求(默认15秒),避免空轮询
- 需手动管理Offset:消费成功后需显式提交Offset
优点
- 灵活性高:可自定义拉取策略(如按业务峰值调整频率)
- 资源可控:避免 Broker 推送过量消息导致消费者过载
- 适合批量处理:单次可拉取多条消息减少网络开销
缺点
- 实时性较低:依赖轮询间隔,消息延迟较高
- 复杂度高:需自行处理队列分配、Offset管理等
适用场景
- 大数据分析:离线批量处理日志
- 资源受限环境:需精确控制内存使用的场景
- 自定义消费逻辑:如分片处理、特殊重试策略
Push消费模式
Broker 主动将消息推送给消费者(底层仍是Pull模拟),由服务端控制推送节奏。
- 客户端主动拉取:消费者发送请求到Broker去拉取消息时,如果有新的消息可以消费,那么就马上返回一批消息给消费者处理。消费者处理完之后,会接着发送请求到Broker机器去拉取下一批消息。
- 服务端长轮询推送:Broker 通过长轮询模拟"推送"效果,当拉取消息的请求发送到Broker,Broker却发现没有新的消息可以处理时,就会让处理请求的线程挂起,默认是挂起15秒。然后在挂起期间,Broker会有一个后台线程,每隔一会就检查一下是否有新的消息。如果有新的消息,就主动唤醒被挂起的请求处理线程,然后把消息返回给消费者。
- 自动Offset提交:消费成功默认自动提交
- 流式处理体验:开发者只需关注业务逻辑
优点
- 实时性高:消息到达后立即触发消费(延迟可低至毫秒级)
- 开发简单:无需管理Offset和轮询逻辑
- 负载均衡:自动处理队列分配和重平衡
缺点
- 背压控制难:突发流量可能导致消费者积压
- 资源占用多:Broker 需维护每个消费者的长轮询连接
适用场景
- 实时交易:订单处理、支付通知
- 事件驱动架构:微服务间异步通信
- 常规业务系统:追求开发效率的场景
Push 模式消息堆积了怎么办
- 增加消费者实例
- 提升 consumeThreadMax
- 检查消费逻辑是否阻塞(如同步DB调用)
消费者负载重平衡
消费者组出现宕机或扩容应如何处理,此时会进入一个Rebalance环节,也就是重新给各个消费者分配各自需要处理的MessageQueue。
比如现在机器01负责MessageQueue0和MessageQueue1,机器02负责MessageQueue2和MessageQueue3。如果现在机器02宕机了,那么机器01就会接管机器02之前负责的MessageQueue2和MessageQueue3。如果此时消费者组加入了一台机器03,那么就可以把机器02负责的MessageQueue3转移给机器03,然后机器01只负责一个MessageQueue2的消费,这就是负载重平衡。
消费模式
怎么实现顺序消费
我们知道一个Topic会有多个MessageQueue,一个消费者处理一个或者多个MessageQueue,那么消费时,要如何实现顺序消费呢?
RocketMq的实现思路有两个:
- 全局有序:只留一个MessageQueue,但是这样只能由一个消费者,消费效率很低,不建议选择
- 分区(局部)有序:同一业务标识(如订单ID)的消息分配到同一MessageQueue队列,保证该业务的消息顺序。
分区(局部)有序的实现方法如下:
- Producer发送消息时,将相同业务标识的消息必须发送到同一个队列(MessageQueue),例如对订单号哈希,得到具体的队列,通过 MessageQueueSelector 将需要顺序处理的消息发送到同一MessageQueue。
- Consumer对队列加锁,确保同一队列同一时刻只有一个线程消费(通过 MessageListenerOrderly 实现),这里可能会有小伙伴有疑问,既然我一个MessageQueue同时只能有一个消费者消费,那为什么还要加锁呢?这是因为消费者内部可能启动多线程(如 ConsumeThreadPool),导致并发消费。
顺序消息场景选择哪个?
必须用 Push 模式:因需依赖 Broker 的队列锁机制
顺序消费怎么提升消费的速度
- 增加消费时每次拉取的数量,批量消费
- 优化消费者代码,提升消费效率
- 注意不能增加消费者的线程数,因为一个队列只能有一个线程消费
怎么实现延迟消息
固定延迟级别:RocketMQ 支持 18 个预设延迟级别(1s~2h),不支持自定义任意时间。实现方式:通过 定时任务+多级队列 模拟延迟。
- 生产者端设置 delayTimeLevel 属性,每个级别对应不同的延迟时间,例如3对应10s。
- Broker端多级队列,延迟消息不会直接写入目标 Topic,而是先存入内部 Topic,例如SCHEDULE_TOPIC_XXXX,内部Topic会分18个级别,对应不同的延迟时间。
- 定时扫描:后台线程 ScheduleMessageService 按延迟级别分桶扫描。到达指定时间后,将消息重新投递到目标 Topic。
Broker端多级队列如下:
SCHEDULE_TOPIC_XXXX
├── queue0 (1s延迟)
├── queue1 (5s延迟)
├── ...
└── queue17 (2h延迟)
注意事项
- 不支持精确任意时间:只能选择预设级别。
- 性能影响:大量延迟消息会增加 Broker 负载。
- 重启失效:Broker 重启后未触发的延迟消息会重新计算时间。
怎么实现事务消息
RocketMQ 事务消息的核心流程也是两阶段提交的思路:
- 第一阶段:发送半消息(Half Message)并执行本地事务。半消息的意思是指:消息的 isWaitStoreMsgOK 属性为 false,表示暂不可被消费。存储位置:写入 RMQ_SYS_TRANS_HALF_TOPIC 内部 Topic,而非目标 Topic。消费者隔离:Consumer 无法看到半消息。
- 第二阶段:根据本地事务结果,向 Broker 发送 Commit/Rollback 指令。
注意这里有个回查机制保证消息的一致性:Producer 崩溃或网络中断,未发送二阶段指令。半消息超过 checkImmunityTime(默认5秒)未被处理。Broker 通过 TransactionListener.checkLocalTransaction 方法回调 Producer。最多回查15次(可配置),超过后默认回滚。
高性能设计
- 顺序追加:见上方
- 通过页缓存异步刷盘:见上方
- 零拷贝技术:消费者拉取时通过 MappedFile(内存映射文件) 和 FileChannel.transferTo() 来减少数据拷贝,和kafka的区别在于,需要用MappedFile做随机读写。
- 批量传输:生产者批量发送:支持合并多条消息为单次网络请求
那既然rocketMq也有这么多磁盘优化的措施,那为什么kafka的吞吐量更高呢?
- rocketMq索引文件需要消耗更多的磁盘操作,这时因为kafka的索引文件是稀疏索引,仅记录部分消息的偏移,但是rocketMq的索引是密集索引,每条消息都记录逻辑位置,因此rocketMq的索引更大。那rocketMq为什么要这样设计呢,因为rocketMq需要支持复杂路由(Tag过滤、Key查询)和事务消息,不得不需要这样的索引结构。
- 副本同步机制:Kafka 默认异步副本和异步刷盘(可配置)最大化吞吐,而 RocketMQ 的同步复制和刷盘选项(如SYNC_FLUSH)会显著降低吞吐(当然也可以选择异步,同步的话可以保持强一致性)。
原文链接:https://blog.csdn.net/m0_69529796/article/details/146381703
https://zhuanlan.zhihu.com/p/19609183417