目录

  • 1.消息队列的作用
  • 2.一个专业的消息队列应该至少具备什么?
  • 3.Redis可以做消息队列吗
  • 3.1 List
  • 3.2 发布/订阅模型
  • 3.3 Stream数据结构
  • 4.Redis作为消息队列和专业的消息队列进行对比
  • 4.1 消息不丢
  • 4.2 消息可堆积
  • 5.总结


从今年的4月份开始就一直在实习,再加上学校的科研和项目,一直挺忙的,没有时间静下心来写点东西。从深圳到广州,从一家初创公司到一家上市“中厂”,在“折腾”的过程中,收获了很多,也学到了很多。今天,聊一聊Redis是否适合做消息队列这个话题。

1.消息队列的作用

我们使用消息队列其实无非以下几个功能:

  • 解耦生产者和消费者:通过消息队列,我们可以将生产者和消费者的逻辑进行解耦,实现系统的可拓展性。在生产者生产能力不足的时候添加生产者,在消费者能力不足的时候增加消费者,当然也可以方便的减少和增加消费者。
  • 异步:生产者通过将消息发送给消息队列即可返回,不用等待消费者消费消息后在返回,实现了消息的异步处理。比如,我们有大量的对数据库的实时性不高的更新或者插入请求,如果采用同步的方式进行更新或者插入的话,可能会造成数据库的负载和压力过大,造成系统的并发能力下降,这时候我们可以将这些操作放在消息队列中,让消费者在数据库压力不大的情况进行异步的更新和插入操作
  • 削峰填谷 :在流量激增的时候,将用户的请求写到消息队列中,然后系统按照自己能够承受的压力进行消费,可以保证系统的可靠性,保证系统的高并发。例如,在电商的秒杀中,流量可能会比之前大好多倍,如果不做任何出力,很有可能会导致系统承受不住压力而宕机。这时候我们可以将流量写到消息队列中,然后系统根据自己消费能力的大小进行消费,消息会出现短暂的积压,但是随着流量高峰期一过,消息最终被完整的消费完,保证了系统的高并发和可靠性。

2.一个专业的消息队列应该至少具备什么?

从前面的消息队列的作用中,我们不难想到一个专业的消息队列应该具备的特性有哪些:

  • 消息不丢,保证消息的可靠,因为消息队列作为一个中间人,消息不丢这一点对它来说很重要!
  • 消息可堆积,在应对激增的流量的时候,要保证消息是可以堆积的。
  • 高吞吐量,消息队列能应对较大的流量,应对多生产者和多消费者场景
  • 消息可重复消费,消费者在宕机后可以重复消费,新来的消费者可以从头消费

3.Redis可以做消息队列吗

Redis是在实习中接触最多的一个中间件了,在一些场景中只能说“真香”。

Redis中含有List数据结构、发布/订阅模型以及Stream数据结构,这些结构可以实现消息队列的一些特性,下面我们来逐一聊一聊。

3.1 List

在Redis中LIst数据结构是一个“双向链表+listpack”,用Redis的List作为消息队列的特性:

  • 生产者可以通过LPUSH往队列中加入消息,消费者通过RPOP往队列中取出消息
  • 消息的持久化可以通过Redis的AOF和RDB进行

在使用RPOP取出数据的时候,如果我们没有取到数据则会返回NULL,但是我们在进行消费消息的时候,一般都是开启一个for循环进行,如果一直没有消息,这时候就会不断返回NULL,造成CPU空转,这是不划算的。

这时候我们可以使用BLPUSH和BRPOP使用阻塞式生产者和消费者,这样在没有消息的时候,可以阻塞线程,让出CPU资源。

Redis的LIst作为消息队列的缺点

  1. 消息丢失:
  • 如果消费者在消费消息的时候,消息丢失了或者消费者宕机重启了,无法重新消费消息之前丢失的消息,因为RPOP操作拉取消息后会将消息从Redis的消费队列中删除掉
  • Redis自身会存在消息丢失的可能,AOF和RDB在一定的情况下会存在消息丢失
  1. 不支持多消费者:因为消息被拉取后就被消息队列删除了,所以新来的消费者无法消费完整的消息
  2. 大Key产生:当消息积压的时候,一方面会占用很多的内存资源,成本昂贵;另一方面,会导致出现大key,在频繁操作大key的时候造成卡顿和阻塞,性能下降。

3.2 发布/订阅模型

Redis中提供了发布/订阅模型,消费者可以订阅某个或者多个频道,生产者可以往这个一个或者多个频道中发送消息,生产者往某个频道发布消息后,订阅这个频道的所有消费者都能收到消息。显然的,Redis的发布订阅可以支持多消费者和多生产者。但是这也是发布/订阅作为消息队列的最大优点。

laravel 队列消息存redis key是什么 redis做消息队列_rabbitmq


然而,发布/订阅模型有着很明显缺点:

  • 消息丢失问题更加严重,具体场景:
  • 消费者下线
  • 消息堆积
  • Redis宕机

原因:

  • 发布/订阅没有基于任何数据结构,所以不会进行消息的存储,仅仅只是一个生产者和消费者通信的 “数据转发通道”,所以在redis宕机后消息会丢失
  • 当消费者下线后,队列不存储消息,没有消费者消费消息,则消息会被丢弃。所以,消费者必须先订阅队列,生产者再发布消息,如果反过来,则消息会丢失。
  • 消息堆积:每个消费者再订阅一个队列的时候,在redis中会给每个消费者分配一块内存消费消息的缓冲区,当由大量的消息过来的时候,消费者消费不及时,造成消息堆积达到一定阈值的时候,redis就会强制让这个消费者下线

3.3 Stream数据结构

Redis中提供了Stream类型,这个类型可以通过XADD和XREAD完成生产和消费模型,本文我们只是针对Stream在消息队列中的应用进行讨论,Stream具体的用法可以参考Stream类型的使用

  • XADD:发布消息
  • XREAD:读取消息

例如生产者发布两条消息:

// *表示让Redis自动生成消息ID
127.0.0.1:6379> XADD list * name zhangsan
"1618469123380-0"
127.0.0.1:6379> XADD list * name lisi
"1618469127777-0"

使用 XADD 命令发布消息,其中的「*」表示让 Redis 自动生成唯一的消息 ID。这个消息 ID 的格式是「时间戳-自增序号」,当然也可以手动的指定ID。

消费这拉取消息:

// 从开头读取5条消息,0-0表示从开头读取
127.0.0.1:6379> XREAD COUNT 5 STREAMS list 0-0
1) 1) "list"
   2) 1) 1) "1618469123380-0"
         2) 1) "name"
            2) "zhangsan"
      2) 1) "1618469127777-0"
         2) 1) "name"
            2) "lisi"

如果想要继续拉取消息,则要传入上一条消息的ID,比如这里的,1618469127777-0,没有消息的时候拉取消息会返回NULL。

由于拉取不到消息的时候会返回NULL,在死循环中一直返回NULL会占用CPU资源,造成CPU空转。和List类似Stream数据结构也提供了阻塞拉去消息的命令,可以使用阻塞拉取消息的办法解决上述CPU空转问题。

// BLOCK 0 表示阻塞等待,不设置超时时间
127.0.0.1:6379> XREAD COUNT 5 BLOCK 0 STREAMS queue 1618469127777-0

(1)Stream支持多生产者和多消费者嘛?
支持。Stream数据结构也支持发布订阅模型,可以支持多个生产者和多个消费者模型

(2)支持消息重复消费嘛?

支持。Stream数据结构中每个消息都有消息ID,保证消息的消费需要用到这个消息ID。当一组消费者处理完消息后,需要执行XACK命令告知Redis,这时候Redis会把这条消息标记为“处理完成”。当消费者在消费消息的时候宕机了,这时候消费者不会发送XACK,当消费者重新上线后,会将消息重新发给消费者。

laravel 队列消息存redis key是什么 redis做消息队列_消息队列_02


(3)Stream数据也会写入到AOF和RDB中进行持久化

(4)消息堆积怎么办?

消息堆积常用的两种办法,第一就是限流,限制生产者的发送速度;第二,丢弃消息,中间件丢弃掉旧的消息,保留一定长度的消息。Redis的Stream数据结构用的就是后者,Stream数据结构会指定一个最大的长度,如果发生消息堆积,超过指定的长度的时候会丢弃旧的消息,防止内存爆炸。

既然Redis的Stream数据结构具有这么多优秀的性能,仿佛就是为消息队列而生,那么我们是不是就可以用它来替代专业的消息队列呢?先别急,我们接下来可以来讨论一下,Redis作为消息队列和专业的消息队列的对比。

4.Redis作为消息队列和专业的消息队列进行对比

对于专业的消息队列,消息不丢消息可堆积对它们来说很重要,下面我们将从这两个方面分析聊一聊Redis和专业消息队列的对比。

4.1 消息不丢

要保证消息不丢,我们可以从三个环节考虑,生产者消费者中间件

  1. 生产者
    生产者在发布消息的时候,可能会发生以下的异常情况:
    (1)消息没有发出去,由于网络原因,消息没有发出,或者丢失了
    (2)不确定是否发送成功,消息发出去了,但是响应超时

如果是情况(1)消息没有发出去,消息重传即可。情况(2)不确定是否成功,则一段时间中间件没有回复消息则重传即可。生产者保证消息不丢失的办法就是重传,但是重传可能会出现消息重复的问题。对于重复的消息,消费者在消费的时候,需要做一些额外的处理,保证业务的正确。

对于生产者保证消息不丢这方面,Redis是可以做到的。

  1. 消费者
    消费者保证消息的不丢,可以通过确认机制(ack),消费者成功消费消息后才会给中间件返回一个ACK,这样可以保证消费者不丢失消息。对于这一点,Redis也是可以做到的
  2. 中间件
    Redis的AOF和RDB持久化机制,可能会导致数据丢失:
    (1)AOF和RDB持久化是有一定的时间间隔的,写磁盘的过程是异步的,在持久化间隔中间可能会导致部分数据丢失
    (2)Redis的主从复制是异步的,也就是说Redis主从之间的数据会存在不同步的现象,如果主节点宕机,从节点成为主节点,则会丢失掉主节点的部分数据

对于这个方面,Redis并不能保证数据的不丢失和数据的强一致性。然而专业的消息队列,如kafka,一般都是集群部署的,而且每个分区会有多个副本,而且kafka是将消息顺序写入磁盘的,以此来保证消息的完整性。即使一个节点挂了,也能保证集群的消息不丢失。

4.2 消息可堆积

Redis是一种基于内存的 K-V 存储库,一旦发生堆积,会导致Redis占用的内存升高,甚至OOM。所以Redis的Stream提供了可以指定队列最大长度的功能,就是为了避免这种情况,但是这样的话意味着在应对消息堆积的时候,Redis必须要喝下“消息丢失”或“内存爆炸”这两瓶“毒药”的其中一瓶;再者,内存的存储成本要比磁盘高许多,对于大量且频繁的消息堆积场景,成本的高昂的。

在这一方面,专业的消息队列,如kafka,则能应对自如,因为消息是按照顺序存储在这些专业消息中间件的磁盘中的,磁盘的容量可以比内存大很多,并且存储成本低许多,可以很轻松的应对消息堆积。

5.总结

Redis有三种方法可以作为消息队列使用,List数据结构,发布/订阅模型,以及Stream数据结构,三种方法的对比如下。

List

Pub/Sub

Stream

阻塞消费

支持

支持

支持

发布订阅

不支持

支持

支持

重复消费

不支持

不支持

支持

持久化

支持

不支持

支持

消息堆积

内存增长

消费者强制下线

旧消息被删除,保留固定长度

消息丢失?

Redis本身持久化的可能会导致数据的丢失

仅仅是一个数据转通道,没有持久化

Redis本身持久化的可能会导致数据的丢失

消息堆积?

Redis内存数据库,消息堆积能力有限,价格昂贵

Redis内存数据库,消息堆积能力有限,价格昂贵

Redis内存数据库,消息堆积能力有限,价格昂贵

讨论:
回到标题的问题,Redis适合做消息队列吗? 既可以也不可以,要看具体的应用场景。举个例子,如果可以容忍消息的丢失,以及堆积的消息不是很多的场景,可以考虑使用Redis作为消息队列,因为Redis对于开发人员来说比较熟悉,部署和运维的也比较简单。

在面对技术的选型的时候,我们不应该不经过思考就直接选择所谓 “最牛” 的技术。在架构思维中有这么一句话,“合适大于世界领先,简单优于复杂,演化优于一步到位”。所以我们在进行技术选型的时候,要立足于具体的应用场景,结合自己以及团队的实际情况,考虑的点包括但是不限于以下两点:

  • 业务功能角度:技术方案的优缺点,哪个更适合这个场景
  • 技术资源角度:公司或者团队没有没这样的技术资源去应用某项技术,如果没有出现问题的时候代价会比较大

例如,对于上述讨论的问题,如果公司或者团队中没有人能够对kafka、rabbitMQ应用自如,不能在出现问题的时候迅速定位并且修复问题的话,盲目追求使用更高级的中间件或者技术,可能会给业务带来适得其反的效果。