中文技术书 · 开放阅读
RocketMQ
从消息模型到可靠系统
原理、Java 实战与生产运维
下载 EPUB,离线阅读继续上次阅读前言:把消息当作一份承诺
一次下单请求成功,不意味着库存、支付、积分、通知已经全部完成。分布式系统把工作交给不同服务,消息队列负责传递这些工作的线索。困难在于:网络会超时,进程会重启,同一件工作可能被执行两次,一件本来顺利的事可能在几分钟后才完成。
这本书围绕一个订单系统展开:订单服务发布事件,库存服务扣减库存,支付结果驱动履约,超时任务关闭未支付订单。我们的目标不是记住参数,而是回答三个问题:一条消息何时算可靠交付;业务如何在重试中保持正确;系统故障时用什么证据恢复服务。
读者需要基本的 Java、SQL、Linux 命令行知识。读完前六章,可以建立完整的发送、存储、消费模型;读完第七至十二章,可以实现常见业务模式;最后几章提供运维、容量和故障演练方法。书中的“练习”都是可独立思考的设计题,附录给出参考方向。
版本与实验约定
本文以 Apache RocketMQ 5.3.2 的经典 Java Remoting
客户端作为可复现实验基线,Maven 坐标是
org.apache.rocketmq:rocketmq-client:5.3.2。不是所有 5.x
客户端都使用同一条协议链路:rocketmq-client-java
是另一套基于 gRPC 的新客户端,通常通过 Proxy 连接服务端,API
与本书示例不通用。
资料核对日为 2026 年 10 月 20 日。核对时 Apache GitHub 最新发布为 5.5.1。本书固定较早的 5.3.2,是为了让命令、配置和源码定位一致;这一选择不是生产版本推荐。部署生产前,应核对当前维护分支、安全公告、Java 支持范围及组织升级策略。官方网站会随发布更新,涉及精确默认值时以固定版本源码为准。
所有实验使用本机单节点、明文连接,目的是观察机制。它不具备生产高可用性。示例明确区分“发送返回”“消息持久化”“消费者业务提交”,不把任何一个局部成功扩大成全系统承诺。涉及外部数据库的
SQL 是设计示例,需要按实际数据库事务语法实现;可执行 Java
示例则保存在仓库 examples/。
如何阅读和使用本书
在线版可以按章跳转、搜索正文,也可以保存完整单页。EPUB
版包含封面、目录和正文,可导入 Apple Books、Calibre
等阅读器离线阅读。代码、书稿、构建脚本同在仓库;正文的引用链接指向官方资料,research/
保存核对快照和实验记录。
本书为独立技术写作,与 Apache 软件基金会无隶属关系。Apache、RocketMQ 及相关标识属于其各自权利人。正文采用 CC BY 4.0,仓库自编示例代码采用 MIT;上游引用和资料快照仍遵守原始许可。
第一章 从订单事件建立消息模型
命令与事件
“请为订单 O100 扣库存”是命令,表达希望对方执行的工作;“订单 O100 已创建”是事件,表达已经发生的事实。两者都可以放入 RocketMQ,但接口契约不同。命令需要明确唯一负责方和失败处理;事件允许多个独立订阅者响应。
假设订单服务发布
OrderCreated。库存、积分、通知分别使用自己的 Consumer
Group。这样每个业务都能独立推进进度;如果三者共用一个集群消费组,一条消息通常只会交给其中一个消费实例,其他业务就会漏掉自己的工作。这是初学者最容易犯的建模错误之一。
建议消息体采用明确的信封:
{
"eventId": "evt-20261020-0001",
"eventType": "OrderCreated",
"schemaVersion": 1,
"orderId": "O100",
"occurredAt": "2026-10-20T08:00:00Z",
"items": [{"sku": "S01", "quantity": 2}]
}eventId 用于幂等、审计和重放;orderId
用于业务关联和顺序分组;schemaVersion 用于兼容演进。Broker
的 Message ID
可以辅助查消息,但应用应拥有跨重试稳定的业务标识。不要每次重试都重新生成
eventId,否则幂等表会把重试看作新业务。
Topic、Queue 与 Group
Topic 是一类消息的逻辑集合。Queue 是实际分配和推进消费进度的单位。一个 Topic 可以分布在多个 Broker 上,每个 Broker 又可以有多个队列。发送者通过路由信息选择目标队列;消费者在组内分担这些队列或消息。
经典客户端的集群消费模式下,同一组内可分配的队列数约束有效消费并行度。假设 Topic 共 8 个可读队列,部署 16 个相同组实例,不代表吞吐能翻倍,一部分实例可能没有队列。5.x 新客户端还支持消息粒度负载均衡,不能把旧客户端的队列分配结论不加区分地套用过去。官方领域模型区分了这些概念。
Consumer Group 表示同一种消费逻辑的实例集合。组名不仅是标签,还关联订阅关系和进度。一个组的所有实例应保持一致的 Topic 与过滤规则,否则重平衡和消息分配可能出现令人困惑的行为。开发、预发、生产必须隔离组名和集群,避免本地程序抢走线上消费任务。
异步化改变了什么
同步调用中,订单服务等待下游结果后再返回;异步化后,订单服务通常只等待本地订单和待投递事件持久化。用户看到“订单已受理”,库存处理可能稍后完成。产品需要显示处理中、失败和补偿状态,而不是把最终一致性隐藏起来。
队列可以吸收短时流量差,但不能消除长期供需不平衡。生产每秒 10,000 条、消费每秒 8,000 条,积压每秒增加 2,000 条。延迟最终耗尽磁盘或超过业务时限。消息队列是一种缓冲和解耦机制,不是无限容量的保险箱。
练习
订单创建之后,短信失败是否应该撤销订单?给库存、短信、风控设计组名,说明为什么不共享同一个组。再列出哪些状态允许用户重试、哪些状态必须等待后台补偿。
第二章 请求走过哪些组件
NameServer 不承载消息正文
经典链路可以写为:
Producer ──查询路由──> NameServer
Producer ──发送消息──> Broker ──存储──> CommitLog
Consumer ──查询路由──> NameServer
Consumer <──拉取消息── Broker
Broker 向 NameServer 注册路由。客户端获取路由并缓存,与 Broker 直接通信。NameServer 不代理每一条发送或消费请求,因此 NameServer 恢复不等于消息数据恢复。多 NameServer 提供多个路由入口;客户端地址列表使用分号分隔。它们不是用来选举 Broker 主节点的通用共识系统。
路由缓存意味着控制面故障的影响有时间差:已有连接和缓存可能让业务暂时继续,但新 Topic 发现、Broker 变化和客户端冷启动会受影响。排障时必须同时看客户端缓存、NameServer 返回和 Broker 自报地址。
Broker 与持久化
Broker 接收发送请求、写入消息、维护消费队列索引、处理拉取与重试。消息正文主要保存在 CommitLog,消费队列通过索引定位正文。可以把 CommitLog 理解为仓库流水账,把 ConsumeQueue 理解为按 Topic 和队列整理的检索目录;两者用途不同,不应按业务想象直接删除文件。
Broker 的数据路径、磁盘余量、刷盘策略、复制模式决定不同故障下能保留什么。仅看进程存活不足以判断服务健康。磁盘满、写入拒绝、复制滞后、线程池排队,都可能让一个“活着”的进程无法满足业务。
Proxy 与两套客户端
RocketMQ 5.x 引入 Proxy,面向新客户端提供协议适配等能力。使用
rocketmq-client-java 时,常见链路是客户端 → Proxy →
Broker;使用本书经典 DefaultMQProducer 时,链路是客户端 →
Broker。不要把 NameServer 的 9876 端口填入 gRPC 客户端的
endpoints,也不要把 Proxy gRPC 地址填入经典客户端的 NameServer
配置。
| 对比项 | 本书实验 | 新 Java 客户端常见方式 |
|---|---|---|
| Maven artifact | rocketmq-client |
rocketmq-client-java |
| 代表 API | DefaultMQProducer |
ClientServiceProvider |
| 协议 | Remoting | gRPC |
| 常见入口 | NameServer 发现 Broker | Proxy endpoint |
| 顺序分组 | 队列选择与顺序监听器 | MessageGroup 与 FIFO 消息 |
新旧链路的能力、消息类型校验和重试行为存在差异。迁移时要验证认证、过滤、顺序、重试和运维工具的实际表现,不能只替换依赖名。
网络排障先画地址图
NameServer 可达而发送超时,常见原因是 Broker
注册了客户端不可达的地址。容器内 IP、宿主机
IP、云内网地址不是同一个访问空间。经典 Broker 通常涉及
10911 和 VIP 通道 10909;开启 HA 复制还涉及
10912。具体开放哪些端口应以部署模式和实际配置为准。本书本机实验关闭客户端
VIP 通道,减少排查变量。
练习:NameServer 正常、Broker 本地发送正常、远程客户端发送失败,如何区分路由地址错误、端口未开放和认证失败?先取路由,再从客户端所在网络测试目标地址,最后核对请求日志,比反复重启更有效。
第三章 搭建第一个可复现实验
准备环境
需要 JDK 17、Maven 3.9+、Python 3、curl、unzip。书籍构建另外需要
Pandoc 3.x。生产版本的 JDK
支持范围要另行确认;这里的版本组合用于本地练习。若机器上只有系统 Java
启动器,先设置 JAVA_HOME,并确认 java -version
指向真实 JDK。
从 Apache 归档获取固定版本:
curl -fLO https://archive.apache.org/dist/rocketmq/5.3.2/rocketmq-all-5.3.2-bin-release.zip
curl -fLO https://archive.apache.org/dist/rocketmq/5.3.2/rocketmq-all-5.3.2-bin-release.zip.sha512
shasum -a 512 rocketmq-all-5.3.2-bin-release.zip
cat rocketmq-all-5.3.2-bin-release.zip.sha512
unzip rocketmq-all-5.3.2-bin-release.zip比较计算值与官方 SHA-512 文件;需要更强发布者验证时再核对 Apache 签名与公钥指纹。不要把随机镜像中的二进制当作同等来源。归档版本下载较慢时,可以先阅读后续模型章节,不必更换到未知构建。
启动单机 Broker
仓库 scripts/lab.sh
管理一个与其他项目隔离的本机实验。它使用回环地址、独立端口和仓库
.lab/ 数据目录。设置解压目录后执行:
export ROCKETMQ_HOME=/absolute/path/to/rocketmq-all-5.3.2-bin-release
bash scripts/lab.sh start脚本会启动 NameServer 和异步刷盘的单个 Master,再显式创建实验 Topic。它不会配置多副本,因此不能用于验证机器损坏时的数据可靠性。脚本针对本书学习环境减少 JVM 堆大小;默认发行脚本的堆参数通常不适合小内存笔记本。
配置重点如下,实际端口以脚本生成配置为准:
brokerClusterName=BookLab
brokerName=book-broker
brokerId=0
brokerIP1=127.0.0.1
brokerRole=ASYNC_MASTER
flushDiskType=ASYNC_FLUSH
autoCreateTopicEnable=false
关闭自动建 Topic,让拼写错误及时失败;生产也应经过容量、权限和消息类型审核后显式创建资源。自动创建订阅组、Topic 是否允许,要按环境统一管理。
运行发送与消费
mvn -f examples/pom.xml -q package
mvn -f examples/pom.xml -q exec:java \
-Dexec.mainClass=book.rocketmq.RoundTrip程序为每轮生成独立事件,先启动消费者,再同步发送,等待收到同一
eventId。成功输出包括发送状态、消息 ID、队列信息和
ROUND_TRIP_OK。超时会以失败结束,不会把“程序能运行”当作收发成功。测试结束会关闭客户端;Broker
由实验脚本管理。
默认 NameServer 为 127.0.0.1:19876,可以使用环境变量
BOOK_NAMESRV 覆盖。由于 Broker
使用回环地址,这套配置只适合本机。如果换成远程或容器实验,应同时调整注册地址、路由可达性与端口映射。
停止实验:
bash scripts/lab.sh stop停止不会删除消息数据。需要从头重做时,先停止,再手动移走
.lab/。不要在 Broker 运行时删除存储目录。
观察而不是猜测
执行 mqadmin topicRoute 查看 Topic 的队列分布,执行
consumerProgress 观察消费组进度。不同工具命令需要
NameServer 和组名等参数,可用
sh bin/mqadmin help <command>
查看。记录每次测试使用的版本、配置、组名和时间,才能把日志与一次具体实验对应。
练习:故意把客户端 NameServer 改成一个不存在的端口;恢复后再把 Topic 拼错。比较两种失败的异常和耗时,建立“路由不可达”与“资源不存在”的区别。
第四章 生产者:正确理解发送结果
一次发送的时间线
经典生产者通常经历路由查找、队列选择、编码与压缩、网络发送、Broker 处理、等待响应。客户端超时只说明在期限内没有拿到确定响应。Broker 可能尚未收到,也可能已经存下消息而响应丢失。因此超时后的业务状态应当是“结果未知”,不能直接归类为“肯定未发送”。
生产者通常在应用启动时创建并复用,应用退出时关闭。不要每次 HTTP 请求都构建一个 Producer,那会反复创建连接和后台线程。Group 名应表达应用身份;消息 Keys 可以放订单号以便查询,但 Keys 并不自动实现去重。
DefaultMQProducer producer = new DefaultMQProducer("order-producer");
producer.setNamesrvAddr("127.0.0.1:19876");
producer.setVipChannelEnabled(false);
producer.start();
Message m = new Message("BookOrders", "Created", "O100",
json.getBytes(StandardCharsets.UTF_8));
SendResult result = producer.send(m);此片段展示 API,完整可运行代码见仓库。应在应用生命周期结束时
shutdown(),并在异常路径记录稳定事件 ID、目标
Topic、调用耗时与结果。
发送方式和确认强度
同步发送让调用方等待
SendResult;异步发送通过回调获取结果,必须限制并发数并处理失败;单向发送不等待结果,适合可接受丢失的辅助数据,不适合订单事实。异步回调失败时把消息直接丢进内存队列,进程重启仍会丢;需要可靠交付就必须有持久化补发来源。
经典 SendStatus 包括
SEND_OK、FLUSH_DISK_TIMEOUT、FLUSH_SLAVE_TIMEOUT、SLAVE_NOT_AVAILABLE。后三者不能解释为消息一定未存储。SEND_OK
的含义也受 Broker
刷盘、复制配置约束。在异步刷盘单副本实验里,它不等于断电后一定可恢复。
5.3.2 经典 DefaultMQProducer
的源码默认同步失败重试次数为 2,发送超时为 3000
毫秒;异步发送另有配置和执行路径。默认值不能替代预算设计:一次用户请求若只允许
500
毫秒,不应该让调用链在后台无限重试。重试次数、每次耗时、调用总预算要一起计算。
应用重试与 SDK 重试叠加
假设 SDK 最多尝试三次,应用又重试三轮,一次业务事件可能触发九次发送尝试。SDK 某次已经成功写入但响应丢失,之后再次发送就可能产生重复消息。无论使用哪个版本,都应按“可能重复”设计消费者。
可靠生产的一种做法是 Outbox:在业务数据库事务里同时保存订单和待发送事件,后台任务负责投递,拿到确认后更新状态。即使确认更新失败,后台再次发送也只会造成重复,由消费端幂等化解决。第十一章会给出完整事务边界。
批量与大消息
批量发送减少网络往返,但受单次请求大小、Topic 一致性及消息类型限制。不要按“条数固定”分批,应按序列化后的字节数分批,并把属性和协议开销留出余量。最大消息大小有客户端与服务端两侧约束,不能只改其中一个参数。
图片、压缩包、完整报表一般放对象存储,消息只带引用、摘要、访问范围和过期策略。这样减少 CommitLog 压力,也避免一次消费把内存顶满。代价是对象与消息的生命周期需要协调:对象过期时间必须覆盖最大重试、积压和重放窗口。
练习:发送超时后,订单接口应该向用户返回什么?设计一个能查询业务最终状态的接口,并说明如何在重复点击时复用同一个业务幂等键。
第五章 消息在磁盘上的一生
从正文到消费索引
经典文件存储中,CommitLog 顺序追加多个 Topic 的消息。ConsumeQueue 保存按 Topic、Queue 排列的位置索引;索引条目帮助 Broker 从逻辑队列偏移找到正文。按 Key 查询还涉及独立索引结构。三者不是三份等价正文副本。
写入之后,Broker 的分发过程构建消费所需索引。于是发送成功、消息可被消费和索引可查之间存在处理步骤。排查短暂不可见时,应核对写入时间、分发进度和路由,而不是立即判定永久丢失。异常持久不可见则需要检查存储错误和消息类型。
队列 offset 表示队列内逻辑位置,CommitLog offset 表示存储文件位置;Message ID 和业务 ID 又是不同层次。日志里若混用这些名字,回放很容易选错边界。跨队列 offset 没有全局大小意义,不能比较两个队列的数字来判断哪条业务事件更早。
Page Cache 与刷盘
操作系统 Page Cache 可以让顺序写高效完成。数据进入缓存并不意味着已进入可抗断电的介质。异步刷盘把刷盘工作放到后台;同步刷盘使发送处理等待刷盘条件,但依然受硬件、文件系统和故障模型约束。
可靠性讨论必须写清故障:只杀
JVM、操作系统崩溃、主机断电、磁盘损坏、整个可用区失效,需要不同保护机制。只做
kill -9
能验证进程崩溃恢复,不能据此宣称通过断电或磁盘损坏测试。消费端数据库提交也不在
Broker 刷盘承诺里。
复制不是万能备份
复制可以降低单节点故障风险,但复制延迟、确认策略和选主约束决定实际 RPO。异步复制时,生产者已收到成功的尾部消息可能尚未到从节点。同步复制相关策略也必须与切主机制共同评估。双副本位于同一块物理盘,不提供磁盘故障域隔离。
备份和复制解决不同问题。误删、恶意操作、错误业务事件可能迅速复制到其他节点;业务还需要审计、保留策略和修复工具。不要把“有从节点”作为唯一数据保护方案。
保留、清理与最慢消费者
RocketMQ 不是等所有组确认后才删除每条正文。文件清理主要受保留时间、磁盘空间及实现策略影响。一个组长时间积压,可能在追赶前就遇到旧数据被清理。消费进度并不是永久保留消息的锁。
因此必须同时监控最老积压年龄、保留窗口和磁盘剩余。设允许最长停机 12 小时、最大追赶时间 6 小时、故障调查 4 小时,仅配置 12 小时保留就不够。还要考虑流量波动、批量回放和安全余量。保留时间也不是无条件最短保证,磁盘压力下的清理需要单独评估。
冷读与性能拐点
消费紧跟生产时,读取常能命中缓存;严重落后时需要冷读磁盘,与写入竞争 I/O。积压恢复阶段可能比平稳阶段更慢。压测不能只测“新消息即时消费”,还应测一段大积压追赶,并观察生产延迟是否受影响。
练习:把一个 Topic 保留窗口从 24 小时改成 72 小时之前,需要增加哪些资源?为什么只按正文大小乘三仍可能低估占用?
第六章 消费者:处理完成才确认
Push 的工作并不是业务回调那么简单
经典 DefaultMQPushConsumer
将拉取、缓存、调度封装在客户端中,业务通过监听器处理消息。名字里的 Push
不应让人误以为每条消息完全由 Broker
直接推入业务线程。理解本地缓存和拉取行为,才能解释扩容、停机和消费延迟。
一个并发监听器的简化形式如下:
consumer.registerMessageListener((MessageListenerConcurrently) (messages, context) -> {
try {
for (MessageExt m : messages) {
processInDatabase(m); // 必须在返回前完成持久化事务
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
} catch (Exception e) {
return ConsumeConcurrentlyStatus.RECONSUME_LATER;
}
});不要在监听器里把消息投进另一个内存线程池后立即返回成功。进程崩溃时,Broker 认为工作已经完成,而新线程还没有提交业务。若确实要异步化,必须先持久化转交任务,并明确由谁承担后续重试。
提交业务和提交进度存在缝隙
典型过程是读取消息、执行数据库事务、返回成功、提交消费进度。数据库已提交而进度尚未生效时进程崩溃,消息会再次被处理。这是重复消费的重要来源。反过来,先确认消息再写数据库,会产生业务永久漏处理的窗口。
正确方向是先提交业务,再确认消息,同时让业务写入幂等。它把风险集中成可控制的重复,而不是不可恢复的漏执行。第七章给出具体 SQL 模型。
起点、进度与重置
新消费组第一次启动的起点由消费策略、Topic 类型等决定。经典客户端的
CONSUME_FROM_LAST_OFFSET
不能被理解成对所有历史场景永远从当前最新位置开始。已有组通常使用已存储进度,修改启动策略并不会自动把组回到最早。
本书实验先启动消费者再发送,并为每轮设置可辨识事件,避免依赖新组起点的细节。生产重置进度时,必须记录旧进度、选定时间或位置、暂停相关消费者,并确认重放范围内的外部副作用可重复。重置是业务操作,不是随手修复积压的通用办法。
组内扩容和重平衡
经典集群消费把队列在实例间重新分配。实例上下线会触发重平衡,短时出现暂停、负载变化和重复处理并不奇怪。扩容前应先看:是否还有足够队列、是否存在热点队列、数据库是否已经满负荷。
优雅停机时先从入口流量移除应用,再停止消费并给在途事务留出完成时间。实际 shutdown 行为和等待时间受客户端配置约束,必须在部署平台的终止宽限期内验证。滚动发布若每次同时下线过多实例,会让重平衡持续扰动吞吐。
慢消费与并发控制
增加线程可隐藏 I/O 等待,却也可能把数据库连接池打满。假设一个实例配置 64 个消费线程,数据库只分配 16 个连接,大部分线程可能只是在等待连接。应把线程数、单条耗时、连接池、下游限流放在同一个预算里。
批量消费失败时,要确认所用客户端如何处理部分成功与重试。最稳妥的初始实现是小批量、逐条幂等、清晰错误分类。以后再根据实际瓶颈扩大批量。
练习:数据库提交成功后故意让监听器返回重试。第二次处理时业务表应发生什么变化?消费日志怎样证明是同一个事件的再次交付?
第七章 幂等、重试与死信
幂等的单位是业务效果
同一条消息收到两次,日志出现两行并不一定是问题;库存扣了两次才是问题。幂等要求重复输入不会产生额外业务效果。不能只凭消息中有 ID 就说系统具备幂等能力,必须有并发安全的约束或状态机。
下面是关系数据库的设计示意:
CREATE TABLE processed_event (
consumer_name VARCHAR(100) NOT NULL,
event_id VARCHAR(100) NOT NULL,
processed_at TIMESTAMP NOT NULL,
PRIMARY KEY (consumer_name, event_id)
);同一个数据库事务内,先插入幂等记录,再执行库存变更,最后一起提交。并发重复处理时,唯一约束只允许一个事务成功。如果插入冲突,应区分“该唯一键已经处理”与其他数据库错误;不能把所有 SQL 异常都当成重复而返回成功。
BEGIN
INSERT processed_event(consumer_name, event_id, processed_at)
UPDATE stock SET available = available - :n
WHERE sku = :sku AND available >= :n
检查更新行数,写入业务流水
COMMIT
库存不足是业务决策,可能记录拒绝并发布失败事件;数据库断线是可重试故障。两者不能用同一段无限重试处理。若事务失败,幂等记录也必须回滚,否则后续重试会把未完成业务误认为已完成。
外部副作用更难
支付网关、短信和第三方 HTTP 调用不在本地事务里。数据库写了“已发送”而外部请求没执行会漏发;外部请求成功而本地状态更新失败会重发。优先使用对方提供的幂等键或查询接口;否则设计发送状态、对账与人工处理流程,并明确剩余风险。
Redis SET NX
可以做短期去重或并发协调,但若先写缓存再更新业务,缓存成功、业务失败会造成漏执行。若
TTL
到期,旧事件又可以执行。缓存是否足够,取决于业务损失和重放窗口,不能替代数据库事务的论证。
重试是有限资源
瞬时网络问题适合带退避的重试;格式非法、必填字段缺失、已停用的协议版本通常不会因为再试一次而变好。区分瞬时失败、永久失败和业务拒绝,分别使用自动重试、隔离及补偿。
经典并发消费通过返回 RECONSUME_LATER
进入重试流程。顺序消费的暂停与重试机制不同,不能直接套用同一监听器状态。新客户端的重试策略又与消息类型、消费方式有关,具体见官方消费重试文档。
一次下游故障可能把所有消息都变成重试流量。应监控新消息速率、重试速率、重试次数分布和下游恢复情况。恢复时限制并发,避免“系统刚恢复又被重试击穿”。
死信必须有人接住
经典集群消费中,超过允许重试次数的消息可进入对应组的死信
Topic,常见名字为 %DLQ%
加组名。存在死信并不等于已经完成补偿:要告警、定位失败原因、修复数据或程序、选择性重放,并保留审计。
死信重放应保留原始 eventId,另加新的重放任务
ID、操作者、原因和时间。否则重放会绕过幂等约束。对于业务确实需要再执行的动作,应生成一个有明确语义的新命令,而不是偷偷修改旧事件
ID。
幂等记录保留时间必须覆盖所有允许的消息重放窗口。若记录 7 天后删除,运维又允许重放一个月前的消息,重复效果就会重新出现。消息保留、审计保留和幂等保留需要一起设计。
练习:一个事件包含三个商品,处理第二个时失败。是整单事务重试,还是每个商品单独幂等?分别说明一致性、锁时间和恢复复杂度。
第八章 顺序消息:把顺序缩小到业务实体
先确定需要哪一种顺序
“所有订单全局有序”会把系统压缩成一个串行瓶颈。多数业务只要求同一订单的创建、支付、发货按顺序处理,不同订单可以并行。因此应把顺序范围定义为订单 ID,而不是整个系统。
消息队列不能自动修复上游已经发错的顺序。两个生产者同时发送同一订单事件,网络和线程调度决定实际到达先后。如果业务必须先支付后发货,需要由数据库状态机、单写入者或可验证的事件序号建立顺序来源。
经典客户端如何选择队列
经典 Producer 可用 MessageQueueSelector
把同一业务键映射到同一队列:
SendResult r = producer.send(message, (queues, msg, arg) -> {
String orderId = (String) arg;
int index = Math.floorMod(orderId.hashCode(), queues.size());
return queues.get(index);
}, orderId);消费者使用
MessageListenerOrderly,由客户端按分配队列的顺序消费机制处理。只做固定队列选择而仍用并发监听器,不构成完整顺序保证。还需控制生产端发送并发,不能让同一业务实体的多个线程同时竞速发送。
Topic 队列数量变化会改变取模映射,旧消息和新消息可能落到不同队列。扩容顺序 Topic 前,需要排空、冻结业务键迁移,或引入能处理版本和序号的路由机制。把队列翻倍当作毫无代价的操作,会破坏既有顺序边界。
消费失败与队头阻塞
一个订单处理失败,顺序消费通常需要暂停或重试,否则后续事件先执行就失去顺序意义。经典队列里可能有很多订单共享同一队列,一个坏订单就可能拖慢其他订单。这是队头阻塞,需要可观测的失败原因和隔离流程。
隔离时不能简单跳过失败事件并宣称顺序仍成立。应根据业务决定:拒绝该订单全部后续事件、把该实体转入待修复状态、或用状态机缓冲后续序号。错误格式与暂时依赖故障,处理方法也应不同。
新客户端 FIFO 的边界
5.x 新客户端使用 MessageGroup 表达分组顺序,要求匹配的 FIFO Topic 与消费 API。官方文档明确区分生产顺序、存储顺序和消费顺序;消息到达顺序仍需要生产端合理约束。不要把经典队列选择代码复制进 gRPC API,也不要把 FIFO 描述理解为任意多生产者自动形成业务因果顺序。
序号和状态机是最后一道约束
给订单事件加
aggregateVersion,数据库记录当前已应用版本。当收到版本 8
而当前是 6,可以暂存、回查源服务或等待版本 7;收到版本 6
或更早,判定为重复或过期。RocketMQ
顺序能力降低乱序概率和处理成本,业务状态机负责拒绝不合法转换。
例如 PAID -> SHIPPED
合法,CLOSED -> SHIPPED
应拒绝并告警。不能因为消息“有序”就取消数据库条件更新。
练习:同一个订单迁移到另一个生产者进程后,怎样防止旧进程迟到的事件覆盖新进程状态?为状态版本设计一个单调校验规则。
第九章 延时与定时:到期之后仍要检查业务
超时关单不是无条件关闭
下单 30 分钟后,如果仍未支付就关闭订单。消息只是提醒“现在可以检查”,真正判断应在数据库里完成:订单可能刚支付、已取消、已关闭,甚至事件因积压而晚到了几个小时。
安全的写法类似:
UPDATE orders
SET status = 'CLOSED', version = version + 1
WHERE order_id = :id
AND status = 'PENDING_PAYMENT'
AND expires_at <= CURRENT_TIMESTAMP;只有更新成功才释放对应预留库存,并通过事务或 Outbox 继续传播结果。支付和关单并发时,要用条件更新、事务锁或业务仲裁确保状态不矛盾。消息到达不能替代业务条件。
经典延迟等级
本书固定版本经典 API 使用
message.setDelayTimeLevel(level)。5.3.2 默认等级从 1
开始,对应:
1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m
9m 10m 20m 30m 1h 2h
例如等级 3 是 10 秒,等级 16 是 30 分钟。此列表来自固定版本
MessageStoreConfig,管理员可改变配置,应用不能在不核对环境的情况下永久依赖数字含义。延迟不是业务线程
sleep,消息在 Broker 内部调度后才进入目标队列供消费。
5.x 定时时间戳
5.x 提供基于定时存储的能力,新客户端可设置投递时间。使用前要核对 Broker 定时功能开关、Topic 类型、允许时间范围和客户端支持;不能因为服务端版本号是 5.x 就假设每个部署都能直接使用所有定时功能。
官方当前文档描述默认最长 24 小时,而本书固定版本
MessageStoreConfig.timerMaxDelaySec
为三天。这说明在线文档和旧版本默认值可能不同。生产契约应以目标版本、实际配置和边界实验为准。本书延时实验使用经典等级,不依赖这一有差异的默认范围。
定时消息通常承诺在目标时间后具备投递条件,不能保证业务恰好在某一毫秒执行。调度、网络、积压、重试和业务耗时都会增加延迟。若关单 SLA 是到期后一分钟内处理,需要监控实际处理时间与业务到期时间的差值。
取消、修改和长周期任务
已发送的延时消息通常不能作为通用可修改任务记录使用。修改订单过期时间时,可以在消息中携带原计划版本,消费时回查最新状态并忽略旧版本。取消订单之后,旧提醒仍可能到达,但状态检查应让它安全失效。
超过允许延迟范围的长周期任务,常见方案是数据库保存任务表,近期开窗扫描并投递短期消息。扫描器也要做到幂等、租约或抢占保护,并有漏扫补偿。不要把消息系统当作唯一长期日历数据库。
练习:用户申请把订单有效期延长十分钟。旧关单消息仍会到达,设计一个无需撤回旧消息的处理规则。
第十章 事务消息:把本地结果告诉 Broker
它协调的是哪一个边界
事务消息帮助协调“生产者本地事务是否成功”和“消息是否对消费者可见”。它不会让消费者数据库事务、外部支付和所有下游一起原子提交,也不能消除消费重复。
经典流程包括:发送半消息;Broker 接收但暂不对普通消费可见;生产者执行本地事务;根据结果提交或回滚消息;结果不确定时,Broker 发起事务状态回查。具体行为见官方事务消息说明。
Producer Broker Business DB
|--半消息----->| |
|<--接收结果---| |
|-----------执行本地事务---------->|
|<------------提交结果------------|
|--提交/回滚-->| |
| |--提交后可消费 |
第二阶段通知可能丢失,回查就是为此存在。把回查视为不该发生的异常,会导致可靠性设计缺掉最关键的一部分。
回查需要可证明的业务结果
回查函数必须依据持久化事实回答。例如本地事务同时写入订单和
transaction_marker(tx_id, result),回查按稳定
tx_id
读取结果。标记和订单应在同一个数据库事务提交,否则标记可能证明了错误状态。
查不到记录不一定代表失败:事务可能还在运行、数据库连接异常、读到了延迟副本。无法确定时返回未知,让系统按配置再次检查;不能把数据库超时直接变成“提交消息”。最终如何处理长时间未知的事务,需要重试预算、告警与业务对账。
经典 TransactionListener 包含
executeLocalTransaction 和
checkLocalTransaction。逻辑可概括为:
executeLocalTransaction(message):
根据 txId 幂等地执行数据库事务
能证明提交 -> COMMIT_MESSAGE
能证明回滚 -> ROLLBACK_MESSAGE
无法确定 -> UNKNOW
checkLocalTransaction(message):
查询持久化事务结果
只按证据返回状态,不重复随意执行业务
这里是接口逻辑说明,不是完整生产代码。尤其不要照搬用内存 Map 记录事务结果的演示程序:进程一重启,回查依据就消失了。
进程、身份和部署
Broker 发起回查时,必须有具备对应逻辑与数据访问能力的生产者实例在线。组身份、事务回查线程池、数据库连接池和部署顺序都影响恢复。代码版本升级后,旧半消息可能仍需由新实例判断,因此事务标记格式必须向后兼容。
回查不得执行无界慢查询。它与业务请求争用数据库时,可能把一次消息异常放大为数据库故障。记录回查次数、未知时长和最终决定,对异常长事务建立人工核对流程。
不要忽略消费者幂等
事务消息提交后,消费链路仍可能重复交付。库存服务依旧需要第七章的幂等事务。事务消息解决生产侧的协调窗口,幂等解决消费侧重复效果,业务状态机解决冲突转换,三者互补。
练习:本地数据库提交成功,生产者立刻崩溃,第二阶段消息没有送出。写出回查恢复所需的最小数据集合,并解释如果事务标记只保留一天会产生什么问题。
第十一章 Outbox:用数据库保存发送义务
为什么“先写库再发消息”不够
先写库后发消息,写库成功、发送前进程崩溃,就没有事件;先发消息后写库,消息成功、写库失败,下游会处理一个不存在的订单。仅仅把两行代码换个位置,无法消除两套持久化系统之间的间隙。
Outbox 把订单与待投递事件放进同一个本地数据库事务。事件行就是一份可恢复的发送义务,后台投递器不断把它发送到 RocketMQ。订单事务不需要同时持有 Broker 的分布式锁。
CREATE TABLE outbox_event (
event_id VARCHAR(100) PRIMARY KEY,
aggregate_id VARCHAR(100) NOT NULL,
payload TEXT NOT NULL,
state VARCHAR(20) NOT NULL,
attempts INTEGER NOT NULL DEFAULT 0,
next_attempt_at TIMESTAMP NOT NULL,
lease_until TIMESTAMP,
created_at TIMESTAMP NOT NULL
);以上是通用示意。生产实现还需要索引、租约持有者、错误摘要、投递时间,以及数据库类型对应的锁语法。
投递器的状态机
订单事务写入
NEW。投递器按到期时间领取一批,把任务租给当前实例;发送成功后标记
SENT;失败则增加次数并设置下一次尝试。进程崩溃留下的任务在租约到期后可以重新领取。
关键点是领取要原子化。多个实例先 SELECT 再无条件
UPDATE,会同时拿到同一批事件。可以使用条件更新、数据库的行锁与
SKIP LOCKED,或其他经过验证的领取机制。数据库差异很大,示例不能跨库照抄。
不要在持有数据库长事务锁时等待网络发送。可以短事务领取、事务外投递、短事务确认。这样产生一个可重复发送窗口:消息已经成功,SENT
更新失败。这个窗口是有意接受的,必须由消费端幂等处理。
发送成功的判定
投递器应检查发送返回状态,针对无法证明成功的情况保留待核对或重试状态。不要捕获所有异常后仍标记
SENT。至少记录 eventId、消息
ID、发送状态、尝试次数、耗时、最后错误。
延迟重试需要抖动,避免所有任务在数据库或 Broker 恢复的一秒内同时冲出。永远重试而不告警会让坏消息长期隐藏;有限尝试后转人工处理也必须保持事件可恢复,而不是删除记录。
清理与对账
已发送事件可以归档,但应保留足够审计窗口。对账可比较订单状态、Outbox 状态和下游处理流水,发现“订单存在但事件缺失”“事件已发但下游长期未生效”。只有有独立事实来源,才能发现链路里未被告警捕获的遗漏。
需要严格同一实体顺序时,多个投递器抢占不能随意改变该实体事件次序。可按实体分区串行投递或依序号做领取约束;Broker 顺序能力无法挽救投递器先发版本 3、后发版本 2 的错误。
与事务消息怎么选
| 维度 | 事务消息 | Outbox |
|---|---|---|
| 恢复依据 | Broker 半消息与本地事务结果 | 数据库事件行 |
| 关键能力 | 正确回查、生产者在线 | 原子写入、可靠领取与投递 |
| 延迟 | 通常较短,异常时等待回查 | 受投递扫描/订阅延迟影响 |
| 改造代价 | 接入事务客户端与回查 | 表结构、投递器、归档与监控 |
| 仍需解决 | 消费幂等与业务补偿 | 消费幂等与业务补偿 |
选择依据是团队现有数据库、运维能力和恢复要求。不要为了追求某个名词而同时叠加两套复杂协调流程。
练习:Outbox 投递器在成功发送之后、更新 SENT
之前崩溃。下一次任务怎样运行,怎样证明订单库存只扣一次?
第十二章 过滤、消息契约与兼容演进
Topic 与 Tag 的职责
Topic 适合表达一类独立资源边界,例如订单事件;Tag
适合在同一类消息里做轻量分类,例如
Created、Paid、Closed。不要为每个订单创建一个
Topic,也不要把需要不同权限、保留或容量策略的业务全塞进同一 Topic,仅靠
Tag 区分。
经典订阅表达式可使用 Created || Paid 等 Tag 组合。Tag
过滤不能替代权限控制:能否读取 Topic
和是否只想处理其中一部分,是不同问题。同一消费组应保持订阅一致,不能让一个实例只订阅
Created,另一个只订阅 Paid,再期待组内自然分工。
SQL92 属性过滤
某些场景需要按地区、业务线或优先级筛选,可以把可过滤字段放入消息用户属性,并使用 SQL92 表达式。服务端通常需要开启属性过滤支持,客户端也必须匹配能力。示意:
message.putUserProperty("region", "east");
consumer.subscribe("BookOrders",
MessageSelector.bySql("region = 'east'"));不要假设 Broker 会解析任意 JSON 消息体做过滤。还应核对属性缺失、字符串与数值类型、空值处理,以及过滤成本。过滤不是通用数据仓库查询引擎,也不是跨消息关联计算。
过滤后不消费的消息也仍然消耗存储。若同一个 Topic 上大量消费组使用复杂过滤,实际开销需要通过目标负载验证。热点属性、表达式变更和历史数据属性缺失都应进入迁移计划。
契约是生产者和消费者的共同接口
至少约定:事件 ID 是否稳定、时间格式和时区、金额最小单位、枚举未知值怎么处理、字段是否可空、个人信息是否允许进入消息。金额使用分或明确精度的十进制,避免二进制浮点产生账务差异。
新增可选字段通常比删除或改变字段类型安全。消费者应容忍未知字段,并对缺失字段有定义好的默认或拒绝策略。把
quantity 从整数改成字符串、把时间从 UTC
改成本地无时区格式,都可能破坏尚未升级的消费者。
迁移可分三步:先发布同时兼容新旧格式的消费者;再发布新生产者;确认旧格式积压、重试、死信和允许的重放窗口结束后,最后移除旧逻辑。只看线上即时流量已切换,并不足以证明旧格式不再出现。
消息体中的敏感数据
消息会进入日志、死信、备份和排障工具。将完整身份证、银行卡或访问令牌放进正文,会扩大数据暴露范围。能传业务引用就不传秘密;需要个人信息时限定字段、权限、保留和脱敏方式。
链路追踪 ID 可以放在用户属性中传递。追踪采样会让某些 span 缺失,因此必须保留独立业务 ID,不能把“查不到 trace”当作“消息没发送”的证据。
练习:给事件增加一个 discount
字段,如何让仍运行旧版本的库存消费者安全处理?如果某个字段原来以元表示,现在改成分,为什么只改字段注释不够?
第十三章 高可用:故障模型先于节点数量
明确 RPO 和 RTO
RPO 是最多能接受丢失多少数据或多长时间窗口的数据,RTO 是多久恢复服务。订单事实、推荐曝光、缓存失效通知往往需要不同目标,不能统一用“高可用”三个字替代。
将故障分成客户端进程退出、Broker 进程退出、主机故障、存储损坏、网络分区、可用区失效。对每一类写出检测机制、切换条件、数据边界和恢复流程。只有明确故障假设,副本数量才有意义。
常见部署机制不要混在一起
传统主从、DLedger、Controller 模式都与 RocketMQ 的高可用演进有关,但它们的元数据、复制与选主机制不相同。传统异步主从不能自动等同于具有共识约束的自动切主;NameServer 也不承担所有这些模式的选主职责。
RocketMQ 5.x 的 Controller 模式利用 Controller 维护相关元数据并参与选主决策,Broker 侧负责对应复制机制。配置时应遵循官方自动故障转移文档,明确 SyncStateSet、选主条件和失去多数派时的行为,而不是从不同教程拼参数。
本书单机实验刻意不模拟生产多副本。要证明高可用,必须搭建真实独立节点或故障域,运行持续生产、消费与业务对账,并记录故障前后的确认和恢复结果。
网络分区与旧主隔离
主节点与控制面失联,但仍能被部分客户端访问,是危险情形。如果没有可靠的主身份约束和隔离机制,就可能出现不同客户端向不同节点写入的风险。切主设计需要回答旧主何时停止服务、恢复后怎样追赶、未确认尾部如何处理。
不能把客户端超时全部交给重试解决。重试只能切换请求尝试,无法决定两个冲突历史哪个正确。真正的数据一致性依赖服务端协议和故障恢复约束。
升级与回滚
先在接近真实数据规模的环境测试升级,备份配置与必要元数据,检查磁盘格式、配置兼容、客户端兼容和滚动升级支持。先少量节点,再逐步扩大;每一步观察发送状态、复制进度、消费延迟和错误率。
回滚不总是“把二进制换回去”。新版本若改写了旧版本不能识别的格式或元数据,降级可能不安全。执行前核对发行说明和官方迁移限制,必要时通过旁路集群迁移或经过验证的数据恢复路径处理。
灾备与跨地域
跨地域复制会增加网络成本、延迟和恢复复杂度。异步灾备必须接受一定 RPO,并有切换后对账;同步跨地域则可能显著影响写入延迟与可用性。消费幂等、生产流量切换、组进度和业务数据库灾备也要一起考虑。
练习:设计一个“主机断电但同机磁盘仍完好”和一个“整块盘永久损坏”的演练。两者分别能证明什么,不能证明什么?
第十四章 可观测性与日常运维
从用户目标选择指标
用户关心订单多久完成,不只关心 Broker 每秒处理多少消息。建议从端到端业务延迟开始,再拆分生产确认时间、Broker 可见延迟、积压年龄、消费执行时间和下游提交耗时。
| 层次 | 关键观察 | 常见判断 |
|---|---|---|
| 生产端 | 成功率、未知结果、延迟分位数、重试数 | 网络、路由或 Broker 写入是否异常 |
| Broker | TPS、磁盘余量、写入延迟、线程池队列、分发落后 | 存储和调度是否饱和 |
| 复制 | 复制差距、同步集合、主身份变化 | 故障后可保留哪些数据 |
| 消费端 | 最老积压年龄、队列分布、重试、死信、处理耗时 | 是不均衡还是整体能力不足 |
| 业务 | 订单状态滞留、对账差异、重复效果 | 技术成功是否转化成业务成功 |
消息条数适合估算工作量,积压年龄更接近业务 SLA。十万条很小消息可能几秒追完,一万条昂贵任务可能需要几个小时。两种指标应一起保留。
一条消息的排查路径
先取得稳定事件 ID、订单号、发送时间范围、Topic、Tag、生产者日志。确认发送结果;用消息 Key 或 Message ID 查询时注意查询能力、索引延迟和数据保留。然后检查消费组订阅、进度、消费者实例和失败日志,最后核对业务数据库。
消息“查得到”不能证明已消费,“消费成功日志”不能证明数据库提交正确,“数据库有一行”也不能证明只执行一次。完整证据链应连接发送、存储、投递、事务提交与幂等记录。
日志应避免完整打印敏感正文。建议打印 eventId、业务
ID、组、Topic、队列、offset、处理次数、错误分类、耗时,并对高频重复错误做采样或聚合。
有界操作与审计
常见管理动作包括创建 Topic、修改权限、扩队列、重置进度、死信重放。执行前记录目标集群、资源、影响范围、预期结果与回退方式。批量脚本必须先列出目标,再执行带边界的操作,避免组名前缀拼错波及无关服务。
例如重置进度之前应暂停目标组、保存旧位置、验证幂等、估计重放量;完成后低速恢复并观察业务重复拒绝、下游容量和积压变化。仅看到命令返回成功,不代表运行中所有实例已经采用新进度。
值班手册的结构
一份可用的手册应包含:症状和用户影响;如何确认集群与业务身份;前五分钟的只读检查;允许自动执行的缓解;需要业务判断的恢复动作;联系与复盘记录。告警阈值按容量和 SLA 设置,不要直接复制某篇文章的数字。
故障后记录时间线、影响范围、丢失或重复数据的对账结论、检测和恢复耗时。改进项应有明确验证方法,例如增加“积压年龄超过十分钟”告警,而不是只写“加强监控”。
练习:Broker 吞吐正常但订单完成延迟持续升高。给出至少四个可能原因,并为每个原因指定能够证伪它的指标或日志。
第十五章 容量规划与性能实验
先计算数量级
假设平均每秒 20,000 条,平均正文 1 KiB,保留 72 小时,两份副本。只算正文:
每天正文 = 20,000 × 1,024 × 86,400 ≈ 1.77 TB
三天正文 ≈ 5.31 TB
两份副本正文 ≈ 10.62 TB
这里使用十进制 TB;换成 TiB 数值会不同。实际还要加消息属性、协议和文件开销、ConsumeQueue、索引、重试、延时存储、文件清理余量和复制追赶空间。平均大小也不代表峰值分布,大消息比例会影响内存和网络。
把两个副本都放在一台机器上,磁盘需求可能满足,但高可用目标仍不成立。资源计算和故障域规划是两个必须同时满足的维度。
吞吐、并发和延迟
用 Little 定律作初步估算:稳定系统中,在途数量约等于到达率乘平均耗时。每秒 2,000 个任务、每个平均 50 毫秒,大约需要 100 个在途处理。它是稳态平均关系,不能预测尾延迟、突发流量或锁竞争。
提高并发前,分别检查 CPU、磁盘、网络、数据库连接与锁。若瓶颈在下游串行锁,增加消费线程可能只让排队变长。若 CPU 很低但线程都在等远程服务,适度并发可能有效,但应遵守对方配额。
积压恢复时间
已有积压 B,当前生产速率 λ,消费能力 μ。只有 μ > λ
时,才有持续追赶能力;理想追赶时间约为 B / (μ - λ)。
例如积压 3,600 万条,生产每秒 10,000 条,消费每秒 15,000 条,理想追赶需要 7,200 秒,即两小时。若业务要求一小时恢复,至少需要每秒额外 10,000 条净消化能力,还要考虑冷读、重试与下游限速。
这个公式假设速率稳定。真实恢复应测一段足够长的窗口,使用实际积压下降曲线估计,而不是拿某一分钟峰值直接承诺完成时间。
压测矩阵
至少覆盖小消息和大消息、正常实时消费和冷积压追赶、同步与异步发送、目标刷盘复制策略、不同队列数和消费组数、节点故障与恢复。业务处理不能只是空回调,否则无法说明真实下游能承受同样吞吐。
分别报告 p50、p95、p99、错误率和持续时长。若负载生成器自身 CPU 满了,观察到的吞吐上限可能来自客户端。记录机器、JDK、版本、网络、磁盘、配置与数据分布,让结论可复核。
保护机制
流量入口限速、生产者有界缓冲、消费端线程与批次上限、下游熔断和重试退避共同防止雪崩。队列的积压容量是最后缓冲,不应成为无限接收的理由。达到危险水位时,业务可能需要降级辅助事件、延后非核心任务或拒绝新请求。
练习:凌晨停机三小时后恢复,白天生产速率翻倍。用分时速率估算恢复窗口,说明为什么只用全天平均值会低估风险。
第十六章 安全与多环境治理
网络和身份是两道边界
NameServer、Broker、Proxy 和管理入口只开放给必要网络。把端口藏在内网并不代表身份可信:同网段的其他应用仍可能误操作资源。按生产者、消费者和运维职责分配最小权限,并隔离开发与生产凭据。
RocketMQ 各版本的 ACL、认证与授权机制存在演进。5.3.2 及后续版本应按对应版本文档选择和验证机制,不能把旧版 ACL YAML 与新版认证配置混用。本书明文本机实验没有接入生产权限系统。
传输加密的配置取决于经典 Remoting、Proxy gRPC、客户端以及证书管理方式。要验证服务器证书、主机名和客户端信任链,不能通过“关闭所有校验”解决连接问题。证书续期和轮换也应纳入演练。
凭据与资源管理
凭据通过部署平台的秘密管理系统注入,不提交进 Git。配置仓库可保留变量名和模板,不保留真实 Access Key、私钥和用户信息。日志、异常栈与截图也可能泄露秘密,排障时同样需要控制。
Topic、Group 和权限变更应能追溯到应用与负责人。命名可以包含业务和环境,但不要把无限高基数数据放进资源名。过期消费组和 Topic 需要经过确认再清理,不能因为短期无流量就直接删除。
隔离与配额
多租户共享集群时,一个大消息生产者或异常重试组可能占满磁盘与 CPU。根据组织边界选择独立集群、独立 Broker 或逻辑资源隔离,并验证服务端和外围治理工具实际提供的限流能力。
配额不只是每秒条数,还包括字节数、连接数、Topic 数、消费组数、保留与重放量。设置配额后,应给应用明确的失败处理方式,否则限流会被无限重试放大。
安全更新
版本固定有助于复现,但不能成为长期不更新的理由。定期检查 Apache RocketMQ 安全公告、依赖漏洞与发行说明,判断漏洞是否影响实际暴露面,然后在隔离环境验证升级路径。对管理入口、配置修改权限和公网暴露优先做清查。
练习:测试环境需要读取生产消息样本来复现问题。设计一条经过脱敏、授权和审计的数据提取路径,避免直接给测试程序生产消费者凭据。
第十七章 故障演练与上线验收
演练需要明确可观察结果
每次演练先写下假设、操作范围、停止条件、预期告警和数据对账方法。只在隔离实验或获准环境执行故障注入。操作完成不是演练成功,恢复后业务状态正确才是。
建议为事件建立三份可比证据:生产侧持久化事件清单、Broker 或客户端投递证据、消费侧业务流水。Broker 查询受保留与索引条件限制,不能作为唯一永久账本。
三个仓库可运行实验
# 正常收发
mvn -f examples/pom.xml -q exec:java \
-Dexec.mainClass=book.rocketmq.RoundTrip
# 第一次处理主动失败,确认之后发生重新投递
mvn -f examples/pom.xml -q exec:java \
-Dexec.mainClass=book.rocketmq.RoundTrip -Dexec.args=retry
# 默认等级 3 的延时投递
mvn -f examples/pom.xml -q exec:java \
-Dexec.mainClass=book.rocketmq.RoundTrip -Dexec.args=delay重试实验会打印 attempt=1
和后续次数。它验证重投递,不验证库存幂等;要证明幂等,必须另加真实数据库事务并检查业务流水只有一份。延时实验验证没有过早收到消息,并给出实际总耗时;它不证明所有压力下都能精确十秒执行。
本仓库 research/runtime-validation.txt
保存本次实际运行结果;环境与失败情况如实记录,不把未执行的演练写成通过。
故障矩阵
| 故障 | 应观察 | 验收关注点 |
|---|---|---|
| 发送成功前后断开网络 | 超时、重复、稳定 eventId | 不把未知结果当成确定失败 |
| 消费提交业务后进程退出 | 再次投递与幂等拒绝 | 业务效果只有一次 |
| 下游数据库暂时不可用 | 重试退避、积压、告警 | 不提前确认、不击穿恢复节点 |
| 慢消费者与冷积压 | 积压年龄、磁盘读与追赶速率 | 旧消息在清理前处理完 |
| 主 Broker 故障 | 主身份、复制差距、客户端恢复 | 实际达到既定 RPO/RTO |
| 非法契约消息 | 失败隔离、死信与告警 | 不长期阻塞所有正常业务 |
| 重置进度 | 重放量、重复拒绝、外部副作用 | 可审计、可控制、业务可核对 |
上线验收是一份证据清单
确认版本、部署模式、客户端协议与连接地址一致;确认 Topic、Group、权限和保留策略已审批;确认生产侧有可靠补发来源,消费侧有并发安全幂等;确认业务状态机、死信处理和对账存在;确认容量、积压恢复时间与故障切换经过目标环境验证。
还要确认值班人员能拿到日志、指标和手册,凭据可轮换,安全更新有人跟踪。一个只有作者笔记本上能启动的系统,不等于团队已经能够维护。
一个最终设计题
设计每天一千万笔订单的消息链路,要求订单创建事件不因应用重启丢失,库存重复扣减为零,支付后不可被超时关单覆盖,单 Broker 故障后十分钟内恢复,允许重放最近七天事件。
先写业务不变量,再选 Outbox 或事务消息,再设计库存幂等和订单状态机,最后选择复制与容量方案。若只先画出三个 Broker,你尚未回答最核心的问题。
附录 A 练习参考与术语
设计题参考方向
第一章:短信失败通常进入独立重试,不撤销已经成立的订单;库存失败可能触发取消或人工处理,取决于业务承诺。建议用
inventory-order-events、notification-order-events、risk-order-events
表达独立逻辑,各组内部再水平扩容。
第四章:对未知发送结果返回“处理中”并提供按业务请求 ID 查询状态的接口。相同幂等键只能建立一份订单与待投递事件,不能每次点击都创建新订单。
第六、七章:业务提交后重投递应命中幂等键,库存不再改变。订单原子事务易于保证整单一致性,但锁持有更久;逐商品处理需要每项独立键、汇总状态和部分失败补偿。
第八章:依靠数据库版本和写入者代际约束拒绝旧写入者。版本并不是随意由每个进程从零开始计数,而应来自权威状态。
第九章:消息携带计划版本,消费端读取最新到期时间,用状态、版本和当前时间做条件更新。旧消息到达时只做一次无副作用的检查。
第十章:最少需要稳定事务 ID、业务关联、可证明提交或回滚的持久化事实,以及足够长的保留窗口。没有证据时应保持未知并调查,不能猜测。
第十一章:租约到期重投相同事件 ID;消费端唯一键与库存更新在同一事务。若发送确认更新失败,允许重复投递但不允许重复业务效果。
第十四章:可能是某个队列热点、下游变慢、重试流量升高、消费者频繁重平衡、定时消息集中到期。分别核对队列级延迟、数据库耗时、重试指标、实例变更和到期流量分布。
最终设计题没有唯一拓扑。首先把不变量落到数据库约束和可恢复发送义务,再按峰值、字节数和七天重放计算容量。十分钟恢复需要故障演练的持续流量证据,不能只依赖静态架构图。
术语速查
| 术语 | 在本书中的含义 |
|---|---|
| Topic | 消息类别与资源管理边界 |
| Queue | 经典客户端分配与顺序推进的重要单位 |
| Consumer Group | 同一种消费逻辑的实例集合与进度身份 |
| CommitLog | 经典存储中的消息正文追加日志 |
| ConsumeQueue | 按 Topic 和 Queue 组织的消费位置索引 |
| Offset | 特定队列或日志内的位置,需说明是哪一种 |
| At-least-once | 可能重复交付,需要业务幂等 |
| 幂等 | 同一业务输入重复处理不增加业务效果 |
| DLQ | 达到失败条件后隔离待处理消息的死信队列 |
| Outbox | 与本地业务同事务保存待投递事件的模式 |
| RPO / RTO | 可接受数据损失目标 / 恢复时间目标 |
| FIFO / MessageGroup | 新客户端中用于表达分组顺序的相关概念 |
附录 B 资料、源码与阅读路径
本书参考官方文档和 Apache RocketMQ
源码,文字为独立编写。在线文档会变化,research/sources.json
记录取证地址、时间与内容哈希;文本快照用于复核,不将网页视为固定版本规范。
官方资料
- Quick Start:服务启动与基本客户端使用。
- Domain Model:Topic、Group、Queue 及消息模型。
- FIFO Message:分组顺序与顺序保证边界。
- Delay Message:定时交付语义与约束。
- Transaction Message:半消息、本地事务与回查。
- Consumer Retry Policy:不同消费类型的重试行为。
- Automatic Failover:Controller 相关部署与故障切换。
- Apache RocketMQ Security:安全通告与报告入口。
- 5.5.1 发布说明:资料核对日所见最新发布,不是实验基线。
固定源码入口
实验基线 tag 为 rocketmq-all-5.3.2,提交为
2baaf044ea9b12a73a89433242b7b7c56a1b89c2。阅读顺序建议从
API 到请求处理,再到存储:
- DefaultMQProducer:发送参数和重试配置。
- DefaultMQProducerImpl:路由、选择队列和发送路径。
- DefaultMQPushConsumerImpl:消费生命周期。
- SendMessageProcessor:Broker 发送请求处理。
- DefaultMessageStore:存储读写与后台任务。
- MessageStoreConfig:延迟等级、定时范围和存储配置默认值。
- TransactionalMessageServiceImpl:事务消息相关状态处理。
阅读源码时先沿一次正常请求走通,再检查超时、异常和恢复分支。方法名相同不代表不同版本实现相同;源码定位、实验日志和业务不变量结合起来,才构成可靠理解。