概述
消息队列的高级篇,无非就是解决可能存在的消息的可靠性传递问题。
那么消息出问题,可能是以下几点出现问题:
- 消费者
- 生产者
- 队列
消费者方的可靠性问题
在生产者成功发送了消息之后(也就是消费者收到了),消费者可能在处理消息过程中突然宕机,那么这个消息相当于直接消失了。
那么我们可以想到,在消费者方处理完消息之后,给消息队列发一个应答,说明自己已经处理好了这条消息,如果没有收到应答,那么消息队列就重新将这个消息入队,然后重新发送给某个消费者。
对于 Rabbitmq来说,消费者消费消息时是自动应答的(收到消息时就返回应答让rabbitmq能删除消息),这显然可靠性并不强,所以我们需要修改成手动应答。
消息手动应答:
先在配置文件开启手动应答:
spring.rabbitmq.addresses=x.xxx.xx.xx
spring.rabbitmq.username=admin
spring.rabbitmq.password=admin
#开启手动应答
spring.rabbitmq.listener.direct.acknowledge-mode=manual
#开启手动应答
spring.rabbitmq.listener.simple.acknowledge-mode=manual
这里配置的是简单模式的手动应答和直接路由的手动应答。
那么我们使用简单模式来配置消息的手动应答代码:
配置类:
@Configuration
public class SimpleConfig {
private static final String QUEUE_NAME="simple.queue";
@Bean
public Queue Simple_Queue(){
return QueueBuilder.durable(QUEUE_NAME).build();
}
}
生产者:
@RestController
@RequestMapping("/simple")
@Slf4j
public class Simple_Producer {
private static final String QUEUE_NAME="simple.queue";
@Autowired
RabbitTemplate rabbitTemplate;
@GetMapping("/sendMessage/{message}")
public String send(@PathVariable("message")String message){
log.info("准备发送数据:"+message);
rabbitTemplate.convertAndSend(QUEUE_NAME, message.getBytes());
return "消息已经发送成功";
}
}
消费者:
@Component
@Slf4j
public class SimpleConsumer {
@Autowired
RabbitTemplate template;
//开启手动回应
@RabbitListener(queues = "simple.queue")
public void receive1(Message message, Channel channel) throws IOException {
String s=new String(message.getBody());
if(s.equals("5"))
//是否批量应答
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
else
//第二个是是否是批量应答,第三个是是否重新入队
channel.basicNack(message.getMessageProperties().getDeliveryTag(),false,false);
log.info("消费者收到了"+s);
}
}
可以发现,我们主要是消费者的监听方法的参数多了一个 Channel channel,我们通过这个对象来进行手动的应答
里面的批量应答如果是true的话,将该消息之前的消息都一次性给应答了。
如果重新入队为true的话就会将该消息重新入队,重新分配给消费者
直接路由模式的手动应答也是差不多的原理,这里就不细说了
生产者方的可靠性问题
消息发送方存在的可靠性问题就是发送的消息可能没有到达消息队列,那么这条消息就相当于消失了。
消息的发布确认:
发布确认模式是指: 一旦消息被投递到所有匹配的队列之后,消息队列就会发送一个确认给生产者(包含消息的唯一 ID),这就使得生产者知道消息已经正确到达目的队列了,如果消息和队列是可持久化的,那么确认消息会在将消息写入磁盘之后发出,broker 回传给生产者的确认消息中 delivery-tag 域包含了确认消息的序列号
具体的操作方法就是在消息生产者方配置回调函数,当消息到达了消息队列就能调用该回调方法:
配置文件:
spring.rabbitmq.addresses=xx.xxx.xxx.xx
spring.rabbitmq.username=admin
spring.rabbitmq.password=admin
#开启消息消费失败返回机制
spring.rabbitmq.publisher-returns=true
#消息发送到交换机时执行回调函数
spring.rabbitmq.publisher-confirm-type=correlated
这里开启了两种回调函数,第一个是消息没有发送到mq的回调函数,第二个是消息发送到mq执行回调方法
配置回调方法:
@Component
@Slf4j
public class SimpleCallBack implements RabbitTemplate.ConfirmCallback,RabbitTemplate.ReturnsCallback {
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
log.info("消息队列已经收到了消息: ");
}
@Override
public void returnedMessage(ReturnedMessage returned) {
log.info("消息被拒绝了: "+new String(returned.getMessage().getBody()));
}
}
然后生产者:
@RestController
@RequestMapping("/simple")
@Slf4j
public class Simple_Producer {
private static final String QUEUE_NAME="simple.queue";
@Autowired
RabbitTemplate rabbitTemplate;
@GetMapping("/sendMessage/{message}")
public String send(@PathVariable("message")String message){
rabbitTemplate.setConfirmCallback(new SimpleCallBack());
rabbitTemplate.setReturnsCallback(new SimpleCallBack());
log.info("准备发送数据:"+message);
rabbitTemplate.convertAndSend(QUEUE_NAME, message.getBytes());
rabbitTemplate.convertAndSend("aa","qqqq");
return "消息已经发送成功";
}
}
将我们的回调方法配置到RabbitTemplate 对象中,然后使用这个对象发送消息,就会产生回调了。
队列方的可靠性问题
前面我们配置消息的手动应答的时候,满足一定条件则应答,否则使用 basicNack()
方法拒绝该消息 ,或者是如果该消息设置了过期时间,过期了也没被消费,那么该消息相当于直接消失了,这就是可靠性问题。
使用死信队列
使用死信队列可以解决我们上面的问题。
死信:由于特定的原因导致 queue 中的某些消息无法被消费,这样的消息如果没有后续的处理,就变成了死信
RabbitMQ会把如下的消息视为死信:
- 消息 TTL 过期
- 队列达到最大长度(队列满了,无法再添加数据到 mq 中)
- 消息被拒绝(basic.reject 或 basic.nack)并且 requeue=false.
死信的架构图如下:
配置类:
@Configuration
public class DeadConfig {
//发消息是要去正常的交换机
private static final String NORMAL_EXCHANGE ="normal.exchange";
//正常队列
private static final String NORMAL_QUEUE="normal.queue";
//死信交换机
private static final String DEAD_EXCHANGE="dead.exchange";
//死信队列
private static final String DEAD_QUEUE="dead.queue";
@Bean("Normal_Exchange")
public Exchange Normal_Exchange(){
return new DirectExchange(NORMAL_EXCHANGE);
}
@Bean("Dead_Exchange")
public Exchange Dead_Exchange(){
return new DirectExchange(DEAD_EXCHANGE);
}
@Bean("Dead_Queue")
public Queue Dead_Queue(){
return QueueBuilder.durable(DEAD_QUEUE).build();
}
//配置正常队列的死信交换机
@Bean("Normal_Queue")
public Queue Normal_Queue(){
return QueueBuilder.durable(NORMAL_QUEUE).deadLetterExchange(DEAD_EXCHANGE).deadLetterRoutingKey("dead").build();
}
//绑定正常队列和正常交换机
@Bean
public Binding normal_bind(@Qualifier("Normal_Exchange")Exchange exchange,@Qualifier("Normal_Queue")Queue queue){
return BindingBuilder.bind(queue).to(exchange).with("normal").noargs();
}
@Bean
public Binding dead_bind(@Qualifier("Dead_Exchange")Exchange exchange,@Qualifier("Dead_Queue")Queue queue){
return BindingBuilder.bind(queue).to(exchange).with("dead").noargs();
}
}
生产者类:
@RestController
@Slf4j
@RequestMapping("/dead")
public class Dead_Producer {
//发消息是要去正常的交换机
private static final String NORMAL_EXCHANGE ="normal.exchange";
@Autowired
RabbitTemplate rabbitTemplate;
@GetMapping("/sendMessage/{message}")
public String send(@PathVariable("message")String message){
rabbitTemplate.convertAndSend(NORMAL_EXCHANGE,"normal",message);
log.info("消息: "+message+" 已经发送到normal交换机");
return "消息已经发送";
}
}
消费者类:
@Component
@Slf4j
public class Dead_Consumer {
private static final String NORMAL_QUEUE="normal.queue";
private static final String DEAD_QUEUE="dead.queue";
@RabbitListener(queues = NORMAL_QUEUE)
public void normal_consumer(Message message, Channel channel) throws IOException {
String s =new String(message.getBody());
log.info("正常消费者收到了消息: "+s);
//如果消息是 5 则拒绝该消息,让他去死信队列
if(s.equals("5")){
log.info("正常消费者拒绝了消息: "+s);
channel.basicNack(message.getMessageProperties().getDeliveryTag(),false,false);
}else{
log.info("正常消费者接收了消息: "+s);
//其他情况就手动应答
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
}
}
@RabbitListener(queues = DEAD_QUEUE)
public void dead_consumer(Message message,Channel channel) throws IOException {
String s=new String(message.getBody());
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
log.info("死信消费者消费了消息: "+s);
}
}
运行之后发现死信队列实现成功