Redis消息队列发展历程

redis是目前最受欢迎的kv类数据库,当然它的功能越来越多,早已不限定在kv场景,消息队列就是Redis中一个重要的功能 。
 
Redis从2010年发布1.0版本就具备一个消息队列的雏形,随着10多年的迭代,其消息队列的功能也越来越完善,作为一个全内存的消息队列,适合应用与要求高吞吐、低延时的场景 。
 
我们来盘一下Redis消息队列功能的发展历程,历史版本有哪些不足,后续版本是如何来解决这些问题的 。

Redis消息队列发展历程

文章插图
 
一 Redis 1.0 list从广义上来讲消息队列就是一个队列的数据结构,生产者从队列一端放入消息,消费者从另一端读取消息,消息保证先入先出的顺序,一个本地的list数据结构就是一个进程维度的消息队列,它可以让模块A写入消息,模块B消费消息,做到模块A/B的解耦与异步化 。但想要做到应用级别的解耦和异步还需要一个消息队列的服务 。
 
1 list的特性 
Redis 1.0发布时就具备了list数据结构,应用A可以通过lpush写入消息,应用B通过rpop从队列中读取消息,每个消息只会被读取一次,而且是按照lpush写入的顺序读到 。同时Redis的接口是并发安全的,可以同时有多个生产者向一个list中生产消息,多个消费者从list中读取消息 。
 
这里还有个问题,消费者要如何知道list中有消息了,需要不断轮询去查询吗 。轮询无法保证消息被及时的处理,会增加延时,而且当list为空时,大部分轮询的请求都是无效请求,这种方式大量浪费了系统资源 。好在Redis有brpop接口,该接口有一个参数是超时时间,如果list为空,那么Redis服务端不会立刻返回结果,它会等待list中有新数据后在返回或是等待最多一个超时时间后返回空 。通过brpop接口实现了长轮询,该效果等同于服务端推送,消费者能立刻感知到新的消息,而且通过设置合理的超时时间,使系统资源的消耗降到很低 。
 
#基于list完成消息的生产和消费#生产者生产消息msg1lpush listA msg1(integer) 1#消费者读取到消息msg1rpop listA"msg1"#消费者阻塞式读取listA,如果有数据立刻返回,否则最多等待10秒brpop listA 10 1) "listA"2) "msg1"
Redis消息队列发展历程

文章插图
 
【Redis消息队列发展历程】使用rpop或brpop这样接口消费消息会先从队列中删除消息,然后再由应用消费,如果应用应用在处理消息前异常宕机了,消息就丢失了 。但如果使用lindex这样的只读命令先读取消息处理完毕后在删除,又需要额外的机制来保证一条消息不会被其他消费者重复读到 。好在list有rpoplpush或brpoplpush这样的接口,可以原子性的从一个list中移除一个消息并加入另一个list 。
 
应用程序可以通过2个list组和来完成消息的消费和确认功能,使用rpoplpush从list A中消费消息并移入list B,等消息处理完毕后在从list B中删除消息,如果在处理消息过程中应用异常宕机,恢复后应用可以重新从list B中读取未处理的消息并处理 。这种方式为消息的消费增加了ack机制 。
 
#基于2个list完成消息消费和确认#从listA中读取消息并写入listBrpoplpush listA listB"msg1"#业务逻辑处理msg1完毕后,从listB中删除msg1,完成消息的确认lrem listB 1 msg1(integer) 1
Redis消息队列发展历程

文章插图
 
2 list的不足之处 
通过Redis 1.0就引入的list结构我们就能实现一个分布式的消息队列,满足一些简单的业务需求 。但list结构作为消息队列服务有一个很致命的问题,它没有广播功能,一个消息只能被消费一次 。而在大型系统中,通常一个消息会被下游多个应用同时订阅和消费,例如当用户完成一个订单的支付操作时,需要通知商家发货,要更新物流状态,可能还会提高用户的积分和等级,这些都是不同的下游子系统,他们全部会订阅支付完成的操作,而list一个消息只能被消费一次在这样复杂的大型系统面前就捉襟见肘了 。
 
可能你会说那弄多个list,生产者向每个list中都投递消息,每个消费者处理自己的list不就行了吗 。这样第一是性能不会太好,因为同一个消息需要被重复的投递,第二是这样的设计违反了生产者和消费者解耦的原则,这个设计下生产者需要知道下游有哪些消费者,如果业务发生变化,需要额外增加一个消费者,生产者的代码也需要修改 。


推荐阅读