首尾相接的双向链表,链表首尾操作时间复杂度为O(1) ;查找中间元素时间复杂度为 ;O(n)。
列表中数据可能会被压缩:
所以一个占用内存很大答结构,可能会被redis压缩成多个ziplist,即quicklist。
具体压缩后如何提高性能,可以看回这篇文章:redis-----01-----redis介绍(redis安装下载、底层存储结构原理剖析)
# 从队列的左侧入队一个或多个元素。如果 key 不存在,那么在进行 push 操作前会创建一个空列表。 如果 key 不是一个 list 的话,那么会返回一个错误。
# 返回值integer-reply: 在 push 操作后的 list 长度。
LPUSH key value [value ...]
# 从队列的左侧弹出一个元素。
# 返回值bulk-string-reply: 返回这个被弹出元素的值,或者当 key 不存在时返回 nil。
LPOP key
# 从队列的右侧入队一个或多个元素。如果 key 不存在,那么在进行 push 操作前会创建一个空列表。 如果 key 不是一个 list 的话,那么会返回一个错误。
# 返回值integer-reply: 在 push 操作后的 list 长度。
RPUSH key value [value ...]
# 从队列的右侧弹出一个元素。
# 返回值bulk-string-reply: 返回这个被弹出元素的值,或者当 key 不存在时返回 nil。
RPOP key
# 返回从队列的 start 和 end 之间的元素 下标从0开始。
# 注意:超过范围的下标时:当下标超过list范围的时候不会产生error。
# 如果start比list的尾部下标大的时候,会返回一个空列表。
# 如果stop比list的实际尾部大的时候,Redis会当它是最后一个元素的下标。
# 返回值array-reply: 返回指定范围里的列表元素。
LRANGE key start end
# 从存于 key 的列表里移除前 count 次出现的值为 value 的元素。
# count > 0: 从头往尾移除值为 value 的元素。
# count < 0: 从尾往头移除值为 value 的元素。
# count = 0: 移除所有值为 value 的元素。
# 返回值integer-reply: 被移除的元素个数。
# 注意:如果key不存在或者key里面没有value值,那么它们就会被当作空list处理,所以它们都会返回 0。
LREM key count value
# 它是 RPOP 的阻塞版本,因为这个命令会在给定list无法弹出任何元素的时候阻塞连接。
# BRPOP指的是:block right pop。
# 返回值多批量回复(multi-bulk-reply): 具体来说:
# 1)当没有元素时被弹出时返回一个 nil 的多批量值,并且 timeout 过期。
# 2)当有元素弹出时会返回一个双元素的多批量值,其中第一个元素是弹出元素的 key,第二个元素是 value。
# 这个命令是比较重要的。
BRPOP key [key ...] timeout
2.2.2 演示LREM
可以看到,当我们想删除的count多于list的实际个数时,那么redis会删除list中的所有的这个value,但不会报错。redis删除个数可以根据返回值查看。
验证LREM的count的情况:
验证LREM的注意点:
2.2.3 延时BRPOP
这演示之前,先重点讲一下BRPOP的阻塞,BRPOP的 阻塞是阻塞在连接上面,而不是阻塞在redis本身。例如下图,有3个后端服务器连接了同一个redis服务器。假设s1进行brpop并阻塞住了,此时s2、s3发送其它命令例如都是get xxx,那么redis可以处理s2、s3的请求。
这里就可以看到,如果阻塞是阻塞在redis,那么有连接进行brpop,其它连接就必须等待,这种设计是不合理的,所以阻塞肯定是阻塞在后端服务器与redis之间的连接上面,而非阻塞在redis服务器本身。
演示BRPOP的返回值:
# 测试方法,首先在一个客户端输入,超时时间按照自己需要设置即可,我这里30s。
BRPOP list 30
# 然后开启另一个客户端,往key=list这个链表push数据,好让上面的客户端在超时时间内可以返回。
LPUSH list hello
结果,其中左边的客户端的BRPOP的返回值的意思:list代表key。hello代表BRPOP获取到的内容value。9.05s代表获取到这个内容的时间。因为我是间隔了9.05s才输入LPUSH list hello这个命令的。
LPUSH + LPOP
# 或者
RPUSH + RPOP
LPUSH + RPOP
# 或者
RPUSH + LPOP
在redis中,队列按照常见的场景,可以再分为异步队列和阻塞队列。
1)异步队列:异步就是我在干着一件事情的同时可以干另一件事。例如下面,web可能不断的往redis的队列产生消息,此时后端服务器会不断循环的从redis队列pop消息。这样web的产生和server的消费相当于redis队列同时干着不同的事情。 web和server相当于两个线程,一个线程在生产,一个在消费。
但是这个"线程安全"问题,需要客户端自己解决,因为redis本身是单线程的,所以自己是安全的,但由于redis本身就是一个共享资源,所以要做到线程安全,需要web、server对redis这个共享资源进行加锁。这个锁是web、server自己内部处理的,和redis无关,让两者在同一时间内只能有一个客户端操作用户状态。
不过加锁我们就需要考虑锁粒度、死锁等问题了,无疑添加了程序的复杂性,不利于维护。
这里简谈了一下redis如何做到“线程安全”的问题。
2)阻塞队列(blocking queue):因为上面的异步队列的server需要不断轮询redis的队列有无消息,这必然导致CPU增高,所以我们可以使用阻塞的方式来pop,即BRPOP,这样当没有数据时,我们可以阻塞等待,从而不占用CPU,但是这样我们在程序中需要额外使用一条redis连接,以此来区别操作redis命令的连接,不然你阻塞住了,你其它redis命令就无法输入了。
LPUSH + BRPOP
# 或者
RPUSH + BLPOP
对于栈和队列的redis实现命令,可以简单使用口诀记住,但前提必须是理解了这层含义。
口诀:栈同队不同
在某些业务场景下,需要获取固定数量的记录。例如微信固定只展示最近的前5条朋友圈,或者游戏的战绩固定只显示最近的50条。
例如下面以固定只展示最近的前5条朋友圈为例子:
lpush says '{["name"]:"tyy1", ["text"]:"Happy Spring Festival!", ["picture"]:["url://image-20220601172741434.jpg", "url://image- 20220601172741435.jpg"], timestamp = 1231231230}'
lpush says '{["name"]:"tyy2", ["text"]:"Happy Spring Festival!", ["picture"]:["url://image-xxx.jpg", "url://image- xxx.jpg"], timestamp = 1231231231}'
lpush says '{["name"]:"tyy3", ["text"]:"Happy Spring Festival!", ["picture"]:["url://image-xxx.jpg", "url://image- xxx.jpg"], timestamp = 1231231232}'
lpush says '{["name"]:"tyy4", ["text"]:"Happy Spring Festival", ["picture"]:["url://image-xxx.jpg", "url://image- xxx.jpg"], timestamp = 1231231233}'
lpush says '{["name"]:"tyy5", ["text"]:"hello world", ["picture"]:["url://image-xxx.jpg", "url://image-xxx.jpg"], timestamp = 1231231234}'
lpush says '{["name"]:"tyy6", ["text"]:"hello world", ["picture"]:["url://image-xxx.jpg", "url://image- xxx.jpg"], timestamp = 1231231235}'
# 因为我们使用lpush进行插入链表,也就是左边的元素必定是最近插入也就是最新的,所以我们每次裁剪左边的5个元素即可保证固定窗口记录。
# 也就是最新的5条朋友圈。
# 裁剪最近5条记录
ltrim says 0 4
# 查看list是否是保存着最新的记录。
lrange says 0 -1
那么看到这里你就会处理 游戏的战绩固定只显示最近的50条 的功能,非常简单。
结果,因为tyy2、tyy3、tyy4、tyy5、tyy6是最近插入的,所以裁剪后肯定剩下它们,而tyy1是最先插入的,不在最近5条的范围内,所以它会被删除掉。
# 下面两条命令如何做到原子执行呢?
# lpush可能会执行多次再裁剪,这里举例为一次而已。
lpush says '**'
ltrim says 0 4
上面2.3的例子知道,只要我每次lpush完数据,就使用ltrim进行裁剪,就能做到固定窗口记录。
但是问题来了,由于redis是单线程的,在lpush完数据后,ltrim裁剪之前,可能也有其它redis连接在操作这个链表,导致数据不同步,进而获取不到自己想要的结果,所以需要确保这两个命令原子执行。
那我们如何确保这两个命令能按照原子操作来执行呢?
答:实际应用过程中,需要保证命令的原子性,所以需要使用 lua 脚本或者使用 pipeline 命令 + 事务。这个会在redis后续的文章讲到。