RabbitMQ 4.4.0 在 10 月 19 日发布。quorum queue 从这个版本起支持发布时就指定投递时间的延迟消息,认的是 x-opt-delivery-time 这个头。Spring AMQP 自带的 setDelayLong 写的却是延迟交换机插件用的 x-delay 头,发到 4.4 的 quorum queue 上,消息会立刻送达。我用 Spring Boot 4.1.1 连 4.4.0 和 4.3.6 两个 broker 跑了六组,只有 long 型的 x-opt-delivery-time 加 quorum queue 这一种组合真的延迟了。

4.4.0 这次改了什么

按 4.4.0 的 Release Notes,消息带上 x-opt-delivery-time,值是未来某个时间点的 Unix 毫秒时间戳,quorum queue 会立刻路由入队,但要等到这个时间才投递给消费者。AMQP 1.0 客户端把它放在 message annotation 里,AMQP 0-9-1 客户端放成 long 型的 header。AMQP 1.0 消费者还能在 modified 结果里带上这个注解,让一条消息延后重投。

4.3 里 quorum queue 已经有 delayed retry,但那只管被退回的消息怎么延后重投,靠队列参数或 policy 配置。4.4 新加的是另一回事:生产者发消息时就定好什么时候能被消费。

这件事跟延迟交换机插件的退场连在一起。插件仓库已经归档,README 写得很直白,团队不再维护它,原因是插件依赖 Mnesia,而 Mnesia 在 4.3.0 的开发周期里已经从 RabbitMQ 彻底移除。README 给的替代方案,第一条就是 4.4 起 quorum queue 自带的延迟投递,第二条是老办法 TTL 加死信。

同一个版本里还有几条升级前要看的。OpenID Connect 的 discovery 响应校验变严,issuer 对不上或者端点不是 https 会直接失败,Cloud Foundry UAA 就在受影响名单里。GET /api/auth/hash_password/{password} 换成了 POST /api/auth/hash_password。classic queue 的共享消息存储有了新的 v2 文件格式,节点上一旦有了 v2 文件就不能再降回 4.3.x。Erlang 方面支持 28 和 29,Release Notes 建议同时升 RabbitMQ 和 Erlang 28 以上时,先升到 4.3.7 或更新的 4.3 补丁版,写这篇时 GitHub 上 4.3 系列最新的发布还是 4.3.6。

实验怎么搭

两个 broker 都用官方 Docker 镜像,4.4.0 映射到 25672,4.3.6 映射到 25673。应用从 start.spring.io 生成,Spring Boot 4.1.1,只加了 Spring for RabbitMQ。启动后声明一个队列,挂一个 SimpleMessageListenerContainer 收消息,然后发消息,打印每条消息从发出到收到用了多少毫秒。发送部分的代码是这样的:

long d = delays.get(i);
MessageProperties props = new MessageProperties();
if (d >= 0 && "x-delay".equals(headerType)) {
    // Spring AMQP 自带的延迟写法,给延迟交换机插件用的 x-delay 头,值是相对毫秒数
    props.setDelayLong(d);
}
else if (d >= 0) {
    long due = t0[0] + d;
    props.setHeader("x-opt-delivery-time", "string".equals(headerType) ? String.valueOf(due) : (Object) Long.valueOf(due));
}
rabbitTemplate.send("", queue, new Message(body.getBytes(), props));

队列类型、头的写法和每条消息的延迟都走命令行参数,六组场景放在同一个 verify.sh 里一次跑完:

### 环境
openjdk version "21.0.12.1" 2026-08-18
<version>4.1.1</version>
docker 26.1.5+dfsg1
2026-10-20 08:22:44 CST

### 启动两个 broker
$ sudo -n docker run -d --name rmq44 -p 25672:5672 rabbitmq:4.4.0 >/dev/null
rmq44 日志出现 Server startup complete,用了 3s
$ sudo -n docker run -d --name rmq43 -p 25673:5672 rabbitmq:4.3.6 >/dev/null
rmq43 日志出现 Server startup complete,用了 3s
$ sudo -n docker ps --filter name=rmq4 --format '{{.Names}} {{.Image}} {{.Status}}'
rmq43 rabbitmq:4.3.6 Up 3 seconds
rmq44 rabbitmq:4.4.0 Up 6 seconds
$ sudo -n docker exec rmq44 rabbitmqctl version
4.4.0
$ sudo -n docker exec rmq43 rabbitmqctl version
4.3.6

quorum queue 加 long 型头,延迟生效

### A: 4.4.0,quorum queue,延迟头 5000ms(long)
$ java -jar target/qqdelay-0.0.1-SNAPSHOT.jar --spring.rabbitmq.port=25672 --demo.queue-type=quorum --demo.delays=5000
broker=4.4.0 queue=demo.quorum x-queue-type=quorum header-type=long
已发送 [m1(delay=5000ms)]
m1(delay=5000ms) 收到于 +5014ms

5 秒后收到,跟设定的一致。

classic queue 和 4.3.6 都不报错,直接投递

同样的头发到 4.4.0 的 classic queue,再发到 4.3.6 的 quorum queue:

### B: 4.4.0,classic queue,同样的延迟头
$ java -jar target/qqdelay-0.0.1-SNAPSHOT.jar --spring.rabbitmq.port=25672 --demo.queue-type=classic --demo.delays=5000
broker=4.4.0 queue=demo.classic x-queue-type=classic header-type=long
已发送 [m1(delay=5000ms)]
m1(delay=5000ms) 收到于 +10ms

### C: 4.3.6,quorum queue,同样的延迟头
$ java -jar target/qqdelay-0.0.1-SNAPSHOT.jar --spring.rabbitmq.port=25673 --demo.queue-type=quorum --demo.delays=5000
broker=4.3.6 queue=demo.quorum x-queue-type=quorum header-type=long
已发送 [m1(delay=5000ms)]
m1(delay=5000ms) 收到于 +15ms

两边都是十几毫秒就收到了。broker 不认这个头,也不拒绝,消息照常投递,生产者和消费者都看不出任何异常。所以代码先上线、broker 还没升到 4.4,或者有人把队列声明成了 classic,延迟都会悄悄失效。延迟只在队列的 x-queue-type 是 quorum、broker 在 4.4 以上时才生效。

延迟的消息不会堵住后面的

先发一条延迟 6 秒的,再发一条不延迟的和一条延迟 2 秒的:

### D: 4.4.0,quorum queue,先发延迟 6000ms,再发不延迟和延迟 2000ms
$ java -jar target/qqdelay-0.0.1-SNAPSHOT.jar --spring.rabbitmq.port=25672 --demo.queue-type=quorum --demo.delays=6000,-1,2000
broker=4.4.0 queue=demo.quorum x-queue-type=quorum header-type=long
已发送 [m1(delay=6000ms), m2(delay=无), m3(delay=2000ms)]
m2(delay=无) 收到于 +9ms
m3(delay=2000ms) 收到于 +2030ms
m1(delay=6000ms) 收到于 +6003ms

收到的顺序是 m2、m3、m1,按到期时间投递。排在前面的延迟消息不会挡住后面已经能投递的消息。TTL 加死信那套做法,按消息设 TTL 时,过期的消息要走到队头才会被死信,前面一条长 TTL 会把后面短 TTL 的一起压住,这是很多人用它做延迟时踩过的坑。之前那篇 Spring Boot中实现订单30分钟自动取消的策略 里的延迟队列方案,用的就是延迟交换机插件加 setDelay。到了 4.4,quorum queue 自己就能做这件事。

头写成字符串,或者用 setDelayLong,都等于没写

### E: 4.4.0,quorum queue,延迟头写成字符串
$ java -jar target/qqdelay-0.0.1-SNAPSHOT.jar --spring.rabbitmq.port=25672 --demo.queue-type=quorum --demo.header-type=string --demo.delays=5000
broker=4.4.0 queue=demo.quorum x-queue-type=quorum header-type=string
已发送 [m1(delay=5000ms)]
m1(delay=5000ms) 收到于 +9ms

### F: 4.4.0,quorum queue,用 Spring AMQP 的 setDelayLong(x-delay 头)
$ java -jar target/qqdelay-0.0.1-SNAPSHOT.jar --spring.rabbitmq.port=25672 --demo.queue-type=quorum --demo.header-type=x-delay --demo.delays=5000
broker=4.4.0 queue=demo.quorum x-queue-type=quorum header-type=x-delay
已发送 [m1(delay=5000ms)]
m1(delay=5000ms) 收到于 +10ms

E 组把时间戳写成了字符串,F 组用的是 Spring AMQP 的 setDelayLong,两组都是 10 毫秒左右就收到了。

字符串这一组对应的是 Release Notes 里那句 AMQP 0-9-1 客户端要用 long 型 header。在 Java 里拿 String.valueOf 拼一下,或者从别的系统透传过来的头本来就是字符串,延迟就没了。

setDelayLong 那一组更容易中招。旧代码里常见的 setDelay(Integer) 在 Spring AMQP 3.1.2 被标成弃用,3.2 起已经删掉,4.1.1 的 MessageProperties 里只剩 setDelayLong。它写的是 x-delay 头,常量 X_DELAY = "x-delay",值是相对毫秒数,这是给延迟交换机插件准备的。quorum queue 认的是 x-opt-delivery-time,值是绝对时间戳。两个头名字不同,值的含义也不同。

Spring AMQP 目前也没有专门写 x-opt-delivery-time 的方法,4.1.1 和 4.2.0-M2 的 MessageProperties 里都找不到。所以从插件迁到 quorum queue,只换交换机不够。发送端得自己算出到期时间,用 setHeader 以 long 型写进 x-opt-delivery-time,就像上面 A 组那样。

参考来源

— 感谢阅读 —
广告

一起交流

分享你的思考,让讨论更进一步。