Java 实现RabbitMq延时队列和死信队列

 当RabbitMQ队列中的消息变成 Dead message(过期消息)、或消息被拒绝(basic.reject / basic.nack)且requeue = false、或队列达到了最大的长度以上的一种情况时 消息会成为死信。如果这个队列有设置 x-dead-letter-exchange (死信交换机)参数,那么这些死信消息会被路由到死信交换机上,跟这个交换机绑定的队列即称为死信队列。死信消息会被存储到死信队列供后续处理,这样确保了异常消息不会丢失并提供了一种异常恢复机制。

之前介绍过:Java 简单操作 RabbitMq ,这里就简单RabbitMq的死信队列来实现延时队列的功能。

创建一个自动加载类在项目启动时,自动创建延时交换机和延时队列,死信交换机和死信队列,并将其对应绑定起来:

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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
package com.demo.www.rabbitmq.config;
 
import com.google.common.collect.Maps;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
 
import java.util.Map;
 
/**
 * RabbitMq 延时队列实现
 * @author AnYuan
 */
 
@Slf4j
@Configuration
public class DelayQueueConfig {
 
    /**
     * 延迟队列
     */
    public static final String DELAY_EXCHANGE = "delay.queue.business.exchange";
    public static final String DELAY_QUEUE = "delay.queue.business.queue";
    public static final String DELAY_QUEUE_ROUTING_KEY = "delay.queue.business.queue.routingKey";
 
    /**
     * 死信队列
     */
    public static final String DEAD_LETTER_EXCHANGE = "delay.queue.deadLetter.exchange";
    public static final String DEAD_LETTER_QUEUE_ROUTING_KEY = "delay.queue.deadLetter.delay_10s.routingKey";
    public static final String DEAD_LETTER_QUEUE = "delay.queue.deadLetter.queue";
 
    /**
     * 声明 死信交换机
     * @return deadLetterExchange
     */
    @Bean
    public DirectExchange deadLetterExchange() {
        return new DirectExchange(DEAD_LETTER_EXCHANGE);
    }
 
    /**
     * 声明 死信队列 用于接收死信消息
     * @return deadLetterQueueA
     */
    @Bean
    public Queue deadLetterQueueA() {
        return new Queue(DEAD_LETTER_QUEUE);
    }
 
    /**
     *  将 死信队列 绑定到死信交换机上
     * @return deadLetterBindingA
     */
    @Bean
    public Binding deadLetterBindingA() {
        return BindingBuilder
                .bind(deadLetterQueueA())
                .to(deadLetterExchange())
                .with(DEAD_LETTER_QUEUE_ROUTING_KEY);
    }
 
    /**
     * 声明 延时交换机
     * @return delayExchange
     */
    @Bean
    public DirectExchange directExchange() {
        return new DirectExchange(DELAY_EXCHANGE);
    }
 
    /**
     * 将 延时队列 绑定参数
     * @return Queue
     */
    @Bean
    public Queue delayQueueA() {
        Map<String, Object> maps = Maps.newHashMapWithExpectedSize(3);
        // 队列绑定DLX参数(关键一步)
        maps.put("x-dead-letter-exchange", DEAD_LETTER_EXCHANGE);
        // 队列绑定 死信RoutingKey参数
        maps.put("x-dead-letter-routing-key", DEAD_LETTER_QUEUE_ROUTING_KEY);
        // 消息过期采用第一种设置队列的 ttl 时间,消息过期时间全部相同。 单位:毫秒,这里设置为8秒
        maps.put("x-message-ttl", 8000);
        return QueueBuilder.durable(DELAY_QUEUE).withArguments(maps).build();
    }
 
    /**
     * 将 延时队列 绑定到延时交换机上面
     * @return delayBindingA
     */
    @Bean
    public Binding delayBindingA() {
        return BindingBuilder
                .bind(delayQueueA())
                .to(directExchange())
                .with(DELAY_QUEUE_ROUTING_KEY);
    }
}

 声明RabbitMq服务接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.demo.www.service;
 
/**
 * rabbiMq服务
 * @author AnYuan
 */
 
public interface RabbitMqService {
 
    /**
     * 统一发送mq
     *
     * @param exchange   交换机
     * @param routingKey 路由key
     * @param msg       消息
     * @param ttl       过期时间
     */
    void send(String exchange, String routingKey, String msg, Integer ttl);
}

服务接口的实现类:

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
package com.demo.www.service.impl;
 
import com.demo.www.service.RabbitMqService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
 
/**
 * rabbitmq服务
 * @author AnYuan
 */
 
@Service
@Slf4j
public class RabbitMqServiceImpl implements RabbitMqService {
 
    @Autowired
    private RabbitTemplate rabbitTemplate;
 
    @Override
    public void send(String exchange, String routingKey, String msg, Integer ttl) {
        MessageProperties messageProperties = new MessageProperties();
        // 第二种方式设置消息过期时间
        messageProperties.setExpiration(ttl.toString());
        // 构建一个消息对象
        Message message = new Message(msg.getBytes(), messageProperties);
        // 发送RabbitMq消息
        rabbitTemplate.convertAndSend(exchange, routingKey, message);
    }
}

创建一个单元测试类:

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
package com.demo.www.service.impl;
 
import com.google.common.collect.Maps;
import com.demo.www.rabbitmq.config.DelayQueueConfig;
import com.demo.www.service.RabbitMqService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
 
import java.time.LocalDateTime;
import java.util.Map;
 
@Slf4j
@SpringBootTest class RabbitMqServiceImplTest {
 
    @Autowired private RabbitMqService rabbitMqService;
 
    @Test public void sendTest() {
      // 手动指定消息过期时间
    int ttl = 10000;
 
    Map<String, Object> msgMap = Maps.newHashMapWithExpectedSize(3);
    msgMap.put("msg", "Hello RabbitMq");
    msgMap.put("time", LocalDateTime.now());
    msgMap.put("ttl", ttl);
 
    // 注意这里发送的交换机是 延时交换机
    rabbitMqService.send(DelayQueueConfig.DELAY_EXCHANGE, DelayQueueConfig.DELAY_QUEUE_ROUTING_KEY, JSONObject.toJSONString(msgMap), ttl);
 
    log.info("消息发送成功:{}", JSONObject.toJSONString(msgMap));
     
    }
}

启动项目后,在RabbitMq的管理后台,可以看到已经自动创建对应的交换机和队列

自动创建的队列,在延时队列的Features栏可以看到有: TTl、DLX、DLK。它们分别代表:(x-message-ttl):设置队列中的所有消息的生存周期(过期时间);(x-dead-letter-exchange)绑定了死信交换机,死信消息会被路由到绑定的死信交换机上;(x-dead-letter-routing-key):死信消息推送到交换机上指定路由键的队列中

运行单元测试后显示发送成功:

马上会看到延时队列里面产生了一条数据:

 

8秒后消息过期变成死信消息,被路由到绑定死信交换机的死信队列里

这样就实现了延时队列,消费死信队列里的消息,就可以满足业务需求或异常恢复了

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
package com.demo.www.rabbitmq.consumers;
 
import com.alibaba.fastjson.JSONObject;
import com.demo.www.rabbitmq.config.DelayQueueConfig;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
 
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
 
/**
 * 延时队列消息消费者
 * @author AnYuan
 */
 
@Component
@Slf4j
public class DelayMsgConsumer {
 
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(DelayQueueConfig.DEAD_LETTER_QUEUE),
            exchange = @Exchange(DelayQueueConfig.DEAD_LETTER_EXCHANGE)))
    public void queueAConsumer(Message message) {
 
        Msg msg = JSONObject.parseObject(new String(message.getBody()), Msg.class);
        LocalDateTime now = LocalDateTime.now();
        Duration duration = Duration.between(msg.getTime(), now);
 
        log.info("DelayMsgConsumer死信队列消费---->Msg:{}, 发送时间:{}, 当前时间:{},  相差时间:{}秒,消息设置的ttl:{}",
                JSONObject.toJSONString(msg),
                localDateTimeToString(msg.getTime()),
                localDateTimeToString(now),
                duration.getSeconds(),
                msg.getTtl());
    }
 
    @Data
    public static class Msg {
        private String ttl;
        private String msg;
        private LocalDateTime time;
    }
 
    private String localDateTimeToString(LocalDateTime localDateTime){
        DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        return dateTimeFormatter.format(localDateTime);
    }
}

重新消费死信队列里的消息即可看到消费的Mq消息,对比time里面的值确认为同一条消息: 

需要注意:发送消息时设置的ttl为10秒,消息过了8秒后就变成死信消息,当创建队列也设置了过期时间,按过期时间短的计算

延时队列的应用场景很多,之前在一个项目里都用到了:订单一定时间内未支付自动取消、出餐超时推送提醒给门店、订单完成后一段时间内推送反馈给用户等等,间隔指定时间后的操作都可以使用延时队列

 

posted @   安逺  阅读(1058)  评论(2编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?
点击右上角即可分享
微信分享提示