RabbitMQ解决消息丢失

目录

1.开启发布确认模式

1.1单个确认发布

1.2批量确认发布

1.3异步确认发布

1.4处理异步未确认的消息

1.5 三种发布方式对比

1.6发布确认高级

 2.消息持久化

2.1队列持久化

2.2消息持久化

 3.消费手动确认

单次消息确认

批量消息确认 

确认消息失败重新入队


        上次我们说过,对于解决消息中间件的问题,都是通过三阶段来保证消息不丢失问题。消息的发送阶段+消息的存储阶段+消息的消费阶段。这次我们来说说RabbitMQ怎样操作来保证消息不丢失。消息的发送阶段:ack机制。生产方将消息投递到broker中,需要等待broker的ack确认和nack。当返回ack,可知消息已经投递匹配的队列。否则失败。

1.开启发布确认模式

        生产者将信道设置成 confirm 模式,一旦信道进入 confirm 模式,所有在该信道上面发布的 消息都将会被指派一个唯一的 ID (从 1 开始),一旦消息被投递到所有匹配的队列之后,broker 就会发送一个确认给生产者(包含消息的唯一 ID),这就使得生产者知道消息已经正确到达目的队列了,如果消息和队列是可持久化的,那么确认消息会在将消息写入磁盘之后发出,broker 回传给生产者的确认消息中 delivery-tag 域包含了确认消息的序列号,此外broker 也可以设置basic.ack 的 multiple 域,表示到这个序列号之前的所有消息都已经得到了处理。confirm 模式最大的好处在于他是异步的,一旦发布一条消息,生产者应用程序就可以在等信道返回确认的同时继续发送下一条消息,当消息最终得到确认之后,生产者应用便可以通过回调方法来处理该确认消息,如果 RabbitMQ 因为自身内部错误导致消息丢失,就会发送一条 nack 消息,生产者应用程序同样可以在回调方法中处理该 nack 消息。

1.1单个确认发布

发布一个消息之后只有它 被确认发布,后续的消息才能继续发布,waitForConfirmsOrDie(long)这个方法只有在消息被确认的时候才返回,如果在指定时间范围内这个消息没有被确认那么它将抛出异常。这种确认方式有一个最大的缺点就是:发布速度特别的慢,因为如果没有确认发布的消息就会阻塞所有后续消息的发布,这种方式最多提供每秒不超过数百条发布消息的吞吐量。当然对于某些应 用程序来说这可能已经足够了。
public static void publishMessageIndividually() throws Exception {
 try (Channel channel = RabbitMqUtils.getChannel()) {
     String queueName = UUID.randomUUID().toString();
     channel.queueDeclare(queueName, false, false, false, null);
     //开启发布确认
     channel.confirmSelect();
     long begin = System.currentTimeMillis();
     for (int i = 0; i < MESSAGE_COUNT; i++) {
     String message = i + "";
     channel.basicPublish("", queueName, null, message.getBytes());
     //服务端返回 false 或超时时间内未返回,生产者可以消息重发
     boolean flag = channel.waitForConfirms();
     if(flag){
     System.out.println("消息发送成功");
     }
   }
     long end = System.currentTimeMillis();
     System.out.println("发布" + MESSAGE_COUNT + "个单独确认消息,耗时" + (end - begin) + 
    "ms");
  }
}

1.2批量确认发布

上面那种方式非常慢,与单个等待确认消息相比,先发布一批消息然后一起确认可以极大地
提高吞吐量,当然这种方式的缺点就是:当发生故障导致发布出现问题时,不知道是哪个消息出现问题了,我们必须将整个批处理保存在内存中,以记录重要的信息而后重新发布消息。当然这种方案仍然是同步的,也一样阻塞消息的发布。
public static void publishMessageBatch() throws Exception {
 try (Channel channel = RabbitMqUtils.getChannel()) {
     String queueName = UUID.randomUUID().toString();
     channel.queueDeclare(queueName, false, false, false, null);
     //开启发布确认
     channel.confirmSelect();
     //批量确认消息大小
     int batchSize = 100;
     //未确认消息个数
     int outstandingMessageCount = 0;
     long begin = System.currentTimeMillis();
     for (int i = 0; i < MESSAGE_COUNT; i++) {
     String message = i + "";
     channel.basicPublish("", queueName, null, message.getBytes());
     outstandingMessageCount++;
     if (outstandingMessageCount == batchSize) {
     channel.waitForConfirms();
     outstandingMessageCount = 0;
     }
   }
     //为了确保还有剩余没有确认消息 再次确认
     if (outstandingMessageCount > 0) {
     channel.waitForConfirms();
    }
     long end = System.currentTimeMillis();
     System.out.println("发布" + MESSAGE_COUNT + "个批量确认消息,耗时" + (end - begin) + 
"ms");
   }
}

1.3异步确认发布

异步确认虽然编程逻辑比上两个要复杂,但是性价比最高,无论是可靠性还是效率都没得说,他是利用回调函数来达到消息可靠性传递的,这个中间件也是通过函数回调来保证是否投递成功,下面就让我们来详细讲解异步确认是怎么实现的。
异步确认发布原理图
RabbitMQ解决消息丢失_第1张图片

 代码

public static void publishMessageAsync() throws Exception {
try (Channel channel = RabbitMqUtils.getChannel()) {
  String queueName = UUID.randomUUID().toString();
  channel.queueDeclare(queueName, false, false, false, null);
  //开启发布确认
  channel.confirmSelect();
  /**
  * 线程安全有序的一个哈希表,适用于高并发的情况
  * 1.轻松的将序号与消息进行关联
  * 2.轻松批量删除条目 只要给到序列号
  * 3.支持并发访问
  */
  ConcurrentSkipListMap outstandingConfirms = new 
  ConcurrentSkipListMap<>();
  /**
  * 确认收到消息的一个回调
  * 1.消息序列号
  * 2.true 可以确认小于等于当前序列号的消息
  * false 确认当前序列号消息
  */
  ConfirmCallback ackCallback = (sequenceNumber, multiple) -> {
  if (multiple) {
  //返回的是小于等于当前序列号的未确认消息 是一个 map
  ConcurrentNavigableMap confirmed = 
  outstandingConfirms.headMap(sequenceNumber, true);
  //清除该部分未确认消息
  confirmed.clear();
  }else{
  //只清除当前序列号的消息
  outstandingConfirms.remove(sequenceNumber);
  }
 };
  ConfirmCallback nackCallback = (sequenceNumber, multiple) -> {
  String message = outstandingConfirms.get(sequenceNumber);
  System.out.println("发布的消息"+message+"未被确认,序列号"+sequenceNumber);
  };
  /**
  * 添加一个异步确认的监听器
  * 1.确认收到消息的回调
  * 2.未收到消息的回调
  */
  channel.addConfirmListener(ackCallback, null);
  long begin = System.currentTimeMillis();
  for (int i = 0; i < MESSAGE_COUNT; i++) {
  String message = "消息" + i;
  /**
  * channel.getNextPublishSeqNo()获取下一个消息的序列号
  * 通过序列号与消息体进行一个关联
  * 全部都是未确认的消息体
  */
  outstandingConfirms.put(channel.getNextPublishSeqNo(), message);
  channel.basicPublish("", queueName, null, message.getBytes());
  }
  long end = System.currentTimeMillis();
  System.out.println("发布" + MESSAGE_COUNT + "个异步确认消息,耗时" + (end - begin) + 
 "ms");
  }
}

1.4处理异步未确认的消息

最好的解决的解决方案就是把未确认的消息放到一个基于内存的能被发布线程访问的队列,
比如说用 ConcurrentLinkedQueue 这个队列在 confirm callbacks 与发布线程之间进行消息的传递。

1.5 三种发布方式对比

单独发布消息:同步等待确认,简单,但吞吐量非常有限。
批量发布消息:批量同步等待确认,简单,合理的吞吐量,一旦出现问题但很难推断出是那条消息出现了问题。
异步处理: 最佳性能和资源使用,在出现错误的情况下可以很好地控制,但是实现起来稍微难些。

1.6发布确认高级

        在生产环境中由于一些不明原因,导致 rabbitmq 重启,在 RabbitMQ 重启期间生产者消息投递失败,导致消息丢失,需要手动处理和恢复。于是,我们开始思考,如何才能进行 RabbitMQ 的消息可靠投递呢?特别是在这样比较极端的情况,RabbitMQ 集群不可用的时候,无法投递的消息该如何处理呢。我们需要知道消息是否成功发送到broker。这和我们上面说到的发布确认不一样,上面是消费已经成功发送到broker后的回调。现在是需要知道消息发送到broker的回调。
步骤:
配置文件
在配置文件当中需要添加
spring.rabbitmq.publisher-confirm-type=correlated
  NONE
禁用发布确认模式,是默认值
CORRELATED
发布消息成功到交换器后会触发回调方法
SIMPLE

经测试有两种效果,其一效果和 CORRELATED 值一样会触发回调方法,其二在发布消息成功后使用 rabbitTemplate 调用 waitForConfirms waitForConfirmsOrDie 方法等待 broker 节点返回发送结果,根据返回结果来判定下一步的逻辑,要注意的点waitForConfirmsOrDie 方法如果返回 false 则会关闭 channel ,则接下来无法发送消息到 broker。

添加配置类

@Configuration
public class ConfirmConfig {
 public static final String CONFIRM_EXCHANGE_NAME = "confirm.exchange";
 public static final String CONFIRM_QUEUE_NAME = "confirm.queue";
 //声明业务 Exchange
 @Bean("confirmExchange")
 public DirectExchange confirmExchange(){
 return new DirectExchange(CONFIRM_EXCHANGE_NAME);
 }
 // 声明确认队列
 @Bean("confirmQueue")
 public Queue confirmQueue(){
 return QueueBuilder.durable(CONFIRM_QUEUE_NAME).build();
 }
 // 声明确认队列绑定关系
 @Bean
 public Binding queueBinding(@Qualifier("confirmQueue") Queue queue,
 @Qualifier("confirmExchange") DirectExchange exchange){
 return BindingBuilder.bind(queue).to(exchange).with("key1");
 }
}

消息生产者

@RestController
@RequestMapping("/confirm")
@Slf4j
public class Producer {
 public static final String CONFIRM_EXCHANGE_NAME = "confirm.exchange";
 @Autowired
 private RabbitTemplate rabbitTemplate;
 @Autowired
 private MyCallBack myCallBack;
 //依赖注入 rabbitTemplate 之后再设置它的回调对象
 @PostConstruct
 public void init(){
 rabbitTemplate.setConfirmCallback(myCallBack);
 }
 @GetMapping("sendMessage/{message}")
 public void sendMessage(@PathVariable String message){
 //指定消息 id 为 1
 CorrelationData correlationData1=new CorrelationData("1");
 String routingKey="key1";
 
rabbitTemplate.convertAndSend(CONFIRM_EXCHANGE_NAME,routingKey,message+routingKey,correl
ationData1);
 CorrelationData correlationData2=new CorrelationData("2");
 routingKey="key2";
 
rabbitTemplate.convertAndSend(CONFIRM_EXCHANGE_NAME,routingKey,message+routingKey,correl
ationData2);
 log.info("发送消息内容:{}",message);
 }
}

回调接口

@Component
@Slf4j
public class MyCallBack implements RabbitTemplate.ConfirmCallback {
 /**
 * 交换机不管是否收到消息的一个回调方法
 * CorrelationData
 * 消息相关数据
 * ack
 * 交换机是否收到消息
 */
 @Override
 public void confirm(CorrelationData correlationData, boolean ack, String cause) {
 String id=correlationData!=null?correlationData.getId():"";
 if(ack){
 log.info("交换机已经收到 id 为:{}的消息",id);
 }else{
 log.info("交换机还未收到 id 为:{}消息,由于原因:{}",id,cause);
 }
 }
}

 消息消费者

@Component
@Slf4j
public class ConfirmConsumer {
 public static final String CONFIRM_QUEUE_NAME = "confirm.queue";
 @RabbitListener(queues =CONFIRM_QUEUE_NAME)
 public void receiveMsg(Message message){
 String msg=new String(message.getBody());
 log.info("接受到队列 confirm.queue 消息:{}",msg);
 }
}

 回退消息

在仅开启了生产者确认机制的情况下,交换机接收到消息后,会直接给消息生产者发送确认消息 果发现该消息不可路由,那么消息会被直接丢弃,此时生产者是不知道消息被丢弃这个事件的 。那么如何让无法被路由的消息帮我想办法处理一下?最起码通知我一声,我好自己处理啊。通过设置 mandatory 参数可以在当消息传递过程中不可达目的地时将消息返回给生产者。

生产者

@Slf4j
@Component
public class MessageProducer implements RabbitTemplate.ConfirmCallback , 
RabbitTemplate.ReturnCallback {
 @Autowired
 private RabbitTemplate rabbitTemplate;
 //rabbitTemplate 注入之后就设置该值
 @PostConstruct
 private void init() {
 rabbitTemplate.setConfirmCallback(this);
 /**
 * true:
 * 交换机无法将消息进行路由时,会将该消息返回给生产者
 * false:
 * 如果发现消息无法进行路由,则直接丢弃
 */
 rabbitTemplate.setMandatory(true);
 //设置回退消息交给谁处理
 rabbitTemplate.setReturnCallback(this);
 }
 @GetMapping("sendMessage")
public void sendMessage(String message){
 //让消息绑定一个 id 值
 CorrelationData correlationData1 = new CorrelationData(UUID.randomUUID().toString());
 
rabbitTemplate.convertAndSend("confirm.exchange","key1",message+"key1",correlationData1)
;
 log.info("发送消息 id 为:{}内容为{}",correlationData1.getId(),message+"key1");
 CorrelationData correlationData2 = new CorrelationData(UUID.randomUUID().toString());
 
rabbitTemplate.convertAndSend("confirm.exchange","key2",message+"key2",correlationData2)
;
 log.info("发送消息 id 为:{}内容为{}",correlationData2.getId(),message+"key2");
}
 @Override
 public void confirm(CorrelationData correlationData, boolean ack, String cause) {
 String id = correlationData != null ? correlationData.getId() : "";
 if (ack) {
 log.info("交换机收到消息确认成功, id:{}", id);
 } else {
 log.error("消息 id:{}未成功投递到交换机,原因是:{}", id, cause);
 }
 }
 @Override
 public void returnedMessage(Message message, int replyCode, String replyText, String 
exchange, String routingKey) {
 log.info("消息:{}被服务器退回,退回原因:{}, 交换机是:{}, 路由 key:{}",
 new String(message.getBody()),replyText, exchange, routingKey);
 }
}

 回调接口

@Component
@Slf4j
public class MyCallBack implements 
RabbitTemplate.ConfirmCallback,RabbitTemplate.ReturnCallback {
 /**
 * 交换机不管是否收到消息的一个回调方法
 * CorrelationData
 * 消息相关数据
 * ack
 * 交换机是否收到消息
 */
 @Override
 public void confirm(CorrelationData correlationData, boolean ack, String cause) {
 String id=correlationData!=null?correlationData.getId():"";
 if(ack){
 log.info("交换机已经收到 id 为:{}的消息",id);
 }else{
 log.info("交换机还未收到 id 为:{}消息,由于原因:{}",id,cause);
 }
 }
 //当消息无法路由的时候的回调方法
 @Override
 public void returnedMessage(Message message, int replyCode, String replyText, String 
exchange, String routingKey) {
 log.error(" 消 息 {}, 被交换机 {} 退回,退回原因 :{}, 路 由 key:{}",new 
String(message.getBody()),exchange,replyText,routingKey);
 }
}

 2.消息持久化

RabbitMQ确保消息不会丢失需要做两件事:我们需要将队列和消息都标记为持久化

2.1队列持久化

队列实现持久化 需要在声明队列的时候把 durable 参数设置为持久化

2.2消息持久化

让消息实现持久化需要在消息生产者修改代, MessageProperties.PERSISTENT_TEXT_PLAIN
加这个属性。

 3.消费手动确认

与生产消息类似,在消费消息时需要关闭自动确认,手动确认消息。

单次消息确认

// 假设已存在channel实例

boolean autoAck = false;
channel.basicConsume(queueName, autoAck, "a-consumer-tag",
     new DefaultConsumer(channel) {
         @Override
         public void handleDelivery(String consumerTag,
                                    Envelope envelope,
                                    AMQP.BasicProperties properties,
                                    byte[] body)
             throws IOException
         {
             long deliveryTag = envelope.getDeliveryTag();
             // 确认一条消息成功传递,消息将会被RabbitMQ丢弃
             channel.basicAck(deliveryTag, false);
         }
     });

批量消息确认 

消费一条消息确认一次,效率太慢。可以进行批次确认。通过将 确认方法的的multiple参数设置为true。当multiple参数被设为true。RabbitMQ将会确认所有传递标签小于给定数值的消息。比如通道Ch上有未确认消息,它们的传递标签是5,6,7,8,如果有确认带的传递标签是8,且multiple参数被设为true,则5-8消息都会被确认;如果multiple参数被设为false,则5,6,7消息仍未被确认。示例如下:


boolean autoAck = false;
channel.basicConsume(queueName, autoAck, "a-consumer-tag",
     new DefaultConsumer(channel) {
         @Override
         public void handleDelivery(String consumerTag,
                                    Envelope envelope,
                                    AMQP.BasicProperties properties,
                                    byte[] body)
             throws IOException
         {
             long deliveryTag = envelope.getDeliveryTag();
             //确认所有传递标签小于deliveryTag的消息已成功传递,并丢弃它们
             channel.basicAck(deliveryTag, true);
         }
     });

确认消息失败重新入队

有时消费者处理能力较弱,但其他消费者有能力处理。这时候可以让消息重新入队让其它消费者进行处理。basic.reject和basic.nack两个方法通常被用于确认消息传递失败,MQ服务器可以丢弃这些消息或重新入队。可通过requeue参数来控制,当设为true时MQ服务器会将指定传递标签的消息重新入队,false会直接丢弃。


boolean autoAck = false;
channel.basicConsume(queueName, autoAck, "a-consumer-tag",
     new DefaultConsumer(channel) {
         @Override
         public void handleDelivery(String consumerTag,
                                    Envelope envelope,
                                    AMQP.BasicProperties properties,
                                    byte[] body)
             throws IOException
         {
             long deliveryTag = envelope.getDeliveryTag();
             // 确认传递标签为deliveryTag的消息传递失败,false:并丢弃它
             channel.basicReject(deliveryTag, false);
             // 确认传递标签为deliveryTag的消息传递失败,true:重新入队
             channel.basicReject(deliveryTag, true);
         }
     });

如果可能RabbitMQ会将重新入队的消息还放在它原来的位置,否则就放到尽可能离队首近的位置。假设某一瞬间出现,所有消费者的预取队列(prefetch)都已经满了(无法再接收消息),则会出现一个重新入队/重新传递的循环,造成网络带宽和内存资源的消耗。消费者需要追踪重新传递的数量,丢弃确认失败的消息,或经过一定时延后再重新入队。使用 basic.nack可以同时入队多条消息,它比basic.reject方法多了一个multiple参数。

    boolean autoAck = false;
    channel.basicConsume(queueName, autoAck, "a-consumer-tag",
     new DefaultConsumer(channel) {
         @Override
         public void handleDelivery(String consumerTag,
                                    Envelope envelope,
                                    AMQP.BasicProperties properties,
                                    byte[] body)
             throws IOException
         {
             long deliveryTag = envelope.getDeliveryTag();
             // 确认所有传递标签小于deliveryTag的消息传递失败,并重新入队它们
             channel.basicNack(deliveryTag, true, true);
             //单个deliveryTag的消息传递失败,并重新入队它
             channel.basicNack(deliveryTag, true, false);
         }
     });


 


 

你可能感兴趣的:(消息队列,rabbitmq,分布式)