如果你的业务需求足够简单,想把 Redis 当作队列来使用,肯定最先想到的就是使用 List 这个数据类型。因为 List 底层的实现就是一个「链表」,在头部和尾部操作元素,时间复杂度都是 O(1),这意味着它非常符合消息队列的模型如果把 List 当作队列,你可以这么来用。
生产者使用 LPUSH 发布消息:
消费者这一侧,使用 RPOP 拉取消息:
这个模型非常简单,也很容易理解。
它可以支持多个生产者和多个消费者并发进出消息,每个消费者拿到的消息都
是不同的列表元素
延时队列得实现
延时队列可以通过 Redis 的 zset (有序列表)来实现。我们将消息序列化成一个字符串作为 zset 的 value,这个消息的到期处理时间作为 score,然后用多个线程轮询zset 获取到期的任务进行处理 。多个结程是为了保障可用性,万一挂了一个线程还有其他线程可以继续处理。因为有多个线程,所以需要考虑并发争抢任务,确保任务不会被多次执行。
伪代码后期实现
延时队列的应用
延时队列( Delayed Job)在项目中应该经常会使用到,比如用户下单超过30分钟没有支付,后台就自动取消订单;再比如新用户注册后,要求10分钟后给用户发一封邮件。这些需求都需要通过延时队列实现。
既然redis可以实现队列,为什么还需要mq消息队列?
与专业的消息队列对比,其实,一个专业的消息队列,必须要做到两大块:
消息不丢
消息可堆积
这里我们换个角度,从一个消息队列的【使用模型】来分析下,才能保证数据不丢失?使用一个消息队列,分为三大块:生产者,队列中间件,消费者
1、生产者会不会丢消息?
2、消费者会不会丢消息?
3、队列中间件会不会丢消息?
- 生产者会不会丢消息?
- 当生产者在发布消息时,可能发生以下异常情况:
消息没发出去:网络故障或其它问题导致发布失败,[中间件]直接返回失败 - 不确定是否发布成功:网络问题导致发布超时,可能数据已发送成功,但读取响应结果超时了
如果是情况 1,消息根本没发出去,那么重新发一次就好了。
如果是情况 2,生产者没办法知道消息到底有没有发成功?所以,为了避免消息丢失,它也只能继续重试,直到发布成功为止。
生产者一般会设定一个最大重试次数,超过上限依旧失败,需要记录日志报警处理。也就是说,生产者为了避免消息丢失,只能采用失败重试的方式来处理。但发现没有?这也意味着消息可能会重复发送。是的,在使用消息队列时,要保证消息不丢,宁可重发,也不能丢弃。
那消费者这边,就需要多做一些逻辑了。
对于敏感业务,当消费者收到重复数据数据时,要设计幂等逻辑,保证业务的正确性。从这个角度来看,生产者会不会丢消息,取决于生产者对于异常情况的处理是否合理。所以,无论是 Redis 还是专业的队列中间件,生产者在这一点上都是可以保证消息不丢的。
- 消费者会不会丢消息?
这种情况消费者拿到消息后,还没处理完成,就异常宕机了,那消费者还能否重新消费失败的消息?
要解决这个问题,消费者在处理完消息后,必须「告知」队列中间件,队列中间件才会把标记已处理,否则仍旧把这些数据发给消费者。
这种方案需要消费者和中间件互相配合,才能保证消费者这一侧的消息不丢。
无论是 Redis 的 Stream,还是专业的队列中间件,例如 RabbitMQ、Kafka,其实都是这么做的。
所以,从这个角度来看,Redis 也是合格的。 - 队列中间件会不会丢消息?
前面 2 个问题都比较好处理,只要客户端和服务端配合好,就能保证生产端、消费端都不丢消息。但是,如果队列中间件本身就不可靠呢?
毕竟生产者和消费这都依赖它,如果它不可靠,那么生产者和消费者无论怎么做,都无法保证数据不丢。
在这个方面,Redis 其实没有达到要求。
Redis 在以下 2 个场景下,都会导致数据丢失。
1.AOF 持久化配置为每秒写盘,但这个写盘过程是异步的,Redis 宕机时会存在数据丢失的可能
2.[主从复制]也是异步的,主从切换时,也存在丢失数据的可能(从库还未同步完成主库发来的数据,就被提成[主库]
基于以上原因我们可以看到,Redis 本身的无法保证严格的数据完整性。
所以,如果把 Redis 当做消息队列,在这方面是有可能导致数据丢失的。
再来看那些专业的[消息队列中间件]是如何解决这个问题的?像 RabbitMQ 或 Kafka 这类专业的队列中间件,在使用时,一般是部署一个集群,生产者在发布消息时,队列中间件通常会写「多个节点」,以此保证消息的完整性。这样一来,即便其中一个节点挂了,也能保证集群的数据不丢失。
也正因为如此,RabbitMQ、Kafka在设计时也更复杂。毕竟,它们是专门针对队列场景设计的。但 Redis 的定位则不同,它的定位更多是当作缓存来用,它们两者在这个方面肯定是存在差异的。最后,我们来看消息积压怎么办?
- 消息积压怎么办?
因为 Redis 的数据都存储在内存中,这就意味着一旦发生消息积压,则会导致 Redis 的内存持续增长,如果超过机器内存上限,就会面临被 OOM 的风险。所以,Redis 的 Stream 提供了可以指定队列最大长度的功能,就是为了避免这种情况发生。但 Kafka、RabbitMQ 这类消息队列就不一样了,它们的数据都会存储在磁盘上,磁盘的成本要比内存小得多,当消息积压时,无非就是多占用一些磁盘空间,相比于内存,在面对积压时也会更加「坦然」。
综上,我们可以看到,把 Redis 当作队列来使用时,始终面临的 2 个问题:
Redis 本身可能会丢数据
面对消息积压,Redis 内存资源紧张
到这里,Redis 是否可以用作队列,我想这个答案你应该会比较清晰了。
如果你的业务场景足够简单,对于数据丢失不敏感,而且消息积压概率比较小的情况下,把 Redis 当作队列是完全可以的。
而且,Redis 相比于 Kafka、RabbitMQ,部署和运维也更加轻量。如果你的业务场景对于数据丢失非常敏感,而且写入量非常大,消息积压时会占用很多的机器资源,那么我建议你使用专业的消息队列中间件。