RabbitMQ 实现延迟消息有两条主流路线:DLQ + TTL(不用装插件,有队头阻塞问题)和 rabbitmq_delayed_message_exchange 插件(精确延迟,需要额外安装)。本文记录插件在 Docker Compose 环境下的完整安装流程、Spring AMQP 的使用示例,以及两种方案在什么场景该选哪一种。
1. 前置条件
- RabbitMQ 3.12+(官方 Docker 镜像
rabbitmq:3.13-management以下都适用) - Docker + Docker Compose
- 能访问 GitHub(下载插件
.ez文件) - Java + Spring Boot 项目(本文示例基于 Spring Boot 3.x + spring-boot-starter-amqp)
2. 下载对应版本的插件
2.1 确认 RabbitMQ 版本
进入运行中的容器查询:
docker exec <容器名> rabbitmqctl version或者查 compose 文件里固定的镜像 tag。
2.2 下载匹配的 .ez 文件
访问 rabbitmq-delayed-message-exchange Releases,在每个 release 的说明里找 Compatible RabbitMQ versions,选一个覆盖你版本的插件 release,下载 rabbitmq_delayed_message_exchange-<version>.ez。
不要按主版本号想当然对应RabbitMQ 3.9 时代流传的”插件版本和 RabbitMQ 主版本号一致”已经不再成立。现在插件有独立的版本号,必须看 release notes 里标注的兼容版本范围,强行用不匹配的插件会导致 RabbitMQ 启动失败。
3. 安装插件:两种方式
3.1 快速方式:docker cp 注入运行中的容器(仅限临时测试)
# 1. 先查一下容器里真实的插件目录docker exec <容器名> rabbitmq-plugins directories -s# Plugin archives directory: /opt/rabbitmq/plugins ← 这一行就是目标路径
# 2. 拷贝进去docker cp rabbitmq_delayed_message_exchange-3.13.0.ez <容器名>:/opt/rabbitmq/plugins/
# 3. 修正文件权限(docker cp 默认 root:root,容器服务以 rabbitmq 用户运行,读不到会启用失败)docker exec -u root <容器名> chown rabbitmq:rabbitmq \ /opt/rabbitmq/plugins/rabbitmq_delayed_message_exchange-3.13.0.ez
# 4. 启用插件(热启用,不需要重启容器)docker exec <容器名> rabbitmq-plugins enable rabbitmq_delayed_message_exchange这种方式的插件活不过容器重建
docker cp进去的文件只存在于当前容器的可写层。一旦执行docker compose down && up或镜像更新,容器重建后插件消失,还要重装一遍。仅适合本地快速测试,生产环境请用 3.2 的 compose 挂载方式。
必要时再重启
rabbitmq-plugins enable是热启用,正常情况下立即生效、不需要重启。只有在启用报错 / 插件没被正确加载时,才需要docker restart <容器名>。
3.2 推荐方式:docker-compose 挂载持久化
把 .ez 文件放在项目目录下,bind mount 到容器的插件目录。这样容器重建、迁移时插件跟着跑。
项目目录结构:
my-rabbitmq/├── docker-compose.yaml├── enabled_plugins└── plugins/ └── rabbitmq_delayed_message_exchange-3.13.0.ezenabled_plugins(用于 RabbitMQ 启动时自动启用插件,省得手动 enable):
[rabbitmq_management,rabbitmq_delayed_message_exchange].注意末尾那个点.这是 Erlang 配置文件语法,末尾的英文句点必须有,否则启动时会报语法错误。
docker-compose.yaml:
services: rabbitmq: image: rabbitmq:3.13-management container_name: rabbitmq restart: unless-stopped ports: - "5672:5672" # AMQP - "15672:15672" # Management UI environment: RABBITMQ_DEFAULT_USER: admin RABBITMQ_DEFAULT_PASS: ChangeMe_StrongPassword volumes: # 持久化消息与元数据 - rabbitmq-data:/var/lib/rabbitmq # 把本地 plugins/ 目录挂到容器插件路径(具体路径用 rabbitmq-plugins directories -s 确认) - ./plugins:/opt/rabbitmq/community-plugins # 预启用插件清单 - ./enabled_plugins:/etc/rabbitmq/enabled_plugins:ro # 通过 RABBITMQ_PLUGINS_DIR 让 RabbitMQ 从默认目录 + 我们挂的目录都查找插件 # 冒号分隔,官方镜像里插件目录为 /opt/rabbitmq/plugins command: > bash -c " export RABBITMQ_PLUGINS_DIR=/opt/rabbitmq/plugins:/opt/rabbitmq/community-plugins && rabbitmq-server "
volumes: rabbitmq-data:启动:
docker compose up -ddocker compose logs -f rabbitmq日志里看到 Starting RabbitMQ 3.13.x ... Applying plugin configuration ... plugins are ready 即为成功。
4. 启用与验证
4.1 确认插件已启用
docker exec rabbitmq rabbitmq-plugins list -e输出中应该看到 [E*] rabbitmq_delayed_message_exchange。
4.2 Web UI 验证
- 浏览器打开
http://<服务器 IP>:15672,用admin/ 上面设置的密码登录 - Exchanges → Add a new exchange
- Type 下拉菜单里应该新增了
x-delayed-message选项
能看到这个类型就说明插件工作正常。
5. Spring Boot 使用示例
5.1 添加依赖(Maven)
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId></dependency>application.yml:
spring: rabbitmq: host: 127.0.0.1 port: 5672 username: admin password: ChangeMe_StrongPassword5.2 声明 delayed-message exchange + queue
@Configurationpublic class RabbitMQConfig {
public static final String DELAYED_EXCHANGE = "delayed.exchange"; public static final String DELAYED_QUEUE = "delayed.queue"; public static final String DELAYED_ROUTING = "delayed.key";
@Bean public CustomExchange delayedExchange() { Map<String, Object> args = new HashMap<>(); // x-delayed-type 指定底层路由行为,direct / topic / fanout 均可 args.put("x-delayed-type", "direct"); return new CustomExchange( DELAYED_EXCHANGE, "x-delayed-message", // 插件提供的 exchange 类型 true, // durable false, // autoDelete args ); }
@Bean public Queue delayedQueue() { return new Queue(DELAYED_QUEUE, true); }
@Bean public Binding delayedBinding() { return BindingBuilder.bind(delayedQueue()) .to(delayedExchange()) .with(DELAYED_ROUTING) .noargs(); }}5.3 发送带 x-delay 的消息
@Service@RequiredArgsConstructorpublic class DelayedMessageProducer {
private final RabbitTemplate rabbitTemplate;
/** * 发送延迟消息 * @param payload 消息内容 * @param delayMillis 延迟毫秒数 */ public void sendDelayed(String payload, long delayMillis) { rabbitTemplate.convertAndSend( RabbitMQConfig.DELAYED_EXCHANGE, RabbitMQConfig.DELAYED_ROUTING, payload, message -> { // x-delay 单位是毫秒;注意 int 类型、最大约 24.8 天 message.getMessageProperties().setHeader("x-delay", (int) delayMillis); return message; } ); }}5.4 消费端
@Component@Slf4jpublic class DelayedMessageConsumer {
@RabbitListener(queues = RabbitMQConfig.DELAYED_QUEUE) public void onMessage(String payload) { log.info("收到延迟消息: {} @ {}", payload, Instant.now()); }}5.5 调用示例
producer.sendDelayed("5 秒后送达", 5_000);producer.sendDelayed("1 分钟后送达", 60_000);5 秒 / 60 秒后,消费端会在对应时刻收到消息,多条不同延迟的消息互不阻塞。
x-delay 最大值上限
x-delay使用 Erlang 的 32-bit 整数,最大约2^32 - 1毫秒,折合约 49.7 天(社区流传 24.8 天是把它当成 32-bit 有符号整数的误解;实际实现是无符号)。超过这个上限直接消息被丢弃,业务里需要超长延迟时应考虑数据库定时扫描 + 短延迟补偿的组合方案,而不是无脑上x-delay。
6. 选型对比:DLQ + TTL vs 延迟消息插件
两种方案都能实现延迟消息,但机制和适用场景完全不同:
| 维度 | DLQ + TTL(传统方案) | delayed-message 插件 |
|---|---|---|
| 是否需要装插件 | ❌ 纯原生 | ✅ 需额外安装 |
| 延迟精度 | 依赖队列头部消息是否到期 | 精确,ms 级 |
| 队头阻塞 | ✅ 有——短 TTL 消息会被队头的长 TTL 消息挡住 | ❌ 无 |
| 每条消息可独立延迟 | 勉强支持(per-message TTL),但会触发队头阻塞 | ✅ 原生支持 |
| 最大延迟 | 理论无上限 | 约 49.7 天(单条 32-bit 毫秒) |
| 消息堆积性能 | 消息在队列中等 TTL,性能平稳 | 延迟消息在 Mnesia 索引中,数量大时有压力 |
| 运维复杂度 | 低 | 需管理插件版本与兼容性 |
| 集群支持 | 原生完美 | 插件在集群下需要注意 Mnesia 状态同步 |
选型建议:
- 所有消息统一延迟(比如”订单创建 30 分钟后统一检查支付状态”)→ DLQ + TTL,无队头阻塞问题、更简单、无依赖。
- 每条消息延迟时间不同(比如用户自选”5 分钟后提醒 / 1 小时后提醒 / 1 天后提醒”)→ delayed-message 插件,能规避 DLQ 方案的队头阻塞。
- 超过 49.7 天的超长延迟 → 两种方案都不合适,改用数据库定时扫描 + 到期前几分钟投递到普通队列的组合。
- 消息量巨大(每天百万级别延迟消息) → 延迟消息插件的内存压力会显现,此时建议转用 Redis ZSet、Kafka 的 delay topic、或专业延迟队列如 RocketMQ 的定时消息。
7. 常见故障排查
| 现象 | 可能原因 | 对策 |
|---|---|---|
rabbitmq-plugins enable 报 plugin not found | .ez 文件不在 RabbitMQ 扫描的插件目录里 | rabbitmq-plugins directories -s 确认路径;docker-compose 方式检查 RABBITMQ_PLUGINS_DIR |
启用报 permission denied | .ez 文件权限不对 | chown rabbitmq:rabbitmq <.ez 文件> |
| RabbitMQ 启动直接 crash,日志报版本不兼容 | 插件版本与 RabbitMQ 不匹配 | 按 2.2 节核对 Compatible versions,换对应 release |
Web UI 里 Exchange Type 下拉没有 x-delayed-message | 插件没启用 / 启用失败 | rabbitmq-plugins list -e 确认;查容器日志 |
消息没按 x-delay 延迟,立刻就到了 | 往普通 exchange 发了、而不是 x-delayed-message 类型 | 确认 CustomExchange 的 type 参数是 "x-delayed-message" |
| 消息到了但延迟不准(慢几秒) | 集群模式下 Mnesia 同步抖动 / 插件精度限制 | 延迟任务场景接受秒级误差;强一致场景换方案 |
x-delay 值超过 49.7 天,消息消失 | 超过 x-delay 支持上限 | 改用数据库定时扫描组合方案 |
作者注本文示例基于 RabbitMQ 3.13 + Spring Boot 3.x +
spring-boot-starter-amqp。不同 RabbitMQ 版本的插件兼容范围会变化,最终以对应 release notes 里的 Compatible versions 为准。
如果这篇文章对你有帮助,欢迎分享给更多人!
