Redis数据结构之list列表
一.list列表
列表相当于数组或者顺序表
它里面的元素是有序的,也就是可以通过下标进行访问。这里的有序的含义是要根据上下文区分的,有的时候,有序指的是升序/降序,有的时候有序指的是顺序很关键,俩个元素交换后就不是同一个集合了。这里的list就是第二种,对应的下标有对应的值。
所以说,同样的一个词,怎么解释要看上下文。就好比栈/堆,是数据结构?还是操作系统的?还是JVM的?
也比如同步,是同步互斥的同步?还是同步异步的同步?
这里的list并不是一个简单的数组,它的底层接近于双端队列deque:
如上图,这个list支持头尾高效的删除插入元素,所以可以将list当成一个栈/队列来使用。
redis有一个典型的应用场景,就是作为消息队列,最早的时候就是通过list类型,只不过后来redis又提供了stream类型
list列表中的元素是允许重复的,但是像hash这样的类型,field是不能够重复的(因为它获取value就是根据field,如果重复了就不知道获取哪个value了;而list就是根据下标进行访问)。
二.相关命令
1.lpush
一次可以插入一个或多个元素,而且是按照顺序依次进行头插,所以全部插入完毕后,最后写下的匀速就是在list的最前边
时间复杂度:O(1)
返回值:list的长度
如果key已经存在,而且对应的value类型不是list,就会报错
2.lrange查看对应的list中指定范围的元素
lrange key start stop 注意,这里的区间是左右都闭
下标支持负数
如果给定的区间非法,比如超出了范围,他就会尽可能地获取到对应的内容如下:
这体现了程序的容错能力,也体现了鲁棒性(你对我越粗鲁,我就表现得越棒)
3.lpushx
与lpush不同的是,当指定的list存在时(也就是key存在),才能将元素放进去,如果list不存在,就直接返回
list2先前没有创建,所以push不进去
4.rpush
其实lpush是left push,所以rpush就是right push,也就是尾插啦
5.lpop rpop
左删和右删
返回值是删除的元素
时间复杂度都是o(1)
小结一下,redis中的list是一个双端队列,俩头插入/删除的时间复杂度都是O(1),搭配使用rpush和lpop就相当于队列(尾进头出),搭配使用rpush和rpop就相当于栈(尾进尾出)。
6.lindex
lindex key index 给定下标,获取对应的元素
时间复杂度是O(N),其实这里的N指的是list中的元素个数,当元素很少时,就能看做O(1)
下标非法就返回nil
7.linsert
linsert key before/after pivot element 在指定元素pivot之前/后插入元素element
返回值是插入之后的list的长度
8.llen
返回list的长度
时间复杂度:O(1)
9.lrem
lrem key count element 指定删除对应的key中的count个element,如果不够count个,那就能删几个删几个
count>0 那就从head开始删
count<0那就从tail开始删
count=0那就删除所有的element
10.ltrim
ltrim key start stop 保留start到stop之间的元素
11.lset
lset key index element 根据下标,修改元素
lindex对于下标的越界访问能够很好的处理,直接返回nil,但是对于lset来说,则会报错
时间复杂度:O(1)
12.阻塞版本
首先讲一下什么是阻塞:就是当前的线程不走了,代码不继续执行了,会在满足一定的条件之后被唤醒。
阻塞版本的头尾删除是:blpop brpop
当list不为空时,blpop和brpop就和lpop rpop效果完全一样;但当为空时,b版本的就会阻塞,直到再次插入元素。
咱们讲过阻塞队列(BlockingQueue)。多线程的时候,讲过一个生产者消费者模型,就是使用队列作为交易场所,期望这个队列右两个特性:1.线程安全2.阻塞->如果队列为空,尝试出队列就产生阻塞,如果队列为满,尝试入队列,就产生阻塞,直到队列不满解除阻塞。
redis中的list也相当于阻塞队列一样。线程安全是通过单线程模型支持的,阻塞则只支持队列为空的条件,不支持队列为满。
阻塞版本会根据timeout阻塞一段时间,但是阻塞期间可以执行其他指令。可以显示设置阻塞时间,超过后就自动返回。
blpop key [key……] timeout
先看指定一个key的版本,打开两个客户端
第一个窗口中先执行blpop,设定时间为100秒
然后第二个窗口里个该list设值
在回到第一个窗口,发现blpop出现了结果,返回值是list的名称加删除的结果加使用的时间
如果针对多个list进行操作,那么他也会尝试获取,但要是都没有元素,那就是最先插入元素的客户端会得到弹出的元素,但如果都有元素,那就是返回最先执行的客户端中的元素
先看都为空的情况:
再看都为满的情况:
如上,最先执行blpop的是lt2,所以返回lt2的
这俩个阻塞命令的用途主要就是作为消息队列,可以一定程度慢则消息队列这样的要求,但是整体来说还是有局限的
三.内部编码
列表的内部编码有两种:
1.ziplist压缩列表,当列表的元素个数小于list-ziplist-entries的配置时(默认是512个)同时列表中每一个元素的长度都小于list-max-ziplist-value的设置是,就会选用ziplist的内部编码方式来减少内存消耗
2.linkedlist,除上述之外都是linkedlist
但现在已经不适用这种方式了,而是直接使用quicklist,quicklist相当于是列表和压缩列表的结合体,就是每一个压缩列表都不要太大,同时再把多个压缩列表用链式结构连起来
四.list应用场景
1.list作为数组
list作为数组存储多个元素
在MySQL中,如下表示学生和班级的信息
如果使用redis,就可以像下面一样:
也就是每一个学生/班级对应一个list,每一个list存放的都是学生信息/班级信息。
2.redis作为消息队列
一个列表,多人获取:
谁先执行brpop这个命令,谁就先拿到新来的元素。
像这样的设定就能够达到“轮询式”的效果:假设消费者执行的顺序是1,2,3,当新元素到达后,消费者1就先拿到元素并且退出,如果消费者1还想再钠元素,就要重新执行brpop命令;此时再来一个新元素就是消费者2拿到元素,它想再拿到元素就得重新执行brpop命令;再来一个新元素就是消费者3拿到了~~,就好像食堂排队打饭一样
多个列表,多人获取:
多个列表/频道,这种场景非常常见,就比如抖音/快手~~
有一个通道是传输视频数据,还有一个通道传输弹幕,还有一个频道传输点赞、转发、收藏数据,还有一个频道传输评论数据
搞成多个频道,就可以在某种数据发生问题时,不会对其他频道的数据产生影响(解耦合)
就比如上图中,brpop key1 key2的key1阻塞了,但是对key2不会有影响,还能够拿到数据,堆key3也不会有影响,最大限度地减少了阻塞带来的风险
3.微博timeline
每一个用户都有属于自己的timeline(微博列表),现在需要分页展示文章列表,就可以使用到list。因为list不单是有许多,同时支持按照索引范围获取元素
首先,每一篇微博使用hash结构存储(一篇微博对应一个hash,一个field对应一个value)
其次,要向用户添加微博,user:<uid>:mblogs就是微博的键
最后,分页获取用户的timeline
但是当前一页中要显示多少数据,不确定,就可能会到这上面的循环次数比较多,从而会触发很多次hgetall,也就是很多次网络请求。此时可以考虑使用pipeline(流水线)模式批量提交用户命令,或者微博不采用hash进行存储,而是用字符串形式,然后用mget来获取(但一般不这样)
pipeline:流水线/管道,多个redis命令合并为一个网络请求进行通信,大大降低了客户端和服务器之间的交互次数