RabbitMQ学习(四)-RabbitMQ的其它操作

本文详细介绍了RabbitMQ中的消息可靠性保障机制,包括事务和Confirm确认机制,以及如何处理消息可能未到达队列的问题。此外,还探讨了如何在SpringBoot中配置Confirm和return机制,以及防止消息重复消费的策略,通过Redis实现幂等性操作。最后展示了在SpringBoot中实现这些功能的实际代码示例。
摘要由CSDN通过智能技术生成

title: RabbitMQ学习(四)-RabbitMQ的其它操作
date: 2021-1-12
tags:

  • 微服务
  • RabbitMQ学习(四)-RabbitMQ的其它操作
  • RabbitMQ
  • spring
  • springboot
    categories:
  • 微服务
  • RabbitMQ
  • RabbitMQ学习(四)-RabbitMQ的其它操作

一、消息可靠性

RabbitMQ的事务:事务可以保证消息的100%传递,可以通过事务回滚去记录日志,后面定时再发送当前消息。但是事务的操作效率太低,加入事务后效率比不加事务慢至少100倍。

生成者在发送消息过程中也可能出现错误或者网络延迟灯故障,导致消息未成功发送到交换机或者队列,或重复发送消息,为了解决这个问题,rabbitmq中有多个解决办法。

RabbitMQ除了事务,还提供了Confirm的确认机制,这种机制的效率比事务高很多。

二、Confirm机制

可以确保生产者将消息发动到exchange中,但并不能保证消息发送到queue中。

2.1 普通Confirm方式

public class Publisher {
    @Test
    public void publish() throws Exception {
        //1.获取connection
        //2. 创建channel
        //3. 发布消息到exchange,同时指定路由规则
		..........
        
        String msg = "Hello-Word"+(new Date());
       
        
        //开启confirm
        channel.confirmSelect();

        channel.basicPublish("","HelloWord",null,msg.getBytes());
		
        //对是否发送成功进行处理
        if(channel.waitForConfirms()){
            System.out.println("send message success!");
        }else {
            System.out.println("send message failure!");
        }

   
        //4.释放资源
      	......
    }

}

2.2 批量Confirm方式

channel.waitForConfirmsOrDie():当你发送的全部消息,有一个失败时,则直接全部失败,并抛出异常。

public class Publisher {
    @Test
    public void publish() throws Exception {
        //1.获取connection
        //2. 创建channel
        //3. 发布消息到exchange,同时指定路由规则
        ............

            
        //开启confirm
        channel.confirmSelect();

        for (int i = 0; i < 10; i++) {
            String msg = "Hello-Word"+i;
            channel.basicPublish("","HelloWord",null,msg.getBytes());
        }
        //当你发送的全部消息,有一个失败时,则直接全部失败,并抛出异常。
        channel.waitForConfirmsOrDie();


        //4.释放资源
		........
    }

}

2.3 异步Confirm方式

public class Publisher {
    @Test
    public void publish() throws Exception {
        //1.获取connection
        //2. 创建channel
        
        ...............
   
        //3. 发布消息到exchange,同时指定路由规则

        //开启confirm
        channel.confirmSelect();

        for (int i = 0; i < 10; i++) {
            String msg = "Hello-Word"+i;
            channel.basicPublish("","HelloWord",null,msg.getBytes());
        }
        
		//开启异步回调
        channel.addConfirmListener(new ConfirmListener() {
            public void handleAck(long deliveryTag, boolean multiple) throws IOException {
                System.out.println("send message success!标识:"+deliveryTag+"是否批量操作:"+multiple);
            }

            public void handleNack(long deliveryTag, boolean multiple) throws IOException {
                System.out.println("send message failure!标识:"+deliveryTag+"是否批量操作:"+multiple);
            }
        });

        //4.释放资源
   
    }

}

三、return机制

3.1 介绍

  • Confirm只能保证到exchange,无法保证可以被exchange分发到queue
  • 而且exchange不能持久化消息,queue才可以持久化消息
  • 采用Return机制来监听消息是否从exchange发送到queue中

3.2 实现

  1. 通过 channel 的 addReturnListener() 方法开启return机制
  2. 在addReturnListener()的形参中创建一个 ReturnListener 类型的接口,并实现它的 handleReturn() 方法( 当消息没有被送达到queue时此才会执行)。
  3. 再通过 basicPublish() 发送消息时将他的第三个形参mandatory(受托者 ;强制性的)设置为true。
public class Publisher {
    @Test
    public void publish() throws Exception {
        //1.获取connection
        //2. 创建channel
        //3. 发布消息到exchange,同时指定路由规则
        
		.........
            
        //开启return机制
        channel.addReturnListener(new ReturnListener() {
            public void handleReturn(int replyCode, String replyText, String exchange, String routingKey, AMQP.BasicProperties properties, byte[] body) throws IOException {
                //当消息没有被送达到queue时才会执行
                System.out.println(new String(body,"UTF-8")+" message not send queue!");
            }
        });

        //开启confirm
        channel.confirmSelect();

        //发送消息
        for (int i = 0; i < 10; i++) {
            String msg = "Hello-Word"+i;
            channel.basicPublish("","xxxxxx",true,null,msg.getBytes());
        }
        
		//异步confirm回调
        channel.addConfirmListener(new ConfirmListener() {
            public void handleAck(long deliveryTag, boolean multiple) throws IOException {
                System.out.println("send message success!标识:"+deliveryTag+"是否批量操作:"+multiple);
            }

            public void handleNack(long deliveryTag, boolean multiple) throws IOException {
                System.out.println("send message failure!标识:"+deliveryTag+"是否批量操作:"+multiple);
            }
        });
        

        System.out.println("发布消息成功!");


        //4.释放资源
		...........
    }

}

四、在spring boot中开启Confirm和return机制

4.1 编辑springboot配置文件

开启Confirm和return机制,需要在配置文件中添加两个属性:

  1. publisher-confirm-type: simple
  2. publisher-returns: true
spring:
  rabbitmq:
    host: 192.168.31.138
    port: 5672
    username: test
    password: test
    virtual-host: /test
    listener:
      direct:
        acknowledge-mode: manual
    publisher-confirm-type: simple
    publisher-returns: true

4.2 编写配置类

  1. 创建名为 PublisherConfirmAndReturnConfig 的配置类并实现 RabbitTemplate 接口的两个抽象方法:
    • RabbitTemplate.ConfirmCallback.confirm
    • RabbitTemplate.ReturnsCallback.returnedMessage
  2. 通过 @PostConstruct 注解在服务器运行时就告诉Rabbit我开启了Confirm和return机制。
  3. 重写并实现confirm和returnedMessage方法。
@Component
public class PublisherConfirmAndReturnConfig implements RabbitTemplate.ConfirmCallback,RabbitTemplate.ReturnsCallback {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @PostConstruct  //init-method
    public void initMethod(){
        rabbitTemplate.setConfirmCallback(this);
        rabbitTemplate.setReturnsCallback(this);
    }


    //confirm回调
    @Override
    public void confirm(CorrelationData correlationData, boolean b, String s) {
        if (b){
            System.out.println("send message success to exchange");
        } else{
            System.out.println("send message failure to exchange");
        }
    }

    //return回调
    @Override
    public void returnedMessage(ReturnedMessage returnedMessage) {
        //只有在消息没有送达到queue时执行
        System.out.println("send message failure to queue!");

    }
}

@PostConstruct 注解:

@PostConstruct该注解被用来修饰一个非静态的 void() 方法。被@PostConstruct修饰的方法会在服务器加载Servlet的时候运行,并且只会被服务器执行一次。PostConstruct在构造函数之后执行, init() 方法之前执行。

通常我们会是在Spring框架中使用到@PostConstruct注解 该注解的方法在整个Bean初始化中的执行顺序:Constructor(构造方法) -> @Autowired(依赖注入) -> @PostConstruct(注释的方法)

4.3 测试

@SpringBootTest
class SpringbootRabbitmqApplicationTests {
    @Autowired
    private RabbitTemplate rabbitTemplate;
    @Test
    void publish() throws IOException {
        rabbitTemplate.convertAndSend("springboot-topic-exchange","slow.white.dog","慢红狗");
        System.in.read();
    }
}

五、消息的重复消费

5.1 介绍

当消费者消费完消息后发生了异常没有给 RabbitMQ 返回一个 ACK 就可能导致消息的重复消费。

重复消费消息,会对非幂等操作造成问题(幂等性操作:执行多次操作不影响最终结果,例如删除操作)

为了解决消息被重复消费的问题,可以采用 Redis ,在消费者消费之前,将消息 id 放到 redis 中。

  • id : 0(正在执行业务)
  • id : 1(执行业务成功)

如果 ack 失败,在RabbitMQ将消息交给其他消费者时,消费者先执行 setnx (redis方法),如果 key 已经存在则获取 key 的值,当获取到的值为0时,消费者什么都不做,当获取到的值为1时,消费者直接 ack。

极端情况:第一个消费者执行业务时出现了死锁,在setnx基础上,再给key设置一个生存时间。

5.2 实现

5.2.1 导入Redis依赖
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.9.0</version>
</dependency>
5.2.2 编写生产者

在发送消息时,在 BasicProperties 中指定消息的 id 。

public class Publisher {
    @Test
    public void publish() throws Exception {
        //1.获取connection
        //2. 创建channel
        ..........

        //3. 发布消息到exchange,同时指定路由规则

        //开启confirm
        channel.confirmSelect();
        AMQP.BasicProperties properties = new AMQP.BasicProperties().builder()
                .deliveryMode(1)
                .messageId(UUID.randomUUID().toString())
                .build();

        String msg = "Hello-Word";
        channel.basicPublish("","HelloWord",properties,msg.getBytes());


        channel.addConfirmListener(new ConfirmListener() {
            public void handleAck(long deliveryTag, boolean multiple) throws IOException {
                System.out.println("send message success!标识:"+deliveryTag+"是否批量操作:"+multiple);
            }

            public void handleNack(long deliveryTag, boolean multiple) throws IOException {
                System.out.println("send message failure!标识:"+deliveryTag+"是否批量操作:"+multiple);
            }
        });

        System.out.println("发布消息成功!");
        
        //4.释放资源
        channel.close();
        connection.close();
    }
}
5.2.3 编写消费者

在消费消息时,获取 消息的 id 并放入 Redis 中。

public class Customer {

    @Test
    public void customer() throws Exception {
        //1.获取connection
        Connection connection = RabbitMQClient.getConnection();

        //2. 创建channel
        final Channel channel = connection.createChannel();

        //3.声明队列(HelloWord)

        channel.queueDeclare("HelloWord",false,false,false,null);

        //4.开启监听指定Queue
        //回调方法
        DefaultConsumer consumer = new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {

                Jedis jedis = new Jedis("192.168.31.138",6379);

                //获取消息的唯一标识id
                String messageId = properties.getMessageId();

                //1.setnx到Redis中,默认指定value为0,表示正在消费消息。
                String result = jedis.set(messageId, "0", "NX", "EX", 10);

                //2.判断此消息是否存在在Redis内
                if (result != null && result.equalsIgnoreCase("OK")){
                    //如果消费成功,设置Redis中该消息的值为1
                    System.out.println("接收到的消息:"+new String(body,"UTF-8"));
                    jedis.set(messageId,"1");
                    //jedis.expire(messageId,5);
                    channel.basicAck(envelope.getDeliveryTag(),false);

                }else {//当该消息已经存在在Redis内似的情况
                    //判断该消息的值,如果为 1 ,手动ack;如果为 0 ,return 0 ;

                    String s = jedis.get(messageId);
                    if ("1".equalsIgnoreCase(s)){
                        channel.basicAck(envelope.getDeliveryTag(),false);
                    }
                }
            }
        };

        channel.basicConsume("HelloWord",false,consumer);

        System.out.println("正在监听队列!");
        //避免程序运行完自动停止,方便测试
        System.in.read();

        //5.释放资源
        channel.close();
        connection.close();
    }

}

六、spring boot 实现消息的重复消费

6.1 导入 Redis 相关依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

6.2 编写生产者

发送消息时设置上消息的 ID。

@SpringBootTest
class SpringbootRabbitmqApplicationTests {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    void publish() throws IOException {
        CorrelationData messageId = new CorrelationData(UUID.randomUUID().toString());

        rabbitTemplate.convertAndSend("springboot-topic-exchange","slow.red.dog","慢红狗",messageId);
        System.in.read();
    }

}

6.3 编写消费者

@Component
public class Consumer {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @RabbitListener(queues = "springboot-queue")
    public void getMessage(String msg, Channel channel, Message message) throws IOException {
        //1.获取id,并放入redis中
        String messageId = message.getMessageProperties().getHeader("spring_returned_message_correlation");
        if (redisTemplate.opsForValue().setIfAbsent(messageId,"0",10, TimeUnit.SECONDS)) {
            //2.消费消息
            System.out.println("接收到的消息:" + msg);

            //3.设置此消息的值为 1
            redisTemplate.opsForValue().set(messageId,"1");

            //手动ack
            channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
        }else {
            //4.获取redis中的值,如果值为 1 ,则手动ack,如果为 0 ,则什么都不做。
            if ("1".equalsIgnoreCase(redisTemplate.opsForValue().get(messageId))){
                //手动Ack
                channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
            }
        }
    }

}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值