对于后端开发者而言,RabbitMQ 的延迟消息不仅是项目中的高频应用,更是面试时的“必考题”——尤其是当面试官抛出这个经典场景时,很多人容易陷入“知其然不知其所以然”的困境:
用户下单后未支付,如何在指定时间(如30分钟)自动取消订单?
相信很多同学都能说出“用 MQ”“用死信队列”,但当被追问“原理是什么”“为什么这么设计”“生产环境怎么落地”时,往往语塞。
今天这篇文章,我将以「面试官视角」拆解考点,结合「真实项目落地场景」,把延迟消息的核心逻辑、两种实现方案,以及订单超时取消的完整代码,一次性讲透、讲明白。
订单超时未支付,如何自动取消?
很多同学:
本文将以 “面试官视角” + “真实项目设计” 的方式,系统讲清:
- RabbitMQ 为什么不直接支持延迟队列
- 延迟消息的 两种主流实现方案
- 订单超时取消的 完整设计 + Java 代码
一、面试引入:订单超时关闭你是怎么做的?
这是一个非常经典的后端面试题,本质考察三点:
- 你有没有真实做过业务
- 你对 MQ 的理解是不是停留在 API 层
- 你是否具备系统设计能力
业务规则(标准描述)
- 用户下单,生成订单(状态:待支付)
- 给用户 30 分钟支付时间
- 超过 30 分钟仍未支付,订单自动取消
❌ 错误 / 低分答案
- 用定时任务每分钟扫数据库(效率低、有延迟,大数据量下性能瓶颈明显)
- 用 while + sleep 轮询(占用资源、扩展性差,完全不适合生产环境)
✅ 高分答案方向
下单时发送一条延迟消息,30 分钟后自动检查订单状态,未支付则取消。
这就是 RabbitMQ 延迟消息的典型应用,既高效又解耦,也是生产环境的主流方案。
二、RabbitMQ 延迟消息的两种实现方案
面试常问:RabbitMQ 支持延迟队列吗?
标准回答:RabbitMQ 本身不直接支持延迟队列,但可以通过 死信队列(TTL + DLX) 或 延迟消息插件 两种方式实现,二者各有适用场景。
| 方案 |
是否官方 |
面试推荐度 |
特点 |
| 死信交换机(TTL + DLX) |
✅ |
⭐⭐⭐⭐⭐ |
原理题必问,理解难度中等,无需额外安装,兼容性强 |
| 延迟消息插件 |
❌(第三方插件) |
⭐⭐⭐⭐ |
实战更优,语义清晰,实现简单,延迟精度更高 |
三、方案一:死信队列实现延迟消息
1️⃣ 面试官最想听到的原理
一句话总结:给消息设置 TTL(过期时间),消息过期后成为死信,被路由到死信交换机,最终由消费者监听死信队列处理业务逻辑。
完整流程(结合订单场景):
- 用户下单成功,系统发送一条携带订单ID的消息到「延迟队列」
- 给这条消息设置 TTL = 30 分钟(即订单支付超时时间)
- 30 分钟内用户未支付,消息过期,成为“死信”
- 死信会被自动路由到预先绑定的死信交换机,再由死信交换机转发到死信队列
- 消费者监听死信队列,接收到消息后查询订单状态,若仍为“待支付”则执行取消操作
提示:面试时能清晰说出这 5 步,延迟消息原理题可直接拿满分。
2️⃣ MQ 结构设计
| 组件 |
名称 |
作用 |
| Exchange |
order.delay.exchange |
延迟交换机,负责接收生产者发送的延迟消息,并路由到延迟队列 |
| Queue |
order.delay.queue |
延迟队列,存储延迟消息,消息在队列中等待过期(不被消费者直接监听) |
| Exchange |
order.dlx.exchange |
死信交换机,专门接收延迟队列中过期的死信消息 |
| Queue |
order.dlx.queue |
死信队列,存储死信消息,消费者监听该队列处理业务(真正执行订单取消) |
3️⃣ Spring Boot 配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| @Configuration public class RabbitMQConfig { @Bean public DirectExchange delayExchange() { return new DirectExchange("order.delay.exchange"); } @Bean public DirectExchange dlxExchange() { return new DirectExchange("order.dlx.exchange"); } @Bean public Queue delayQueue() { return QueueBuilder.durable("order.delay.queue") .withArgument("x-dead-letter-exchange", "order.dlx.exchange") .withArgument("x-dead-letter-routing-key", "order.dlx.key") .build(); } @Bean public Queue dlxQueue() { return new Queue("order.dlx.queue", true); } @Bean public Binding delayBinding() { return BindingBuilder.bind(delayQueue()) .to(delayExchange()) .with("order.delay.key"); } @Bean public Binding dlxBinding() { return BindingBuilder.bind(dlxQueue()) .to(dlxExchange()) .with("order.dlx.key"); } }
|
4️⃣ 发送延迟消息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| @Service public class OrderProducer { @Autowired private RabbitTemplate rabbitTemplate;
public void sendDelayOrderMessage(Long orderId) { rabbitTemplate.convertAndSend( "order.delay.exchange", "order.delay.key", orderId, message -> { message.getMessageProperties().setExpiration(String.valueOf(30 * 60 * 1000)); return message; } ); } }
|
一键获取完整项目代码
💡 面试加分点:TTL 单位是毫秒,支持为每条消息设置不同的延迟时间(比如不同订单类型设置不同支付超时时间),灵活性更高。
5️⃣ 消费死信,取消订单(核心业务逻辑)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| @Component public class OrderTimeoutConsumer { @Autowired private OrderService orderService;
@RabbitListener(queues = "order.dlx.queue") public void handleTimeoutOrder(Long orderId) { Order order = orderService.getById(orderId); if (order == null) { return; } if (OrderStatus.PENDING_PAYMENT.equals(order.getStatus())) { orderService.cancelOrder(orderId); } } }
|
四、方案二:延迟消息插件(实战更优,生产首选)
面试怎么说?
如果公司允许使用第三方插件,生产环境中我更倾向于使用 x-delayed-message 延迟插件实现延迟消息。相比死信队列,它的语义更清晰(直接声明延迟交换机),实现更简单,且延迟精度更高,无需额外维护死信交换机和死信队列。
核心区别
- 延迟逻辑在 交换机层面 完成,无需依赖延迟队列+死信队列的组合
- 通过设置消息头
x-delay 指定延迟时间(单位:毫秒)
核心代码(Spring Boot 实现)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
| @Bean public CustomExchange delayExchange() { Map<String, Object> arguments = new HashMap<>(); arguments.put("x-delayed-type", "direct"); return new CustomExchange("order.delay.exchange", "x-delayed-message", true, false, arguments); }
@Bean public Queue delayQueue() { return new Queue("order.delay.queue", true); }
@Bean public Binding delayBinding() { return BindingBuilder.bind(delayQueue()) .to(delayExchange()) .with("order.delay.key") .noargs(); }
@Service public class OrderProducer { @Autowired private RabbitTemplate rabbitTemplate;
public void sendDelayOrderMessage(Long orderId) { rabbitTemplate.convertAndSend( "order.delay.exchange", "order.delay.key", orderId, message -> { message.getMessageProperties().setHeader("x-delay", 30 * 60 * 1000); return message; } ); } }
@Component public class OrderTimeoutConsumer { @Autowired private OrderService orderService;
@RabbitListener(queues = "order.delay.queue") public void handleTimeoutOrder(Long orderId) { Order order = orderService.getById(orderId); if (order != null && OrderStatus.PENDING_PAYMENT.equals(order.getStatus())) { orderService.cancelOrder(orderId); } } }
|
一键获取完整项目代码
五、两种方案对比
| 对比点 |
死信队列(TTL + DLX) |
延迟插件(x-delayed-message) |
| 官方支持 |
是(原生支持,无需额外安装) |
否(第三方插件,需手动安装) |
| 原理复杂度 |
中(需理解死信、TTL、交换机绑定关系) |
低(直接声明延迟交换机,语义清晰) |
| 面试价值 |
⭐⭐⭐⭐⭐(原理题高频,考察基础理解) |
⭐⭐⭐⭐(实战场景考察,体现项目经验) |
| 项目使用 |
广泛(兼容性强,无插件依赖) |
非常广泛(生产首选,实现简洁、精度高) |
| 延迟精度 |
中等(受队列消息堆积影响) |
高(基于插件定时触发,不受消息堆积影响) |
六、面试总结话术
RabbitMQ 本身不直接支持延迟队列,在实际开发和面试中,我通常会根据场景选择两种实现方式:
若不允许使用第三方插件,我会用死信队列(TTL + DLX)实现:下单时发送一条设置 TTL 的消息到延迟队列,消息过期后成为死信,被路由到死信队列,消费者监听死信队列,查询订单状态,若未支付则执行取消操作。
若项目允许使用插件,我会优先选择 x-delayed-message 延迟插件,它在交换机层面实现延迟逻辑,语义更清晰、实现更简单,延迟精度也更高,更适合生产环境的实战场景。
补充:两种方案都能实现订单超时取消,核心是利用延迟消息解耦业务,避免定时任务的性能问题,同时保证业务的可靠性和幂等性。