RocketMQ初步总结和辨析

消息队列和RocketMQ入门总结

Posted by Haiming on February 17, 2020

看到一篇文章之中对消息队列和RocketMQ做了初步的总结,觉得比较好,在此加上自己对于部分功能的解释在这里写一篇。

原文链接:https://juejin.im/post/5df0825b51882512420af94a

参考链接:https://www.cnblogs.com/chjxbt/p/11407890.html

消息队列简介

对于消息队列,之前有过梳理,此处只是对之前叙述不够完善和不够详细的地方做一些补充。

消息队列为什么会出现?

分布式应用必定涉及到各个系统之间的通信问题,因为分布式应用必定涉及到各个系统之间的通信问题。可以说分布式是消息队列的基础,也是消息队列产生的土壤。

消息队列可以用来干嘛?

异步需求,削峰填谷,系统解耦

消息队列有什么副作用?

  • 降低系统可用性
  • 增加系统复杂度
  • 重复消费的问题
  • 顺序消费的问题:有些消息天然是具有顺序性的,比如对某条记录进行删除增加修改三种操作,但是在发布订阅模型之中,主题是没有顺序的,那么这个时候就会导致消费者使用信息的顺序修改。
  • 分布式事务问题:不像在单个系统之中,例如在Spring之中加入@Transactional即可,但是在不同的系统之中如何保证事务呢?
  • 消息堆积的问题:如果消息的产生速率非常高,但是与此同时其消费速率又很低,那么会产生消息堆积的问题,当其数量足够多的时候,会将消息堆积在消息队列之中。

队列模型和主题模型

队列模型

消息中间件的队列模型真的就只是一个队列,如下图所示:

img

一开始有一个“广播”的概念,那么这个单个队列的模型就远远不能满足需求了,当然可以让Producer生产消息放入多个队列之中,然后每一个队列对应一个消费者。

创建队列并且赋值多个消息是非常浪费性能的,这甚至需要生产者需要具体知道消费者的数量,并且复制对应数量的消息队列。创建消息队列和复制消息队列是很影响资源和性能的。

而且这种情况下会导致生产者知道具体的消费者个数,并且根据个数去复制对应数量的消息队列,这就违背了解耦这一原则。

如果多个消费者对应一个队列的消息,那么其实他们是竞争关系,每个消费者只能收到队列之中的一部分信息,也就是一条消息只能被一个消费者收到

主题模型

如何解决这一问题?就是使用主题模型或者发布——订阅模型

在主题模型之中,消息的生产者被称为发布者,消息的消费者称为订阅者,存放消息的容器称为主题

img

对比

对比这两种模型,其本质并没有区别,主要是一份数据是否可以被消费多次的问题。如果只有一个订阅者,那么其和队列模型就基本相同了,也就是说,发布——订阅模型是兼容队列模型的。

RabbitMQ和RocketMQ

RabbitMQ的消息模型

RabbitMQ还是坚持使用队列模型,使用Exchange模块解决多个消费者的问题,Exchange模块位于生产者和队列之间,生产者并不关心将消息发到哪个队列,而是将消息发送给Exchange,由 Exchange上面配置的策略来决定将消息投递到哪些队列之中。

img

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

RocketMQ的消息模型

RockerMQ使用的消息模型是标准的发布——订阅模型,但是其中也有队列(queue)这个概念。

消息队列的消费机制:

几乎所有的消息队列产品都会使用一种非常朴素的“请求——确认”机制来确保其在传输过程之中不会消失。

这种确认机制很好的保证了信息传递之中的可靠性。但是随之而来的也有了新的问题:为了确保消息的有序性,在某一条消息被成功消费之前,下一条消息不能被消费。也就是说,每个主题在任意时刻,都只能有最多一个消费者来进行消费。(需要等待一个消费者取完信息才可以完成“确认——请求”的步骤)

为了解决此问题,RocketMQ在主题下面增加了队列的概念:

img

  • 每个主题之中包含多个队列,且RocketMQ在主题层面上面无法保证消息的严格顺序。
  • 生产者会对所有队列发消息,但是一条消息只会向某个队列之中发送一次
  • 针对一个消费组,虽然每个队列上面只能串行消费,但是多个队列一起就是并行消费了。队列数量越多,并行度越大,所以水平扩展可以提升消费性能
  • 每个队列对于每个消费组都会维护一个消费位置(offset),记录在这个消费组上面的队列消费到哪了。
  • 订阅者是通过消费组来实现的,每个消费组之中都有一份完整的信息,不同消费组之间的消费进度彼此不影响。
  • 消费组之中包含多个消费者,彼此之间是竞争消费的关系。
  • 因为信息要被不同的组进行多次消费,所以消费完的信息并不会被立刻清除,那么就像之前提到的,RocketMQ会为了每个队列维护一个消费位置(Consumer Offset)

顺序消费问题

RocketMQ在主题上是无序的,但是在单个队列上面是有序的,那么就只要使用Hash取模法保证同一个订单在同一个队列之中就可以了。

重复消费

解决方法就两个字:幂等。

怎么结合业务实现幂等呢?有下面几种方法:

  • 写入Redis来保证幂等,因为Redis的key和value天然就是支持幂等的。
  • 使用数据库插入法,基于数据库的唯一键来保证重复数据不会被插入多余。

最主要的还是需要根据特定场景使用特定的解决方案。要知道消息的属性,是完全不可以忍受重复的还是可以忍受重复消费的,然后再选择强校验或弱校验的方式。

消息堆积问题

此处其实更多是业务问题。要解决这种情况,我们需要将消费者的速度增加,或者将生产者的速度减缓。

减缓生产者速度:限流降级

增加消费者速度:

  • 先从是否有错误来看:检查是否是消费者出现了大量的消费错误,或者是某一个线程卡死,锁不释放等等

  • 最快的解决信息堆积的方法还是增加消费者实例,但是同时记得要增加每个主题的队列数量。在RocketMQ之中,一个队列只会被一个消费者消费,如果仅仅增加消费者实例就会出现下图之中的情况:

    img

RocketMQ刷盘机制

刷盘机制(flush disk),指的是将内存之中的数据写入磁盘来进行持久化的过程。下面分几种情况来讨论一下。

同步刷盘和异步刷盘

img

首先看名词就可以区分其二者的区别了:

同步刷盘,指的是在刷盘的过程之中等待一个刷盘成功的ACK;而异步刷盘之中,在刷盘进行的过程之中不需要等待ACK,而是直接后台异步线程提交,这种方式降低了读写延迟,提高了MQ的吞吐量。

一般而言,异步刷盘只会在Broker意外宕机的时候丢失部分数据,可以在Broker的参数之中FlushDiskType来调整刷盘策略。

同步复制和异步复制

上面的同步刷盘和异步刷盘是在单个节点层面的,而同步和异步复制指的是Broker的主从模式下,主节点返回消息给客户端的时候是否需要同步从节点。

  • 同步复制:也叫“同步双写”,也就是说,只有消息同步双写到主从节点的时候才返回成功。
  • 异步复制:消息写入主节点就算写入成功。

异步复制这种模式不会影响消息的可靠性,因为二者是不同的概念。

消息的可靠性,是通过不同种类的刷盘策略保证的,而这种异步同步的复制策略仅仅影响了可用性。其原因是RocketMQ本身不支持主从切换,主节点挂掉之后,生产者就没法给这个主节点生产信息了。要是采用异步复制,那么在主节点还没有发送完需要同步的消息的时候就挂掉,这个时候从节点就少了一部分信息。

这种情况下,生产者无法继续生产消息给主节点了,但是消费者可以自动切换到从节点上面进行消费,所以主节点挂掉的时间之中只会产生主从节点的消息短暂不一致的现象,降低了可用性。但是当主节点重启之后,从节点那部分没来得及复制的消息还会继续复制。