学习RabbitMQ

71 阅读10分钟

1.消息队列

  • 消息队列是指在消息传输过程中保存消息的容器,可以帮助不同进程 / 应用之间异步通信。

    • 最关键的三个优点,异步、削峰、解耦。

    • 缺点:

      • 系统的可用性降低:如果消息队列一挂那就全崩了
      • 系统的复杂性提高:一系列问题,例如防止消息丢失,消息的重复消费,消息传递的顺序性。
      • 一致性问题:就拿异步举例子,如果消息队列里面异步处理的系统失败了,但是信息在写入数据库时早就返回了成功,那该怎么办

1.1异步处理

场景说明:用户注册后发送注册邮件和注册短信
  • 传统做法

    • 串行:只有等全部任务执行完才会拿到返回结果 image.png
    • 并行: image.png
  • 消息队列:将无关紧要的业务进行异步处理,不需要等待异步处理的结果 image.png

1.2应用解耦

场景说明:双11下单
  • 传统做法

    • 在用户下单后,订单系统通过调用库存系统的接口来通知 image.png

      • 缺点:如果库存系统出现问题,订单就会失败,耦合度太高。
  • 引入消息队列

    • 订单系统:用户下单后,订单系统先进行持久化,然后将消息写入消息队列,返回用户订单下单成功。

    • 库存系统:订阅下单的消息,获取下单消息,进行库操作。 image.png

      • 即使库存系统出现问题,这个订单也不会丢失。

1.3流量削峰

场景:秒杀活动

作用:

  • 通过控制消息流向下一个处理系统的速率来进行解耦。
  • 可以缓解短时间的高流量压垮应用 image.png

2.ActiveMQ,RocketMQ,RabbitMQ,Kafka

  1. ActiveMQ

    • 社区对ActiveMQ的维护较少,高吞吐的场景较少使用。
  2. Kafka:为大数据而生,在大数据领域以及日志采集时被广泛使用,大型公司建议用,非常契合日志采集。

    • 优点:高吞吐吞吐量达到百万级,可用性高,性能好,一个数据多个副本,数据难丢失。
    • 缺点:社区更新慢,消费不支持重试。
  3. RocketMQ:出自阿里巴巴(应用于阿里的双11),为金融互联网而生,对于可靠性要求高的场景例如(订单处理,流量削峰,进行大量交易时)

    • 优点:吞吐量达到10万级,可用性高,消息可以做到0丢失,支持10亿级别的消息堆积,消息堆积对性能影响很小,java语言实现。
    • 缺点:支持的客户端语言不多目前支持java和部分c++。
  4. RabbitMQ:适合数据量没有那么大。中小型公司可以选择功能比较完整的RabbitMQ

    • 优点:性能好,吞吐达到万级,支持多种语言,社区比较活跃,更新频率较快,资源占用低,配置简单开箱即用,能够支持多种交换器,支持多种协议。
    • 缺点:消息堆积能力一般,吞吐量一般,不适合超大规模数据流

3.消息队列协议

协议:在TCP/IP协议基础之上构建的一种约定成的规范和机制,是为了让客户端进行沟通和通讯,并且这种协议必须具有持久性,高可用,高可靠的性能。因为TCP/IP协议太简单,并不能承载消息的内容和载体,因此在此之上增加了一些内容。 消息的中间件负责数据的传递,存储,和分发消费三个部分,数据的存储和分发的过程需要遵循某种规范而这些规范就称为协议。

所谓协议是指:
1. 计算机底层操作系统和应用程序通讯时共同遵守的约定,只有遵循共同的约定和规范,系统和底层操作系统之间才能相互交流。
2. 和一般的网络应用程序不同,它主要负责数据的接收和传递,所以性能比较的高。 
3. 协议对数据格式和计算机之间交换数据都必须严格遵守规范。

4.Work Queues

工作队列(又称任务队列)的主要思想是避免立即执行资源密集型任务,而不得不等待它完成。相反我们安排任务在之后执行。我们把任务封装为消息并将其发送到队列。在后台运行的工作进程将弹出任务并最终执行作业。当有多个工作线程时,这些工作线程将一起处理这些任务,默认情况下采用的轮询策略。

5.rabbitMQ里的订阅模型的概念

- publisher:生产者,将消息发送到交换机里
- exchange:交换机,消息的中转站根据交换机的类型将消息递交到对应的队列中
- queue:队列,存储消息要和交换机绑定
- consumer:消费者,监测消息队列中的消息
交换机的类型
    - fanout:广播,将发送者发送的消息发送到与交换机绑定的所有队列中
    - direct:订阅,根据路由键(RoutingKey)发送到绑定的队列中
    - topic:通配符订阅,与direct相似,只不过进行路由键匹配的时候可以使用通配符 "*"(只占用一个字符), "#"(占用多个或一个字符) 等
    - header:头匹配,根据MQ的消息头进行匹配

如果这里MQ通知失败,支付服务中支付流水显示支付成功,而交易服务中的订单状态却显示未支付,数据出现了不一致

6.确保消息的可靠性

  • 确保生产者一定把消息发送到MQ
  • 确保MQ不会将消息丢失
  • 确保消费者一定处理消息

6.1消息发送的可靠性

  • 当生产者发送消息时出现网络波动这时可以使用生产者重试机制,SpringAMQP的重试是阻塞式的重试,会阻塞线程

    spring:
      rabbitmq:
        connection-timeout: 1s # 设置MQ的连接超时时间
        template:
          retry:
            enabled: true # 开启超时重试机制
            initial-interval: 1000ms # 失败后的初始等待时间
            multiplier: 1 # 失败后下次的等待时长倍数,下次等待时长 = initial-interval * multiplier
            max-attempts: 3 # 最大重试次数
    
  • 在网络顺畅的情况下采用生产者确认机制

    spring:
      rabbitmq:
        publisher-confirm-type: correlated # 开启publisher confirm机制,并设置confirm类型
        publisher-returns: true # 开启publisher return机制
    

    type的类型有三种

    • none:关闭confirm机制
    • simple:同步阻塞等待MQ的回执
    • correlated(推荐):MQ异步回调返回回执

6.2MQ的可靠性

消息到达MQ以后,如果MQ不能及时保存,那就会导致消息丢失,所以MQ的可靠性很重要。

6.2.1数据的持久化

  • 交换机的持久化

    • 在设置交换机时设置为durable
  • 队列的持久化

    • 设置队列的时候设置为durable
  • 消息的持久化

    • 也可以在控制台中设置

6.2.2lazyQueue

为了解决消息堆积的问题引入了lazyQueue

  • 接收到消息后直接存入磁盘而非内存
  • 消费者要消费消息时才会从磁盘中读取并加载到内存
  • 支持百万条的消息存储

6.3消费者的可靠性

6.3.1消费者确认机制

  • ack:成功处理消息,RabbitMQ从队列中删除该消息

    • SpringAMQP设置了三种ACK模式

      • none:不处理,将消息传给消费者后立刻ack,消息会从MQ中删除。不安全,不建议使用。

      • manual:手动模式需要调用api发送ack或reject

      • auto:业务能够正常执行就自动返回ack,业务出现异常时根据异常类型返回结果

        • 业务异常,自动返回nack
        • 消息处理或校验异常,返回reject
  • nack:消息处理失败,RabbitMQ需要再次投递该消息

  • reject:消息处理失败并拒绝该消息,RabbitMQ从队列中删除该消息

6.3.2失败重试机制

为了应对队列一直重新投递没被消费的消息的情况,SpringAMQP引入了失败重试的设置

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

6.3.3失败处理策略

在使用失败重试机制后,如果达到最大重试次数消息会被丢弃。在某些对于消息可靠性要求较高的情况就不太合适了。

因此Spring允许我们自定义重试次数耗尽的消息处理策略,这个策略是MessageRecovery接口来定义

  • RejectAndDontRequeueRecover: 重试耗尽后,直接reject,丢弃消息。
  • ImmediateRequeueMessageRecoverer:重试耗尽后,返回nack,消息重新入队。
  • RepublishMessageRecoverer:重试耗尽后,将失败消息投递到指定的交换机。

6.3.4业务幂等性

为了保证业务的幂等性有以下两种方法

  • 唯一消息ID

    • 每一条消息都生成一个唯一的id,与消息一起投递给消费者

    • 消费者在接收到消息后处理自己的业务,业务处理成功后将ID保存到数据库

    • 如果下次接收到相同消息,去数据库查询判断是否存在,存在则为重复消息放弃处理

      @Bean
      public MessageConverter messageConverter(){
          // 1.定义消息转换器
          Jackson2JsonMessageConverter jjmc = new Jackson2JsonMessageConverter();
          // 2.配置自动创建消息id,用于识别不同消息,也可以在业务中基于ID判断是否是重复消息
          jjmc.setCreateMessageIds(true);
          return jjmc;
      }
      
  • 业务状态判断(推荐)

    • 例如支付修改订单业务里,先判断要修改的订单是否存在载更新数据

6.3.5兜底方案

如果MQ通知不一定能到对应的服务里,那么那个服务就必须自己出动去查询支付状态,这样即使MQ通知失败,也能通过主动查询来保证订单状态一致。 image.png 一般都是使用的定时任务进行定期查询,并判断相应的状态

7.延迟消息

在电商支付业务场景中,对于一些库存有限的商品,通常都会在下单后立刻扣除库存。

但这样存在一个问题,如果用户一直不支付,那就会一直占用库存资源导致其他客户无法正常交易。

因此,电商中通常的做法是:对于一定时间未支付的订单,应该立刻取消订单并释放库存

那我们怎么才能准确的实现在下单后第30分钟去检查支付状态呢?

这时我们要使用延迟任务,而延迟任务最简单的方案是利用MQ的延迟消息。

RabbitMQ实现延迟消息的方案:

  • 死信交换机 + TTL
  • 延迟消息插件

7.1私信交换机和延迟消息

7.1.1私信交换机

死信

  • 消费者basic.reject和basic.nack声明消费失败,并且requeue参数设置为false
  • 消息是一个过期消息,超时无人消费
  • 要投递的队列消息满了,无法投递

如果一个队列中的消息已经成为死信,这个队列通过dead-letter-exchange指定一个交换机,队列中的死信会投递到这个交换机中,而这个交换机称为死信交换机。

死信交换机的作用

1.收集因处理失败而拒绝的消息

2.收集因队列满了而拒绝的消息

3.收集TTL到期的消息

7.1.2延迟消息

image.png

7.2DelayExchange插件

7.3订单状态同步

在实际业务中大多数用户在一分钟以内就支付了,如果我们还是在30分钟时才去检测订单状态,那就会消耗MQ资源。因此我们应该设置好多检测几次订单状态,而不是在第30分钟时才检测。当途中检测到了订单是已支付状态,那就取消后续的检测。

image.png