RocketMQ
阅读提示
建议先看题目目录,再按“概念 -> 原理 -> 场景 -> 优化”顺序复习。
每题先讲结论,再补关键机制和项目实践,回答会更稳。
适用范围:本文以 Apache RocketMQ 4.9.x 的经典 Remoting Java 客户端(
DefaultMQProducer/DefaultMQPushConsumer)与主从部署为基础;5.x 新客户端的 Proxy、FIFO 消息组和时间戳定时消息会单独说明,不能将两套 API 与语义直接混用。
1、MQ的作用是什么呢?
- MQ 常用于异步处理、服务解耦和削峰填谷。例如下单成功后发送事件,由积分、通知服务各自消费,减少下单请求等待下游服务的时间。
- 引入 MQ 也引入了消息积压、重复投递和最终一致性问题;需要即时返回下游结果的调用,不能仅靠改成异步消息解决。
2、RocketMQ由哪些角色组成,每个角色作用和特点是什么?
- Producer:生产消息,获取路由后将消息发送到 Broker。
- Broker:存储消息,提供投递、消费进度与重试等能力。
- Consumer:订阅 Topic,处理消息并反馈消费结果;Consumer Group 标识一个逻辑订阅者。
- NameServer:提供 Topic 与 Broker 的路由发现;各节点独立,不转发消息正文。
- 5.x 新客户端通常通过 Proxy 接入:Proxy 提供协议适配和请求转发,可与 Broker 同进程或独立部署;这不是 4.9.x 经典客户端必须经过的角色。
3、RocketMQ Broker中的消息被消费后会立即删除吗?(了解)
- 不会。消息正文追加到 CommitLog,ConsumeQueue 保存指向正文的索引。4.9.x 集群消费提交的是按 Consumer Group、Topic、Queue 维度维护的逻辑队列偏移(下一条待消费的位置),不是 CommitLog 的物理字节偏移;广播模式的进度由客户端本地保存。
- 文件清理按保留时间、磁盘空间等条件进行,并不等待所有消费组确认。因此消费太慢也可能遇到消息过期被清理,不能把 MQ 当成永久归档。
4、RocketMQ消费模式有几种?
- 集群消费(4.9.x
CLUSTERING):同组实例分摊队列,一条消息通常由其中一个实例处理;不同组独立订阅、独立推进进度。失败重投、重平衡等情况下仍可能重复,不能理解为“只处理一次”。同组实例应保持订阅关系一致。 - 广播消费(4.9.x
BROADCASTING):同组的每个实例各消费一份。该模式不提供服务端消费失败重试,失败处理需要业务自行实现。 - 5.x 新客户端主要采用消费组负载均衡模型,不应直接套用上述广播 API;需要多个独立订阅者各收一份时,可使用不同消费组。
5、如何保证消息不丢失(了解)
消息可靠性要分别看生产、存储、消费三个阶段,不能仅凭一次发送成功就承诺任何故障下都不丢消息。
- 生产:同步发送检查
SendResult与异常,异步发送检查回调;4.9.x 的FLUSH_DISK_TIMEOUT、FLUSH_SLAVE_TIMEOUT、SLAVE_NOT_AVAILABLE不能等同于满足持久化要求的成功。超时意味着结果不确定,Broker 可能已写入;重试需要有界退避并配合消费幂等。按业务 Key 查询消息可辅助排查,但查不到不能证明此前未写入。单向发送没有响应确认,不适合要求确认可靠投递的关键消息。 - 存储:刷盘和复制是两个独立维度。经典主从模式的
flushDiskType=SYNC_FLUSH在消息要求等待存储确认时等待本机刷盘;brokerRole=SYNC_MASTER等待从节点复制确认。异步刷盘在断电等情况下可能丢失尚在 PageCache 的数据,异步复制在主节点故障时可能丢失未复制数据。复制确认也不等价于主从都完成同步刷盘,更不覆盖多副本磁盘同时损坏。5.x Controller 等部署模式应按对应的副本确认与选主配置分析,不能只套用经典brokerRole参数。 - 消费:业务处理成功后才返回成功。4.9.x 并发监听器失败时返回
RECONSUME_LATER;集群模式由重试机制重新投递,超过重试上限进入死信队列,需要监控并补偿。不是简单地保证“下一次拉取一定还是原消息”。5.xSimpleConsumer则需在处理成功后显式 ACK,未确认的消息在不可见时间到期后可再次投递。
即使发送确认可靠,如果“提交订单数据库后、发送消息前”进程退出,事件仍可能缺失;这种跨数据库与 MQ 的边界需要事务消息或本地消息表等一致性方案。
参考:4.x 发送结果与可靠性建议、4.x 服务端参数、5.x 消费重试。
6、如何防止消息重复消费(了解)
- 对分布式消息队列来说,同时做到确保一定投递和不重复投递是很难的,就是所谓的“有且仅有一次” 。RocketMQ 常规消费需要按至少一次投递设计,在配置与故障边界内通过确认、重试提高可靠性,但仍可能重复;不提供业务副作用的端到端 Exactly-once 保证。处理消息重复问题,主要由业务端自己保证,主要的方式有两种:业务幂等和消息去重。
- 业务幂等:第一种是保证消费逻辑的幂等性,也就是多次调用和一次调用的效果是一样的。这样一来,不管消息消费多少次,对业务都没有影响。
- 消息去重:第二种是业务端,对重复的消息就不再消费了。这种方法,需要保证每条消息都有一个唯一的编号,通常是业务相关的,比如订单号,消费的记录需要落库,而且需要将去重记录与业务数据库变更放在同一个本地事务中;数据库事务无法直接与 MQ 确认组成原子操作。事务提交后再确认,确认丢失导致重投时用去重记录安全跳过。
- 具体做法是可以建立一个消费记录表,拿到这个消息做数据库的insert操作。给这个消息做一个唯一主键(primary key)或者唯一约束,那么就算出现重复消费的情况,就会导致主键冲突,那么在确认这是已成功提交的同一业务事件后,就不再重复处理。唯一键应包含消费方及业务事件标识;同一订单的创建和支付是不同事件,不宜只用订单号去重。若先单独提交去重记录再执行业务,崩溃可能造成业务永远漏执行。
收到事件 → 开启数据库事务 → 插入去重记录(唯一约束)
→ 更新业务数据 → 提交事务 → 返回消费成功
重投且唯一键已存在 → 确认此前事务已提交 → 返回消费成功外部支付、发邮件等副作用不能随本地事务回滚,需要下游幂等键或补偿机制。参考:官方消费幂等建议。
7、如何让RocketMQ保证消息的顺序消费?
- 4.9.x:同一业务键(如订单号)的消息路由到同一队列,生产端串行发送并处理发送失败;消费端使用
MessageListenerOrderly保证队列内串行处理。普通并发监听器即使读取同一队列,业务完成顺序也可能不同。 - 单队列只能提供局部顺序,不能推出跨队列全局顺序;队列数变更可能改变业务键的路由,需考虑迁移期间的顺序边界。异步交给线程池后立即返回成功同样会破坏业务顺序。
- 5.x 新客户端:使用 FIFO 类型 Topic 和相同
MessageGroup表达同一业务序列,生产端仍需保证发送顺序;不同消息组之间不保证顺序。不能把给普通消息设置 Key 当成开启 FIFO。
参考:4.x 顺序消息、5.x FIFO 机制。
8、Broker把自己的信息注册到哪个NameServer上?
- Broker 会向其配置的所有 NameServer 注册路由并周期性更新,而不是只注册其中一个。NameServer 之间不靠相互同步维护路由;注册失败、节点故障时,各节点的视图可能短暂不同。
9、RocketMq的工作流程是怎样的?(了解)
- 首先启动NameServer。NameServer启动后监听端口,等待Broker、Producer以及Consumer连上来
- 启动Broker。启动之后,会跟所有的NameServer建立并保持一个长连接,定时发送心跳包。心跳包中包含当前Broker信息(ip、port等)、Topic信息以及Broker与Topic的映射关系
- 创建Topic。创建时需要指定该Topic要存储在哪些Broker上,4.x 在 Broker 允许自动建 Topic 时也可能发送时创建,但生产环境宜预先创建并明确队列数和权限
- Producer发送消息。启动时先跟NameServer集群中的其中一台建立长连接,并从NameServer中获取当前发送的Topic所在的Broker;然后按发送策略(普通消息常用轮询,顺序消息需按业务键选择)选择一个队列,与队列所在的Broker建立长连接,进行消息的发送
- Consumer消费消息。跟其中一台NameServer建立长连接,获取当前订阅Topic存在哪些Broker上,然后直接跟Broker建立连接通道,进行消息的消费
上述是 4.9.x 经典客户端的发现与数据通路,NameServer 不承载消息正文;5.x 新客户端的数据请求通常经 Proxy 转发。
10、RocketMQ是如何实现定时消息的? (了解)
- 定时消息发送后先处于不可消费状态,到期后才对消费者可见;到期可见不代表业务一定在该时刻执行,调度负载、积压和消费耗时都会带来延迟。
- 4.9.x 等级延迟:
setDelayTimeLevel选择预设延迟级别,默认 18 级(1 秒到 2 小时),并不是任意时间戳。Broker 将消息改写到内部SCHEDULE_TOPIC_XXXX,保留原 Topic 和 Queue 信息;ScheduleMessageService扫描对应延迟队列,到期后恢复目标信息并重新写入正常队列。 - 5.x 时间戳定时消息:新客户端使用投递时间戳,目标 Topic 类型为
DELAY,服务端需具备并启用相应定时消息能力。其时间存储机制不能用 4.x 的等级延迟队列完整概括;时间戳以毫秒表示也不等于毫秒级准时执行。
参考:4.x 延迟级别与示例、5.x 定时消息机制、4.9.8 调度实现。
11、说说RocketMQ怎么对文件进行读写的?(了解)
RocketMQ对文件的读写巧妙地利用了操作系统的一些高效文件读写方式——PageCache、顺序读写、零拷贝。
PageCache、顺序读取
在RocketMQ中,ConsumeQueue逻辑消费队列存储的数据较少,并且是顺序读取,在page cache机制的预读取作用下,Consume Queue文件的读性能几乎接近读内存,但大量积压使读取超出缓存后,仍会产生磁盘 I/O 并影响吞吐和延迟。而对于CommitLog消息存储的日志数据文件来说,当跨队列读取、积压回溯导致缓存未命中时,可能产生随机磁盘读取并降低性能。I/O 调度策略的效果取决于内核、块设备和工作负载,不能将某种调度器视为通用优化结论。
页缓存(PageCache)是OS对文件的缓存,用于加速对文件的读写。一般来说,命中缓存的读取和写入缓存的操作可以减少即时磁盘访问,但缓存未命中和持久化仍受磁盘性能限制,主要原因就是由于OS使用PageCache机制对读写访问操作进行了性能优化,将一部分的内存用作PageCache。对于数据的写入,OS会先写入至Cache内,随后通过异步的方式由内核回写机制将Cache内的数据刷盘至物理磁盘上。对于数据的读取,如果一次读取文件时出现未命中PageCache的情况,OS从物理磁盘上访问读取文件的同时,会顺序对其他相邻块的数据文件进行预读取。
零拷贝
另外,RocketMQ主要通过MappedByteBuffer对文件进行读写操作。其中,利用了NIO中的FileChannel模型将磁盘上的物理文件直接映射到用户态的内存地址中(这种Mmap的方式减少了传统IO,将磁盘文件数据在操作系统内核地址空间的缓冲区,和用户应用程序地址空间的缓冲区之间来回进行拷贝的性能开销),将对文件的操作转化为直接对内存地址进行操作,从而极大地提高了文件的读写效率(文件段容量固定便于映射和滚动管理,但不代表每条消息定长;CommitLog 消息记录可变长,经典 ConsumeQueue 索引项为 20 字节)。
说说什么是零拷贝?
在操作系统中,使用传统的方式,数据需要经历几次拷贝,还要经历用户态/内核态切换。
从磁盘复制数据到内核态内存;
从内核态内存复制到用户态内存;
然后从用户态内存复制到网络驱动的内核态内存;
最后是从网络驱动的内核态内存复制到网卡中进行传输。
所以,可以通过零拷贝的方式,减少用户态与内核态的上下文切换和内存拷贝的次数,用来提升I/O的性能。mmap 是常见手段之一,Java 可通过 MappedByteBuffer 访问映射区域;它不会消除磁盘到内存的传输,也不保证网络发送路径完全没有拷贝。实际路径还取决于传输实现与配置。
12、RocketMQ消息长轮询了解吗?(了解)
- 4.9.x 经典 PushConsumer 底层由客户端持续发起 Pull 请求。长轮询是当队列暂时没有可返回消息且请求允许挂起时,Broker 暂存请求,而不是立即返回空结果;这不是服务端无请求地推送。
PullMessageProcessor#processRequest判断是否挂起,PullRequestHoldService管理挂起请求;新消息到达通知或周期性超时检查会触发重新检查,满足条件或超时后再响应。挂起不会为每个请求长期占用一个业务处理线程。- 客户端收到响应后继续拉取;长轮询减少空轮询请求与等待新消息的延迟,但仍需为请求超时、流控和断线重连留出处理逻辑。5.x 新客户端的接收 API 不应直接套用这组 4.x 内部类。
Consumer -- Pull(offset) --> Broker
├─ 有匹配消息:返回数据
└─ 暂无消息:挂起请求
└─ 到达通知 / 超时检查 → 重新判断并响应
Consumer <-- 响应 ----------- Broker → 客户端再发起下一轮 Pull