浅谈Redis适合做消息队列吗?

从今年的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在一定的情况下会存在消息丢失
  2. 不支持多消费者:因为消息被拉取后就被消息队列删除了,所以新来的消费者无法消费完整的消息

  3. 大Key产生:当消息积压的时候,一方面会占用很多的内存资源,成本昂贵;另一方面,会导致出现大key,在频繁操作大key的时候造成卡顿和阻塞,性能下降。

3.2 发布/订阅模型

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

在这里插入图片描述
然而,发布/订阅模型有着很明显缺点:

  • 消息丢失问题更加严重,具体场景:

    • 消费者下线
    • 消息堆积
    • 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,当消费者重新上线后,会将消息重新发给消费者。
在这里插入图片描述
(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数据结构,三种方法的对比如下。

ListPub/SubStream
阻塞消费支持支持支持
发布订阅不支持支持支持
重复消费不支持不支持支持
持久化支持不支持支持
消息堆积内存增长消费者强制下线旧消息被删除,保留固定长度
消息丢失?Redis本身持久化的可能会导致数据的丢失仅仅是一个数据转通道,没有持久化Redis本身持久化的可能会导致数据的丢失
消息堆积?Redis内存数据库,消息堆积能力有限,价格昂贵Redis内存数据库,消息堆积能力有限,价格昂贵Redis内存数据库,消息堆积能力有限,价格昂贵

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

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

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

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

因此,具体问题具体分析,学习新的技术是必要的,但是盲目追求“更高级”的技术是不可取的,适合团队的才是最重要的。


才疏学浅,如有不对,欢迎批评指正~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值