1、死信队列
因为某些原因导致消息在queue里面无法被消费,并且这些消息没有后续的处理,那么这些消息就称为死信,后续处理死信的队列就是死信队列。
1.1、产生死信的来源
- 消息TTL过期;
- 队列达到最大长度,无法添加数据到队列中;
- 消息被拒绝并且没有设置直接丢弃;
1.2、模型图
1.2.1、TTL过期实例
import com.example.wwy.rabbit.units.ChannelUnit;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import java.nio.charset.StandardCharsets;
//生产者
public class DeadProducer {
public static final String NORMAL_EXCHANGE="normal_exchange";
public static void main(String[] args) throws Exception {
Channel channel= ChannelUnit.getChannel();
//配置交换机
channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
//设置消息的TTL时间
AMQP.BasicProperties build = new AMQP.BasicProperties().builder().expiration("10000").build();
//发送消息
for(int i=1;i<11;i++){
String message="消息"+i;
channel.basicPublish(NORMAL_EXCHANGE,"zhangsan",
build,message.getBytes(StandardCharsets.UTF_8));
System.out.println("生产者发送消息:"+message);
}
}
}
import com.example.wwy.rabbit.units.ChannelUnit;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import java.util.HashMap;
import java.util.Map;
//消费者C1
public class Consumer01 {
//普通交换机名称
private static final String NORMAL_EXCHANGE="normal_exchange";
//死信交换机名称
private static final String DEAD_EXCHANGE = "dead_exchange";
//普通队列名称
private static final String NORMAL_QUEUE = "normal-queue";
//死信队列名称
private static final String DEAD_QUEUE = "dead-queue";
public static void main(String[] args) throws Exception {
Channel channel = ChannelUnit.getChannel();
//声明普通交换机
channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
//声明死信交换机
channel.exchangeDeclare(DEAD_EXCHANGE,BuiltinExchangeType.DIRECT);
//声明死信队列
channel.queueDeclare(DEAD_QUEUE,false,false,true,null);
//死信队列绑定死信交换机
channel.queueBind(DEAD_QUEUE,DEAD_EXCHANGE,"lisi");
//正常队列绑定死信信息
Map<String,Object> map=new HashMap<>();
//设置死信交换机信息 key是固定的
map.put("x-dead-letter-exchange",DEAD_EXCHANGE);
//设置死信绑定信息
map.put("x-dead-letter-routing-key","lisi");
//声明普通队列
channel.queueDeclare(NORMAL_QUEUE,false,false,false,map);
//普通交换机绑定普通队列
channel.queueBind(NORMAL_QUEUE,NORMAL_EXCHANGE,"zhangsan");
DeliverCallback deliverCallback=(var1,var2)->{
String message=new String(var2.getBody(),"utf-8");
System.out.println("Consumer01接受的消息是:"+message);
};
CancelCallback cancelCallback=(var1)->{
System.out.println("Consumer01接受的消息失败");
};
channel.basicConsume(NORMAL_QUEUE,true,deliverCallback,cancelCallback);
}
}
import com.example.wwy.rabbit.units.ChannelUnit;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
//接受死信消息C2
public class DeadConsumer {
//死信交换机名称
private static final String DEAD_EXCHANGE = "dead_exchange";
//死信队列名称
private static final String DEAD_QUEUE = "dead-queue";
public static void main(String[] args) throws Exception {
Channel channel= ChannelUnit.getChannel();
//声明交换机
channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT);
//声明队列
channel.queueDeclare(DEAD_QUEUE,false,false,true,null);
//队列绑定交换机
channel.queueBind(DEAD_QUEUE,DEAD_EXCHANGE,"lisi");
DeliverCallback deliverCallback=(var1, var2)->{
String message=new String(var2.getBody(),"utf-8");
System.out.println("Consumer02接受的消息是:"+message);
};
CancelCallback cancelCallback=(var1)->{
System.out.println("Consumer02接受的消息失败");
};
channel.basicConsume(DEAD_QUEUE,true,deliverCallback,cancelCallback);
}
}
1.2.2、队列超出最大长度
import com.example.wwy.rabbit.units.ChannelUnit;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
//生产者
public class DeadProducer {
public static final String NORMAL_EXCHANGE="normal_exchange";
public static void main(String[] args) throws Exception {
Channel channel= ChannelUnit.getChannel();
//配置交换机
channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
//发送消息
for(int i=1;i<11;i++){
String message="消息"+i;
channel.basicPublish(NORMAL_EXCHANGE,"zhangsan", null,message.getBytes(StandardCharsets.UTF_8));
System.out.println("生产者发送消息:"+message);
}
}
}
import com.example.wwy.rabbit.units.ChannelUnit;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import java.util.HashMap;
import java.util.Map;
//消费者C1
public class Consumer01 {
//普通交换机名称
private static final String NORMAL_EXCHANGE="normal_exchange";
//死信交换机名称
private static final String DEAD_EXCHANGE = "dead_exchange";
//普通队列名称
private static final String NORMAL_QUEUE = "normal-queue";
//死信队列名称
private static final String DEAD_QUEUE = "dead-queue";
public static void main(String[] args) throws Exception {
Channel channel = ChannelUnit.getChannel();
//声明普通交换机
channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
//声明死信交换机
channel.exchangeDeclare(DEAD_EXCHANGE,BuiltinExchangeType.DIRECT);
//声明死信队列
channel.queueDeclare(DEAD_QUEUE,false,false,true,null);
//死信队列绑定死信交换机
channel.queueBind(DEAD_QUEUE,DEAD_EXCHANGE,"lisi");
//正常队列绑定死信信息
Map<String,Object> map=new HashMap<>();
//设置死信交换机信息 key是固定的
map.put("x-dead-letter-exchange",DEAD_EXCHANGE);
//设置死信绑定信息
map.put("x-dead-letter-routing-key","lisi");
//设置普通队列的长度
map.put("x-max-length",6);
//声明普通队列
channel.queueDeclare(NORMAL_QUEUE,false,false,false,map);
//普通交换机绑定普通队列
channel.queueBind(NORMAL_QUEUE,NORMAL_EXCHANGE,"zhangsan");
DeliverCallback deliverCallback=(var1,var2)->{
String message=new String(var2.getBody(),"utf-8");
System.out.println("Consumer01接受的消息是:"+message);
};
CancelCallback cancelCallback=(var1)->{
System.out.println("Consumer01接受的消息失败");
};
channel.basicConsume(NORMAL_QUEUE,true,deliverCallback,cancelCallback);
}
}
1.2.3、消息拒绝
生产者和死信消费者都不变
import com.example.wwy.rabbit.units.ChannelUnit;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import java.util.HashMap;
import java.util.Map;
//消费者C1
public class Consumer01 {
//普通交换机名称
private static final String NORMAL_EXCHANGE="normal_exchange";
//死信交换机名称
private static final String DEAD_EXCHANGE = "dead_exchange";
//普通队列名称
private static final String NORMAL_QUEUE = "normal-queue";
//死信队列名称
private static final String DEAD_QUEUE = "dead-queue";
public static void main(String[] args) throws Exception {
Channel channel = ChannelUnit.getChannel();
//声明普通交换机
channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
//声明死信交换机
channel.exchangeDeclare(DEAD_EXCHANGE,BuiltinExchangeType.DIRECT);
//声明死信队列
channel.queueDeclare(DEAD_QUEUE,false,false,true,null);
//死信队列绑定死信交换机
channel.queueBind(DEAD_QUEUE,DEAD_EXCHANGE,"lisi");
//正常队列绑定死信信息
Map<String,Object> map=new HashMap<>();
//设置死信交换机信息 key是固定的
map.put("x-dead-letter-exchange",DEAD_EXCHANGE);
//设置死信绑定信息
map.put("x-dead-letter-routing-key","lisi");
//设置普通队列的长度
map.put("x-max-length",6);
//声明普通队列
channel.queueDeclare(NORMAL_QUEUE,false,false,false,map);
//普通交换机绑定普通队列
channel.queueBind(NORMAL_QUEUE,NORMAL_EXCHANGE,"zhangsan");
DeliverCallback deliverCallback=(var1,var2)->{
String message=new String(var2.getBody(),"utf-8");
/**
* 直接拒绝
* 1、tag标识
* 2、是否直接丢弃
* 3、是否再次入队
*/
channel.basicNack(var2.getEnvelope().getDeliveryTag(),true,false);
System.out.println("Consumer01拒绝接受的消息是:"+message);
};
CancelCallback cancelCallback=(var1)->{
System.out.println("Consumer01接受的消息失败");
};
channel.basicConsume(NORMAL_QUEUE,false,deliverCallback,cancelCallback);
}
}
2、延时队列
延时队列就是存放那些需要在指定时间被处理的消息队列。
2.1、使用场景
1、订单半小时后未付款自动取消;
2、新用户注册三天后,没有登陆,则发消息提醒;
3、用户发起退款后,1天没有处理,则通知相关运营人员等等
这些场景都是在某一件事情发生之后,需要指定时间去执行某一项任务。
2.2、RabbitMQ中的TTL
TTL 是 RabbitMQ 中一个消息或者队列的属性,表明一条消息或者该队列中的所有消息的最大存活时间,单位是毫秒
换句话说,如果一条消息设置了 TTL 属性或者进入了设置TTL 属性的队列,那么这条消息如果在TTL 设置的时间内没有被消费,则会成为"死信"。
如果同时配置了队列的TTL 和消息的TTL,那么较小的那个值将会被使用,有两种方式设置 TTL。
2.2.1、队列TTL和消息TTL的区别:
1.如果设置了队列的 TTL 属性,那么一旦消息过期,就会被队列丢弃(如果配置了死信队列被丢到死信队
列中)
2. 消息即使过期,也不一定会被马上丢弃,因为消息是否过期是在即将投递到消费者之前判定的,如果当前队列有严重的消息积压情况,则已过期的消息也许还能存活较长时间
3. 如果不设置 TTL,表示消息永远不会过期
4. 如果将 TTL 设置为 0,则表示除非此时可以直接投递该消息到消费者,否则该消息将会被丢弃。
2.3、延迟队列的实战
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<!--RabbitMQ 测试依赖-->
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
import com.example.wwy.rabbit.units.DateUtile;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
//生产者
@Slf4j
@RequestMapping("ttl")
@RestController
public class SendMsgController {
@Autowired
private RabbitTemplate rabbitTemplate;
@GetMapping("sendMsg/{message}")
public void sendMsg(@PathVariable String message){
log.info("当前时间:{},发送一条信息给两个 TTL 队列:{}", DateUtile.getDateToString(), message);
rabbitTemplate.convertAndSend("X", "XA", "消息来自 ttl 为 10S 的队列: "+message);
rabbitTemplate.convertAndSend("X", "XB", "消息来自 ttl 为 40S 的队列: "+message);
}
//可以自定义延时时间
@GetMapping("sendExpirationMsg/{message}/{ttlTime}")
public void sendMsg(@PathVariable String message,@PathVariable String ttlTime) {
//设置消息的过期时间
rabbitTemplate.convertAndSend("X", "XC", message,
correlationData ->{correlationData.getMessageProperties().setExpiration(ttlTime);
return correlationData;
});
log.info("当前时间:{},发送一条时长{}毫秒 TTL 信息给队列 C:{}", new Date(),ttlTime, message);
}
}
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
//绑定的配置类
@Configuration
public class TtlQueueConfig {
public static final String X_EXCHANGE = "X";
public static final String QUEUE_A = "QA";
public static final String QUEUE_B = "QB";
public static final String QUEUE_C = "QC";
public static final String Y_DEAD_LETTER_EXCHANGE = "Y";
public static final String DEAD_LETTER_QUEUE = "QD";
//声明 xExchange
@Bean
public DirectExchange xExchange(){
return new DirectExchange(X_EXCHANGE);
}
// 声明 yExchange
@Bean("yExchange")
public DirectExchange yExchange(){
return new DirectExchange(Y_DEAD_LETTER_EXCHANGE);
}
//声明队列 QA ttl 为 10s 并绑定到对应的死信交换机
@Bean("queueA")
public Queue queueA(){
Map<String, Object> args = new HashMap<>(3);
//声明当前队列绑定的死信交换机
args.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);
//声明当前队列的死信路由 key
args.put("x-dead-letter-routing-key", "YD");
//声明队列的 TTL 延时10S
args.put("x-message-ttl", 10000);
return QueueBuilder.durable(QUEUE_A).withArguments(args).build();
}
// 声明队列 QA 绑定 X 交换机
@Bean
public Binding queueaBindingX(@Qualifier("queueA") Queue queueA,
@Qualifier("xExchange") DirectExchange xExchange){
return BindingBuilder.bind(queueA).to(xExchange).with("XA");
}
//声明队列 QB ttl 为 40s 并绑定到对应的死信交换机
@Bean("queueB")
public Queue queueB(){
Map<String, Object> args = new HashMap<>(3);
//声明当前队列绑定的死信交换机
args.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);
//声明当前队列的死信路由 key
args.put("x-dead-letter-routing-key", "YD");
//声明队列的 TTL 延时40S
args.put("x-message-ttl", 40000);
return QueueBuilder.durable(QUEUE_B).withArguments(args).build();
}
//声明队列 QB 绑定 X 交换机
@Bean
public Binding queuebBindingX(@Qualifier("queueB") Queue queue1B,
@Qualifier("xExchange") DirectExchange xExchange){
return BindingBuilder.bind(queue1B).to(xExchange).with("XB");
}
//声明死信队列 QD
@Bean("queueD")
public Queue queueD(){
return new Queue(DEAD_LETTER_QUEUE);
}
//声明死信队列 QD 绑定关系
@Bean
public Binding deadLetterBindingQAD(@Qualifier("queueD") Queue queueD,
@Qualifier("yExchange") DirectExchange yExchange){
return BindingBuilder.bind(queueD).to(yExchange).with("YD");
}
//声明队列 QC
@Bean
public Queue queueC(){
Map<String,Object> map=new HashMap<>(2);
//声明当前队列绑定的死信交换机
map.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);
//声明当前队列的死信路由 key
map.put("x-dead-letter-routing-key", "YD");
return QueueBuilder.durable(QUEUE_C).withArguments(map).build();
}
//声明 队列 QC 绑定关系
@Bean
public Binding queuecBindingX(@Qualifier("queueC") Queue queueC,
@Qualifier("xExchange") DirectExchange xExchange){
return BindingBuilder.bind(queueC).to(xExchange).with("XC");
}
}
import com.example.wwy.rabbit.units.DateUtile;
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.io.IOException;
//消费者
@Slf4j
@Component
public class DeadLetterQueueConsumer {
@RabbitListener(queues = "QD")
public void receiveD(Message message, Channel channel) throws IOException{
String msg = new String(message.getBody());
log.info("当前时间:{},收到死信队列信息{}", DateUtile.getDateToString(), msg);
}
}
如果使用在消息属性上设置 TTL 的方式,消息可能并不会按时“死亡“,因为 RabbitMQ 只会检查第一个消息是否过期,如果过期则丢到死信队列,
如果第一个消息的延时时长很长,而第二个消息的延时时长很短,第二个消息并不会优先得到执行,可以通过插件去解决上述问题。
3、Rabbitmq 插件实现延迟队列
3.1、安装插件
在官网上下载 https://www.rabbitmq.com/community-plugins.html,下载rabbitmq_delayed_message_exchange 插件
进入 RabbitMQ 的安装目录下的 plgins 目录 /usr/lib/rabbitmq/lib/rabbitmq_server-3.8.8/plugins
安装命令:
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
重启rabbitmq服务
停止服务 /sbin/service rabbitmq-server stop
启动服务 /sbin/service rabbitmq-server start
安装成功, 交换机多一个 X-delayed-message
3.2 、简单Demo
import com.wwy.springbootrbmq.config.DelayedQueueConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Date;
//生产者
@Slf4j
@RestController
@RequestMapping(value = "ttl")
public class DelayedController {
@Autowired
private RabbitTemplate rabbitTemplate;
@GetMapping(value = "sendDelayMsg/{message}/{delayTime}")
public void sendDelayMsg(@PathVariable String message,@PathVariable Integer delayTime){
rabbitTemplate.convertAndSend(DelayedQueueConfig.DELAYED_EXCHANGE_NAME,
DelayedQueueConfig.DELAYED_ROUTING_KEY,message,
correlationData->{
correlationData.getMessageProperties().setDelay(delayTime);
return correlationData;
});
log.info(" 当 前 时 间 : {}, 发 送 一 条 延 迟 {} 毫秒的信息给队列 delayed.queue:{}", new
Date(),delayTime, message);
}
}
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
//配置类
@Configuration
public class DelayedQueueConfig {
public static final String DELAYED_QUEUE_NAME = "delayed.queue";
public static final String DELAYED_EXCHANGE_NAME = "delayed.exchange";
public static final String DELAYED_ROUTING_KEY = "delayed.routingkey";
@Bean
public Queue delayedQueue(){
return new Queue(DELAYED_QUEUE_NAME);
}
@Bean
public CustomExchange delayedExchange(){
Map<String,Object> map=new HashMap<>();
//自定义交换机的类型
map.put("x-delayed-type","direct");
return new CustomExchange(DELAYED_EXCHANGE_NAME,"x-delayed-message",true,false,map);
}
@Bean
public Binding delayedBinding(@Qualifier("delayedQueue")Queue queue,
@Qualifier("delayedExchange")CustomExchange exchange){
return BindingBuilder.bind(queue).to(exchange).with(DELAYED_ROUTING_KEY).noargs();
}
}
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.io.UnsupportedEncodingException;
import java.util.Date;
//消费者
@Slf4j
@Component
public class DelayedListener {
public static final String DELAYED_QUEUE_NAME = "delayed.queue";
@RabbitListener(queues = "delayed.queue")
public void delayedQueue(Message message) throws UnsupportedEncodingException {
String msg=new String(message.getBody(),"UTF-8");
log.info("当前时间:{},收到延时队列的消息:{}", new Date().toString(), msg);
}
}
2秒的延迟消息,优先被消费,符合预期。
4、备份交换机的实现
对于交换机无法投递消息,就可以为某一个交换机声明一个对应的备份交换机,当交换机接收到一条不可路由消息时,将会把这条消息转发到备份交换机中,由备份交换机来进行转发和处理,这样就能把所有消息都投递到与其绑定
的队列中,然后我们在备份交换机下绑定一个队列,这样所有那些原交换机无法被路由的消息,就会都进入这个队列了。当然,我们还可以建立一个报警队列,用独立的消费者来进行监测和报警。
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class BackUpQueueConfig {
public static final String CONF_EXCHANGE="conf_exchange";
public static final String BAUP_EXCHANGE="baup_exchange";
public static final String CONF_QUEUE="conf_queue";
public static final String BAUP_QUEUE="baup_exchange";
public static final String WARNING_QUEUE="warning_exchange";
//主交换机
@Bean
public DirectExchange getConfExchange(){
//声明备份交换机
ExchangeBuilder exchangeBuilder=ExchangeBuilder.directExchange(CONF_EXCHANGE)
.durable(true)
//设置该交换机的备份交换机
.withArgument("alternate-exchange",BAUP_EXCHANGE);
return exchangeBuilder.build();
}
//备份交换机
@Bean
public FanoutExchange getBaupExchange(){
return new FanoutExchange(BAUP_EXCHANGE);
}
@Bean
public Queue getConfigQueue(){
return new Queue(CONF_QUEUE);
}
@Bean
public Queue getBaupQueue(){
return new Queue(BAUP_QUEUE);
}
@Bean
public Queue getWarningQueue(){
return new Queue(WARNING_QUEUE);
}
@Bean
public Binding setConfQueueToExchange(@Qualifier("getConfExchange") DirectExchange exchange,
@Qualifier("getConfigQueue") Queue queue){
return BindingBuilder.bind(queue).to(exchange).with("key1");
}
@Bean
public Binding setBaupQueueToExchange(@Qualifier("getBaupExchange") FanoutExchange exchange,
@Qualifier("getBaupQueue") Queue queue){
return BindingBuilder.bind(queue).to(exchange);
}
@Bean
public Binding setWarningQueueToExchange(@Qualifier("getBaupExchange") FanoutExchange exchange,
@Qualifier("getWarningQueue") Queue queue){
return BindingBuilder.bind(queue).to(exchange);
}
}