读完这篇,你能做什么
能按 RocketMQ 5 模型选择消息类型,并说明其不能保证的跨系统结果。
学习理由、准备要求与知识点
为什么学它
顺序、延迟和事务消息都有明确前提,误把局部保证当全局事务会留下数据缺口。
先读这些内容
知识点清单
- 消息组顺序
- 生产串行
- 消费串行
- 投递时间
- 延迟精度边界
- 半消息
- 事务回查
- 最终一致性
运行前先确认消息语义参考 RocketMQ 5.x 官方文档;事务源码对照经典 Java 客户端 rocketmq-all-5.3.1。新 gRPC SDK 的 API 形式不同。算法使用 Java 17 离散时间模型,不承诺真实定时精度或外部事务原子性。
01先弄懂它是什么
除了稍后处理事件,业务还可能要求“先创建再支付”“十五分钟后检查”“本地订单提交后事件才可见”。这分别涉及顺序、延迟和事务消息,解决的是不同问题。使用这些类型,不代表自动得到全局顺序、精确到毫秒的完成时间或所有服务一起提交。先写清业务必须保证什么,再看消息系统能保证哪一部分。
顺序通常针对同一个业务对象,例如同一订单的创建、支付和取消。RocketMQ 5.x 用 message group 把需要保持顺序的事件放在一组,生产者也必须先形成明确先后。多个生产者同时给出矛盾事件时,Broker 不能只凭到达时间猜出真实因果;消费者若把顺序收到的事件交给任意线程并行执行,数据库最终提交也可能反过来。
延迟消息约束较晚进入可消费状态,不能保证业务恰好在那一毫秒完成;队列积压、调度和数据库都可能继续延迟。事务消息把生产者本地事务结果与消息最终可见性联系起来,不把所有下游数据库加入同一个本地事务。订单创建已提交以后,积分消费失败需要下游重试或补偿,不能期待 Broker 自动撤销已经提交的订单。
文中这几个词,先认一认
- Message group
- 把同一业务对象需要排序的事件放在一组,组内遵守先后要求,不同组可并行。
- Delivery timestamp
- 希望消息开始能被消费的绝对时间,不是业务处理必定完成的时间。
- Half message
- 等待生产者本地事务结果决定、暂时不对普通消费者可见的消息阶段。
- 事务回查
- 根据已经持久保存的业务结果,再判断事务消息该提交、回滚还是仍未知,以恢复丢失的决定。
02看看它怎么工作
顺序链条包括生产顺序、存储路由和消费完成顺序。可用 orderId 作为组键,但热订单或一个过粗的全局组会串行化大量任务。一次消费失败若跳过继续执行后面的依赖事件,就可能破坏业务状态机;坚持严格顺序则意味着后续消息可能被阻塞。应明确允许丢弃、补偿还是等待修复,而不是只调整重试次数掩盖顺序策略。
延迟订单关闭应该被建模成“到期后重新检查”。消息内保存 orderId 与期望版本,到达后在数据库条件更新中检查仍未支付且已过期,再转关闭状态。订单可能已支付、已关闭,或者延迟消息被重复投递;这些情况应成为无操作或明确冲突,而不是再次扣库存。绝对投递时间还要核对毫秒单位、允许范围和版本配置,不能照搬旧客户端的固定延迟等级。
事务消息先送到 Broker(消息服务器)保存,但暂不交给消费者;这时它叫“半消息”。生产者执行本地事务后,再通知 Broker 提交还是回滚这条消息。若本地已经提交,通知却丢了,Broker 会回头询问,生产者要根据数据库里的业务流水回答。
查询超时或业务仍在执行时,应回答未知,不能把“暂时查不到”直接当成回滚。回查的等待时间和次数都有上限,长期没有明确结果就要告警、对账,不能期待系统一直替你等下去。
正在加载流程图…
查看流程图源码(Mermaid)
sequenceDiagram
participant P as 生产者
participant B as Broker
participant D as 生产者数据库
participant C as 消费者
P->>B: 发送半消息
B-->>P: 半消息接收结果
P->>D: 执行本地业务事务
D-->>P: 已提交的持久状态
P--xB: 提交决议可能丢失
B->>P: 回查事务结果
P->>D: 按事务业务 ID 查询
D-->>P: 返回已提交证据
P-->>B: COMMIT
B->>C: 消息可见后投递
Note over C,D: 消费者的业务事务是另一边界按顺序看,每一步做了什么
- 先选择业务真正需要的消息类型与 Topic 配置。
- 顺序场景明确组键与唯一可解释的发送顺序。
- 延迟场景将到期消息作为触发检查,条件更新业务状态。
- 事务场景先建立半消息,再执行本地事务并通知决议。
- 回查读取可持久验证的状态;恢复后对账所有未知和失败结果。
03对照实现代码
rocketmq-all-5.3.1 · DefaultMQProducerImpl#sendMessageInTransaction 与 endTransaction
原创局部 Java 时序模型,突出半消息、本地执行、决议三段。它没有复制 SDK 的网络与状态编码,也不实现数据库事务;真实调用入口与新 SDK 不可混用。
import java.util.function.*;
final class TransactionModel {
enum Decision { COMMIT, ROLLBACK, UNKNOWN }
static Decision execute(Runnable sendHalf, Supplier<Decision> local,
Consumer<Decision> notifyBroker) {
sendHalf.run(); // 未得到可接受的半消息结果,不能继续本模型的业务路径。
Decision result = Decision.UNKNOWN; // 缺省未知,禁止猜测成功。
try {
Decision observed = local.get(); // 真实实现应执行或查询持久事务状态。
if (observed != null) result = observed;
} catch (RuntimeException uncertain) {
// 调用失败可能是结果未知,后续需回查;不能擅自认定已回滚。
}
notifyBroker.accept(result); // 网络通知仍可能失败,不能回滚已提交的数据库事实。
return result;
}
}在固定标签源码中,sendMessageInTransaction 先给消息加入事务相关属性并调用发送,再根据发送状态决定是否执行本地事务监听器。这个先后关系解释了为什么事务发送不能简单实现成“本地提交后普通发送一次”。普通发送失败后没有同样的半消息状态可供 Broker 回查,需要另一种持久补发机制。
本地监听器返回提交、回滚或未知,方法还需要处理空返回和异常。教学模型默认 UNKNOWN,强调没有明确证据时不应捏造结果。真实异常来源可能在提交之前,也可能在提交结果返回途中,应用必须用业务流水区分。数据库查询失败时返回“没有记录”,会把基础设施故障错误转换成业务回滚。
endTransaction 把本地事务的处理结论发给 Broker,但这次通知和数据库提交不是一个可以一起成功、一起回滚的操作。通知失败时,本地订单可能早已提交,应该让后续回查确认结果,不能在 catch 中随意删订单。事务发送的返回对象包含多个状态,需要分别看各阶段结果,不能只读一个字段就认定全部完成。
回查可能执行多次,应该查询随本地事务一起提交的状态记录,并控制查询开销,不能依赖原来线程中的变量。记录要能回答“当时那次事务最终是否提交”,并保存必要版本。如果订单后来被人工撤销,当前查不到订单也不能证明当初创建失败;有中间状态时,还要明确什么时候才能作出不再改变的结论。
代码先调用谁,再调用谁
TransactionMQProducer进入事务发送逻辑DefaultMQProducerImpl.sendMessageInTransaction发送半消息TransactionListener.executeLocalTransaction执行本地事务endTransaction将决议通知 Broker- 结果未知或通知丢失时,事务回查读取持久业务状态
04动手写一个例子
到期事件调度:最小堆与确定性的同时间排序
实现内存调度器,添加带唯一正整数 id 与非负 due 时间的任务;drain(now) 返回所有 due≤now 的任务 ID,按到期时间、再按 id 排序。相同待定 ID 不允许重复。模型只决定哪些事件已到期,不负责执行业务或持久恢复。
- 输入 1
加入 (3,20),(2,10),(1,10);drain(10)- 输出 1
[1,2]
- 输入 2
随后 drain(19)、drain(20)- 输出 2
[]、[3]
每次判断到期任务时,扫描全部任务会对尚未到期的任务反复工作。最小堆让最早到期任务位于根部。如果根部仍在未来,所有其它任务都不可能更早;如果根部已到期,取出后继续检查新的根。这个局部顺序性质足以支持到期批量提取,不需要每次将整个集合完全排序。
同一时间可能有多个任务,PriorityQueue 不保证比较器认为相等的元素保持插入顺序。因此我们明确用 id 作为第二排序键,让测试与输出具有确定性。比较 long 值应使用 comparingLong 或 Long.compare,不能把两时间相减再强转 int;差值可能溢出,破坏比较器的传递性并使堆失去可靠顺序。
额外 Set 保存待定 ID,阻止相同任务同时进入堆两次。出堆时同步删除 ID,允许以后按接口约定重新安排该任务;这只是内存待定去重,绝不等价于业务终身幂等。生产场景还需要任务状态、租约、执行结果和失败重试,将出堆与业务处理视为两个不同阶段。
为什么这样写是对的堆根始终是全部待定任务中按 (due,id) 最小者;Set 与堆中 ID 一一对应。drain 只移除已经到期的最小前缀,因此结果有序且没有遗漏到期项。
每次堆插入和删除最多沿树高移动 O(log n) 层,peek 为常数。Set 操作为通常散列假设下均摊常数;若讨论恶意键或持久索引,需要另行计入相应成本。
保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。
展开收起代码 · Main.java · 完整可运行程序
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
import java.util.*;
public class Main {
record Task(long id, long due) {}
static final class Scheduler {
private final PriorityQueue<Task> heap = new PriorityQueue<>(
Comparator.comparingLong(Task::due).thenComparingLong(Task::id));
private final Set<Long> pendingIds = new HashSet<>();
synchronized void add(long id, long due) {
if (id <= 0 || due < 0) throw new IllegalArgumentException("id or due");
if (!pendingIds.add(id)) throw new IllegalArgumentException("duplicate id");
heap.add(new Task(id, due)); // 到期时间优先,相同时按 ID 排序。
}
synchronized List<Long> drain(long now) {
if (now < 0) throw new IllegalArgumentException("now");
List<Long> ready = new ArrayList<>();
while (!heap.isEmpty() && heap.peek().due <= now) {
Task task = heap.remove(); // 根到期才移除,未来任务保持待定。
pendingIds.remove(task.id);
ready.add(task.id);
}
return ready;
}
}
static void eq(Object expected, Object actual) {
if (!Objects.equals(expected, actual)) throw new AssertionError(actual);
}
public static void main(String[] args) {
Scheduler s = new Scheduler();
eq(List.of(), s.drain(0));
s.add(3, 20); s.add(2, 10); s.add(1, 10);
eq(List.of(1L, 2L), s.drain(10)); // 恰好到期,且同时间确定排序。
eq(List.of(), s.drain(19));
eq(List.of(3L), s.drain(20));
s.add(4, Long.MAX_VALUE);
eq(List.of(), s.drain(Long.MAX_VALUE - 1));
eq(List.of(4L), s.drain(Long.MAX_VALUE)); // 比较不依赖减法。
s.add(5, 100);
boolean bad = false;
try { s.add(5, 101); } catch (IllegalArgumentException e) { bad = true; }
if (!bad) throw new AssertionError("duplicate accepted");
System.out.println("PASS 7");
}
}这些特殊情况也要试一试
- 恰好到期应允许提取,比较使用 ≤。
- 同时间不依赖堆的稳定性,显式提供第二排序键。
- 极大时间戳比较不能做溢出的差值转换。
- 本模型允许时间回退但已取出的任务不会恢复,真实墙钟跳变需单独设计。
进阶选读 · 顺序消息组的乱序缓冲与有限等待
场景事件携带订单版本号 1、2、3,但传输与重试导致到达顺序为 3、1、2,业务仍要求按版本推进。
做法为每个订单维护 nextVersion 与 TreeMap 缓冲。只应用等于 nextVersion 的事件,成功后连续排空相邻版本;小于当前版本的事件按摘要核对重复,大于当前版本的事件暂存。缺口超过时间或容量上限触发查询与对账,禁止无限缓存。
复杂度每个事件插入 O(log b),b 为该组缓冲量;成功排空每条一次,整个批次保守 O(n log b)。总空间受所有活跃组及各组上限约束。
好处与代价缓冲恢复逻辑顺序,却增加状态存储与缺口等待。严格顺序与低尾延迟有冲突;若业务可以交换或合并事件,重新设计状态机可能比无限等待更好。
05项目里怎么用
订单支付与超时关闭竞争时,延迟消息只是触发条件检查的一方。可用数据库条件更新确保只把 UNPAID 且版本匹配的订单转成 CLOSED;支付流程也必须遵守同一状态机。只在 Java 中先查再改而没有锁或条件更新,两条请求可能都基于过期快照成功,消息类型无法弥补业务层的竞争漏洞。
事务回查要能凭稳定的业务标识快速查到记录,并限制等待时间,监控 UNKNOWN 有多少、最久持续了多久。记录仍在提交中时,应继续返回未知;只有业务处理规则能证明它以后也不会提交,才可以判定终止。长期未知的记录需要对账恢复,否则半消息被终止后,本地已经成功的业务可能收不到后续处理。回查也有期限和容量限制,不能当成永远有效的重试。
顺序、延迟和事务测试要分别注入故障:同组并发发送观察次序,消费转交线程池观察完成次序,延迟触发时订单已支付检查无副作用,本地提交后中断通知检查回查结果。测试报告分开记录协议结果和最终业务状态。生产发布先验证小流量与回查容量,再逐步扩大,避免一次引入大量无法决议的半消息。
容易踩的坑
- 把 message group 局部顺序写成整个 Topic 全局顺序。
- 把到期可消费误认为到期业务已完成。
- 回查数据库暂不可用时直接返回 ROLLBACK。
- 回查依赖进程内布尔值,进程重启后失去证据。
- 下游事务失败时期待
RocketMQ自动回滚上游订单。
几种做法,怎么选
表格有四列,窄屏可左右滑动查看代价和适用条件。
| 方案 | 优势 | 代价与限制 | 适用条件 |
|---|---|---|---|
| 普通消息加业务版本检查 | 保持异步并行,版本可显式拒绝过期事件。 | 应用必须处理缺口、乱序和补偿。 | 业务允许独立处理或可交换更新。 |
| FIFO 消息组 | 同实体顺序更容易表达。 | 热组串行、失败会阻塞后续。 | 严格先后顺序且组键可合理分散。 |
| 事务消息 | 半消息与回查协调上游事务和消息可见性。 | 需要持久决议、回查容量及下游幂等。 | 接受最终一致且消息发布依赖本地提交。 |
| 本地 Outbox | 业务与待发记录同库提交,事件恢复可直接扫描。 | 额外调度、去重及积压管理。 | 希望事务边界集中在本地数据库。 |
06检验是否读懂了
先自己想、自己写,再展开答案核对。场景题是假设的练习情境;遇到还没学过的内容,可以做完基础后再回来。
先用自己的话解释
01同订单两个生产者同时发送能否自动得到业务因果顺序?
展开参考答案与推导
不能仅凭同组键推断。需要上游建立可解释的单一序列或版本规则,服务端到达顺序并不等于业务发生顺序。
02延迟关闭为什么必须复查订单状态?
展开参考答案与推导
消息等待期间订单可能支付或被其它流程关闭,也可能重复投递;业务条件更新防止过期触发产生副作用。
03事务消息提交后积分失败是否回滚订单?
展开参考答案与推导
不会自动回滚。积分是下游独立事务,需要重试、幂等或显式业务补偿。
换个情况,还会处理吗
01回查读取数据库超时,应返回未找到订单吗?
展开参考答案与推导
不应。查询失败意味着证据不可用,应按协议表达 UNKNOWN 并告警,不能把基础设施错误伪装成确定回滚。
02消费者按接收顺序提交到线程池后结果逆序,问题在哪?
展开参考答案与推导
接收顺序不保证并行完成顺序;同消息组需串行执行或使用业务版本与顺序缓冲,不能只调整 Broker 参数。
03大量半消息持续 UNKNOWN,如何处理?
展开参考答案与推导
检查本地状态记录是否共同提交、索引和查询可用性、回查线程与超时容量;按业务 ID 对账,修复决议来源后分批恢复并监控时限。
写代码试一试
01加入过期时间正好等于 now 的任务测试。
- 输入 / 条件
task=(1,50),drain(50)- 输出 / 预期
[1]
提示 比较边界应为小于等于。
展开参考答案与推导
同时测试 drain(49) 为空,避免把到期条件写成严格小于。
02为调度器添加取消操作。
- 输入 / 条件
add(1,20),cancel(1),drain(30)- 输出 / 预期
[]
提示 可用带版本的惰性删除,出堆时核对有效版本。
展开参考答案与推导
Map 保存当前版本,取消删除活动记录,堆中旧项到期时跳过;注意残留堆项会占内存,需要阈值压缩。
03设计关闭订单的条件更新。
- 输入 / 条件
订单 PAID,收到重复超时消息- 输出 / 预期
更新行数 0,支付状态保持不变
提示 把状态与版本写进 WHERE。
展开参考答案与推导
仅允许满足 UNPAID、已到期及预期版本的记录变为 CLOSED;并发支付也使用同一状态机约束,测试两种提交先后。
读完之后,独立完成
课后作业:自己做一遍
本篇共 10 道作业,先完成 8 道基础与综合任务,再按前置知识挑战进阶题。其中包含 1 道思维拓展,试着换个条件,看看原来的办法是否还管用。不附参考答案。
能按 RocketMQ 5 模型选择消息类型,并说明其不能保证的跨系统结果。
本篇重点加练
业务因果顺序与事务回查必须依赖可追踪事实,传输先后本身不足以替代业务决议。
- 消息组顺序
- 事务回查
重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。
先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。
基础必做
- 作业 01代码推演
同一订单经历创建、支付、退款,两个生产实例都可能发消息。设计带业务版本的事件样本,画出发送次序与业务发生次序不一致的时间线。
- 提交内容
- 事件样本与时序图
- 怎样算完成
- 写清消息组选择
- 区分生产串行能力与业务因果
练到的知识点消息组顺序 · 生产串行
- 作业 02面试场景
同一消息组的两项业务处理耗时分别为 2 秒和 20 毫秒,另有订单关闭消息计划 30 分钟后投递。讨论串行处理和定时投递各自限制的对象。
- 提交内容
- 执行时间线与约束表
- 怎样算完成
- 标出实际业务完成时刻
- 注明投递时间使用的时钟与单位
练到的知识点消费串行 · 投递时间
综合应用
- 作业 03原理问答
延迟消息超过预约时刻才到达,事务半消息在生产者重启后仍未决定。分别列出这两类消息的合法状态以及业务可以采取的动作范围。
- 提交内容
- 两份状态表
- 怎样算完成
- 区分迟到与事务未决
- 不把预约时间当作精确执行保证
练到的知识点延迟精度边界 · 半消息
- 作业 04设计评审
本地支付事务可能提交、回滚或结果未知,事务回查又遇到暂时读不到的数据。制定回查所依赖的持久事实及最终一致性核对口径。
- 提交内容
- 回查契约与对账计划
- 怎样算完成
- 区分不存在和不可判定
- 包含迟到回查及重复消费
练到的知识点事务回查 · 最终一致性
- 作业 05编程实践
实现本地 Java 订单事件状态机模拟器,支持延迟交付、重复消息、事务决议晚到和处理异常;用可控时钟输出状态转移日志。
- 提交内容
- 源码、事件脚本与不变量检查
- 怎样算完成
- 同组和跨组行为分开验证
- 明确未验证真实
RocketMQ时序和持久化
练到的知识点消息组顺序 · 生产串行 · 消费串行 · 投递时间 · 延迟精度边界 · 半消息 · 事务回查 · 最终一致性
- 作业 06代码推演
订单消息组曾按商户编号划分,现改为订单编号,两类生产者在灰度期并存。评审同一业务链的顺序声明从何时起才有可验证前提。
- 提交内容
- 灰度期分组及因果图
- 怎样算完成
- 包含在途旧消息
- 区分组内排序与跨组关联
练到的知识点消息组顺序
- 作业 07面试场景
回查读取的是可延迟的副本,主库可能已经提交,但回查只看到旧记录。定义回查证据的新鲜度及不可判定状态的处理职责。
- 提交内容
- 证据来源与决议契约
- 怎样算完成
- 区别旧读和事实不存在
- 说明依赖异常的观测字段
练到的知识点事务回查
- 作业 08编程实践
写本地因果检查器读取订单事件版本、消息组与事务决议日志,标记顺序声明缺少前提或决议互相冲突的事件链。
- 提交内容
- 程序、事件文件和报告
- 怎样算完成
- 允许报告未知而非强行判断
- 注明不模拟真实事务回查网络
练到的知识点消息组顺序 · 事务回查
进阶挑战 · 掌握前置后选做
- 作业 09设计评审
支付成功事件与预约关闭事件在同一分钟到达两个工作者。为订单状态、资金事实和外发事件设计审查表,说明发生冲突时应保留的调查证据。
- 提交内容
- 并发时序与验收矩阵
- 怎样算完成
- 包含两个到达顺序和重复处理
- 区分消息先后与业务状态合法性
练到的知识点消息组顺序 · 消费串行 · 投递时间 · 事务回查 · 最终一致性
思维拓展
先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。
- 作业 10思维拓展 · 条件变化
把系统改成两个地区都能接受同一订单修改,地区间有较长断连。评审原消息组和回查事实来源的适用边界。
- 提交内容
- 多地区约束与反例
- 怎样算完成
- 覆盖并发合法修改
- 明确哪些一致性承诺需改变
练到的知识点消息组顺序 · 事务回查
完成后,再检验一次
给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。
继续阅读
先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。
- 必读参考文档英文
RocketMQ:FIFO messages (新标签页)↗
阅读消息组、生产顺序和消费顺序的前提,限制有序性的适用范围。
- 实践参考文档英文
RocketMQ:Delay messages (新标签页)↗
阅读投递时间与调度限制,区分消息可消费时间和业务完成时间。
- 深入参考文档英文
RocketMQ:Transaction messages (新标签页)↗
阅读半消息、二次确认和事务回查,解释本地事务与消息可见性的关系。
- 深入参考文档英文
RocketMQ 经典生产者源码 (新标签页)↗
定位
sendMessageInTransaction、endTransaction和事务监听器调用。