RabbitMQ学习笔记

笔记来自:https://www.bilibili.com/video/BV1mN4y1Z7t9?p=1&vd_source=b9b918c3541cd04d8da1944cc702695b

安装

1.下载erlang并安装,地址:http://erlang.org
2.下载mq并安装,地址:http://www.rabbitmq.com/download.html
3.安装完成后,管理后台地址:http://localhost:15672,初始账号和密码:guest/guest

优缺点

优点:解耦、削峰、数据分发
缺点:

  • 系统可用性降低;系统引入的外部依赖越多,系统稳定性越差。一旦MQ宕机,就会对业务造成影响
  • 系统复杂度提高;MQ的加入大大增加了系统的复杂性,以前系统间是同步的远程调用,现在是通过进行异步调用。如何保证MQ的高可用?如何保证消息没有被重复消费?怎么处理消息丢失情况?怎么保证消息传递的顺序性?
  • 一致性问题;A系统处理完业务,通过MQ给BCD三个系统发消息数据,如果B系统,C系统处理成功,D系统处理失败。如何保证消息数据处理的一致性?

对比

ActiveMQRabbitMQRocketMQkafka
开发语言javaerlangjavascala
单击吞吐量万级万级十万级十万级
时效性ms级us级ms级ms级以内
可用性高(主从架构)高(主从架构)非常高(分布式架构)非常高(分布式架构)
功能特性成熟的产品,在很多公司得到应用,有较多的文档;各种协议支持较好基于erlang开发,所以并发能力很强,性能及其好,延时很低;管理界面丰富,缺点是很难进行二次开发MQ功能比较完备,扩展性佳只支持主要的MQ功能,像一些消息查询,消息回溯等功能没有提供,毕竟是为大数据准备的,在大数据领域应用广。

在Spring Boot项目中使用rabbitMQ

Spring Boot项目中加入如下代码

//Spring Boot项目中引入amqp包
<dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

//配置文件中加入如下配置
spring.rabbitmq.host=127.0.0.1
spring.rabbitmq.port=5672
spring.rabbitmq.virtual-host=/
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest

一、直接发送消息到队列,不经过交换机

1、简单队列,只有一个消费者

在这里插入图片描述

    /**
     * 简单队列
     */
    @Test
    public void testSimpleQueue(){
        String queueName ="simple.queue";
        String message ="testSimpleQueue";
        rabbitTemplate.convertAndSend(queueName,message);
    }

    @RabbitListener(queues = "simple.queue")
    public  void listenSimpleQueue(String msg) {
        System.out.println("消费者收到消息====================="+msg);
    }
2、工作队列,多个消费者

    /**
     * 工作队列
     */
    @Test
    public void testWorkQueue() throws InterruptedException {
        String queueName ="work.queue";
        String message ="testWorkQueue";
        for (int i = 0; i < 50; i++) {
            rabbitTemplate.convertAndSend(queueName,message+i);
            Thread.sleep(20);
        }

    }

    @RabbitListener(queues = "work.queue")
    public  void listenWorkQueue1(String msg) {
        System.out.println("消费者work.queue 1收到消息====================="+msg);
    }

    @RabbitListener(queues = "work.queue")
    public  void listenWorkQueue2(String msg) throws InterruptedException {
        System.out.println("消费者work.queue 2收到消息====================="+msg);
        Thread.sleep(100);
    }

每个消息只会被消费一次,默认情况下消费者轮流消费,可以通过设置spring.rabbitmq.listener.simple.prefetch=1来确保同一时刻最多投递给消费者1条消息,从而避免消息堆积

二、消息经过交换机

交换机的作用是对消息进行路由,但不具备保存消息的能力。

1.Fanout交换机

会将接收到的消息广播到每一个跟其绑定的队列,所以也叫广播模式

举例,交换机和队列进行绑定如下,

    /**
     * fanout交换机
     */
    @Test
    public void testSimpleQueue(){
        String exchangeName ="test.fanout";
        String message ="hello,everyone!";
        rabbitTemplate.convertAndSend(exchangeName,null,message);
    }

    @RabbitListener(queues = "fanout.queue")
    public  void listenFanoutQueue1(String msg) {
        System.out.println("消费者fanout.queue 收到消息====================="+msg);
    }

    @RabbitListener(queues = "fanout.queue1")
    public  void listenFanoutQueue2(String msg) {
        System.out.println("消费者fanout.queue 1收到消息====================="+msg);
    }

结果:
消费者fanout.queue 收到消息=====================hello,everyone!
消费者fanout.queue 1收到消息=====================hello,everyone!
2.Direct交换机

Direct交换机会将接收到的消息根据规则路由到指定的队列,因此称为定向路由。
每一个队列都与交换机设置一个BindingKey;
发布者发送消息时,指定消息的RoutingKey;
交换机将消息路由到BingingKey与消息RoutingKey一致的队列;

举例,交换机与队列的BindingKey如下,

    @Test
    public void testDirectQueue(){
        String exchangeName ="test.direct";
        String message ="hello!";
        rabbitTemplate.convertAndSend(exchangeName,"red",message);
        rabbitTemplate.convertAndSend(exchangeName,"blue",message);
    }

    @RabbitListener(queues = "direct.queue1")
    public  void listenDirectQueue1(String msg) {
        System.out.println("消费者direct.queue1 收到消息====================="+msg);
    }

    @RabbitListener(queues = "direct.queue2")
    public  void listenDirectQueue2(String msg) {
        System.out.println("消费者direct.queue2 收到消息====================="+msg);
    }

结果:
消费者direct.queue1 收到消息=====================hello!
消费者direct.queue2 收到消息=====================hello!
消费者direct.queue1 收到消息=====================hello!
3.Topic交换机

与Direct交换机类似,区别在于RoutingKey可以是多个单词的列表,以.分隔。BindingKey可以使用通配符。#代指0个或多个单词,*代指一个单词(比如routingKey是多个单词,就不匹配)

举例,当交换机与队列的BindingKey如下时,

    @Test
    public void testTopicQueue(){
        String exchangeName ="test.topic";
        rabbitTemplate.convertAndSend(exchangeName,"japan.news","日本红色预警!");
        rabbitTemplate.convertAndSend(exchangeName,"china.news","中国蓝色预警!");
        rabbitTemplate.convertAndSend(exchangeName,"china.weather","阳光明媚!");
    }

    @RabbitListener(queues = "topic.queue1")
    public  void listenTopicQueue1(String msg) {
        System.out.println("消费者topic.queue1 收到消息====================="+msg);
    }

    @RabbitListener(queues = "topic.queue2")
    public  void listenTopicQueue2(String msg) {
        System.out.println("消费者topic.queue2 收到消息====================="+msg);
    }

结果:
消费者topic.queue2 收到消息=====================日本红色预警!
消费者topic.queue1 收到消息=====================中国蓝色预警!
消费者topic.queue2 收到消息=====================中国蓝色预警!
消费者topic.queue1 收到消息=====================阳光明媚!

三、声明队列和交换机

上面操作的时候都是直接在管理后台进行操作的,不方便而且容易出错。

1.基于bean的方式来声明

SpringAMQP提供了几个类,用来声明队列,交换机及其绑定关系:

Queue:用于声明队列,可以用工厂类QueueBuilder构建;
Exchange:用于声明交换机,可以用工厂类ExchangeBuilder构建;
Binging:用于声明队列和交换机的绑定关系,可以用工厂类BingingBuilder构建;

备注,一般就消费者会关心队列,所以队列和交换机一般在消费端声明。

举例,声明Fanout交换机、队列以及绑定关系,

@Configuration
public class FanoutConfig {
    @Bean
    public FanoutExchange fanoutExchange(){
        //方式一
        return ExchangeBuilder.fanoutExchange("test.fanout2").build();
        //方式二
         //return new FanoutExchange("test.fanout2");
    }

    @Bean
    public Queue fanoutQueue3(){
        //方式一
        //durable表示持久化,如果直接new的话,默认就是会持久化
        return QueueBuilder.durable("fanout.queue3").build();
        //方式二
        //return new Queue("fanout.queue3");
    }

    @Bean
    public Queue fanoutQueue4(){
        return new Queue("fanout.queue4");
    }


    @Bean
    public Binding bindFanoutQueue3(Queue fanoutQueue3,FanoutExchange fanoutExchange){
        return BindingBuilder.bind(fanoutQueue3).to(fanoutExchange);
    }

    @Bean
    public Binding bindFanoutQueue4(){
        return BindingBuilder.bind(fanoutQueue4()).to(fanoutExchange());
    }
}
2.基于注解的方式来声明

上面的声明方式,对于direct交换机来说,当routingKey有多个时,绑定的时候要声明很多次,太麻烦了。
所以SpringAMQP还提供了基于@RabbitListener注解来声明队列和交换机的方式:

    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "direct.queue3",durable = "true"),
            exchange = @Exchange(name = "test.direct2",type = ExchangeTypes.DIRECT),
            key ={"red","blue"}
    ))
    public  void listenDirectQueue3(String msg) {
        System.out.println("消费者direct.queue3 收到消息====================="+msg);
    }

四、消息转换器

当发送的消息是个对象的时候,默认会采用java提供的序列化方式进行序列化,消息长度变得很多,而且有安全问题(比如篡改后就会导致无法反序列化)。
因此建议采用JSON序列化来代替,方式如下:

    //引入Jackson依赖
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
    </dependency>

    //配置MEssageConverter
    @Bean
    public MessageConverter messageConverter(){
        return new Jackson2JsonMessageConverter();
    }

    //测试
    @Test
    public void testSimpleQueue(){
        String queueName ="simple.queue";
        Map<String,String> map =new HashMap<>(4);
        map.put("test","消息测试");
        rabbitTemplate.convertAndSend(queueName,map);
    }

    @RabbitListener(queues = "simple.queue")
    public  void listenSimpleQueue(Map<String,String> msg) {
        for (String s : msg.keySet()) {
            System.out.println("key="+s+"value="+ msg.get(s));
        }
        System.out.println("消费者收到消息====================="+msg);
    }

结果:
key=testvalue=消息测试
消费者收到消息====================={test=消息测试}

五、消息可靠性

RabbitMQ消息丢失的情况分为如下几种:

1、生产者发送消息到MQ时,因为网络问题等原因弄丢消息
2、MQ接到消息还未持久化就挂掉了
3、消费者还未处理完挂掉了

1.生产者重连

当网络波动,可能会出现生产者连接MQ失败的情况。可以开启失败重试。注意重试是阻塞式的,对业务性能有要求的,建议禁用重试机制。当然也可以使用异步的方式来做发送消息的操作。

#设置连接超时时间
spring.rabbitmq.connection-timeout=1s
#开启超时重试机制
spring.rabbitmq.template.retry.enable=true
#失败后的初始等待时间
spring.rabbitmq.template.retry.initial-interval=1000ms
#失败后下次的等待时长倍数,下次等待时长=initial-interval*multiplier
spring.rabbitmq.template.retry.multiplier=1
#最大重试次数
spring.rabbitmq.template.retry.max-attempts=3
2.生产者确认

RabbitMQ提供了Publisher Confirm和Publisher Return两种确认机制。
Publisher Confirm 确认消息是否到达交换机,Publisher Return确认消息是否到达队列。
开启确认机制后,在MQ成功收到消息后会返回确认消息给生产者。返回的结果有以下几种情况:

1、消息投递到了MQ,但是路由失败。此时会通过PublisherReturn返回路由异常原因,然后返回ACK。
2、临时消息投递到MQ,并且入队成功,返回ACK。
3、持久消息投递到了MQ,并且入队完成持久化,返回ACK。
4、其他情况都会返回NACK。

代码实现生产者确认,只需在配置文件中配置:

# none:关闭confirm机制  simple:同步阻塞等待MQ的回执消息  correlated:MQ异步回调方式返回回执消息
spring.rabbitmq.publisher-confirm-type=correlated
# 开启Publisher Return机制,一般情况下无需开启,因为大多数问题都是开发人员导致的
spring.rabbitmq.publisher-returns=true

如果是异步回调方式返回回执消息,需要提供回调。

对于ReturnCallback,每个RabbitTemplate只能配置一个,因此需要在项目启动过程中进行配置:

@Slf4j
@Configuration
public class CommonConfig implements ApplicationContextAware {
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        RabbitTemplate rabbitTemplate = applicationContext.getBean(RabbitTemplate.class);
        rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) ->{
            log.info("消息发送失败,应答码={},原因={},交换机={},路由key={},消息={}",replyCode,replyText,exchange,routingKey,message);
        } );
    }
}

对于ConfirmCallback,每个消息要单独指定,因为每个消息都要确认是否发送成功。

    @Test
    public void testSimpleQueue() throws InterruptedException {
        String exchangeName ="test.fanout";
        String message ="hello,everyone!";
        CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
        correlationData.getFuture().addCallback(new ListenableFutureCallback<CorrelationData.Confirm>() {
            @Override
            public void onFailure(Throwable throwable) {
            	//这个失败不用处理,一般不会失败
                log.error("消息回调失败",throwable);
            }

            @Override
            public void onSuccess(CorrelationData.Confirm confirm) {
                log.info("收到confirm callback回执");
                if(confirm.isAck()){
                    //消息发送成功
                    log.info("消息发送成功,收到ack");
                }else{
                    //消息发送失败,此处应该进行失败重试
                    log.error("消息发送失败,收到nack,原因:{}",confirm.getReason());
                }
            }
        });
//当故意把交换机名写错,结果:
消息发送失败,收到nack,原因:channel error; protocol method: #method<channel.close>(reply-code=404, reply-text=NOT_FOUND - no exchange 'test.fanouta' in vhost '/', class-id=60, method-id=40)        

如何处理生产者的确认消息?

生产者确认需要额外的网络和系统资源开销,尽量不要使用(个人理解:网络问题会抛异常,程序能感知。如果是无法到达交换机或者无法路由,都属于代码层面的问题)
如果一定要使用,无需开启Publisher-Return机制,因为一般路由失败是自己业务问题
对于nack消息可以有限次数重试,依然失败则记录异常消息

3.MQ的可靠性

在默认情况下,RabbitM0会将接收到的信息保存在内存中以降低消息收发的延迟。这样会导致两个问题

1、一旦MQ宕机,内存中的消息会丢失
2、内存空间有限,当消费者故障或处理过慢时,会导致消息积压,引发MO阻塞(当达到一定数量时,MQ会将早期的消息移到磁盘,也就是Paged Out,这期间无法接收消息)

RabbitMQ实现数据持久化包括3个方面:交换机持久化,队列持久化,消息持久化
使用Spring的话,交换机与队列的durable默认就是true。消息持久化默认deliveryModel就是2。

从RabbitMQ的3.6.0版本开始,增加了Lazy Queue的概念,也就是惰性队列。惰性队列的特征如下:

接收到消息后直接存入磁盘而非内存(内存中只保留最近的消息,默认2048条)
消费者要消费消息时才会从磁盘中读取并加载到内存
支持数百万条的消息存储

在3.12版本后,所有队列都是Lazy Queue模式,无法更改。

设置队列为惰性队列的方式:

	//方式一
    @Bean
    public Queue fanoutQueue3(){
        return QueueBuilder.durable("fanout.queue3").lazy().build();
    }

	//方式二
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "direct.queue3",durable = "true",arguments = @Argument(name = "x-query-mode",value = "lazy")),
            exchange = @Exchange(name = "test.direct2",type = ExchangeTypes.DIRECT),
            key ={"red","blue"}
    ))
    public  void listenDirectQueue3(String msg) {
        System.out.println("消费者direct.queue3 收到消息====================="+msg);
    }
4.消费者可靠性

为了确认消费者是否成功处理消息,RabbitMQ提供了消费者确认机制(Consumer Acknowledgement)。当消费者处理消息结束后,应该向RabbitMO发送一个回执,告知RabbitMQ自己消息处理状态。回执有三种可选值:

  • ack:成功处理消息,RabbitMQ从队列中删除该消息
  • nack:消息处理失败,RabbitMo需要再次投递消息
  • reject: 消息处理失败并拒绝该消息,RabbitMQ从队列中删除该消息

SpringAMOP已经实现了消息确认功能。并允许我们通过配置文件选择ACK处理方式,有三种:

  • none:不处理。即消息投递给消费者后立刻ack,消息会立刻从MQ删除。非常不安全,不建议使用

  • manual: 手动模式。需要自己在业务代码中调用api,发送ack或reject,存在业务入侵,但更灵活

  • auto:自动模式。SpringAMOP利用AOP对我们的消息处理逻辑做了环绕增强,当业务正常执行时则自动返回ack,当业务出现异常时,根据异常判断返回不同结果:如果是业务异常,会自动返回nack;如果是消息处理(MessageConversionException)或校验异常,自动返回reject。

//消费方在配置文件中加入如下配置
spring.rabbitmq.listener.simple.acknowledge-mode=auto

//处理消息时抛出异常
    @RabbitListener(queues = "simple.queue")
    public  void listenSimpleQueue(Map<String,String> msg) {
        System.out.println("消费者收到消息====================="+msg);
        throw new RuntimeException("抛异常");
    }
//然后发消息后,会发现消息不断的投递。
 

当消费者出现异常后,消息会不断requeue(重新入队)到队列,再重新发送给消费者,然后再次异常,再次requeue无限循环,导致mq的消息处理飙升,带来不必要的压力。可以利用Spring的retry机制,在消费者出现异常时利用本地重试。

#开启消费者失败重试
spring.rabbitmq.listener.simple.retry.enabled=true
#初始的等待时长为1s
spring.rabbitmq.listener.simple.retry.initial-interval=1000ms
#下次失败的等待时长倍数,下次的等待时长=multiplier*last-interval
spring.rabbitmq.listener.simple.retry.multiplier=1
#最大重试次数
spring.rabbitmq.listener.simple.retry.max-attempts=3
#true无状态;false有状态,如果业务中包含事务,这里改为false
spring.rabbitmq.listener.simple.retry.stateless=true

在开启重试模式后,重试次数耗尽,如果消息依然失败,则需要有MessageRecoverer接口来处理,它包含三种不同的实现:

  • RejectAndDontRequeueRecoverer:重试耗尽后,直接reject,丢弃消息。默认就是这种方式

  • ImmediateRequeueMessageRecoverer:重试耗尽后,返回nack,消息重新入队,不成功还是会在投递

  • RepublishMessageRecoverer:重试耗尽后,将失败消息投递到指定的交换机(方便后期人工处理,比如发邮件给开发)

RepublishMessageRecoverer用法示例:

@Configuration
//当spring.rabbitmq.listener.simple.retry.enabled=true时,该bean才生效
@ConditionalOnProperty(prefix = "spring.rabbitmq.listener.simple.retry",name="enabled",havingValue = "true")
public class RabbitMQErrorConfiguration {
    @Bean
    public DirectExchange errorExchange(){
        return new DirectExchange("error.direct");
    }

    @Bean
    public Queue errorQueue(){
        return new Queue("error.queue");
    }

    @Bean
    public Binding errorDirectQueue(Queue errorQueue,FanoutExchange errorExchange){
        return BindingBuilder.bind(errorQueue).to(errorExchange).with("error");
    }

    @Bean
    public MessageRecoverer messageRecoverer(RabbitTemplate rabbitTemplate){
        return new RepublishMessageRecoverer(rabbitTemplate,"error.direct","error");
    }
}

一句话总结就是,开启消费者确认机制为auto,由Spring确认消息处理成功后返回ack,异常时返回nack。开启消费者失败重试机制,并设置MessageRecover,多次重试失败后将消息投递到异常交换机,交由人工处理。

六、业务幂等性

幂等是指同一个业务,执行一次或多次结果一样。

唯一消息id

给每个消息都设置一个唯一id,利用id区分是否是重复消息:

每一条消息都生成一个唯一的id,与消息一起投递给消费者。
消费者接收到消息后处理自己的业务,业务处理成功后将消息ID保存到数据库。
如果下次又收到相同消息,去数据库查询判断是否存在,存在则为重复消息,放弃处理。

	//生成消息ID的方式
   @Bean
    public MessageConverter messageConverter(){
        Jackson2JsonMessageConverter jjmc = new Jackson2JsonMessageConverter();
        //配置自动创建id,用于识别不同消息,也可以在业务中基于ID判断是否是重复消息
        jjmc.setCreateMessageIds(true);
        return jjmc;
    }
结合业务做判断

比如首次消费业务状态已改,下次再消费,由于业务状态已经变了,就不需要往下执行了。

七、延迟消息

生产者发送消息时指定一个时间,消费者不会立刻收到消息,而是在指定时间之后才收到消息。实现延迟消息的方案有:
死信交换机、延迟消息插件。

1.死信交换机

当一个队列中的消息满足下列情况之一时,就会成为死信 (dead letter) :

  • 消费者使用basic.reject或 basic.nack声明消费失败,并且消息的requeue参数设置为false
  • 消息是一个过期消息(达到了队列或消息本身设置的过期时间),超时无人消费
  • 要投递的队列消息堆积满了,最早的消息可能成为死信

如果队列通过dead-letter-exchange属性指定了一个交换机,那么该队列中的死信就会投递到这个交换机中。这个交换机称为死信交换机 (Dead Letter Exchange,简称DLX)
在这里插入图片描述

2.延迟消息插件

RabbitMO的官方也推出了一个插件,原生支持延迟消息功能。该插件的原理是设计了一种支持延迟消息功能的交换机,当消息投递到交换机后可以暂存一定时间,到期后再投递到队列。

延迟消息过多,也会对服务器造成很大的压力,所以不适用于延迟时间特别长的消息。
在这里插入图片描述
在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值