SpringBoot2.0实战 | 第十八章:整合RabbitMQ之死信队列

相关知识

什么是死信队列

“死信”是RabbitMQ中的一种消息机制,当你在消费消息时,如果队列里的消息出现以下情况:

  • 消息被否定确认,使用 channel.basicNack 或 channel.basicReject ,并且此时requeue 属性被设置为false。
  • 消息在队列的存活时间超过设置的TTL时间。
  • 消息队列的消息数量已经超过最大队列长度。

那么该消息将成为“死信”,“死信”消息会被RabbitMQ进行特殊处理。
如果配置了死信队列信息,那么该消息将会被丢进死信队列中,如果没有配置,则该消息将会被丢弃。

应用场景

当一个队列中的消息必须要被正确消费,但是却无法被正确消费,有很多情况可能导致出现这个问题,
可能是消息本身的数据导致业务无法通过校验,也可能是相关服务宕机导致,甚至可能只是网络波动导致。
此时该消息不能确认,也不能丢弃,更不能一直 requeue,那将变成一个死循环,
这种消息不能一直呆在当前业务队列阻塞其它消息,但又需要处理,把它们导航至死信队列就是一种解决办法,
通过订阅死信队列可以对此类消息进行例外处理,例如通知相关人员检查数据进行人工处理。

目标

整合 Spring boot 提供的 spring-boot-starter-amqp,实现消息消费失败转死信队列,及实现延迟队列

操作步骤

添加依赖

引入 Spring Boot Starter 父工程

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.5.RELEASE</version>
</parent>

添加 spring-boot-starter-amqp 的依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

添加后的整体依赖如下

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <scope>provided</scope>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-amqp</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

配置

spring:
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: admin
    password: admin
#虚拟host 可以不设置,使用server默认host
#    virtual-host: JCcccHost

编码(发送方)

定义队列

定义一个测试队列 TestDeadQueue,并为该队列配置死信队列,
配置的方法就是在声明队列的时候,添加参数 x-dead-letter-exchangex-dead-letter-routing-key
其实就是在消费失败时,将消息使用该 exchange 及 routing 发送至指定队列

@Configuration
public class DeadConfig {

    @Bean
    public Queue testDeadQueue() {
        Map<String, Object> params = new HashMap<>();
        params.put("x-dead-letter-exchange", "DeadExchange");
        params.put("x-dead-letter-routing-key", "DeadRouting");
        return new Queue("TestDeadQueue",true, false, false, params);
    }

    @Bean
    DirectExchange testDeadExchange() {
        return new DirectExchange("TestDeadExchange");
    }

    @Bean
    Binding bindingTestDeadQueue() {
        return BindingBuilder.bind(testDeadQueue()).to(testDeadExchange()).with("TestDeadRouting");
    }

    @Bean
    public Queue deadQueue() {
        return new Queue("DeadQueue",true);
    }

    @Bean
    DirectExchange deadExchange() {
        return new DirectExchange("DeadExchange");
    }

    @Bean
    Binding bindingDeadQueue() {
        return BindingBuilder.bind(deadQueue()).to(deadExchange()).with("DeadRouting");
    }

}
测试发送
@RunWith(SpringRunner.class)
@WebAppConfiguration
@SpringBootTest(classes = SenderApplication.class)
public class MqTest {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    public void testDeadQueue() throws Exception {
        String msgId = String.valueOf(UUID.randomUUID());
        String sendTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        Map<String,Object> map = new HashMap<>(4);
        map.put("msgId", msgId);
        map.put("msgData", "test dead letter queue");
        map.put("sendTime", sendTime);
        rabbitTemplate.convertAndSend("TestDeadExchange", "TestDeadRouting", map);
    }

}

编码(消费方)

配置

配置消费消息使用手动确认模式

spring:
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: admin
    password: admin
    # 虚拟host 可以不设置,使用server默认host
    #virtual-host: JCcccHost
    listener:
      type: simple
      simple:
        acknowledge-mode: manual
定义队列
@Configuration
public class Config {

    @Bean
    public Queue testDirectQueue() {
        Map<String, Object> params = new HashMap<>();
        params.put("x-dead-letter-exchange", "TestDeadExchange");
        params.put("x-dead-letter-routing-key", "TestDeadRouting");
        return new Queue("TestDeadQueue",true, false, false, params);
    }

    @Bean
    public Queue TestDeadQueue() {
        return new Queue("DeadQueue",true);
    }

}
消费

定义队列 TestDirectQueue 的消费方法,在方法中调用了 basicReject 方法,用于告诉 RabbitMQ 消费失败,
调用 basicReject 方法时 requeue 参数必须为 false,不然就会把消息重新加入当前队列,
由于队列 TestDirectQueue 配置了死信队列,于是 RabbitMQ 在接收到消费失败的 ACK 后,
将当前消息根据配置的 exchange 及 routing 进行再次发送,并从当前队列中删除,
消息最终被发送至队列 DeadQueue。

@Component
public class ConsumerWithAck {

    @RabbitListener(queues = "TestDeadQueue")
    @RabbitHandler
    public void process(Map obj, Channel channel, Message message) throws IOException {
        try {
            System.out.println("DirectQueue消费者收到消息并NACK返回  : " + obj.toString());
            channel.basicReject(message.getMessageProperties().getDeliveryTag(), false);
        } catch (Exception e) {
            e.printStackTrace();
            channel.basicReject(message.getMessageProperties().getDeliveryTag(), false);
        }
    }

    @RabbitListener(queues = "DeadQueue")
    @RabbitHandler
    public void processDead(Map obj, Channel channel, Message message) throws IOException {
        try {
            System.out.println("DeadQueue消费者收到消息并ACK返回  : " + obj.toString());
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        } catch (Exception e) {
            e.printStackTrace();
            channel.basicReject(message.getMessageProperties().getDeliveryTag(), false);
        }
    }

}

源码地址

本章源码 : https://gitee.com/gongm_24/spring-boot-tutorial.git

结束语

死信队列其实并没有什么神秘的地方,不过是绑定在死信交换机上的普通队列,而死信交换机也只是一个普通的交换机,不过是用来专门处理死信的交换机。

总结一下死信消息的生命周期:

Created with Raphaël 2.2.0 客户端发送消息 业务队列 消费者消费 是否消费成功 结束 队列是否配置死信队列 死信队列 yes no yes no
发布了153 篇原创文章 · 获赞 22 · 访问量 10万+

猜你喜欢

转载自blog.csdn.net/gongm24/article/details/103913603