⏰ 延迟消息的实现原理:让消息准时送达!

65 阅读13分钟

📖 开场:外卖小哥的预约配送

想象你点了一份外卖 🍔:

普通外卖(立即送达)

下单 → 商家制作 → 立即配送 → 送达
时间:30分钟

预约外卖(延迟送达)

下单 → 预约2小时后送达 ⏰
    ↓
等待2小时...
    ↓
商家制作 → 配送 → 准时送达!✅

这就是延迟消息(Delayed Message)!

定义:消息不立即投递,而是在指定时间后才能被消费


🎯 延迟消息的应用场景

场景1:订单超时自动取消 🛒

用户下单 → 发送延迟消息(30分钟后)
    ↓
30分钟内用户支付 → 取消延迟消息 ✅
    ↓
30分钟后用户未支付 → 延迟消息到达 → 自动取消订单 ❌

不使用延迟消息的做法

方案1:定时任务每分钟扫描数据库
- 缺点:延迟不准确,数据库压力大 💀

方案2:Timer或ScheduledExecutorService
- 缺点:应用重启丢失,无法持久化 💀

方案3:Quartz定时任务
- 缺点:配置复杂,资源消耗大 💀

使用延迟消息

发送延迟消息 → MQ自动延迟 → 到期自动投递 ✅
- 优点:准确、高效、分布式 ⭐⭐⭐⭐⭐

场景2:定时任务调度 📅

发送定时消息:明天上午10点发送营销短信
    ↓
MQ延迟到明天10点
    ↓
准时投递 → 消费者发送短信 ✅

场景3:重试机制 🔄

处理失败 → 发送延迟消息(5秒后重试)
    ↓
5秒后消息到达 → 重新处理
    ↓
处理成功 ✅

场景4:限流削峰 🌊

瞬时流量 10000 QPS → 转为延迟消息,延迟1-10秒
    ↓
流量被平滑分散到10秒内 → 1000 QPS ✅

🔧 延迟消息的实现方案

方案1:RocketMQ延迟消息 ⭐⭐⭐⭐⭐

1.1 固定延迟级别

RocketMQ支持18个延迟级别

级别    延迟时间
1       1秒
2       5秒
3       10秒
4       30秒
5       1分钟
6       2分钟
7       3分钟
8       4分钟
9       5分钟
10      6分钟
11      7分钟
12      8分钟
13      9分钟
14      10分钟
15      20分钟
16      30分钟
17      1小时
18      2小时

配置文件

# RocketMQ Broker配置
messageDelayLevel=1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h

1.2 发送延迟消息

import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;

public class DelayMessageProducer {
    
    public static void main(String[] args) throws Exception {
        DefaultMQProducer producer = new DefaultMQProducer("delay_producer_group");
        producer.setNamesrvAddr("localhost:9876");
        producer.start();
        
        // 创建消息
        Message msg = new Message(
            "OrderTopic",
            "TagA",
            "订单ID-12345,30分钟后检查支付状态".getBytes()
        );
        
        // ⭐ 设置延迟级别:5 = 1分钟后投递
        msg.setDelayTimeLevel(5);
        
        // 记录发送时间
        long sendTime = System.currentTimeMillis();
        System.out.println("发送时间: " + new Date(sendTime));
        
        // 发送消息
        SendResult result = producer.send(msg);
        System.out.println("发送结果: " + result);
        
        producer.shutdown();
    }
}

运行结果

发送时间: 2025-10-24 10:00:00
发送结果: SendResult [sendStatus=SEND_OK, msgId=...]

1.3 消费延迟消息

import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.*;
import org.apache.rocketmq.common.message.MessageExt;
import java.util.List;

public class DelayMessageConsumer {
    
    public static void main(String[] args) throws Exception {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("delay_consumer_group");
        consumer.setNamesrvAddr("localhost:9876");
        consumer.subscribe("OrderTopic", "*");
        
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(
                List<MessageExt> msgs,
                ConsumeConcurrentlyContext context
            ) {
                for (MessageExt msg : msgs) {
                    // 记录消费时间
                    long consumeTime = System.currentTimeMillis();
                    long storeTime = msg.getStoreTimestamp();
                    long delay = consumeTime - storeTime;
                    
                    System.out.printf("消费时间: %s%n", new Date(consumeTime));
                    System.out.printf("延迟时长: %d 秒%n", delay / 1000);
                    System.out.printf("消息内容: %s%n", new String(msg.getBody()));
                    
                    // 业务处理:检查订单支付状态
                    checkOrderPayment(new String(msg.getBody()));
                }
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        
        consumer.start();
        System.out.println("延迟消息消费者启动成功!");
    }
    
    private static void checkOrderPayment(String orderInfo) {
        System.out.println("检查订单支付状态: " + orderInfo);
        // 查询数据库,如果未支付则取消订单
    }
}

运行结果

消费时间: 2025-10-24 10:01:00
延迟时长: 60 秒  ← 正好1分钟!
消息内容: 订单ID-12345,30分钟后检查支付状态
检查订单支付状态: 订单ID-12345,30分钟后检查支付状态

1.4 RocketMQ延迟消息原理

核心思想:使用多个内部Topic存储不同延迟级别的消息

┌─────────────────────────────────────────────────┐
│                Producer                         │
│  发送延迟消息(delayLevel=51分钟)            │
└─────────────────┬───────────────────────────────┘
                  │
                  ↓
┌─────────────────────────────────────────────────┐
│              Broker接收消息                     │
│                                                 │
│  1. 原始Topic: OrderTopic                       │
│  2. 延迟级别: 5 (1分钟)                         │
│                                                 │
│  ⭐ 修改Topic和Queue:                          │
│     原Topic: OrderTopic                         │
│     → 修改为: SCHEDULE_TOPIC_XXXX               │
│     原Queue: 0                                  │
│     → 修改为: 4 (delayLevel - 1)                │
│                                                 │
│  3. 存储到延迟Topic                             │
└─────────────────┬───────────────────────────────┘
                  │
                  ↓
┌─────────────────────────────────────────────────┐
│        SCHEDULE_TOPIC_XXXX (内部Topic)          │
│                                                 │
│  Queue-0: 延迟1秒的消息                         │
│  Queue-1: 延迟5秒的消息                         │
│  Queue-2: 延迟10秒的消息                        │
│  Queue-3: 延迟30秒的消息                        │
│  Queue-4: 延迟1分钟的消息 ← 我们的消息在这里!   │
│  ...                                            │
│  Queue-17: 延迟2小时的消息                      │
│                                                 │
└─────────────────┬───────────────────────────────┘
                  │
                  ↓
┌─────────────────────────────────────────────────┐
│        定时任务(每秒扫描)                      │
│                                                 │
│  for (每个延迟级别的Queue) {                     │
│      // 取出到期的消息                          │
│      if (消息到期时间 <= 当前时间) {            │
│          // ⭐ 恢复原始Topic和Queue             │
│          msg.setTopic("OrderTopic");            │
│          msg.setQueueId(0);                     │
│          // 重新投递到原Topic                   │
│          broker.putMessage(msg);                │
│      }                                          │
│  }                                              │
│                                                 │
└─────────────────┬───────────────────────────────┘
                  │
                  ↓
┌─────────────────────────────────────────────────┐
│           OrderTopic (原始Topic)                │
│                                                 │
│  消息恢复到原Topic,Consumer可以消费了!         │
│                                                 │
└─────────────────┬───────────────────────────────┘
                  │
                  ↓
┌─────────────────────────────────────────────────┐
│               Consumer消费消息                  │
└─────────────────────────────────────────────────┘

流程总结

1. Producer发送延迟消息(delayLevel=5)
2. Broker接收,修改Topic为SCHEDULE_TOPIC_XXXX,修改Queue为4
3. 存储到延迟Queue-4
4. 定时任务每秒扫描Queue-4,找到到期的消息
5. 恢复原始Topic和Queue
6. 重新投递到OrderTopic
7. Consumer从OrderTopic消费消息

1.5 优缺点

优点

  • ✅ 实现简单,只需设置delayLevel
  • ✅ 性能高,基于时间轮算法
  • ✅ 可靠性高,持久化到磁盘

缺点

  • 只支持固定的18个延迟级别(不能自定义任意延迟时间)
  • ❌ 不支持取消延迟消息
  • ❌ 延迟时间最长只有2小时

适用场景

  • 延迟时间是固定的几个级别
  • 大部分业务场景 ⭐⭐⭐⭐⭐

方案2:RabbitMQ延迟消息 ⭐⭐⭐⭐

2.1 基于TTL + 死信队列

原理

1. 消息发送到延迟队列(设置TTL)
2. TTL到期,消息变成死信
3. 死信转发到死信交换机
4. 死信交换机路由到目标队列
5. Consumer从目标队列消费

架构图

Producer
    ↓
┌─────────────────────────────────────────┐
│  延迟交换机 (Delay Exchange)            │
└─────────────┬───────────────────────────┘
              │
              ↓
┌─────────────────────────────────────────┐
│  延迟队列 (Delay Queue)                 │
│  - TTL: 60000ms (1分钟)                 │
│  - 不绑定Consumer ⭐                    │
│  - 死信交换机: Target Exchange          │
└─────────────┬───────────────────────────┘
              │ 消息过期(1分钟后)
              ↓
┌─────────────────────────────────────────┐
│  目标交换机 (Target Exchange)           │
└─────────────┬───────────────────────────┘
              │
              ↓
┌─────────────────────────────────────────┐
│  目标队列 (Target Queue)                │
└─────────────┬───────────────────────────┘
              │
              ↓
          Consumer

2.2 代码实现

配置类

import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class DelayQueueConfig {
    
    // ========== 延迟队列配置 ==========
    
    @Bean
    public Queue delayQueue() {
        return QueueBuilder.durable("delay.queue")
            .withArgument("x-message-ttl", 60000)  // ⭐ TTL: 1分钟
            .withArgument("x-dead-letter-exchange", "target.exchange")  // ⭐ 死信交换机
            .withArgument("x-dead-letter-routing-key", "target.routing.key")  // ⭐ 死信路由键
            .build();
    }
    
    @Bean
    public DirectExchange delayExchange() {
        return new DirectExchange("delay.exchange");
    }
    
    @Bean
    public Binding delayBinding(Queue delayQueue, DirectExchange delayExchange) {
        return BindingBuilder.bind(delayQueue)
            .to(delayExchange)
            .with("delay.routing.key");
    }
    
    // ========== 目标队列配置 ==========
    
    @Bean
    public Queue targetQueue() {
        return new Queue("target.queue", true);
    }
    
    @Bean
    public DirectExchange targetExchange() {
        return new DirectExchange("target.exchange");
    }
    
    @Bean
    public Binding targetBinding(Queue targetQueue, DirectExchange targetExchange) {
        return BindingBuilder.bind(targetQueue)
            .to(targetExchange)
            .with("target.routing.key");
    }
}

生产者

import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class DelayMessageProducer {
    
    @Autowired
    private RabbitTemplate rabbitTemplate;
    
    /**
     * 发送延迟消息
     */
    public void sendDelayMessage(String message) {
        System.out.println("发送延迟消息: " + message);
        System.out.println("发送时间: " + new Date());
        
        // ⭐ 发送到延迟交换机
        rabbitTemplate.convertAndSend(
            "delay.exchange",
            "delay.routing.key",
            message
        );
    }
}

消费者

import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@Component
public class DelayMessageConsumer {
    
    /**
     * 消费目标队列的消息
     */
    @RabbitListener(queues = "target.queue")
    public void receiveMessage(String message) {
        System.out.println("消费延迟消息: " + message);
        System.out.println("消费时间: " + new Date());
        
        // 业务处理
        processMessage(message);
    }
    
    private void processMessage(String message) {
        // 业务逻辑:检查订单支付状态
        System.out.println("处理消息: " + message);
    }
}

2.3 支持任意延迟时间

方法1:动态设置TTL

public void sendDelayMessage(String message, long delayMillis) {
    rabbitTemplate.convertAndSend(
        "delay.exchange",
        "delay.routing.key",
        message,
        msg -> {
            // ⭐ 动态设置TTL
            msg.getMessageProperties().setExpiration(String.valueOf(delayMillis));
            return msg;
        }
    );
}

// 使用
producer.sendDelayMessage("订单12345", 30 * 60 * 1000);  // 30分钟
producer.sendDelayMessage("订单67890", 2 * 60 * 60 * 1000);  // 2小时

⚠️ 注意:动态TTL有个坑!

问题:RabbitMQ只会检查队列头部的消息是否过期
如果队列中有多条消息,且TTL不同:

消息1: TTL=10秒
消息2: TTL=5秒 ← 虽然先到期,但在消息1后面
消息3: TTL=20秒

结果:消息2要等消息1过期后才能被处理!💀

解决方案:rabbitmq-delayed-message-exchange插件


方法2:使用延迟交换机插件 ⭐⭐⭐⭐⭐

安装插件

# 下载插件
wget https://github.com/rabbitmq/rabbitmq-delayed-message-exchange/releases/download/3.12.0/rabbitmq_delayed_message_exchange-3.12.0.ez

# 复制到RabbitMQ插件目录
cp rabbitmq_delayed_message_exchange-3.12.0.ez /usr/lib/rabbitmq/plugins/

# 启用插件
rabbitmq-plugins enable rabbitmq_delayed_message_exchange

# 重启RabbitMQ
systemctl restart rabbitmq-server

配置类

@Configuration
public class DelayExchangeConfig {
    
    @Bean
    public CustomExchange delayExchange() {
        Map<String, Object> args = new HashMap<>();
        args.put("x-delayed-type", "direct");  // ⭐ 延迟类型
        
        return new CustomExchange(
            "delay.exchange",
            "x-delayed-message",  // ⭐ 类型:延迟消息
            true,
            false,
            args
        );
    }
    
    @Bean
    public Queue targetQueue() {
        return new Queue("target.queue", true);
    }
    
    @Bean
    public Binding binding(Queue targetQueue, CustomExchange delayExchange) {
        return BindingBuilder.bind(targetQueue)
            .to(delayExchange)
            .with("target.routing.key")
            .noargs();
    }
}

生产者

public void sendDelayMessage(String message, long delayMillis) {
    rabbitTemplate.convertAndSend(
        "delay.exchange",
        "target.routing.key",
        message,
        msg -> {
            // ⭐ 设置延迟时间(毫秒)
            msg.getMessageProperties().setDelay((int) delayMillis);
            return msg;
        }
    );
    
    System.out.printf("发送延迟消息,延迟%d秒: %s%n", delayMillis / 1000, message);
}

// 使用
producer.sendDelayMessage("订单12345", 30 * 60 * 1000);  // 30分钟后投递
producer.sendDelayMessage("订单67890", 2 * 60 * 60 * 1000);  // 2小时后投递

优点

  • ✅ 支持任意延迟时间
  • ✅ 没有TTL队列头部阻塞问题
  • ✅ 性能好

方案3:Kafka延迟消息(自己实现)⭐⭐⭐

Kafka本身不支持延迟消息,需要自己实现

3.1 方案:延迟Topic + 定时轮询

/**
 * 延迟消息服务
 */
@Service
@Slf4j
public class KafkaDelayMessageService {
    
    @Autowired
    private KafkaTemplate<String, String> kafkaTemplate;
    
    /**
     * 发送延迟消息
     */
    public void sendDelayMessage(String topic, String message, long delayMillis) {
        // 封装延迟消息
        DelayMessage delayMsg = new DelayMessage();
        delayMsg.setTargetTopic(topic);
        delayMsg.setMessage(message);
        delayMsg.setDeliverTime(System.currentTimeMillis() + delayMillis);  // 投递时间
        
        // ⭐ 发送到延迟Topic
        kafkaTemplate.send("delay-topic", JSON.toJSONString(delayMsg));
        
        log.info("发送延迟消息,{}秒后投递: {}", delayMillis / 1000, message);
    }
}

@Data
class DelayMessage {
    private String targetTopic;  // 目标Topic
    private String message;      // 消息内容
    private long deliverTime;    // 投递时间戳
}

定时任务:轮询延迟Topic

@Component
@Slf4j
public class DelayMessageScheduler {
    
    @Autowired
    private KafkaTemplate<String, String> kafkaTemplate;
    
    /**
     * 每秒扫描一次延迟Topic
     */
    @Scheduled(fixedRate = 1000)
    public void scanDelayMessages() {
        // 消费延迟Topic
        ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));
        
        long currentTime = System.currentTimeMillis();
        
        for (ConsumerRecord<String, String> record : records) {
            DelayMessage delayMsg = JSON.parseObject(record.value(), DelayMessage.class);
            
            if (delayMsg.getDeliverTime() <= currentTime) {
                // ⭐ 到期了,投递到目标Topic
                kafkaTemplate.send(delayMsg.getTargetTopic(), delayMsg.getMessage());
                log.info("延迟消息到期,投递: {}", delayMsg.getMessage());
            } else {
                // ⭐ 未到期,重新发送到延迟Topic(设置更短的延迟)
                kafkaTemplate.send("delay-topic", JSON.toJSONString(delayMsg));
            }
        }
    }
}

缺点

  • ❌ 需要定时轮询,性能开销
  • ❌ 精度有限(1秒轮询一次)
  • ❌ 实现复杂

推荐:使用RocketMQ或RabbitMQ的原生支持


方案4:Redis + 定时任务 ⭐⭐⭐

原理:使用Redis的Sorted Set存储延迟消息,定时扫描

@Service
@Slf4j
public class RedisDelayMessageService {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    @Autowired
    private KafkaTemplate<String, String> kafkaTemplate;
    
    private static final String DELAY_KEY = "delay:messages";
    
    /**
     * 发送延迟消息
     */
    public void sendDelayMessage(String topic, String message, long delayMillis) {
        long deliverTime = System.currentTimeMillis() + delayMillis;
        
        DelayMessage delayMsg = new DelayMessage();
        delayMsg.setTargetTopic(topic);
        delayMsg.setMessage(message);
        delayMsg.setDeliverTime(deliverTime);
        
        // ⭐ 存储到Redis Sorted Set(score=投递时间)
        redisTemplate.opsForZSet().add(
            DELAY_KEY,
            JSON.toJSONString(delayMsg),
            deliverTime
        );
        
        log.info("发送延迟消息,{}秒后投递: {}", delayMillis / 1000, message);
    }
    
    /**
     * 定时扫描到期的消息
     */
    @Scheduled(fixedRate = 1000)
    public void scanExpiredMessages() {
        long currentTime = System.currentTimeMillis();
        
        // ⭐ 查询score <= currentTime的消息(已到期)
        Set<String> expiredMessages = redisTemplate.opsForZSet()
            .rangeByScore(DELAY_KEY, 0, currentTime);
        
        if (expiredMessages != null && !expiredMessages.isEmpty()) {
            for (String msgJson : expiredMessages) {
                DelayMessage delayMsg = JSON.parseObject(msgJson, DelayMessage.class);
                
                // 投递到目标Topic
                kafkaTemplate.send(delayMsg.getTargetTopic(), delayMsg.getMessage());
                log.info("延迟消息到期,投递: {}", delayMsg.getMessage());
                
                // ⭐ 从Redis删除
                redisTemplate.opsForZSet().remove(DELAY_KEY, msgJson);
            }
        }
    }
}

优点

  • ✅ 支持任意延迟时间
  • ✅ 实现简单
  • ✅ 性能较高

缺点

  • ❌ 依赖Redis,增加了复杂度
  • ❌ Redis故障会导致消息丢失(需要持久化)

📊 方案对比总结

方案任意延迟最大延迟性能复杂度推荐度
RocketMQ(固定级别)2小时⚡⚡⚡😊 简单⭐⭐⭐⭐
RabbitMQ(TTL+死信)无限制⚡⚡😐 中等⭐⭐⭐⭐⭐
RabbitMQ(插件)无限制⚡⚡⚡😊 简单⭐⭐⭐⭐⭐
Kafka(自己实现)无限制😰 复杂⭐⭐
Redis + 定时任务无限制⚡⚡😐 中等⭐⭐⭐

🎓 面试题速答

Q1: 什么是延迟消息?

A: 延迟消息 = 消息不立即投递,而是在指定时间后才能被消费

应用场景

  • 订单超时自动取消
  • 定时任务调度
  • 重试机制
  • 限流削峰

Q2: RocketMQ如何实现延迟消息?

A:

  1. 设置延迟级别msg.setDelayTimeLevel(5)(1分钟)
  2. 修改Topic:Broker将消息存储到内部Topic SCHEDULE_TOPIC_XXXX
  3. 定时扫描:定时任务每秒扫描延迟Queue,找到到期的消息
  4. 恢复原Topic:将到期的消息恢复到原始Topic
  5. 投递消费:Consumer从原Topic消费消息

限制

  • 只支持18个固定延迟级别
  • 最长延迟2小时

Q3: RabbitMQ如何实现延迟消息?

A: 两种方案!

方案1:TTL + 死信队列

  1. 消息发送到延迟队列(设置TTL)
  2. TTL到期,消息变成死信
  3. 死信转发到目标队列
  4. Consumer消费

方案2:延迟交换机插件(推荐)

  • 安装rabbitmq-delayed-message-exchange插件
  • 发送消息时设置x-delay
  • 支持任意延迟时间

Q4: Kafka支持延迟消息吗?

A: Kafka本身不支持延迟消息

自己实现的方案

  1. 延迟Topic + 定时轮询

    • 发送到延迟Topic
    • 定时任务扫描,到期后投递到目标Topic
  2. Redis + 定时任务

    • 存储到Redis Sorted Set
    • 定时任务扫描,到期后投递

推荐:使用RocketMQ或RabbitMQ的原生支持


Q5: 如何选择延迟消息方案?

A: 如果使用RocketMQ

  • 延迟时间是固定的18个级别 → 直接用RocketMQ ⭐⭐⭐⭐⭐
  • 需要任意延迟时间 → 考虑切换RabbitMQ

如果使用RabbitMQ

  • 直接使用延迟交换机插件 ⭐⭐⭐⭐⭐

如果使用Kafka

  • 自己实现(不推荐)
  • 或者引入RocketMQ/RabbitMQ处理延迟消息

🎯 最佳实践

订单超时自动取消

@Service
@Slf4j
public class OrderService {
    
    @Autowired
    private DelayMessageProducer producer;
    
    /**
     * 创建订单
     */
    @Transactional
    public void createOrder(Order order) {
        // 1. 保存订单到数据库(状态:待支付)
        orderRepository.save(order);
        
        // 2. 发送延迟消息:30分钟后检查支付状态
        DelayMessage msg = new DelayMessage();
        msg.setOrderId(order.getId());
        msg.setAction("CHECK_PAYMENT");
        
        producer.sendDelayMessage(
            JSON.toJSONString(msg),
            30 * 60 * 1000  // 30分钟
        );
        
        log.info("订单创建成功,30分钟后检查支付状态: {}", order.getId());
    }
    
    /**
     * 用户支付成功
     */
    @Transactional
    public void payOrder(String orderId) {
        // 更新订单状态为已支付
        Order order = orderRepository.findById(orderId);
        order.setStatus(OrderStatus.PAID);
        orderRepository.save(order);
        
        log.info("订单支付成功: {}", orderId);
        
        // ⭐ 注意:延迟消息已经发送,无法取消
        // 但是消费者会检查订单状态,如果已支付则忽略
    }
}

消费者

@Component
@Slf4j
public class OrderDelayMessageConsumer {
    
    @Autowired
    private OrderRepository orderRepository;
    
    /**
     * 消费延迟消息:检查订单支付状态
     */
    @RabbitListener(queues = "order.delay.queue")
    public void handleDelayMessage(String message) {
        DelayMessage msg = JSON.parseObject(message, DelayMessage.class);
        
        if ("CHECK_PAYMENT".equals(msg.getAction())) {
            // 查询订单状态
            Order order = orderRepository.findById(msg.getOrderId());
            
            if (order.getStatus() == OrderStatus.UNPAID) {
                // ⭐ 30分钟后仍未支付,自动取消订单
                order.setStatus(OrderStatus.CANCELLED);
                orderRepository.save(order);
                
                log.info("订单超时未支付,自动取消: {}", order.getId());
                
                // 可以发送通知给用户
                notifyUser(order);
            } else {
                log.info("订单已支付,无需处理: {}", order.getId());
            }
        }
    }
}

🎬 总结

              延迟消息实现方案全景图

┌──────────────────────────────────────────────┐
│         RocketMQ(固定18个级别)             │
│  ├─ 优点:简单、性能高                       │
│  ├─ 缺点:只支持固定延迟时间                 │
│  └─ 适用:大部分业务场景 ⭐⭐⭐⭐            │
└──────────────────────────────────────────────┘

┌──────────────────────────────────────────────┐
│         RabbitMQ(TTL + 死信队列)           │
│  ├─ 优点:支持任意延迟时间                   │
│  ├─ 缺点:队列头部阻塞问题                   │
│  └─ 适用:一般场景 ⭐⭐⭐                    │
└──────────────────────────────────────────────┘

┌──────────────────────────────────────────────┐
│      RabbitMQ(延迟交换机插件)⭐⭐⭐⭐⭐    │
│  ├─ 优点:支持任意延迟,无头部阻塞           │
│  ├─ 缺点:需要安装插件                       │
│  └─ 适用:推荐方案!                         │
└──────────────────────────────────────────────┘

┌──────────────────────────────────────────────┐
│         Kafka(自己实现)                    │
│  ├─ 优点:可以实现                           │
│  ├─ 缺点:复杂,性能差                       │
│  └─ 适用:不推荐 ⭐⭐                        │
└──────────────────────────────────────────────┘

            推荐:RabbitMQ延迟交换机插件 🏆

🎉 恭喜你!

你已经完全掌握了延迟消息的实现原理!🎊

核心要点

  1. RocketMQ:18个固定延迟级别,简单高效
  2. RabbitMQ:TTL+死信队列,或延迟交换机插件(推荐)
  3. Kafka:不支持,需自己实现

下次面试,这样回答

"延迟消息是指消息不立即投递,而是在指定时间后才能被消费。常用于订单超时取消、定时任务等场景。

RocketMQ通过内部Topic和定时任务实现,支持18个固定延迟级别,实现简单性能高。

RabbitMQ可以通过TTL+死信队列实现,或者安装延迟交换机插件,支持任意延迟时间。

Kafka本身不支持延迟消息,需要自己实现,比如使用延迟Topic+定时轮询,或Redis+定时任务。

我们项目中使用RabbitMQ的延迟交换机插件,因为它支持任意延迟时间,实现简单,性能也不错。"

面试官:👍 "很好!你对延迟消息理解很全面!"


本文完 🎬

上一篇: 189-如何设计一个高可用的消息队列架构.md
下一篇: 191-消息队列的推拉模式的优劣对比.md

作者注:写完这篇,我点外卖都知道怎么预约送达时间了!🍔⏰
如果这篇文章对你有帮助,请给我一个Star⭐!