Springboot2整合Rabbitmq消息确认机制、幂等性、重试机制

4 篇文章 0 订阅
2 篇文章 0 订阅

1.消息确认机制

友情提示该文章是跟着上一篇文章继续的,连接:https://blog.csdn.net/qq_41085151/article/details/107102962

问题1:如果在发送消息的时候,消费者出现了异常,那么你监听的消息就会一直循环消费。比如

一.提供者:

@Component
public class HelloProvider{

    @Autowired
    private RabbitTemplate rabbitTemplate1;

    public void send2(User user) {
        JSONObject jsonObject=new JSONObject();
        String userjson = jsonObject.toJSONString(user);
        Message message = MessageBuilder.withBody(userjson.getBytes())
                .setContentType(MessageProperties.CONTENT_TYPE_JSON).setContentEncoding("utf-8").build();
        rabbitTemplate1.convertAndSend("directExchange","direct", message);
    }
    
}

二.消费者:

@Component
public class HelloConsumer {

    @RabbitListener(queues = "direct")
    @RabbitHandler
    public void process(Message message, Channel channel) throws Exception {
        String msg = new String(message.getBody());
        JSONObject jsonObject = JSONObject.parseObject(msg);
        String name = jsonObject.getString("name");
        System.out.println(jsonObject);
        int a = 1 / 0;
    }
}
 
  需要加入pom
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.28</version>
        </dependency>

在控制层方法中调用提供者的send2()方法,控制台就会一直报错,因为rabbitmq中消息确认机制是发送消者(Producer)发现消息到交换机中,消费者是否确定消费此条消息,如果没有开启ack消息确认,rabbitmq会认为这条消息没有被消费,会将消息再次放入到队列中,再次让你消费,形成死循环。

三.在配置文件中添加设置消费端手动ack确认

server.port=8889

spring.rabbitmq.host=192.168.221.150
spring.rabbitmq.port=5672
spring.rabbitmq.username=zl
spring.rabbitmq.password=123
#开启消息确认机制
spring.rabbitmq.publisher-confirms=true
#支持消息发送失败返回队列
spring.rabbitmq.publisher-returns=true

#设置为 true 后 消费者在消息没有被路由到合适队列情况下会被return监听,而不会自动删除
spring.rabbitmq.template.mandatory=true

spring.rabbitmq.connection-timeout=15000
#用户虚拟机权限名称
spring.rabbitmq.virtual-host=/

#设置消费端手动 ack   none不确认  auto自动确认  manual手动确认
spring.rabbitmq.listener.simple.acknowledge-mode=manual

四.消费端中配置手动确定

 @RabbitListener(queues = "direct")
    @RabbitHandler
    public void process(Message message, Channel channel) throws Exception {
        String  msg = new String(message.getBody());
        JSONObject jsonObject = JSONObject.parseObject(msg);
        String name = jsonObject.getString("name");
        System.out.println(jsonObject);
        try {
            int a=1/0;
     /**
     * 确认一条消息:<br>
     * channel.basicAck(deliveryTag, false); <br>
     * deliveryTag:该消息的index <br>
     * multiple:是否批量.true:将一次性ack所有小于deliveryTag的消息 <br>
     */
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        } catch (Exception e) {
             //出现异常
         /**
         * 拒绝确认消息:<br>
         * channel.basicNack(long deliveryTag, boolean multiple, boolean requeue) ; <br>
         * deliveryTag:该消息的index<br>
         * multiple:是否批量.true:将一次性拒绝所有小于deliveryTag的消息。<br>
         * requeue:被拒绝的是否重新入队列 <br>
         */
            channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
            e.printStackTrace();
        }
    }

分析:一般如果是代码中出现的异常比如类型转换的异常,是必须要修改代码更正的。而一般在真实的业务场景,是我调用某个

系统的接口返回的是空,那么就可以做处理。现在又会发现一个问题,一般业务场景不会轻易的把消息丢失也就是代码中的

channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);

最后一个参数requeue一般都会为true,此次没调用到数据,把这个消息返回到队列中再消费,如果代码中出现了int a=1/0,那么还是会造成死循环。

2.消息重试机制

当你开启了手动ack的时候再消费端如果在消费的时候出现异常也会导致循环消费,所以要启动消息重试机制,默认是3次重试去消费一条消息,如果没有消费完成,则丢弃(删除)该消息或者放入死信队列中或者进行人工补偿。  

 在自动签收模式下,如果消费者抛出异常,我们还可以通过 spring.rabbitmq.listener.simple.retry.enabled=true 开启消息重新投递

一.配置文件

server.port=8889

spring.rabbitmq.host=192.168.221.150
spring.rabbitmq.port=5672
spring.rabbitmq.username=zl
spring.rabbitmq.password=123
#开启消息确认机制
spring.rabbitmq.publisher-confirms=true
#支持消息发送失败返回队列
spring.rabbitmq.publisher-returns=true

#设置为 true 后 消费者在消息没有被路由到合适队列情况下会被return监听,而不会自动删除
spring.rabbitmq.template.mandatory=true

spring.rabbitmq.connection-timeout=15000
#用户虚拟机权限名称
spring.rabbitmq.virtual-host=/

#设置消费端手动 ack   none不确认  auto自动确认  manual手动确认
spring.rabbitmq.listener.simple.acknowledge-mode=auto
#消费者最小数量
spring.rabbitmq.listener.simple.concurrency=1
#消费之最大数量
spring.rabbitmq.listener.simple.max-concurrency=1

#开启消费者重试机制(为false时关闭消费者重试(开启消息重新投递),这时消费端代码异常会一直重复收到消息)
spring.rabbitmq.listener.simple.retry.enabled=true
#重试次数5(重新投递次数)
spring.rabbitmq.listener.simple.retry.max-attempts=5
#重试时间间隔
spring.rabbitmq.listener.simple.retry.initial-interval=5000

#重试次数超过上面的设置之后是否丢弃(消费者listener抛出异常,是否重回队列,默认true:重回队列, false为不重回队列(结合死信交换机)
spring.rabbitmq.listener.simple.default-requeue-rejected=true

#在单个请求中处理的消息个数,他应该大于等于事务数量(unack的最大数量)
spring.rabbitmq.listener.simple.prefetch=2

触发重试机制需要消费者抛出异常,而不能try/catch捕捉异常,不然会死循环。

二.消费者代码


public class HelloConsumer {
   
    @RabbitListener(queues = "direct")
    @RabbitHandler
    public void process(Message message, Channel channel) throws Exception {
        String  msg = new String(message.getBody());
        JSONObject jsonObject = JSONObject.parseObject(msg);
        String name = jsonObject.getString("name");
        System.out.println(jsonObject);

            if ("李四".equals(name)) {
                channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
            }else
            {
                System.out.println("消息重试!");
                throw  new RuntimeException("查询到的结果为空!");
            }
        } 
    }

控制台

{"pass":"123456","name":"张三"}
消息重试!
{"pass":"123456","name":"张三"}
消息重试!
{"pass":"123456","name":"张三"}
消息重试!
{"pass":"123456","name":"张三"}
消息重试!
{"pass":"123456","name":"张三"}
消息重试!

重试5次后就删除该消息了。具体看你如果没有消费完成,则丢弃(删除)该消息或者放入死信队列中或者进行人工补偿。

 

3.消息幂等性

Rabbitmq中消息重复消费的问题,在消费者有些消息处理了,但是没来的及提交offset,消费者挂了的情况,再重启可能导致重复消费。怎么判断该消息是否已经消费了呢?

解决办法:

使用全局MessageID判断消费方使用同一个,解决幂等性。

一.修改提供方的send2方法

 public void send2(User user) {
        JSONObject jsonObject=new JSONObject();
        String userjson = jsonObject.toJSONString(user);
        UUID uuid = UUID.randomUUID();
        // 设置消息唯一id 保证每次重试消息id唯一
        Message message = MessageBuilder.withBody(userjson.getBytes())
                .setContentType(MessageProperties.CONTENT_TYPE_JSON).setContentEncoding("utf-8")
                .setMessageId(uuid + "").build(); //消息id设置在请求头里面 用UUID做全局ID
        rabbitTemplate1.convertAndSend("directExchange","direct", message);
    }

二.修改消费端方法

@Component

public class HelloConsumer {

    @RabbitListener(queues = "direct")
    @RabbitHandler
    public void process(Message message, Channel channel) throws Exception {
        String messageId = message.getMessageProperties().getMessageId();  //id获取之
        String ok= redis.get("msg"+messageId);//从redis中获取是否消费过的id
         if (ok=="sucess"){  //消费过了的消息直接丢弃然后返回。
         channel.basicNack(message.getMessageProperties().getDeliveryTag(),false, false);
            return;
        }
        String  msg = new String(message.getBody());
        JSONObject jsonObject = JSONObject.parseObject(msg);
        String name = jsonObject.getString("name");
        System.out.println(jsonObject);

            if ("李四".equals(name)) {
                System.out.println("消费成功");
                redis.set("msg"+messageId,"success");//设置redis
                channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
            }else
            {
                System.out.println("消息重试!");
                throw  new RuntimeException("查询到的结果为空!");
            }
        }
    }

其实解决方式:用一个消息消费表来记录每一条消息,给每个一个消息设置一个id(uuid),消费了就保存到表中去。消息过来的时候先查询是否已经消费。消息的幂等性。是差不多的。看你怎么用吧。

其实使用redis可以看我的springboot整合redis篇链接:https://blog.csdn.net/qq_41085151/article/details/106904937

文章中都是我个人理解,也是亲测过,欢迎各位小伙伴提出问题喔,觉得好的可不可以点个赞呢!

  • 7
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值