一、消息重试

1. 在什么情况下消费者需要实现重试策略?

A.消费者获取消息后,调用第三方接口,但是调用第三方接口失败呢?是否需要重试?

该情况下需要实现重试策略,网络延迟只是暂时调用不通,重试多次有可能会调用通。

B.消费者获取消息后,因为代码问题抛出数据异常,是否需要重试?

该情况下是不需要实现重试策略,就算重试多次,最终还是失败的。可以将日志存放起来,后期通过定时任务或者人工补偿形式。

2. application.yml开启自动重试

spring:
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: guest
    password: guest
    virtual-host: /MuPing
    listener:
      simple:
        retry:
          #开启重试
          enabled: true
          #最大重试次数
          max-attempts: 3
          #重试间隔(ms)
          initial-interval: 5000
        #设置ack模式(一般我们在项目中都会设置为手动模式,其余两种容易出bug)
        #manual:手动ack,不发送ack服务端就不会删除消息
        #NONE:无ack,消息默认消费成功,服务端不会缓存数据
        #AUTO:自动模式,根据代码是否报错向服务端发送ack tag
        acknowledge-mode: manual
server:
  port: 8083

二、消息幂等性

1、什么是消息的幂等性

幂等:在编程中一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。这是百度百科给出的幂等的概念。

消息的幂等性:就是即使多次收到了消息,也不会重复消费。所以保证消息的幂等性就是保证消息不会重复消费,这在开发中是很重要的。比如客户点击付款,如果点击了多次,那也只能扣一次费。

2、MQ的幂等性如何保证

我们所熟悉的RabbitMQ、RocketMQ、kafka,都有可能出现消息的重复发送,这个是MQ无法保障的。但是我们又不能在系统中去重复消费消息,那么就需要我们开发人员去保证消息的幂等性。

3. MQ出现非幂等性的情况

· 生成者重复发送消息给MQ:

  生产者把消息发送给MQ之后,MQ收到消息在给生产者返回ack的时候,网络中断了。这时MQ明明已经接收到了消息,但是生产者没接收到确定消息,就会认为MQ没有接收到消息。因此,在网络重新连接后,生产者会把已经发送的消息再次发送到MQ,如果MQ没有去重措施的话,那么就接收到了重复的消息。

· MQ重复发送消息给消费者
  消费者从MQ中拉取消息进行消费,当消费者已经消费了消息但还没向MQ返回ack的时候,消费者宕机或者网络断开了。所以消费者成功消费了消息的情况,MQ并不知道。当消费者重启或网络重连后,消费者再次去请求MQ拉取消息的时候,MQ会把已经消费的消息再次发送给消费者,如果消费者没有去重就直接消费,那么就会造成重复消费的情况。便会造成数据的不一致。

4. 保证消息幂等性的办法

· 生产者不重复发送消息到MQ
  mq内部可以为每条消息生成一个全局唯一、与业务无关的消息id,当mq接收到消息时,会先根据该id判断消息是否重复发送,mq再决定是否接收该消息。

· 消费者不重复消费
  消费者怎么保证不重复消费的关键在于消费者端做控制,因为MQ不能保证不重复发送消息,所以应该在消费者端控制:即使MQ重复发送了消息,消费者拿到了消息之后,要判断是否已经消费过,如果已经消费,直接丢弃。所以根据实际业务情况,有下面几种方式:

①、如果从MQ拿到数据是要存到数据库,那么可以根据数据创建唯一约束,这样的话,同样的数据从MQ发送过来之后,当插入数据库的时候,会报违反唯一约束,不会插入成功的。(或者可以先查一次,是否在数据库中已经保存了,如果能查到,那就直接丢弃就好了)。

②、让生产者发送消息时,每条消息加一个全局的唯一id,然后消费时,将该id保存到redis里面。消费时先去redis里面查一下有么有,没有再消费。(其实原理跟第一点差不多)。

③、如果拿到的数据是直接放到redis的set中的话,那就不用考虑了,因为set集合就是自动有去重的。

三、demo

 1 package com.sdkj;
 2 
 3 import com.rabbitmq.client.Channel;
 4 import lombok.extern.slf4j.Slf4j;
 5 import org.apache.commons.lang3.StringUtils;
 6 import org.springframework.amqp.core.Message;
 7 import org.springframework.amqp.rabbit.annotation.RabbitHandler;
 8 import org.springframework.amqp.rabbit.annotation.RabbitListener;
 9 import org.springframework.stereotype.Component;
10 
11 /**
12  * @Author wangshuo
13  * @Date 2022/5/7, 19:19
14  * Please add a comment
15  */
16 @Component
17 @Slf4j
18 public class SmsConsumer {
19 
20     @RabbitListener(queues = "fanout_sms_queue")
21     @RabbitHandler
22     public void process(String msg, Message message, Channel channel){
23 
24         try {
25 
26             if (StringUtils.isEmpty(msg)){//空消息根据需求写
27                 //无需重复消费
28                 channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
29                 return;//return CommonReturnType.create("空消息");
30             }
31             //根据需求 数据库查询msg是否重复消费
32             OrderEntity dbOrderEntity = orderMapper.getOrder(msg);
33             if (dbOrderEntity != null) {
34                 log.info("重复消费!");
35                 //消息消费过了 无需继续重试 发送ack
36                 channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
37                 return;//return CommonReturnType.create("成功");
38             }
39             //向数据库存储数据
40             int result = orderManager.addOrder(orderEntity);
41             log.info(">>插入数据库中数据成功<<");
42             if (result >= 0) {
43                 //消费成功 发送ack
44                 channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
45             }
46         } catch (Exception e) {
47             e.printStackTrace();
48             //数据库交互报错 记录消息,方便排错与手动插入
49             orderManager.addErrmsg(orderEntity);
50         }
51     }
52 }