SpringBoot 整合 RabbitMQ 实现延时消息实战(死信队列方案)
【摘要】 在业务开发中经常遇到延时场景,例如订单超时自动关闭、预约到期提醒、消息延迟推送。本文基于SpringBoot + RabbitMQ,使用死信队列实现延时消息,完整讲解交换机、队列绑定、消息TTL、死信转发逻辑,附带可直接运行的代码,同时对比延时插件方案优缺点,给出生产环境落地建议。
前言
很多业务场景需要延时能力:用户下单后30分钟未支付,自动关闭订单;活动预约到期提醒;定时重试失败任务。
RabbitMQ原生没有直接的延时消息,主流两种实现:
- 死信队列(DLX + TTL):不需要额外装插件,兼容性好,适合中小项目,也是最常用方案
- rabbitmq-delayed-message-exchange 延时插件:原生延时交换机,使用更简单,但需要运维安装插件,云厂商部分MQ实例不支持
本文采用死信队列方案,开箱即用,适合华为云RabbitMQ服务直接部署。
原理说明
- 消息发送到普通业务队列,给消息设置TTL(存活时间)
- 消息超时没有被消费,变成死信(Dead Letter)
- 普通队列绑定死信交换机,死信自动转发到死信交换机
- 死信交换机路由到延时消费队列
- 消费者监听延时消费队列,到达时间后执行业务逻辑
注意:TTL是队列级别或者消息级别,队列级TTL:整个队列所有消息超时时间一样;消息级TTL可以每条消息单独设置。队列级TTL性能更好。
环境依赖
<!-- pom.xml -->
<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: guest
password: guest
virtual-host: /
# 消息确认
publisher-confirm-type: correlated
publisher-returns: true
listener:
simple:
acknowledge-mode: manual # 手动ACK,防止消息丢失
核心配置类
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class RabbitDelayConfig {
// 普通交换机
public static final String NORMAL_EXCHANGE = "normal.order.exchange";
// 普通队列(消息在这里等待超时)
public static final String NORMAL_QUEUE = "normal.order.queue";
// 普通队列路由key
public static final String NORMAL_ROUTING_KEY = "order.create";
// 死信交换机 DLX
public static final String DLX_EXCHANGE = "dlx.order.exchange";
// 死信消费队列(延时后真正消费的队列)
public static final String DLX_QUEUE = "dlx.order.queue";
public static final String DLX_ROUTING_KEY = "order.delay";
// 1. 声明普通交换机
@Bean
public DirectExchange normalExchange() {
return ExchangeBuilder.directExchange(NORMAL_EXCHANGE).durable(true).build();
}
// 2. 声明死信交换机
@Bean
public DirectExchange dlxExchange() {
return ExchangeBuilder.directExchange(DLX_EXCHANGE).durable(true).build();
}
// 3. 普通队列,绑定死信交换机 + TTL 30分钟(单位ms)
@Bean
public Queue normalQueue() {
Map<String, Object> args = new HashMap<>();
// 设置死信交换机
args.put("x-dead-letter-exchange", DLX_EXCHANGE);
// 设置死信路由key
args.put("x-dead-letter-routing-key", DLX_ROUTING_KEY);
// 队列TTL:30分钟
args.put("x-message-ttl", 30 * 60 * 1000);
return QueueBuilder.durable(NORMAL_QUEUE).withArguments(args).build();
}
// 4. 死信消费队列
@Bean
public Queue dlxQueue() {
return QueueBuilder.durable(DLX_QUEUE).build();
}
// 绑定:普通交换机 <-> 普通队列
@Bean
public Binding normalBinding() {
return BindingBuilder.bind(normalQueue()).to(normalExchange()).with(NORMAL_ROUTING_KEY);
}
// 绑定:死信交换机 <-> 死信队列
@Bean
public Binding dlxBinding() {
return BindingBuilder.bind(dlxQueue()).to(dlxExchange()).with(DLX_ROUTING_KEY);
}
}
生产者:发送延时消息
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
@Component
public class OrderDelayProducer {
@Resource
private RabbitTemplate rabbitTemplate;
public void sendOrderDelayMsg(String orderNo) {
rabbitTemplate.convertAndSend(
RabbitDelayConfig.NORMAL_EXCHANGE,
RabbitDelayConfig.NORMAL_ROUTING_KEY,
orderNo
);
System.out.println("发送延时消息,订单号:" + orderNo + ",30分钟后自动关闭");
}
}
消费者:监听延时队列,处理超时订单
import com.rabbitmq.client.Channel;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.support.AmqpHeaders;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Component
public class OrderDelayConsumer {
@RabbitListener(queues = RabbitDelayConfig.DLX_QUEUE)
public void consume(String orderNo, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long tag) throws IOException {
try {
// 业务逻辑:查询订单状态,如果未支付,关闭订单、释放库存
System.out.println("收到延时消息,执行订单超时关闭:" + orderNo);
// TODO 调用订单Service:orderService.closeExpireOrder(orderNo);
// 手动ACK,确认消费成功
channel.basicAck(tag, false);
} catch (Exception e) {
// 消费失败,NACK,重回队列或者转入人工补偿队列
channel.basicNack(tag, false, true);
e.printStackTrace();
}
}
}
测试接口
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.UUID;
@RestController
public class OrderTestController {
@Resource
private OrderDelayProducer producer;
@GetMapping("/sendDelayMsg")
public String send(@RequestParam(required = false) String orderNo) {
if(orderNo == null) {
orderNo = UUID.randomUUID().toString();
}
producer.sendOrderDelayMsg(orderNo);
return "消息发送成功,orderNo:" + orderNo;
}
}
踩坑重点(生产必看)
- 死信队列方案短板:如果队列设置统一TTL,消息是排队超时。比如第一条消息1小时TTL,第二条10分钟TTL,第二条要等第一条超时之后才会被消费!不同延时时间不能放同一个队列。
✅ 解决方案:按TTL分组创建多个队列,或者改用延时插件。 - 消息丢失风险:一定要开启手动ACK;消息确认回调做好日志记录。
- 订单状态校验:延时消息到达后,必须先查库,如果订单已经支付,直接忽略这条消息,不能直接关闭。
- 幂等:消费者要做幂等,防止消息重复投递导致重复关闭订单(可以用订单状态、Redis标记)。
- 不要把TTL设置极大,堆积大量死信会加重MQ压力。
方案对比
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 死信队列DLX | 无需插件、云MQ通用、稳定 | 同队列消息TTL会阻塞,多延时需要多队列 | 延时时间固定、变化少的业务 |
| 延时插件Exchange | 支持每条消息自定义延时,顺序不受影响 | 需要运维安装插件,部分云厂商不支持 | 延时时间灵活、多样化 |
总结
死信队列是RabbitMQ延时消息最经典的实现方式,上手简单,不需要额外运维改造,适合订单超时这类固定延时场景。核心就是利用消息过期后自动转发死信的特性。
开发中最容易踩的坑就是同队列不同TTL消息阻塞,这个点一定要在业务设计时规避。如果业务里延时时间非常零散多变,优先考虑延时插件方案,或者改用Redis ZSet轮询的延时方案。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
- 点赞
- 收藏
- 关注作者
评论(0)