RabbitMQ - 看这篇就够了

725 阅读9分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

RabbitMQ - 看这篇就够了

本文将从概念梳理,结合官网的6种工作模式引入 企业级 RabbitMQ最佳实战(附代码)

MQ的基本概念

1.MQ概述

MQ全称 Message Queue(消息队列),是在消息的传输过程中保存消息的容器。多用于分布式系统之间进行通信。

image.png

2.MQ的优劣势

  • MQ的优势

    1. 应用解耦;提高系统容错性和可维护性
    2. 异步提速;提升用户体验和系统吞吐量
    3. 削峰填谷;提高系统稳定性
  • MQ的劣势

    1. 系统可用性降低,系统引入的外部依赖越多,系统稳定性越差。
      • 解决:保证MQ的高可用
    2. 系统复杂度提高,之前是同步的远程调用,现在是通过MQ进行异步调用。
      • 解决:保证消息的不丢失、保证不重复消费、消费失败的重试处理等等

3.常见的MQ产品

RabbitMQActiveMQRocketMQKafka
公司/社区RabbitApache阿里Apache
开发语言ErlangJavaJavaScala&Java
协议支持AMQP,XMPP,SMTP,STOMPOpenWire,STOMP,REST,XMPP,AMQP自定义自定义协议,社区封装了http协议支持
客户端支持语言官方支持Erlang,Java,Ruby等,社区产出多种API,几乎支持所有语言Java,C,C++,Python,PHP,Perl,.net等Java,C++(不成熟)官方支持Java,社区产出多种API,如PHP,Python等
单机吞吐量万级(其次)万级(最差)十万级(最好)十万级(次之)
消息延迟微妙级毫秒级毫秒级毫秒以内
功能特性并发能力强,性能极其好,延时低,社区活跃,管理界面丰富老牌产品,成熟度高,文档较多MQ功能比较完备,扩展性佳只支持主要的MQ功能,毕竟是为大数据领域准备的。

4.RabbitMQ基本概念

RabbitMQ 架构图

image.png

  1. **Broker **: MQ接收和分发消息的应用;理解为 MQ服务

  2. Virtual host : 类比 MySQL的一个database

    • 出于多租户和安全因素设计的,把AMQP的基本组件划分到一个虚拟的分组中,类似于网络中的namespace概念。类比MySQL的一个database
    • 当多个不同的用户使用同一个RabbitMQ serve提供的服务时,可以划分出多个vhost,每个用户在自己的vhost创建 exchange/queue等
  3. Connection : publisher生产者 / consumer消费者 和 broker 之间的TCP连接

    如果每一次访问 RabbitMQ 都建立一个 Connection,在消息量大的时候建立 TCP Connection的开销将是巨大的,效率也较低

    • RabbitMQ 采用类似 NIO(Non-blocking I/O)的做法,选择 TCP 连接复用,不仅可以减少性能开销,同时也便于管理。
  4. Channel 信道 : channel是建立在connection连接之上的一个虚拟连接,是双向数据流通道

    • 一个Connection下可以建立多个Channel。 channel实例不能再线程间共享,每个线程开辟一个channel连接。
    • Channel 是在 connection 内部建立的逻辑连接,如果应用程序支持多线程,通常每个thread创建单独的 channel 进行通讯,AMQP method 包含了channel id 帮助客户端和message broker 识别 channel,所以 channel 之间是完全隔离的。
    • Channel 作为轻量级的 Connection 极大减少了操作系统建立 TCP connection 的开销
  5. Connection 和 Channel 信道

    1. 保持Connection长链接
    2. 一个进程对应一个Connection
    3. Producer和Consumer分别使用不同的Connection进行消费发送和消费
  6. Exchange : 交换机

  • message 到达 broker 的第一站,根据分发规则,匹配查询表中的 routing key,分发消息到queue 中去。
  • 常用的交换机类型有
    • fanout (multicast):广播,将消息交给所有绑定到交换机的队列
    • direct (point-to-point):定向,把消息交给符合指定routing key的队列
    • topic (publish-subscribe):把消息交给符合routing pattern(路由模式) 的队列
  1. Queue : 队列

    • 消息最终被送到这里等待 consumer 取走
  2. **Binding **

    • exchange 和 queue 之间的虚拟连接,binding 中可以包含 routing key。Binding 信息被保存到 exchange 中的查询表中,用于 message 的分发依据

六种工作模式

  1. 简单模式:一般不使用

image.png

  1. Work queues:工作队列模式(1、2两种工作模式不涉及交换机和路由Key)

image.png

  1. Publish/Subscribe 发布与订阅模式:

image.png

  1. Routing 路由模式:目前工作实践

image.png

  1. Topic 通配符模式

通配符规则:# 匹配一个或多个词,* 匹配不多不少恰好1个词, 例如:item.# 能够匹配 item.insert.abc 或者 item.insert,item.* 只能匹配 item.insert

image.png

  1. RPC 远程调用模式(远程调用,不太算 MQ;暂不作介绍)

RabbitMQ 高级特性

1.消息可靠性投递

  • rabbitmq 整个消息投递的路径为: producer ---> 【 rabbitmq broker ---> exchange ---> queue 】---> consumer
生产者Product的callback

利用下面两个callback 控制消息的可靠性投递

  1. 消息从 producer —> exchange 则会返回一个 confirmCallback 。 返回参数中包含true false

    1. 设置ConnectionFactory的publisher-confirms="true" 开启 确认模式。

    2. 使用rabbitTemplate.setConfirmCallback设置回调函数。当消息发送到exchange后回调confirm方法。在方法中判断ack,如果为true,则发送成功,如果为false,则发送失败,需要处理。

      //confirmCallback 代码示例:
      
      //测试   Confirm 模式
      @Test
      public void testConfirm() {
      
        //定义回调
          rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
              /**
               *
               * @param correlationData 相关配置信息
               * @param ack   exchange交换机 是否成功收到了消息。true 成功,false代表失败
               * @param cause 失败原因
               */
              @Override
              public void confirm(CorrelationData correlationData, boolean ack, String cause) {
                  //System.out.println("confirm方法被执行了...."+correlationData.getId());
      
                   //ack 为  true表示 消息已经到达交换机
                  if (ack) {
                      //接收成功
                      System.out.println("接收成功消息" + cause);
                  } else {
                      //接收失败
                      System.out.println("接收失败消息" + cause);
                      //做一些处理,让消息再次发送。
                  }
              }
          });
      
          //进行消息发送
          for (int i = 0; i < 5; i++) {
              rabbitTemplate.convertAndSend("test_exchange_confirm","confirm","message Confirm...");
      
          }
      
          //进行睡眠操作
          try {
              Thread.sleep(1000);
          } catch (Exception e) {
              e.printStackTrace();
          }
      }
      
      
  2. **消息从 exchange —> queue 投递失败则会返回一个 returnCallback **

  3. 设置ConnectionFactory的publisher-returns="true" 开启 退回模式。

  4. 使用rabbitTemplate.setReturnCallback设置退回函数,当消息从exchange路由到queue失败后,如果设置了rabbitTemplate.setMandatory(true)参数,则会将消息退回给producer。并执行回调函数returnedMessage。

    //returnCallback 代码示例
    //测试 return模式
    @Test
    public void testReturn() {
    
        //设置交换机处理失败消息的模式   为true的时候,消息达到不了 队列时,会将消息重新返回给生产者
        rabbitTemplate.setMandatory(true);
    
        //定义回调
        rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
            /**
             *
             * @param message   消息对象
             * @param replyCode 错误码
             * @param replyText 错误信息
             * @param exchange  交换机
             * @param routingKey 路由键
             */
            @Override
            public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
                System.out.println("return 执行了....");
    
                System.out.println("message:"+message);
                System.out.println("replyCode:"+replyCode);
                System.out.println("replyText:"+replyText);
                System.out.println("exchange:"+exchange);
                System.out.println("routingKey:"+routingKey);
    
                //处理
            }
        });
        //进行消息发送
        rabbitTemplate.convertAndSend("test_exchange_confirm","confirm","message return...");
    
        //进行睡眠操作
        try {
            Thread.sleep(5000);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    
消费者Consumer ACK

ack指Acknowledge 确认。表示消费端收到消息后的确认方式。

  1. 无ack模式:acknowledge = “none”

    NONE = no acks will be sent (incompatible with channelTransacted=true). RabbitMQ calls this “autoack” because the broker assumes all messages are acked without any action from the consumer. ————————————————

    broker假设所有的消息都被ack了,**不需要消费者的任何操作。 **

    消息一旦被Consumer接收到,则自动确认收到,并将相应 message 从 RabbitMQ 的消息缓存中移除。

    • ack模式:效率高,存在丢失大量消息的风险。

    • ack模式:效率低,不会丢消息。

  2. 手动确认:acknowledge = ”manual“

    MANUAL = the listener must acknowledge all messages by calling Channel.basicAck().

    ————————————————

    MANUAL =监听器必须通过调用Channel.basicAck()来确认所有消息。

    AcknowledgeMode.MANUAL模式需要人为地获取到channel之后调用方法向server发送ack(或消费失败时的nack)信息。

  3. 自动确认:acknowledge = "auto"

    AcknowledgeMode.AUTO模式下,由spring-rabbit依据消息处理逻辑是否抛出异常自动发送ack(无异常)或nack(异常)到server端。

消息可靠性总结
  1. 持久化
    • exchange要持久化
    • queue要持久化
    • message要持久化
  2. 生产者确认Confirm
  3. 消费者确认Ack模式
  4. MQ服务高可用集群

2.消费端限流

1.为什么要对消费端限流

假设一个场景,首先,我们 Rabbitmq 服务器积压了有上万条未处理的消息,我们随便打开一个消费者客户端,会出现这样情况: 巨量的消息瞬间全部推送过来,但是我们单个客户端无法同时处理这么多数据!

当数据量特别大的时候,我们对生产端限流肯定是不科学的,因为有时候并发量就是特别大,有时候并发量又特别少,我们无法约束生产端,这是用户的行为。所以我们应该对消费端限流,用于保持消费端的稳定,当消息数量激增的时候很有可能造成资源耗尽,以及影响服务的性能,导致系统的卡顿甚至直接崩溃。

2.限流的 api讲解

RabbitMQ 提供了一种 qos (服务质量保证)功能,即在非自动确认消息的前提下,如果一定数目的消息(通过基于 consume 或者 channel 设置 Qos 的值)未被确认前,不进行消费新的消息。

/**
* Request specific "quality of service" settings.
* These settings impose limits on the amount of data the server
* will deliver to consumers before requiring acknowledgements.
* Thus they provide a means of consumer-initiated flow control.
* @param prefetchSize maximum amount of content (measured in
* octets) that the server will deliver, 0 if unlimited
* @param prefetchCount maximum number of messages that the server
* will deliver, 0 if unlimited
* @param global true if the settings should be applied to the
* entire channel rather than each consumer
* @throws java.io.IOException if an error is encountered
*/
void basicQos(int prefetchSize, int prefetchCount, boolean global) throws IOException;
  • prefetchSize:0,单条消息大小限制,0代表不限制
  • prefetchCount:一次性消费的消息数量。会告诉 RabbitMQ 不要同时给一个消费者推送多于 N 个消息,即一旦有 N 个消息还没有 ack,则该 consumer 将 block 掉,直到有消息 ack。
  • global:true、false 是否将上面设置应用于 channel,简单点说,就是上面限制是 channel 级别的还是 consumer 级别。当我们设置为 false 的时候生效,设置为 true 的时候没有了限流功能,因为 channel 级别尚未实现。
  • 注意:prefetchSize 和 global 这两项,rabbitmq 没有实现,暂且不研究。特别注意一点,prefetchCount 在 no_ask=false 的情况下才生效,即在自动应答的情况下这两个值是不生效的。
3.如何对消费端进行限流
  1. 第一步,关闭自动ack; 消费端的确认模式一定为手动确认。acknowledge="manual"

  2. 第二步, listener.simpl.prefetch 参数,设置消费端 一次性拉取多少消息

    spring:
      rabbitmq:
        listener:
          simple:
            acknowledge-mode: manual #开启手动签收
            prefetch: 3 #一次就收三条
    

3.TTL 过期时间

  • 设置队列过期时间使用参数:x-message-ttl,单位:ms(毫秒),会对整个队列消息统一过期。
  @Bean
    public Queue demoKdQueue(){
        Map<String, Object> args = new HashMap<>(8);
        // x-message-ttl 这里声明队列的TTL 消息存活时间
        args.put("x-message-ttl", 20000);//10秒
        //x-dead-letter-exchange    这里声明当前队列绑定的死信交换机
        args.put("x-dead-letter-exchange", "dead-demoKdExchange");
        //x-dead-letter-routing-key  这里声明当前队列的死信路由key
        args.put("x-dead-letter-routing-key", "dead-demoKd");



        //声明 demoKdQueue队列,并绑定死信;下面两种形式皆可
        return QueueBuilder.durable("demoKdQueue").withArguments(args).build();
        //return new Queue("demoKdQueue",true,false,false,args);

    }


  • 设置消息过期时间使用参数:expiration。单位:ms(毫秒),当该消息在队列头部时(消费时),会单独判断这一消息是否过期。
@RestController
public class TTLController {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @PostMapping("/testTTL")
    public String testTTL() {
        MessageProperties messageProperties = new MessageProperties();
        messageProperties.setExpiration("20000"); // 设置过期时间,单位:毫秒
        byte[] msgBytes = "测试消息自动过期".getBytes();
        Message message = new Message(msgBytes, messageProperties);
        rabbitTemplate.convertAndSend("TTL_EXCHANGE", "TTL", message);
        return "ok";
    }
}

  • 如果两者都进行了设置,以时间短的为准。

4.死信队列

  • 1.死信交换机和死信队列和普通的没有区别
  • 2. 当消息成为死信后,如果该队列绑定了死信交换机,则消息会被死信交换机重新路由到死信队列
  • 3、消息成为死信的三种情况:
  • 1. 队列消息长度到达限制;
  • 2. 消费者拒接消费消息,basicNack/basicReject,并且不把消息重新放入原目标队列,requeue=false;
  • 3. 原队列存在消息过期设置,消息到达超时时间未被消费
  @Bean
    public Queue demoKdQueue(){
        Map<String, Object> args = new HashMap<>(8);
        // x-message-ttl 这里声明队列的TTL 消息存活时间
        args.put("x-message-ttl", 20000);//10秒
        //x-dead-letter-exchange    这里声明当前队列绑定的死信交换机
        args.put("x-dead-letter-exchange", "dead-demoKdExchange");
        //x-dead-letter-routing-key  这里声明当前队列的死信路由key
        args.put("x-dead-letter-routing-key", "dead-demoKd");



        //声明 demoKdQueue队列,并绑定死信;下面两种形式皆可
        return QueueBuilder.durable("demoKdQueue").withArguments(args).build();
        //return new Queue("demoKdQueue",true,false,false,args);

    }

5.延迟队列

  • 1. 延迟队列 指消息进入队列后,可以被延迟一定时间,再进行消费。
  • 2. RabbitMQ没有提供延迟队列功能,但是可以使用 : TTL + DLX 来实现延迟队列效果。
  • 延迟队列的使用场景
  • 1. 下单后,30分钟未支付,取消订单,回滚库存。
  • 2. 新用户注册成功7天后,发送短信问候。
  
  // 此队列无消费者监听,消费者监听死信队列:dead-demoKdQueue
  @Bean
    public Queue demoKdQueue(){
        Map<String, Object> args = new HashMap<>(8);
        // x-message-ttl 这里声明队列的TTL 消息存活时间
        args.put("x-message-ttl", 20000);//10秒
        //x-dead-letter-exchange    这里声明当前队列绑定的死信交换机
        args.put("x-dead-letter-exchange", "dead-demoKdExchange");
        //x-dead-letter-routing-key  这里声明当前队列的死信路由key
        args.put("x-dead-letter-routing-key", "dead-demoKd");



        //声明 demoKdQueue队列,并绑定死信;下面两种形式皆可
        return QueueBuilder.durable("demoKdQueue").withArguments(args).build();
        //return new Queue("demoKdQueue",true,false,false,args);

    }

6.防止消息重复消费,幂等性保障

Redission 分布式锁
		// 锁key
        String lockKey = req.getSyncPosShopBaseInfoBo().getShopId().toString();
        RLock lock = redissonClient.getLock(lockKey);
        try {
            // 最多等待10秒钟,获得锁返回true,10秒还未获得锁返回false,获得锁100秒后,如果锁没有释放,自动释放
            if (lock.tryLock(10, 100, TimeUnit.SECONDS)) {
                log.info("抢到锁");
              
                //todo 执行业务:
				
				
				
				
            } else {
                log.error("没有抢到锁");
                throw new Http400Exception("system_busy", "请稍后重试!");
            }
        } catch (Exception e) {
            log.error("加锁时异常:",e);

			//记录日志
            throw new Http400Exception("error", "信息同步失败!");
        } finally {
            // 当前线程是否持有锁
            if (lock.isHeldByCurrentThread()) {
                // 释放锁
                lock.unlock();
            }
        }

7.如何处理消息积压

产生的原因
  • 消费者宕机积压
  • 消费者消费能力不足导致积压
  • 生产者发流量太大
解决方案
  • 1、上线更多的消费者监听队列
  • 2、先将积压的消息存到数据库中,然后依次取出慢慢消费

SpringBoot整合RabbitMQ

生产者
  • 定义交换机,队列以及绑定关系的配置类
  • 注入RabbitTemplate,调用方法,完成消息发送
  • 如有需要做好 消息投递可靠性,利用两个callback
消费者
  • 定义监听类,使用@RabbitListener注解完成队列监听。

三个场景 - 覆盖企业级RabbitMQ实战

1、异步解耦,消费失败不重试
  • 配置消费者 手动确认消息
  • 生产者 发送消息到 队列 ;消费者监听队列、
    • 消费成功ack
    • 消费失败nack,且不放回原队列;消息成为死信,自动丢到死信队列
  • 死信队列消费者 监听 死信队列
  • 做兜底业务:企业微信消息通知等等
2、解耦,消费失败需要重试3次
  • 配置:自动确认 + 重试参数

    • 注意:retry配置的重发是在消费端应用内处理的,不是rabbitMq服务重发
  • 消费失败,记录错误日志,手动抛异常;消费端会按照retry配置参数进行重发

  • 重发次数完,还是抛异常的话,消息自动成为死信,进入死信队列;

  • 前提配置两个参数:

    acknowledge-mode: auto
    default-requeue-rejected: false
    
3、订单在30分钟内未支付,则订单关闭
  • 使用 TTL + 死信队列,实现延时队列
  • 生产者 —>正常队列:队列TTL为 30min
  • 定义死信交换机和死信队列;正常队列绑定死信交换机,死信路由
  • 消费者监听 死信队列,判断订单是否支付,处理业务逻辑

附录:实战代码仓库:

gitee.com/kaikaid/cod…

项目启动后,可以访问http://127.0.0.1:8080/swagger-ui.html 灵活调用 controller接口测试