MQ | 消息队列核心基础学习总结

本文是Edison学习《消息队列高手课》基础篇的学习总结,掌握消息队列的核心基础知识对我们开发后端微服务大有裨益。

1 MQ的适用场景

(1)异步处理
更快地返回结果,减少客户等待时间,提升总体性能.

(2)流量控制

优点在于 能够根据下游的处理能力自动调节流量,达到“削峰填谷”的作用

缺点在于 增加了系统调用链环节,总体响应时间也会延长,同时也增加了系统的复杂度

另一种限流方式:令牌桶控制流量

令牌桶的基本原理:单位时间内只发放固定数量的令牌到令牌桶中,规定服务在处理请求之前必须先从令牌桶中拿出一个令牌,如果令牌桶中没有令牌,则拒绝请求。这样就可以保证在单位时间内,能处理的请求不会发放令牌的数量,起到流量控制的作用。

MQ | 消息队列核心基础学习总结

(3)服务解耦

可以实现各个系统应用之间的解耦,达到下游系统的增加或变化,上游服务不需要更改的效果。

引入MQ同样会带来一些问题
  • 引入MQ会带来延迟问题(需要考虑业务容忍度)

  • 增加了系统的复杂度(多了一个中间件,也多了运维成本)

  • 可能会产生数据的不一致问题(无法实现强一致性)

2 如何选择MQ?

作为一款合格的MQ产品,必须具备几个特性:
  • 消息的可靠传递:确保不丢失消息;

  • Cluster:支持集群,确保不会因为某个节点宕机导致服务不可用,当然也不能丢消息;

  • 性能:具备足够好的性能,能够满足大多数场景的性能要求;

目前市面上主流的可供选择的MQ产品:
  • RabbitMQ

    • 优点:最流行的MQ之一,支持灵活的路由配置(Exchange)、支持众多的客户端编程语言

    • 问题:

      • 对消息堆积的支持并不太好,当大量消息积压时,会导致RabbitMQ性能急剧下降

      • 性能不够好,虽然每秒能够处理几万到十几万消息,但对部分性能要求很高的场景不太够用

      • Erlang非常小众,学习成本高,不太适合主流开发人员做扩展和二次开发

  • RocketMQ

    • 优点:不错的性能、稳定性 和 可靠性,还在持续的成长,此外还有非常活跃的中文社区,易于做扩展和二次开发

      • 性能:每秒大概能处理几十万条消息,高出RabbitMQ一个数量级

    • 问题:国际上的流行程度 和 与周边生态系统的集成和兼容程度要略逊一筹

  • Kafka

    • 优点:与周边生态系统的兼容性是最好的,尤其在大数据和流计算领域。大量地使用批量和异步设计思想,做到了超高的性能。

      • 性能:每秒大概能处理几十万条消息,与RocketMQ没有量级上的差异

    • 问题:同步收发消息的响应延迟比较高,不太适合在线业务场景。因为它是攒一波再一起处理的设计,对于每秒钟消息数量没有那么多的时候,时延会比较高。

3 各个MQ的消息模型

3.1 RabbitMQ

同一份消息如果要被多个消费者来消费,需要配置多个Exchange将消息发送到多个队列,每个队列中都存放一份完整的消息数据,可以为一个消费者提供消费服务。

因此,RabbitMQ实际上是基于队列模型变相地实现了“发布-订阅”模型
MQ | 消息队列核心基础学习总结
3.2 RocketMQ
RocketMQ通过为主题(Topic)设置多个队列,实现多个队列多实例并行生产和消费。订阅者的概念是通过消费组(Consumer Group)来体现的,每个消费组都消费主题中一份完整的消息,不同的消费组之间消费进度彼此不受影响。
MQ | 消息队列核心基础学习总结
3.3 Kafka
Kafka消息模型几乎和RocketMQ一致,只不过在Kafka中,队列这个概念的名称不叫Queue,而叫Partition(分区),含义和功能并没有任何区别。

4 MQ实现分布式事务

事务消息需要MQ提供相应的功能才能实现,Kafka和RocketMQ都提供了事务相关的能力

以一个订单提交的场景来看:

MQ | 消息队列核心基础学习总结

如果在第四步提交事务消息失败了,Kafka会直接抛出异常,让用户自行处理,比如可以在业务代码中反复重试,直到提交成功,或者删除之前创建的订单进行补偿。

RocketMQ则提供了一种解决方案:事务反查机制

MQ | 消息队列核心基础学习总结

RocketMQ的事务反查机制会通过定期反查事务状态,来补偿提交事务消息可能出现的通信失败。而在Kafka中,没有类似的机制。

5 如何确保消息可靠性

5.1 检测消息丢失的方法

为Producer生成的消息添加序号,并且附加Producer的标识,然后在Consumer端按照每个Producer分别来检测序号的连续性。
Consumer实例的数量最好和分区数量保持一致,做到Consumer和分区一一对应,这样会比较方便地在Consumer内检测消息序号的连续性。
5.2 确保消息可靠传递
(1)生产阶段

解法:通过常见的 请求确认机制(ACK)来保证消息的可靠传递。

要点:在异步发送时,需要在回调方法里面进行检查,切不可忘记。

(2)存储阶段
背景:正常情况下Broker正常运行不会出现丢失消息,但如果Broker出现故障(如进程死掉了或服务器宕机)就会丢失消息。
解法:通过配置Broker参数来避免因为宕机丢消息,即当Broker收到消息后将消息写入磁盘后才给Producer返回ACK确认。
要点:针对Broker集群,需要配置为 只有将消息发送到2个以上的节点,再给客户端回复ACK确认。这样可以确保消息不会因为某个Broker宕机或者磁盘损坏而丢失。
(3)消费阶段

解法:和生产阶段类似,在执行完消费业务逻辑成功后再给Broker发送ACK确认。

要点:不要在收到消息后就立即发送ACK确认,手动设置AutoAck=false。

6 如何确保消息不重复消费?

主体思路:用幂等性解决重复消费问题
幂等性的特点:同样的参数,一次调用执行和多次调用执行,对系统产生的影响是相同的。
设计幂等操作的方法:
  • 利用数据库的唯一约束实现幂等

    • 支持“INSERT IF NOT EXIST”语义的存储类系统,同样也可以基于Redis的SETNX命令来替代DB唯一约束来实现,即类似于分布式锁的功能

  • 为更新的数据设置前置条件

    • 类似于乐观锁,给数据增加一个版本号属性,每次更新前,先比较当前数据的版本号是否和MQ中消息的版本号一致,如果不一致就拒绝更新数据,一致就更新数据,同时将版本号+1

  • 记录并检查操作(实现难度和复杂度较高,一般不建议使用)

    • 在发送消息时,给每条消息指定一个全局唯一的ID,消费时先根据这个ID检查这条消息是否有被消费过,如果没有消费过,才更新数据,然后再将消费状态置为已消费

    • 难点:消费端的三个步骤“检查消费状态,然后更新数据 并且 设置消费状态”必须保证原子性,才能实现真正的幂等!

7 如何处理消息积压?

基本认知:消息积压的直接原因一定是系统中的某个部分出现了性能原因,来不及处理上游发送的消息,才会导致消息积压。
7.1 优化性能避免消息积压
  • 发送端性能优化

    • 检查是不是发送消息之前的业务逻辑耗时太多导致的

    • 解法:设置合适的并发和批量的大小

  • 消费端性能优化

    • 大部分的性能问题都是出现在消费端,消费端的消费速度跟不上发送端的生产速度导致的

    • 在设计系统时,提前考虑保证消费端的消费性能要高于生产端的发送性能,这样系统才能持续健康运行

    • 解法:一是优化消费业务逻辑,二是通过水平扩容增加消费端的并发数来提高总体消费性能

    • 要点:在扩容消费端实例的同时,必须同步扩容主题中的分区(也即队列)数量,确保消费端的实例数和分区数是相等的

7.2 线上消息积压如何处理
日常系统正常运转,突然某个时刻,开始积压消息并持续上涨,无法快速找到消息积压的原因,需要先解决积压(一般是通过扩容或服务降级),再分析原因,保证系统可用性是首要原则
导致积压突然增加,基本上有两个问题点:
  • 发送变快了

  • 消费变慢了

这时我们需要通过MQ内置的监控功能,监控数据,确定原因。
  • 如果是单位时间发送的消息增多(比如双11,618大促),短时间内无法优化消费端代码,只有通过扩容消费端的实例来提升总体消费能力

    • 如果短时间内没有足够的服务器资源扩容,那只有将系统降级,通过关闭一些不重要的业务,减少发送方发送的数据量,保证核心业务的稳定性

  • 如果发送消息和消费消息的速度没什么变化,这时需要检查一下消费端,看看是不是消费失败导致的一条消息反复消费,这种情况也会拖慢整个系统的消费速度

  • 如果是消费变慢了,需要检查消费端实例,检查日志是否有大量的消费错误,或者打印堆栈信息看看消费线程是否在什么地方卡主不动了,比如触发了死锁或等待某些资源

8 如何保证消息的顺序

基本认知:大部分情况下,我们不需要保证全局严格顺序,只要保证局部有序
解决办法:
  • 在发送端使用账户ID作为Key,采用一致性哈希计算出队列编号,指定队列来发送消息

  • 一致性哈希算法可以保证,相同的Key的消息总是发送到同一个队列上,从而保证相同Key的消息是严格有序的

  • 如果不考虑队列扩容,也可以用队列数量取模的简单哈希算法来计算队列编号

Ref 参考资料

极客时间,李玥《消息队列高手课》