跟我学MQ系列之四rabbitMQ原理优化方案
死信队列
RabbitMQ的死信队列不像RocketMQ一样时原本就存在的,它需要我们自己设置一个交换机然后绑定队列,我们在语义上将其用作为存放无法消费的消息的队列。
RabbitMQ的死信是通过为普通队列设置死信参数,当该队列出现无法消费的消息,就会将这些消息转移到设置的死信队列中。
死信消息产生原因
- 消息 TTL 过期
- 队列达到最大长度(队列满了,无法再添加数据到 mq 中)
- 消息被拒绝(basic.reject 或 basic.nack)并且 requeue=false
RabbitMQ中的TTL
TTL 是 RabbitMQ 中一个消息或者队列的属性,表明一条消息或者该队列中的所有消息的最大存活时间,单位是毫秒。
换句话说,如果一条消息设置了 TTL 属性或者进入了设置 TTL 属性的队列,那么这条消息如果在 TTL 设置的时间内没有被消费,则会成为"死信"。如果同时配置了队列的 TTL 和消息的TTL,那么 较小 的那个值将会被使用,有两种方式设置 TTL。
设置TTL的方式
消息设置TTL
Message msg = new Message(s.getBytes(StandardCharsets.UTF_8));
//参数四 MessagePostProcessor:用于在执行消息转换后添加/修改标头或属性。
//它还可以用于在侦听器容器和AmqpTemplate接收消息时修改入站消息。
rabbitTemplate.convertAndSend("MqSendService-One","One",msg,correlationData->{
correlationData.getMessageProperties().setExpiration("1000");
return correlationData;
});
//也可在创建消息时指定
msg.getMessageProperties().setExpiration("1000");
队列设置TTL
@Bean
public DirectExchange directExchange(){
Map<String, Object> args = new HashMap<>(3);
//声明队列的 TTL
args.put("x-message-ttl", 10000);
//参数介绍
//1.交换器名 2.是否持久化 3.自动删除 4.其他参数
return new DirectExchange("MqSendService-One",false,false,args);
}
@Bean
public Queue directQueue(){
//需要的属性可以通过构建者不断添加
Queue queue = QueueBuilder.noDurable("TTL_Queue").ttl(100).build();
return queue;
}
二者的区别
如果设置了队列的 TTL 属性,那么一旦消息过期,就会被队列丢弃(如果配置了死信队列被丢到死信队列中),
而消息设置TTL方式,消息即使过期,也不一定会被马上丢弃,因为 RabbitMQ 只会检查第一个消息是否过期,如果过期则丢到死信队列,如果第一个消息的延时时长很长,而第二个消息的延时时长很短,第二个消息并不会优先得到执行。
另外,还需要注意的一点是,如果 不设置 TTL,表示消息永远不会过期,如果将 TTL 设置为 0,则表示除非此时可以直接投递该消息到消费者,否则该消息将会被丢弃。
代码实现
1.语义声明死信交换机
@Bean
public DirectExchange deadExchange(){
//参数介绍
//1.交换器名 2.是否持久化 3.自动删除 4.其他参数
return new DirectExchange("Dead_Exchange",false,false,null);
}
2.声明死信队列,并建立绑定关系
@Bean
public Queue directQueue(){
//参数介绍
//1.队列名 2.是否持久化 3.是否独占 4.自动删除 5.其他参数
return new Queue("Dead_Queue",false,false,false,null);
}
3.为正常队列设置死信参数(重点)
@Bean
public Queue directQueue(){
Map<String, Object> args = new HashMap<>(3);
//声明当前队列绑定的死信交换机
args.put("x-dead-letter-exchange", "dead_exchange");
//声明当前队列的死信路由 key
args.put("x-dead-letter-routing-key", "dead");
//参数介绍
//1.队列名 2.是否持久化 3.是否独占 4.自动删除 5.其他参数
return new Queue("directQueue-One",false,false,false,args);
}
@Bean
public Queue directQueue2(){
Queue queue = QueueBuilder
.durable("dis")
.autoDelete()
.ttl(100)
.deadLetterExchange("Dead_Exchange") //设置死信交换机参数
.deadLetterRoutingKey("Dead") //设置死信队列的路由key
.build();
return queue;
}
延迟队列
利用死信队列达到
RabbitMQ的延迟队列可以通过设置TTL的时间再配合设置死信队列的参数达到。
例:创建一个队列并设置TTL时间,但无人监听消费,那么当TTL时间达到,该消息就会进入死信队列,这时设置一个监听死信队列的消 费者,从而达到延迟消费的效果。
利用官网延迟队列插件达到
优先级队列
介绍
RabbitMQ支持为队列设置优先级,从而达到优先级高的队列中消息被优先消费。
实现代码
@Bean
public Queue directQueue2() {
//设置队列优先级
//args.put("x-max-priority",5)
Queue queue = QueueBuilder
//持久化并设置队列名
.durable("dis")
//开启队列优先级,并设置优先级数
.maxPriority(5)
.build();
return queue;
}
惰性队列
介绍
默认情况下,当生产者将消息发送到 RabbitMQ 的时候,队列中的消息会尽可能的存储在内存之中,这样可以更加快速的将消息发送给消费者。即使是持久化的消息,在被写入磁盘的同时也会在内存中驻留一份备份。
惰性队列会尽可能的将消息存入磁盘中,而在消费者消费到相应的消息时才会被加载到内存中,它的一个重要的设计目标是 支持更多的消息存储。当消费者由于各种各样的原因(比如消费者下线、宕机亦或者是由于维护而关闭等)而致使长时间内不能消费消息造成堆积时,惰性队列就很有必要了。
代码实现
队列存在两种模式:default 和 lazy。lazy即为惰性队列模式。
@Bean
public Queue directQueue2() {
//设置惰性队列
//args.put("x-queue-mode", "lazy");
Queue queue = QueueBuilder
//持久化并设置队列名
.durable("dis")
//设为惰性队列
.lazy()
.build();
return queue;
}
灾难防护
Message acknowledgment(消息确认)
从安全角度考虑,网络是不可靠的,接收消息的应用也有可能在处理消息的时候失败。基于此原因,AMQP模块包含了一个消息确认(message acknowledgements)的概念:当消息从队列投递给消费者的时候,消费者服务器需要返回一个ack(确认信息),当broker收到了确认才会将该消息删除;消息确认可以是自动的,也可以是由消费端手动确认。此外也支持生产端向broker发送消息得到broker的ack,从而针对做出响应逻辑。
发布端消息确认(发布确认)
确认模式
-
NONE
禁用发布确认模式,是默认值
-
CORRELATED
发布消息成功到交换器后会触发回调方法
-
SIMPLE
经测试有两种效果,其一效果和 CORRELATED 值一样会触发回调方法;
其二在发布消息成功后使用 rabbitTemplate 调用 waitForConfirms 或 waitForConfirmsOrDie 方法等待 broker 节点返回发送结果,根据返回结果来判定下一步的逻辑,要注意的点是waitForConfirmsOrDie 方法如果返回 false 则会关闭 channel,则接下来无法发送消息到 broker。
1.配置文件设置发布确认方式
spring: application: name: produer-mq-7001 rabbitmq: addresses: 127.0.0.1 username: guest password: guest # 发布确认方式,默认NONE publisher-confirm-type: correlated
2.配置RabbitTemplate
由于发布确认需要设置回调,但是Spring默认是单例的,如果直接注入RabbitTemplate,那么在设置发布确认回调时,会被认为是重新设置回调方法;而一个RabbitTemplate只能有初始的一个发布确认回调。
public class RabbitTemplate extends RabbitAccessor implements ... { ... public void setConfirmCallback(ConfirmCallback confirmCallback) { Assert.state(this.confirmCallback == null || this.confirmCallback.equals(confirmCallback), "Only one ConfirmCallback is supported by each RabbitTemplate"); this.confirmCallback = confirmCallback; } ... } public abstract class Assert { public Assert() { } public static void state(boolean expression, String message) { if (!expression) { throw new IllegalStateException(message); } } ... }
解决方式:
-
使用多例,可以达到不同的消息发布使用不同的确认回调
@Bean @Scope("prototype") public RabbitTemplate getRabbitTemplate(ConnectionFactory connectionFactory){ RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory); return rabbitTemplate; }
-
使用单例,在初始时即配置确认回调(仅能有一个确认回调)
@Bean public RabbitTemplate getRabbitTemplate(ConnectionFactory connectionFactory){ RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory); rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() { @Override public void confirm(CorrelationData correlationData, boolean b, String s) { if (!b){ ReturnedMessage dataReturned = correlationData.getReturned(); String str = new String(dataReturned.getMessage().getBody()); System.out.println(str); log.error("消息发送失败,请重试"); return; } } }); return rabbitTemplate; } @Autowired private RabbitTemplate rabbitTemplate; //依赖注入 rabbitTemplate 之后再设置它的回调对象 @PostConstruct public void init(){ rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() { @Override public void confirm(CorrelationData correlationData, boolean b, String s) { if (!b){ ReturnedMessage dataReturned = correlationData.getReturned(); String str = new String(dataReturned.getMessage().getBody()); System.out.println(str); log.error("消息发送失败,请重试"); return; } } }); }
回退消息
在仅开启了生产者确认机制的情况下,交换机接收到消息后,会直接给消息生产者发送确认消息,如果发现该消息不可路由,那么消息会被直接丢弃,此时生产者是不知道消息被丢弃这个事件的。
此时通过设置 mandatory 参数可以在当消息传递过程中不可达目的地时将消息返回给生产者,需搭配使用 ReturnsCallback
@Bean public RabbitTemplate getRabbitTemplate(ConnectionFactory connectionFactory){ RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory); //true:交换机无法将消息进行路由时,会将该消息返回给生产者 //false:如果发现消息无法进行路由,则直接丢弃;默认false rabbitTemplate.setMandatory(true); //设置回退消息交给谁处理 rabbitTemplate.setReturnsCallback(new RabbitTemplate.ReturnsCallback() { @Override public void returnedMessage(ReturnedMessage returned) { System.out.println("--------无法路由,回退处理--------"); } }); //设置确认回调 rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() { @Override public void confirm(CorrelationData correlationData, boolean b, String s) { if (!b){ ReturnedMessage dataReturned = correlationData.getReturned(); String str = new String(dataReturned.getMessage().getBody()); System.out.println(str); log.error("消息发送失败,请重试"); return; } } }); return rabbitTemplate; }
消费端消息确认
消息端确认模式
-
**NONE:**不确认,即监听器监听到消息后直接确认
-
**MANUAL:**手动确认,需要消费端手动回复确认
-
**AUTO:**容器将根据监听器是正常返回还是抛出异常来发出 ack/nack,注意与NONE区分
Spring
默认requeue-rejected配置为true,所以在消费消息发生异常后该消息会重新入队。 并且若存在消费集群,会将某个消费端Nack的消息交给其他消费者。
消息确认实现方式
- 方式一:配置文件
spring: application: name: consumer-mq-7100 rabbitmq: addresses: 127.0.0.1 cache: channel: size: 25 # 指定消费端消息确认方式 listener: simple: acknowledge-mode: manual
- 方式二:@RabbitListener 指定
@RabbitListener(bindings = @QueueBinding( value = @Queue(value = "directQueue-One", durable = "false"), exchange = @Exchange(value = "MqSendService-One", type = "direct", durable = "false"), key = "One"), ackMode = "MANUAL") //指定消费端消息确认方式 public void tsAckDirectMsg(Message data, Channel channel) throws IOException { String str = new String(data.getBody()); System.out.println(str + "-----:" + seq); System.out.println(); seq.incrementAndGet(); System.out.println(data.getMessageProperties().getDeliveryTag()); System.out.println(channel.getChannelNumber()); channel.basicAck(data.getMessageProperties().getDeliveryTag(),false); }
channel.basicAck() 方法
参数:
-
long deliveryTag:
消息的索引。通常设为 data.getMessageProperties().getDeliveryTag()。
每个消息在一个channel中都有唯一的一个deliveryTag,每次发送一条,deliveryTag都会+1,从0开始计数;
确认消息传入的deliveryTag需保证和渠道内的一致,否则无法确认,该消息会被设置为 ready 状态。**注意:**当deliveryTag被固定一个数字m时,当m > deliveryTag就会换个渠道重新监听消费。
无法确认的消息(deliveryTag不匹配,通道已关闭,连接已关闭或 TCP 连接丢失)会重新入队,被设为 ready 状态,如果存在其他消费者,会将消息发送 给其他消费者,否则反复尝试仅存消费者。但没进行确认的消息会被设为 Unacked。
-
boolean multiple:
是否批量确认。
当设为true时,会批量确认deliveryTag小于传入deliveryTag参数的消息。
channel.basicNack() 方法
参数多了一个 boolean requeue 是否重新入队,前两个参数同上。
Message durability(消息持久化)
默认情况下 RabbitMQ 退出或由于某种原因崩溃时,它忽视队列和消息。
队列的持久化
在声明队列的时候设置持久化为 true。
需要注意的就是如果之前声明的队列不是持久化的,需要把原先队列先删除,或者重新创建一个持久化的队列,不然就会出现错误。
@Bean public Queue directQueue(){ //参数介绍 //1.队列名 2.是否持久化 3.是否独占 4.自动删除 5.其他参数 return new Queue("directQueue-One",true,false,false,null); }
有了消息回退的功能我们可以感知到消息的投递情况,但是对于这些无法路由到的消息我们可能只能做一个记录的功能,然后再手动处理;并且消息回退会增加生产者的复杂性;那么现在如何想要实现不增加生产者的复杂性,并保证消息不丢失呢?因为消息是不可达的,所以显然无法通过死信队列机制实现。所以通过这种备用交换机的机制可以实现。
实现原理
它是通过在声明交换机的时候,为该交换机设置一个备用的交换机;当主交换机接收一条消息不可达后,会将该消息转发到备用交换机,它在将这些消息发到自己绑定的队列,一般备用交换机的类型都设置为 Fanout(广播类型)。这样我们可以统一设置一个消费者监听该交换机下的队列对其进行统一处理。
实现代码
mandatory 参数与备份交换机可以一起使用的时候,如果两者同时开启,谁优先级高,经测试备份交换机优先级高
@Configuration public class RabbitDirectConfig { @Bean public Queue alternateQueue(){ //参数介绍 //1.队列名 2.是否持久化 3.是否独占 4.自动删除 5.其他参数 Queue queue = QueueBuilder.durable("alternateQueue") .autoDelete() .build(); return queue; } @Bean public FanoutExchange alternateExchange(){ return new FanoutExchange("Alternate_Exchange",true,false,null); } @Bean public DirectExchange directExchange(){ //参数介绍 //1.交换器名 2.是否持久化 3.自动删除 4.其他参数 Map<String,Object> args = new HashMap<>(3); args.put("alternate-exchange","Alternate_Exchange"); return new DirectExchange("MqSendService-One",false,false,args); } @Bean public Binding bingAlternateExchange(){ return BindingBuilder.bind(alternateQueue()) //绑定队列 .to(alternateExchange()); //队列绑定到哪个交换器 } @Bean public Binding bingExchange(){ return BindingBuilder.bind(directQueue()) //绑定队列 .to(directExchange()) //队列绑定到哪个交换器 .with("One"); //路由key,必须指定 } }
浙公网安备 33010602011771号