Redis作为队列的一些优缺点

  • 设计决策考虑

  • redis做消息队列的方式

    1. 基于List的LPUSH+BRPOP的实现
    2. PUB/SUB,订阅/发布模式
    3. 基于Sorted-Set的实现
    4. 基于Stream类型的实现
  • 主要注意问题

    1. 消息顺序保证

    2. 消息重复消费

      1. 重复消息的来源可能是来自生产者,来自消息队列,甚至消费者。
      • 可能是生产者没做幂等性处理,或者mq的速度慢,导致重试机制触发,又发了一条;
      • 消费者消费完一条数据响应 ack 信号消费成功时,MQ 突然挂了,导致 MQ 以为消费者还未消费该条数据,MQ恢复后再次推送了该条消息,导致了重复消费;
      • 消费者已经消费完了一条消息,正准备但是还未给 MQ 发送 ack 信号时,此时消费者挂了,服务重启后 MQ 以为消费者还没有消费该消息,再次推送了该条消息。
    3. 消息丢失

      1. 生产者在发布消息时异常:

        • 网络故障或其他问题导致发布失败(直接返回错误,消息根本没发出去)
        • 网络抖动导致发布超时(可能发送数据包成功,但读取响应结果超时了,不知道结果如何)
          • 第一种情况还好,消息根本没发出去,那么重新发一次就好了。但是第二种情况就没办法知道到底有没有发布成功,所以也只能再发一次。所以这两种情况,生产者都需要重新发布消息,直到成功为止(一般设定一个最大重试次数,超过最大次数依旧失败的需要报警处理)。这就会导致消费者可能会收到重复消息的问题,所以消费者需要保证在收到重复消息时,依旧能保证业务的正确性(设计幂等逻辑),一般需要根据具体业务来做,例如使用消息的唯一ID,或者版本号配合业务逻辑来处理。
      2. 消费者在处理消息时异常:

        • 也就是消费者把消息拿出来了,但是还没处理完,消费者就挂了。这种情况,需要消费者恢复时,依旧能处理之前没有消费成功的消息。使用List当作队列时,也就是利用老师文章所讲的备份队列来保证,代价是增加了维护这个备份队列的成本。而Streams则是采用ack的方式,消费成功后告知中间件,这种方式处理起来更优雅,成熟的队列中间件例如RabbitMQ、Kafka都是采用这种方式来保证消费者不丢消息的。
      3. 消息队列中间件丢失消息

      • 上面2个层面都比较好处理,只要客户端和服务端配合好,就能保证生产者和消费者都不丢消息。但是,如果消息队列中间件本身就不可靠,也有可能会丢失消息,毕竟生产者和消费这都依赖它,如果它不可靠,那么生产者和消费者无论怎么做,都无法保证数据不丢失。
        • 在用Redis当作队列或存储数据时,是有可能丢失数据的:一个场景是,如果打开AOF并且是每秒写盘,因为这个写盘过程是异步的,Redis宕机时会丢失1秒的数据。而如果AOF改为同步写盘,那么写入性能会下降。另一个场景是,如果采用主从集群,如果写入量比较大,从库同步存在延迟,此时进行主从切换,也存在丢失数据的可能(从库还未同步完成主库发来的数据就被提成主库)。总的来说,Redis不保证严格的数据完整性和主从切换时的一致性。我们在使用Redis时需要注意。
        • 而采用RabbitMQ和Kafka这些专业的队列中间件时,就没有这个问题了。这些组件一般是部署一个集群,生产者在发布消息时,队列中间件一般会采用写多个节点+预写磁盘的方式保证消息的完整性,即便其中一个节点挂了,也能保证集群的数据不丢失。当然,为了做到这些,方案肯定比Redis设计的要复杂(毕竟是专们针对队列场景设计的)。
    4. 消息堆积

  • 第一种方法:List

    • 基于list的方法很简单,就是lpush, rpop,这样基本有序肯定能保证,问题是这里有个性能上的考虑,就是生产者生产了消息,但是消费者要去轮询,或者一个while loop去判断,这里cpu时间就会一直在这个上浪费时间了,所以最好用BRPOP,这样客户端去读,读不到的时候回阻塞,直到有新的数据写入队列,相对rpop来说会节约cpu时间;
    • 接着看重复消息问题, 这里有个幂等性问题,怎么保证消费者处理一次消息和处理多次消息的结果是一致的,从这个角度说,我可能需要个全局的消息ID来判断
    • 消息可靠性怎么保证,会不会丢失,也是需要考虑的。因为我们是list来做消息队列,那如果碰到没处理成功又该如何呢?这里就需要一种恢复机制来保证可靠性,最简单的,我可以做一个list,要处理之前,先把id加入该list,如果失败,可以指定是哪个id没处理成功,重新做一次,但是这样仍然还是无法保证的,因为还有一种情况是redis本身挂了,那list里能否保证加入id成功,也是未知的,所以这里只是提了个简单方法,并无法完全保证可靠性得到解决
    • 对于消息的堆积问题,就是生产消息太快,消费太慢导致的,目前redis似乎没有直接处理的方式,不支持分组,但是stream方式可以支持,一会儿会谈到。
  • 第二种方法:Pub/Sub

    • 发布订阅模式使用PUBLISH/SUBSCRIBE channel message来做订阅,消息的顺序性上说感觉也是比较直观的,场景上,可以做聊天,或者实时的一些配置的更新,通知,公告这里都可以用发布订阅简单实现
    • 重复性上说,如果消息重复,还是需要订阅者判断,比如通过状态判断,每次消费后把状态记录下来,下次直接去查询下看看是否消费过,也可以依赖于数据库的唯一性约束防止重复的消费,所以这块幂等性的处理很重要。
    • 发布订阅过程中,如果有订阅者掉线了,重新上线之后,掉线的消息是丢失的
    • 消息堆积问题和回溯都有问题,也无法保证每个订阅的人消息收到的时间是一样的,而且生产远大于消费的时候,有可能强制断开导致消息丢失
  • 第三种方法:基于Sorted-Set的实现

    • 这种可能不是特别关心顺序,反而更倾向于实现类似优先级队列之类的功能,比如做各种排行榜之类的功能
    • 不允许重复消息,不支持分组消费
    • 为了防止消息丢失,可能需要自己实现消费确认机制
    • 堆积问题,可以用多个消费者来消费
  • 第四种方法:基于Stream类型的实现

    • 有序性是通过XADD/XREAD,XADD插入有序,自动生成全局ID,XREAD可以通过ID读取。可以使用XREAD block来实现类似阻塞读的功能
    • 重复性可以处理,因为有全局唯一ID
    • 可靠性可以依靠内部的pending list自动留存消息,可以用XPENDING查看,使用XACK确认消息
    • 堆积问题,可以使用消费分组来做,增加消费速度

总结

Redis可以用作队列,而且性能很高,部署维护也很轻量,但缺点是无法严格保数据的完整性(个人认为这就是业界有争议要不要使用Redis当作队列的地方)而使用专业的队列中间件,可以严格保证数据的完整性,但缺点是,部署维护成本高,用起来比较重。所以我们需要根据具体情况进行选择,如果对于丢数据不敏感的业务,例如发短信、发通知的场景,可以采用Redis作队列。如果是金融相关的业务场景,例如交易、支付这类,建议还是使用专业的队列中间件