本文口味:爆炒鱿鱼   预计阅读:15分钟

一、说明

RabbitMQ是流行的开源消息队列系统,使用erlang语言开发,由于其社区活跃度高,维护更新较快,性能稳定,深得很多企业的欢心(当然,也包括我现在所在公司【手动滑稽】)。

为了保证订单业务的消息数据不丢失,需要使用到RabbitMQ的死信队列机制,当消息消费发生异常时,将消息投入死信队列中。但由于对死信队列的概念及配置不熟悉,导致曾一度陷入百度的汪洋大海,无法自拔,很多文章都看起来可行,但是实际上却并不能帮我解决实际问题。最终,在官网文档中找到了我想要的答案,通过官网文档的学习,才发现对于死信队列存在一些误解,导致配置死信队列之路困难重重。

于是本着记录和分享的精神,将死信队列的概念和配置完整的写下来,以便帮助遇到同样问题的朋友。

二、本文大纲

以下是本文大纲:

本文阅读前,需要对RabbitMQ有一个简单的了解,偏向实战配置讲解。

三、死信队列是什么

死信,在官网中对应的单词为“Dead Letter”,可以看出翻译确实非常的简单粗暴。那么死信是个什么东西呢?

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

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

那么该消息将成为“死信”。

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

四、如何配置死信队列

这一部分将是本文的关键,如何配置死信队列呢?其实很简单,大概可以分为以下步骤:

  1. 配置业务队列,绑定到业务交换机上
  2. 为业务队列配置死信交换机和路由key
  3. 为死信交换机配置死信队列

注意,并不是直接声明一个公共的死信队列,然后所以死信消息就自己跑到死信队列里去了。而是为每个需要使用死信的业务队列配置一个死信交换机,这里同一个项目的死信交换机可以共用一个,然后为每个业务队列分配一个单独的路由key。

有了死信交换机和路由key后,接下来,就像配置业务队列一样,配置死信队列,然后绑定在死信交换机上。也就是说,死信队列并不是什么特殊的队列,只不过是绑定在死信交换机上的队列。死信交换机也不是什么特殊的交换机,只不过是用来接受死信的交换机,所以可以为任何类型【Direct、Fanout、Topic】。一般来说,会为每个业务队列分配一个独有的路由key,并对应的配置一个死信队列进行监听,也就是说,一般会为每个重要的业务队列配置一个死信队列。

有了前文这些陈述后,接下来就是惊险刺激的实战环节,这里省略了RabbitMQ环境的部署和搭建环节。

先创建一个Springboot项目。然后在pom文件中添加 spring-boot-starter-amqpspring-boot-starter-web 的依赖,接下来创建一个Config类,这里是关键:

@Configuration
public class RabbitMQConfig { public static final String BUSINESS_EXCHANGE_NAME = "dead.letter.demo.simple.business.exchange";
public static final String BUSINESS_QUEUEA_NAME = "dead.letter.demo.simple.business.queuea";
public static final String BUSINESS_QUEUEB_NAME = "dead.letter.demo.simple.business.queueb";
public static final String DEAD_LETTER_EXCHANGE = "dead.letter.demo.simple.deadletter.exchange";
public static final String DEAD_LETTER_QUEUEA_ROUTING_KEY = "dead.letter.demo.simple.deadletter.queuea.routingkey";
public static final String DEAD_LETTER_QUEUEB_ROUTING_KEY = "dead.letter.demo.simple.deadletter.queueb.routingkey";
public static final String DEAD_LETTER_QUEUEA_NAME = "dead.letter.demo.simple.deadletter.queuea";
public static final String DEAD_LETTER_QUEUEB_NAME = "dead.letter.demo.simple.deadletter.queueb"; // 声明业务Exchange
@Bean("businessExchange")
public FanoutExchange businessExchange(){
return new FanoutExchange(BUSINESS_EXCHANGE_NAME);
} // 声明死信Exchange
@Bean("deadLetterExchange")
public DirectExchange deadLetterExchange(){
return new DirectExchange(DEAD_LETTER_EXCHANGE);
} // 声明业务队列A
@Bean("businessQueueA")
public Queue businessQueueA(){
Map<String, Object> args = new HashMap<>(2);
// x-dead-letter-exchange 这里声明当前队列绑定的死信交换机
args.put("x-dead-letter-exchange", DEAD_LETTER_EXCHANGE);
// x-dead-letter-routing-key 这里声明当前队列的死信路由key
args.put("x-dead-letter-routing-key", DEAD_LETTER_QUEUEA_ROUTING_KEY);
return QueueBuilder.durable(BUSINESS_QUEUEA_NAME).withArguments(args).build();
} // 声明业务队列B
@Bean("businessQueueB")
public Queue businessQueueB(){
Map<String, Object> args = new HashMap<>(2);
// x-dead-letter-exchange 这里声明当前队列绑定的死信交换机
args.put("x-dead-letter-exchange", DEAD_LETTER_EXCHANGE);
// x-dead-letter-routing-key 这里声明当前队列的死信路由key
args.put("x-dead-letter-routing-key", DEAD_LETTER_QUEUEB_ROUTING_KEY);
return QueueBuilder.durable(BUSINESS_QUEUEB_NAME).withArguments(args).build();
} // 声明死信队列A
@Bean("deadLetterQueueA")
public Queue deadLetterQueueA(){
return new Queue(DEAD_LETTER_QUEUEA_NAME);
} // 声明死信队列B
@Bean("deadLetterQueueB")
public Queue deadLetterQueueB(){
return new Queue(DEAD_LETTER_QUEUEB_NAME);
} // 声明业务队列A绑定关系
@Bean
public Binding businessBindingA(@Qualifier("businessQueueA") Queue queue,
@Qualifier("businessExchange") FanoutExchange exchange){
return BindingBuilder.bind(queue).to(exchange);
} // 声明业务队列B绑定关系
@Bean
public Binding businessBindingB(@Qualifier("businessQueueB") Queue queue,
@Qualifier("businessExchange") FanoutExchange exchange){
return BindingBuilder.bind(queue).to(exchange);
} // 声明死信队列A绑定关系
@Bean
public Binding deadLetterBindingA(@Qualifier("deadLetterQueueA") Queue queue,
@Qualifier("deadLetterExchange") DirectExchange exchange){
return BindingBuilder.bind(queue).to(exchange).with(DEAD_LETTER_QUEUEA_ROUTING_KEY);
} // 声明死信队列B绑定关系
@Bean
public Binding deadLetterBindingB(@Qualifier("deadLetterQueueB") Queue queue,
@Qualifier("deadLetterExchange") DirectExchange exchange){
return BindingBuilder.bind(queue).to(exchange).with(DEAD_LETTER_QUEUEB_ROUTING_KEY);
}
}

这里声明了两个Exchange,一个是业务Exchange,另一个是死信Exchange,业务Exchange下绑定了两个业务队列,业务队列都配置了同一个死信Exchange,并分别配置了路由key,在死信Exchange下绑定了两个死信队列,设置的路由key分别为业务队列里配置的路由key。

下面是配置文件application.yml:

spring:
rabbitmq:
host: localhost
password: guest
username: guest
listener:
type: simple
simple:
default-requeue-rejected: false
acknowledge-mode: manual

这里记得将default-requeue-rejected属性设置为false。

接下来,是业务队列的消费代码:

@Slf4j
@Component
public class BusinessMessageReceiver { @RabbitListener(queues = BUSINESS_QUEUEA_NAME)
public void receiveA(Message message, Channel channel) throws IOException {
String msg = new String(message.getBody());
log.info("收到业务消息A:{}", msg);
boolean ack = true;
Exception exception = null;
try {
if (msg.contains("deadletter")){
throw new RuntimeException("dead letter exception");
}
} catch (Exception e){
ack = false;
exception = e;
}
if (!ack){
log.error("消息消费发生异常,error msg:{}", exception.getMessage(), exception);
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
} else {
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}
} @RabbitListener(queues = BUSINESS_QUEUEB_NAME)
public void receiveB(Message message, Channel channel) throws IOException {
System.out.println("收到业务消息B:" + new String(message.getBody()));
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}
}

然后配置死信队列的消费者:

@Component
public class DeadLetterMessageReceiver { @RabbitListener(queues = DEAD_LETTER_QUEUEA_NAME)
public void receiveA(Message message, Channel channel) throws IOException {
System.out.println("收到死信消息A:" + new String(message.getBody()));
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} @RabbitListener(queues = DEAD_LETTER_QUEUEB_NAME)
public void receiveB(Message message, Channel channel) throws IOException {
System.out.println("收到死信消息B:" + new String(message.getBody()));
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}
}

最后,为了方便测试,写一个简单的消息生产者,并通过controller层来生产消息。

@Component
public class BusinessMessageSender { @Autowired
private RabbitTemplate rabbitTemplate; public void sendMsg(String msg){
rabbitTemplate.convertSendAndReceive(BUSINESS_EXCHANGE_NAME, "", msg);
}
}
@RequestMapping("rabbitmq")
@RestController
public class RabbitMQMsgController { @Autowired
private BusinessMessageSender sender; @RequestMapping("sendmsg")
public void sendMsg(String msg){
sender.sendMsg(msg);
}
}

一切准备就绪,启动!

可以从RabbitMQ的管理后台中看到一共有四个队列,除默认的Exchange外还有声明的两个Exchange。

接下来,访问一下url,来测试一下:

http://localhost:8080/rabbitmq/sendmsg?msg=msg

日志:

收到业务消息A:msg
收到业务消息B:msg

表示两个Consumer都正常收到了消息。这代表正常消费的消息,ack后正常返回。然后我们再来测试nck的消息。

http://localhost:8080/rabbitmq/sendmsg?msg=deadletter

这将会触发业务队列A的NCK,按照预期,消息被NCK后,会抛到死信队列中,因此死信队列将会出现这个消息,日志如下:

收到业务消息A:deadletter
消息消费发生异常,error msg:dead letter exception
java.lang.RuntimeException: dead letter exception
... 收到死信消息A:deadletter

可以看到,死信队列的Consumer接受到了这个消息,所以流程到此为止就打通了。

五、死信消息的变化

那么“死信”被丢到死信队列中后,会发生什么变化呢?

如果队列配置了参数 x-dead-letter-routing-key 的话,“死信”的路由key将会被替换成该参数对应的值。如果没有设置,则保留该消息原有的路由key。

举个栗子:

如果原有消息的路由key是testA,被发送到业务Exchage中,然后被投递到业务队列QueueA中,如果该队列没有配置参数x-dead-letter-routing-key,则该消息成为死信后,将保留原有的路由keytestA,如果配置了该参数,并且值设置为testB,那么该消息成为死信后,路由key将会被替换为testB,然后被抛到死信交换机中。

另外,由于被抛到了死信交换机,所以消息的Exchange Name也会被替换为死信交换机的名称。

消息的Header中,也会添加很多奇奇怪怪的字段,修改一下上面的代码,在死信队列的消费者中添加一行日志输出:

log.info("死信消息properties:{}", message.getMessageProperties());

然后重新运行一次,即可得到死信消息Header中被添加的信息:

死信消息properties:MessageProperties [headers={x-first-death-exchange=dead.letter.demo.simple.business.exchange, x-death=[{reason=rejected, count=1, exchange=dead.letter.demo.simple.business.exchange, time=Sun Jul 14 16:48:16 CST 2019, routing-keys=[], queue=dead.letter.demo.simple.business.queuea}], x-first-death-reason=rejected, x-first-death-queue=dead.letter.demo.simple.business.queuea}, correlationId=1, replyTo=amq.rabbitmq.reply-to.g2dkABZyYWJiaXRAREVTS1RPUC1DUlZGUzBOAAAPQAAAAAAB.bLbsdR1DnuRSwiKKmtdOGw==, contentType=text/plain, contentEncoding=UTF-8, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, redelivered=false, receivedExchange=dead.letter.demo.simple.deadletter.exchange, receivedRoutingKey=dead.letter.demo.simple.deadletter.queuea.routingkey, deliveryTag=1, consumerTag=amq.ctag-NSp18SUPoCNvQcoYoS2lPg, consumerQueue=dead.letter.demo.simple.deadletter.queuea]

Header中看起来有很多信息,实际上并不多,只是值比较长而已。下面就简单说明一下Header中的值:

字段名 含义
x-first-death-exchange 第一次被抛入的死信交换机的名称
x-first-death-reason 第一次成为死信的原因,rejected:消息在重新进入队列时被队列拒绝,由于default-requeue-rejected 参数被设置为falseexpired :消息过期。maxlen : 队列内消息数量超过队列最大容量
x-first-death-queue 第一次成为死信前所在队列名称
x-death 历次被投入死信交换机的信息列表,同一个消息每次进入一个死信交换机,这个数组的信息就会被更新

六、死信队列应用场景

通过上面的信息,我们已经知道如何使用死信队列了,那么死信队列一般在什么场景下使用呢?

一般用在较为重要的业务队列中,确保未被正确消费的消息不被丢弃,一般发生消费异常可能原因主要有由于消息信息本身存在错误导致处理异常,处理过程中参数校验异常,或者因网络波动导致的查询异常等等,当发生异常时,当然不能每次通过日志来获取原消息,然后让运维帮忙重新投递消息(没错,以前就是这么干的= =)。通过配置死信队列,可以让未正确处理的消息暂存到另一个队列中,待后续排查清楚问题后,编写相应的处理代码来处理死信消息,这样比手工恢复数据要好太多了。

七、总结

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

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

  1. 业务消息被投入业务队列
  2. 消费者消费业务队列的消息,由于处理过程中发生异常,于是进行了nck或者reject操作
  3. 被nck或reject的消息由RabbitMQ投递到死信交换机中
  4. 死信交换机将消息投入相应的死信队列
  5. 死信队列的消费者消费死信消息

死信消息是RabbitMQ为我们做的一层保证,其实我们也可以不使用死信队列,而是在消息消费异常时,将消息主动投递到另一个交换机中,当你明白了这些之后,这些Exchange和Queue想怎样配合就能怎么配合。比如从死信队列拉取消息,然后发送邮件、短信、钉钉通知来通知开发人员关注。或者将消息重新投递到一个队列然后设置过期时间,来进行延时消费。

本篇文章中的demo项目已上传至github,有需要的朋友可以自行下载查阅​。https://github.com/MFrank2016/dead-letter-demo​

如果本文对你有帮助,记得点个赞,也希望能分享给更多的朋友。也欢迎关注我的公众号进行留言交流。

【RabbitMQ】一文带你搞定RabbitMQ死信队列的更多相关文章

  1. 【RabbitMQ】一文带你搞定RabbitMQ延迟队列

    本文口味:鱼香肉丝   预计阅读:10分钟 一.说明 在上一篇中,介绍了RabbitMQ中的死信队列是什么,何时使用以及如何使用RabbitMQ的死信队列.相信通过上一篇的学习,对于死信队列已经有了更 ...

  2. 【项目实践】一文带你搞定Spring Security + JWT

    以项目驱动学习,以实践检验真知 前言 关于认证和授权,R之前已经写了两篇文章: [项目实践]在用安全框架前,我想先让你手撸一个登陆认证 [项目实践]一文带你搞定页面权限.按钮权限以及数据权限 在这两篇 ...

  3. 【项目实践】一文带你搞定Session和JWT的登录认证方式

    以项目驱动学习,以实践检验真知 前言 登录认证,估计是所有系统中最常见的功能了,并且也是最基础.最重要的功能.为了做好这一块而诞生了许多安全框架,比如最常见的Shiro.Spring Security ...

  4. 一文带你搞定AOP切面

    摘要:AOP在spring中又叫"面向切面编程",是对传统我们面向对象编程的一个补充,主要操作对象就是"切面",可以简单的理解它是贯穿于方法之中,在方法执行前. ...

  5. 轻松搞定RabbitMQ(六)——主题

    转自 http://blog.csdn.net/xiaoxian8023/article/details/48806871 翻译地址:http://www.rabbitmq.com/tutorials ...

  6. RabbitMQ:从入门到搞定面试官

    安装 使用docker安装,注意要安装tag后缀为management的镜像(包含web管理插件),我这里使用的是rabbitmq:3.8-management 1. 拉取镜像 shell docke ...

  7. CHUCK手把手带你搞定OPENSTACK

    一.OpenStack初探 1.1 OpenStack简介 OpenStack是一整套开源软件项目的综合,它允许企业或服务提供者建立.运行自己的云计算和存储设施.Rackspace与NASA是最初重要 ...

  8. 【springcloud】一文带你搞懂API网关

    作者:aCoder2013 https://github.com/aCoder2013/blog/issues/35 前言 假设你正在开发一个电商网站,那么这里会涉及到很多后端的微服务,比如会员.商品 ...

  9. 一文带你搞懂 Kafka 的系统架构(深度好文,值得收藏)

    Kafka 简介 Kafka 是一种高吞吐.分布式.基于发布和订阅模型的消息系统,最初是由 LinkedIn 公司采用 Scala 和 java 开发的开源流处理软件平台,目前是 Apache 的开源 ...

随机推荐

  1. CefSharp部分效果实现

    CefSharp For WPF隐藏滚动条 CefSharp For WPF自定义右键菜单栏 CefSharp For WPF响应页面点击事件

  2. 基于IdentityServer4的单点登录——Client

    以MvcClient项目为例 1.新建项目并添加引用 新建一个asp .net core 2.0的项目引用IdentityModel 2.配置 比之前的控制台客户端多这个步骤,需要配置这个客户端的Cl ...

  3. js div的显示和隐藏

    <head>    <title></title>    <style type="text/css">        div    ...

  4. WPF关于控件 父级控件,子级控件,控件模板中的控件,等之间的相互访问

    原文:WPF关于控件 父级控件,子级控件,控件模板中的控件,等之间的相互访问 1,在菜单中访问 弹出菜单的控件 var mi = sender as MenuItem;//菜单条目 MenuItem ...

  5. 如何在XAML中转义大括号

    原文:如何在XAML中转义大括号 如何在XAML中转义大括号                                       周银辉 我们知道大括号"{}"在XAML中 ...

  6. Windows Phone 8.1之应用设置存储的简单应用

    应用设置存储主要分为本地存储和漫游存储两种方式.本地存储的指将应用的设置信息存储在本地存储空间中,而漫游存储则是指将应用的设置信息存储在网络服务器中.相对于本地存储而言,漫游存储支持多台设备之间的应用 ...

  7. Python写的嗅探器——Pyside,Scapy

    使用Python的Pyside和Scapy写的嗅探器原型,拥有基本框架,但是功能并不十分完善,供参考. import sys import time import binascii from PySi ...

  8. 喜迎Win10周年版,芒果TV UWP完善手机视频离线下载

    喜迎Win10周年版更新,湖南卫视旗下唯一官方视频平台<芒果TV>近日向Win10商店提交了芒果TV UWP V3.1.0版,这次不仅在上一版(V3.0.0)的基础上完善了用户呼声最高的手 ...

  9. ArcGIS for Desktop入门教程_第七章_使用ArcGIS进行空间分析 - ArcGIS知乎-新一代ArcGIS问答社区

    原文:ArcGIS for Desktop入门教程_第七章_使用ArcGIS进行空间分析 - ArcGIS知乎-新一代ArcGIS问答社区 1 使用ArcGIS进行空间分析 1.1 GIS分析基础 G ...

  10. Android零基础入门第56节:翻转视图ViewFlipper打造引导页和轮播图

    原文:Android零基础入门第56节:翻转视图ViewFlipper打造引导页和轮播图 前面两期学习了 ViewAnimator及其子类ViewSwitcher的使用,以及ViewSwitcher的 ...