使用 Redis 实现简单的消息队列


在没有搭建消息中间件(比如rabbitMQ、rocketMQ、kafka)的情况下,可以使用Redis来实现消息队列。

1、普通消息队列

利用的是 Redis 的 List 这个数据结构。

1-1、Redis的数据结构:List

Redis的List是链表型的数据结构,可以使用LPUSH/RPUSH/LPOP/RPOP等命令在List的两端执行插入元素和弹出元素的操作。

虽然List也支持在特定index上插入和读取元素的功能,但其时间复杂度较高(O(N)),应尽量避免使用。

1-2、List的主要操作命令

  • LPUSH:向指定List的左侧(即头部)插入1个或多个元素,返回插入后的List长度。时间复杂度O(N),N为插入元素的数量

  • RPUSH:同LPUSH,向指定List的右侧(即尾部)插入1或多个元素

  • LPOP:从指定List的左侧(即头部)移除一个元素并返回,时间复杂度O(1)

  • RPOP:同LPOP,从指定List的右侧(即尾部)移除1个元素并返回

  • LPUSHX/RPUSHX:与LPUSH/RPUSH类似,区别在于,LPUSHX/RPUSHX操作的List如果不存在(长度为0),则不会进行任何操作

  • LLEN:返回指定List的长度,时间复杂度O(1)

    扫描二维码关注公众号,回复: 9826120 查看本文章
  • LRANGE:返回指定List中指定范围的元素(双端包含,即LRANGE key 0 10会返回11个元素),时间复杂度O(N)。

1-3、List的注意事项

时间复杂度为O(N)的以下命令都应谨慎使用

  • 用LRANGE应尽可能控制一次获取的元素数量,一次获取过大范围的List元素会导致延迟,同时对长度不可预知的List,避免使用LRANGE key 0 -1这样的完整遍历操作。

  • LINDEX:返回指定List指定index上的元素,如果index越界,返回nil。index数值是回环的,即-1代表List最后一个位置,-2代表List倒数第二个位置。时间复杂度O(N)

  • LSET:将指定List指定index上的元素设置为value,如果index越界则返回错误,时间复杂度O(N),如果操作的是头/尾部的元素,则时间复杂度为O(1)

  • LINSERT:向指定List中指定元素之前/之后插入一个新元素,并返回操作后的List长度。如果指定的元素不存在,返回-1。如果指定List不存在(长度为0),不会进行任何操作,时间复杂度O(N)

由于Redis的List是链表结构的,上述命令的算法效率较低,需要对List进行遍历,命令的耗时无法预估,在List长度大的情况下耗时会明显增加,应谨慎使用。

换句话说,Redis的List实际是设计来用于实现队列,而不是用于实现类似ArrayList这样的列表的。如果不是想要实现一个双端出入的队列,那么请尽量不要使用Redis的List数据结构。

1-4、实现消息队列

(1)基础方法

既然List这种数据结构就是被设计用来实现队列的,那么就可以直接使用List来实现消息队列。

生产者使用RPUSH不停地向队列右端添加信息;消费者使用LPOP不停地从队列左端读取消息,读取不到消息时,使用SLEEP命令定期重读即可。

(2)改进SLEEP

不使用SLEEP命令,直接将LPOP命令替换为BLPOP即可,这个B代表Block,是一个阻塞式命令。List为空时,阻塞连接,直到List中有对象可获取时再返回。

这相当于Java中的BlockingQueue数据类型。

另外,BLPOP的命令参数中可以设定超时时间,timeout之后如果队列中仍然没有对象,则直接返回。

BLPOP key [key …] timeout

(3)实现订阅模式

使用 Redis 的发布订阅(pub/sub)模式,可以实现1:N的消息队列(即fanout)。

实现概要如下。

【多个消费者订阅channel(即双方约定的List)】

SUBSCRIBE fanoutChannel

【生产者向channel中发布消息】

PUBLISH fanoutChannel “msg1”
PUBLISH fanoutChannel “msg2”

【所有消费者都会收到该消息】

1) "message"
2) "fanoutChannel"
3) "msg1"
1) "message"
2) "fanoutChannel"
3) "msg2"

但是要注意,消费者下线再上线后,无法获取此期间生产的消息(无持久化)。

2、延时队列

利用的是 Redis 的 Sorted Set 这个数据结构的一点点使用技巧。

2-1、Redis的数据结构:Sorted Set

Redis Sorted Set是有序的、不可重复的String集合。

Sorted Set中的每个元素都需要指派一个分数(score),Sorted Set会根据score对元素进行升序排序。

如果多个元素拥有相同的score,则以字典序进行升序排序。

Sorted Set非常适合用于记录排名、热点话题等场景。

2-2、Sorted Set的主要操作命令

  • ZADD:向指定Sorted Set中添加1个或多个member,时间复杂度O(Mlog(N)),M为添加的member数量,N为Sorted Set中的member数量

  • ZREM:从指定Sorted Set中删除1个或多个member,时间复杂度O(Mlog(N)),M为删除的member数量,N为Sorted Set中的member数量

  • ZCOUNT:返回指定Sorted Set中指定score范围内的member数量,时间复杂度:O(log(N))

  • ZCARD:返回指定Sorted Set中的member数量,时间复杂度O(1)

  • ZSCORE:返回指定Sorted Set中指定member的score,时间复杂度O(1)

  • ZRANK/ZREVRANK:返回指定member在Sorted Set中的排名,ZRANK返回按升序排序的排名,ZREVRANK则返回按降序排序的排名。时间复杂度O(log(N))

  • ZINCRBY:同INCRBY,对指定Sorted Set中的指定member的score进行自增,时间复杂度O(log(N))

2-3、Sorted Set的注意事项

时间复杂度较高的以下命令都应谨慎使用

  • ZRANGE/ZREVRANGE:返回指定Sorted Set中指定排名范围内的所有member,ZRANGE为按score升序排序,ZREVRANGE为按score降序排序,时间复杂度O(log(N)+M),M为本次返回的member数(下同)

  • ZRANGEBYSCORE/ZREVRANGEBYSCORE:返回指定Sorted Set中指定score范围内的所有member,返回结果以升序/降序排序,min和max可以指定为-inf和+inf,代表返回所有的member。时间复杂度O(log(N)+M)

  • ZREMRANGEBYRANK/ZREMRANGEBYSCORE:移除Sorted Set中指定排名范围/指定score范围内的所有member。时间复杂度O(log(N)+M)

上述命令中应避免传递[0 -1]或[-inf +inf]这样的参数,来对Sorted Set做一次性的完整遍历,特别是在Sorted Set的尺寸不可预知的情况下。

可以通过ZSCAN命令来进行游标式的遍历

ZSCAN key cursor [MATCH pattern] [COUNT count]

或通过LIMIT参数来限制返回member的数量(适用于ZRANGEBYSCORE和ZREVRANGEBYSCORE命令),以实现游标式的遍历。

2-4、实现延时队列

思路为使用Sorted Set,拿时间戳作为score。

生产者将消息内容作为member,时间戳作为score调用ZADD来生产消息;

ZADD key score member [[score member] [score member] …]

消费者用ZRANGEBYSCORE命令获取N秒之前的数据进行轮询处理,使用min和max向前推N秒来卡延时的消息。

ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]

2-plus、使用rabbitMQ实现延时队列的思路

与 Redis 无关,但是 rabbitMQ 本身实际上也没有直接提供延时队列的功能,所以在这里多说一句,提一下在 rabbitMQ 里面实现延时队列的思路。

rabbitMQ中可以对Message设置 x-message-ttl(TTL = Time To Live)来控制消息的生存时间。超时以后消息变为dead letter(死信)。

同时,RabbitMQ的Queue可以配置x-dead-letter-exchange 和x-dead-letter-routing-key(可选)两个参数,如果队列内出现了dead letter,则按照这两个参数重新路由转发到指定的队列。

利用这样的特性,设置两个队列,A队列无消费者,生产者向该队列发送消息,消息设定TTL;同时设定A中出现死信以后将消息转发到B队列;B队列使用正常设定即可,所有消费者从B读取消息。

即可完成延时时间为TTL的延时队列。

另外,rabbitMQ中,除了超时以外,队列达到最大长度 或 者消息被消费端拒绝并且requeue=false 时,消息也会变成死信,需要注意一下别跟需要延时的消息混杂在一起。

发布了33 篇原创文章 · 获赞 19 · 访问量 3152

猜你喜欢

转载自blog.csdn.net/zmflying8177/article/details/103541978