各消息队列原理与比较

510 阅读7分钟

好用的消息队列有很多,一般来说其中的一种就可以满足大部分的需求了。消息队列作为重要的消息中间件,对注册中心也有要求,所以在技术选型的时候,除了考虑应用场景以外,也需要考虑到各个组件之间的搭配。

常见的消息队列:kafka、rocketMQ、rabbitMQ、redis 等

各种消息队列各有千秋,但是要满足项目的最优解,还是要考虑到各方各面:

  1. 首先是库的支持,如果项目本身没有库能够作为某个队列引擎,那么就只能换一个队列或者自己造轮子。
  2. 其次是组件兼容,早期 kafka 是必须部署 zookeeper 作为注册中心的,后来才放弃使用。类似的,如果你的项目中的某一组件不兼容某个队列,最好换一个更搭配的。
  3. 最后是项目需求,项目中对于消息的可靠性和并发性需求也是选型的重点难点之一。毕竟成本有限,所以常见的设计模式是 redis + 其他队列。因为 redis 本身就是万金油,但由于 redis 本身不可靠,所以只能用来传输一些并不太重要的消息。
消息队列架构消息存储堆积消息事务负载均衡集群方式可用性消息重复
kafkabroker内存+磁盘+数据库大量支持支持无状态集群非常高at least once、at most once
rabbitMQexchange+queue内存+磁盘少量支持较差简单集群同上
rocketMQname server+broker磁盘大量支持支持主从集群非常高at least once
消息队列吞吐量订阅形式和消息分发顺序消费消息确认消息回溯消息重试并发度
kafka极大topic、topic 正则匹配支持支持支持可实现
rabbitMQ较大direct、topic、Headers 和 fanout不支持支持不支持可实现极高
rocketMQtopic/messageTag、消息类型/属性正则匹配支持支持不支持支持

一般来说 kafka 和 rocketMQ 就足以应对所有的场景,但是如果项目不算太大,并发量没有达到百万级别的,选择 rabbitMQ 可能更合适。

接下来将对 kafka 和 rocketMQ 的各个组件进行更深度的研究:

消息存储

  • 共同点: 一个实例对应一个 broker,通常一个 broker 对应一台服务器。
  • 不同点:
    • kafka 最小的存储单元是分区,主从分区数据相同;rocketMQ 最小的存储单元是 broker,主从 broker 数据相同。
    • kafka 消息存储在磁盘和内存中; rocketMQ 消息存储在磁盘的 commitLog 文件中,队列中实际只存放 commitLog 的索引信息,消费时会根据索引去 commitLog 取。

commitLog 文件限制大小为 1G,满了以后会创建一个新的 commitLog 文件保存数据。 队列存储格式的特性保证了写过程的顺序写盘,大量数据 IO 都顺序发生在同一个 commitLog。 加上 RocketMQ 是累计 4k 才强制从 PageCache 刷入磁盘的,所以高并发写性能突出。

消息事务

消息事务即分布式事务,在消费者消费消息成功之后,生产者才能进行事务提交。 若其中任意角色发生了异常,都会导致消息失效以及事务回滚。

但现实场景中会出现存在必须强一致性的分布式场景,这就需要依靠一致性事务来解决了。 kafka 和 rocketMQ 都同时支持 XA 事务和 TCC 补偿性事务;

不管是那种事务,本质上都是对事务状态的流转。

强一致性场景下,要避免使用事件驱动,消息的发送不可靠,所以需要是同步的。 事件驱动可以通过返回异步+消息通道来实现结果的回调,异常处理程序订阅异常消息,但是设计难度大。

对于一致性的问题,有两种解决办法:

  1. 不使用一致性事务,将需要强一致性操作的操作同步进行,不需要强一致性的操作交给消息队列。 消费成功将结果存到一个指定的地方提供给用户查询。 消费失败达到一定次数,消息就会放到死信队列中交给专门消费者处理。 但是之后如果强一致性操作增加,则会增加耦合。
  2. 使用一致性事务,可能会导致用户等待时间过长。
  3. 使用一致性事务,且将服务设置为异步。将服务处理设置为异步之后,就可以直接返回响应。 然后将处理结果(成功 or 失败)存到一个指定的地方提供给用户查询。 当异步线程池满时,则返回服务器繁忙。

负载均衡

  • 相同点:
    • 同一个 topic 的生产者,默认通过轮询的方式
    • 同一个 topic 的消费者,会均匀分散到不同的分区上进行请求
  • 不同点:
    • kafka 通过主分区和副本实现;rocketMQ 通过主从 broker 实现。
    • kafka 可以指定发送到某个分区;rocketMQ 不行。
    • kafka 元数据存放在 broker 上,客户端会从 broker 中获取 topic 的路由信息。 rocketMQ 元数据存放在 nameserver 上,客户端需要从 nameserver 上获取 topic-broker 的路由信息。

集群模式

  • 不同点: kafka 是无状态集群,每台服务器既是 Master 也是 Slave。 rocketMQ broker 是多主从集群,但 name server 几乎是无状态节点。

消息重复

  • 相同点: 支持 at least once(至少一次)
  • 不同点: kafka 支持 at most once(最多一次)

至少消费一次是通过有状态消息实现的。消息消费完成之后,需要返回 ack 到消息队列进行状态流转。

最多消费一次和至少消费一次的区别在于,他是在发送消息之后,立刻进行消息状态流转。

不管是哪个队列都无法准确实现 exactly once,即精确消费一次,因为消息处理所耗费的时间无法确定。 一般来说,需要确保 at least once。对于消息的幂等,可以通过客户端实现。

吞吐量 TPS

  • 不同点:
    • Kafka 按批次发送消息和消费消息,允许将多个小消息合并发生,也允许取出一个批次的小消息。需要通过幂等解决部分消息消费后失败的情况。
    • RocketMQ 只能单次发送消息,但是允许 consumer 设置每次获取的消息数。

订阅形式和消息分发

  • 相同点:
    • 同个 topic 消息,producer 默认轮询队列发送消息。
    • 消费同一个 topic 的 consumer 个数要少于对应的 topic 队列个数,多余的 consumer 无法进行消费。
  • 不同点:
    • kafka 可以指定发达的分区;rocketMQ 不允许。
    • kafka 正常情况下 consumer 只会订阅一个分区; rocketMQ 则会每隔一段时间去 namespace 中读取 topic-broker 信息,可能存在切换。
    • kafka 消息只会被同个 consumer group 下的一个 consumer 消费 rocketMQ 消息可以选择集群消费模式(单播)和广播消费模式

消息重试

  • kafka 可以通过指定分区 offset 回溯实现消息重试。
  • rocketMQ 内置消息定时重发机制。 发送端会定时轮转到下一个 broker 进行重试,总时长不超过设置的阈值。 接收端的重试使用的比较少,因为消息本身是允许重复消费的,而消费时出现异常是不会返回 ack 的。

并发度

相同点:一个线程一个消费者,可以通过消费者多开线程来提高并发。