redis的数据类型:List

发布于:2025-09-08 ⋅ 阅读:(21) ⋅ 点赞:(0)

文章目录

list类型简介

在这里插入图片描述

list内部(编码方式)并非是一个简单的数组,而是更接近于“双端队列”(deque) ,可以在两端进行高效的插入删除操作(时间复杂度均为O(1))
2.

List列表中的元素是有序的,如何理解有序?

有序意味着我们可以通过索引下标获取某个元素或者某个范围的元素列表
在这里插入图片描述

例如我们要获取上图中的第 5 个元素e,可以执行 :
lindex user:1:messages 4 或者lindex user:1:messages -1 就可以得到元素 e

“有序”的含义,要根据上下文区分

  • 有的时候,谈到有序,指的是“升序”,“降序”
  • 有的时候,谈到的有序,指的是,顺序很关键。即如果把元素位置颠倒,顺序调换,此时得到的新的List和之前的List是不等价的!!

同样一个词,怎么理解,务必要结合上下文,结合具体场景
比如栈 / 堆. (数据结构的,操作系统的,JVM的)
同步 (同步和互斥的同步,还是同步和异步的同步)

List获取操作与List删除操作的区别和联系是什么?

联系:这俩操作都会返回对应的元素,List获取操作是仅仅获取,List删除是删除对应位置的元素,并把这个元素返回给用户
在这里插入图片描述

区别:上图的 lrem 1 b 是从列表中把从左数遇到的前 1 个 b 元素删除,这个操作会导致列表的长度从 5 变成 4;但是执行 lindex 4 只会获取元素,但列表长度是不会变化的。

LIST列表中的元素允许重复吗?

列表中的元素是允许重复的,例如下图列表中就包含了两个 a 元素。
在这里插入图片描述

对于Redis中很多类型像hash、set,field是不能重复的,如果set一个已有的field,只能是覆盖写,而List的元素可以重复,因为它们是通过数据下标来区分的

因为当前的List,头和尾都能高效的插入删除元素,就可以把这个List当做一个栈/队列来使用。Redis有一个典型的应用场景,就是作为消息队列。最早的时候,就是通过List类型~~后来Redis又提供了一个stream类型,专门用来做消息队列

List类型指令介绍

LPUSH

LPUSH中的L是left的缩写,意思是从链表的最左边插入
语法:LPUSH key element1 element2 …

LPUSH一次可以插入一个元素,也可以插入多个元素。假如说我LPUSH key 1 2 3 4 ,请问插入完毕之后,谁在最前面?谁在最后面?

LPUSH key element [element …] 一次可以插入一个元素,也可以插入多个元素。1 2 3 4按照顺序,依次头插这几个元素。全都插入完毕,4是在最前面,1在最后面

LPUSH插入的时间复杂度是多少?指令执行之后的返回值是什么?

时间复杂度 O(1),返回值是list的长度。

如果LPUSH key 1 2 3 4 执行之前,key已经存在,那么执行结果是什么?

  • 如果key已经存在,并且key对应的value类型,不是list,此时lpush命令就要报错。
  • 如果在执行 LPUSH key 1 2 3 4 之前,key 已经存在且是一个列表(list)类型,那么执行后会将新元素 1、2、3、4 依次插入到列表的头部(左侧)

注意:LPUSH 的特性是左侧插入,且多个元素会按参数顺序依次插入(即 1 先插入,2 插在 1 的左侧,以此类推,最终 4 会在最左侧)。

LPUSHX

语法:LPUSHX key element [element …]
时间复杂度:O(1)
返回值:插入后list的长度。

LPUSHX 与LPUSH的区别是什么?

LPUSHX只有在 key 存在时,才会将一个或者多个元素从左侧放入(头插)到 list 中。不存在,直接返回0。
LPUSH在key不存在时,会自动创建一个List,然后将一个或者多个元素从左侧放入(头插)到 list 中

RPUSH

RPUSH 与LPUSH的区别是什么?

LPUSH是从左边插入,相当于头插
RPUSH是从右边插入,相当于尾插

RPUSH的作用是什么?怎么用?时间复杂度是多少?返回值是什么?

RPUSH作用:将一个或者多个元素从右侧放入(尾插)到 list 中。
语法:RPUSH key element1 element2 …
时间复杂度:O (1)
返回值:返回插入后 list 的长度

假如说我RPUSH key 1 2 3 4 5 6 7 8 ,请问插入完毕之后,谁在最前面?插入的时间复杂度是多少?如果这条命令执行之前,key已经存在,那么执行结果是什么?

依次尾插这几个元素,全都插入完毕,1是在最前面!!!
返回值是list的长度。如果key已经存在,并且key对应的value类型,不是list,此时lpush命令就要报错,如果key对应的value类型是list,那就要向key对应的List依次尾插1 2 3 4 5 6 7 8

RPUSHX

这个命令和LPUSH命令几乎完全相同,唯一的不同就在于,RPUSHX是尾插,LPUSHX是头插
这个命令与RPUSH的区别在于,RPUSHX只有在key存在时,才能完成尾插的操作,如果list不存在,直接返回0

LPOP与RPOP

LPOP key对key对应的List进行头删操作
RPOP key对key对应的List进行尾删操作
LPOP/RPOP时间复杂度:O(1)
LPOP/RPOP返回值: 返回删除的元素,如果没东西删就返回nil

Redis中的list是一个双端队列,从两头插入/删除元素都是非常高效~~O(1)
搭配使用rpush和lpop,就相当于队列了。搭配使用rpush和rpop,就相当于栈了。

当使用 LPOP 命令时,如果指定的 key 不存在,Redis 会怎样处理?

Redis 不会报错,而是会返回 nil
这是因为 Redis 将不存在的 key 视为空列表,LPOP 操作在空列表上执行时,自然没有元素可移除和返回,所以返回 nil 来表示这种情况。

LPOP的count参数

LPOP和RPOP在当前的redis 5版本中,都是没有count参数。从redis 6.2版本,新增了一个count参数,指令变成了LPOP key count,count就描述这次要删几个元素

lrange

lrange 命令用于查看 list 中指定范围的元素
语法:LRANGE key start stop
此处描述的区间也是闭区间,即[start, stop],下标支持负数,-n 表示从列表尾部开始数的第n 个元素。

既然有lrange,那有没有rrange呢?

这里的lrange是 “list range” 的意思,用于获取 list 中指定范围的元素 ,不是 “left range”
但是Redis里面确实有不少这样的对应指令,比如lpush 和 rpush、lpop 和 rpop

如何查看list中的所有元素?

LRANGE key 0 -1,即start=0,stop=-1
在这里插入图片描述

如果按照插入的顺序来排,8的数组下标不应该是0吗,为什么是1?
此处的序号仅仅是标识下返回元素的顺序,和下标无关。hash类型就没有下标的概念~
在这里插入图片描述

在Redis的List中,如果下标超出范围,Redis会怎么处理呢?

谈到下标,往往会关注超出范围的情况
C++中,下标超出范围,一般会认为这是一个“未定义行为”,而未定义行为导致的后果是不可控的,就跟开盲盒一样,既有可能会导致程序崩溃,也可能会得到一个不合法的数据,还可能会得到一个看起来合法但是错误的数据,也有可能恰好得到一个符合要求的数据……

缺点:程序猿不一定能第一时间发现问题!!
优点:效率是最高的!!

而Java中,下标超出范围,一般会抛出异常多出一步下标合法性的验证~~
缺点:速度比上面要慢
优点:出现问题能及时发现.
思考一下:是机器执行的快重要?还是程序猿代码写的快重要??
肯定是程序猿代码写的快重要,因为和我们利益相关,所以JAVA的优化对我们来说是更友好了

在Redis中并没有采用上述的两种设定,Redis的做法,是直接尽可能的获取到给定区间的元素。如果给定区间非法,比如超出下标,就会尽可能的获取对应的内容,使得程序的鲁棒性很强
在这里插入图片描述

什么叫鲁棒性?“鲁棒性”就是你对我越粗鲁,我就表现的越棒,鲁棒性强就意味着程序的容错能力更强~~

执行lrange key 0 -10的时候,若列表中的元素数量少于 10 个,执行该指令的结果是什么?

执行lrange key 0 -10的时候,若列表中的元素数量少于 10 个,那么 stop 索引会被视为超出范围,此时该命令会返回从第一个元素到列表最后一个元素的所有元素,也就是整个列表。

列表不存在:
如果 key 对应的列表不存在,该命令会返回一个空列表。

lindex

作用:给定下标,返回对应下标的元素
语法:lindex key index

  • key用于指定List
  • index用于指定list中的下标

时间复杂度O(N) 此处N指的是list中的元素个数.
如果下标非法,返回nil

linsert

作用:在特定位置插入元素。
时间复杂度:O (N)
返回值:返回插入之后list的 长度。
语法:LINSERT key <BEFORE | AFTER> pivot element

  • 其中<BEFORE | AFTER>表示一个必选选项,你需要从中选择一个。BEFORE 表示在基准元素之前插入新元素;AFTER 表示在基准元素之后插入新元素。
  • pivot:基准元素,也就是作为参考的元素。命令会在列表中查找这个元素,然后根据你选择的 BEFORE 或 AFTER 选项,在相应位置插入新元素。
  • element:是要插入到列表中的新元素
  • key是某个List类型的键值

万一要插入的列表中,基准值pivot存在多个(就你在List中可以找到多个值为pivot的元素,没法确定下标),咋办?

linsert进行插入的时候,要根据基准值,找到对应的位置。从左往右找,找到第一个符合基准值的位置,从这里插入。(以从左到右第一个pivot的下标为准)

LLEN

语法:LLEN key
作用:返回由 key 标识的List列表的长度。
返回值:正常情况下返回由 key 标识的List的长度,如果 key 不存在,返回 0;如果 key 对应的值不是列表类型,返回nil

lrem

语法:LREM key count element

  • key:列表的键名

  • count:移除元素的数量,有三种情况:

    • 当 count 大于 0 时,从列表头部开始向尾部搜索,移除与 element 相等的元素,一共移除 count 个。(如果重复元素不够count个,则能删多少删多少)
    • 当 count 小于 0 时,从列表尾部开始向头部搜索,移除与 element 相等的元素,一共移除 |count| 个(|count| 表示 count 的绝对值)。(如果重复元素不够count个,则能删多少删多少)
    • 当 count 等于 0 时,移除列表中所有与 element 相等的元素。
  • element:要从列表中移除的元素值。

此命令的作用是从指定列表(由 key 标识)里移除与给定值 element 相等的元素,移除多少个这样的重复元素由 count 参数决定。(lrem中的rem表示 remove )
时间复杂度:O(N+M),N是列表总长度,M是移除元素个数

LTRIM

作用:LTRIM 命令用于修剪(截断)列表,通过保留[start, stop]内的元素,移除列表中其他元素,从而改变列表的长度和内容。
语法:LTRIM key start stop

  • key:指定要操作的列表的键名,用于在 Redis 中定位对应的列表数据结构。
  • start:表示保留元素范围的起始索引。索引从 0 开始计数,负数表示从列表尾部开始计数,如 -1 表示最后一个元素。
  • stop:表示保留元素范围的结束索引,同样支持正数(从 0 开始计数)和负数(从列表尾部计数)。

时间复杂度为 O(N)

ACL catagories

Redis有很多命令,把每个命令都打上对应的ACL catagories(比如这个命令需要读权限和写权限),打好所有的标签之后,管理员就可以给每个redis用户配置不同的权限(允许该用户执行哪些标签对应的命令)
比如:LTRIM的ACL catagories是write、List、slow,如果我给一个用户禁掉了write权限,那所有ACL catagories中含有write的命令,该用户都不能执行

lset

语法:LSET key index element
作用:根据下标修改元素,LSET 命令可以将key对应列表中下标index指向的元素替换为新的值element。如果执行成功,列表中该索引位置的元素就会被更新为你指定的值。

如果LSET key index element命令中,index越界了,结果会怎样?

会报错,而lindex中index越界,并不会报错,而是返回nil

BLPOP与BRPOP

语法:BLPOP key [key ...] timeout
blpop和brpop是lpop和rpop的阻塞版本,在列表中有元素的情况下,阻塞和非阻塞表现是一致的。但如果列表中没有元素,非阻塞版本会立即返回nil,但阻塞版本会根据timeout,阻塞一段时间,期间Redis可以执行其他命令,但要求执行该命令的客户端会表现为阻塞状态

BLPOP就是在LPOP列表头插的基础上,加了一个阻塞机制(阻塞Block)

  • 如果list中存在元素,blpop和brpop就和lpop以及rpop作用完全相同。
  • 如果list为空,blpop和brpop就会被阻塞,一直等到list不为空,就会被唤醒去执行对应的Pop操作

BLPOP与BRPOP这俩阻塞命令主要用来干啥呢?

这俩阻塞命令用途主要就是用来作为"消息队列"
当前这俩命令虽然可以一定程度的满足"消息队列"这样的需求. 整体来说,这俩命令功能还是比较有限,没有专业软件方便
blpop和brpop有些英雄迟暮~~ 以后用途越来越少了

参数timeout

使用 brpop 和 blpop 的时候,是可以显式设置阻塞时间timeout的!!!意思就是说,timeout指定阻塞等待时间,表示我等你这么长的时间,在这段时间期间, blpop 操作陷入阻塞状态,等待队列不为空时,将 blpop 操作唤醒,期间Redis可以执行其他命令。但如果在这段时间之内,blpop 操作一直没有得到唤醒,到时间之后就不再等待,Redis 立即返回 nil 给客户端。(是不是感觉有些熟悉?这个timeout参数的含义与epoll里面timeout的含义完全一样!)
由于期间Redis可以执行其他命令,所以此处的blpop和brpop看起来好像耗时很久,但是实际上并不会对redis服务器产生负面影响!!

BLPOP的多路转接

BLPOP还可以指定一个key或者 多个key,每个key都对应一个list(这不就是典型的多路转接方案嘛)
如果这些list有任何一个非空,blpop都能够把这里的元素给获取到. 立即返回.如果这些list都为空,此时就需要阻塞等待,等待其他客户端往这些list中插入元素了.
此处还可以指定 超时时间timeout . 单位是 秒 (Redis 6, 超时时间允许设定成小数. Redis 5中, 超时时间, 得是整数)

BLPOP的返回值

返回的结果相当于一个pair (二元组),第一个值是告诉我们当前的数据来自于哪个key,第二个值是告诉我们pop的数据是谁~~
在这里插入图片描述

BLPOP 命令中如果设置了多个键,即BLPOP key1 key2… timeout,请问最终Pop时,是会对每个List都做一次头删吗?

这个依旧与多路转接方案保持一致,只要等到了一个,就立即对其进行POP,然后返回。

  • 如果BLPOP key1 key2… timeout里面的所有key指向的List都为空,那这条命令会开始阻塞等待,timeout指定阻塞等待的时间,如果在阻塞等待期间,一旦有一个键对应的列表不为空,可以Pop,则我们就立即执行Pop,然后返回。注意,就返回这一个值,并不是要Pop所有的List,而是只要我等待的这么多List中,有一个list可以Pop,我就Pop一下,然后该命令结束
  • 举个例子:今晚上我要吃饭,我约了三个女神,哪个女神回复我了,我就去跟她一块吃饭,如果有多个女神同时回复我,那我也只能选一个去一块吃饭,可不敢让劈腿被发现

如果多个客户端同时对一个key执行pop,请问这些客户端都能获得pop出的元素吗?

只有最先执行命令的客户端会得到弹出的元素。
举个例子:同一个女神,但是有多个屌丝去约,当女神有空了(队列不为空了)的时候,哪个人先约的,哪个人就能先约到女神

BLPOP与LPOP简单对比

  1. 列表为不空,timeout=5,5s内没有新元素加入
    在这里插入图片描述

2.列表不为空,timeout=5,5s内有新元素加入
在这里插入图片描述

命令总结

操作类型 命令 时间复杂度
添加 rpush key value [value ...] O ( k ) O(k) O(k) k k k 是元素个数
添加 lpush key value [value ...] O ( k ) O(k) O(k) k k k 是元素个数
添加 linsert key before | after pivot value O ( n ) O(n) O(n) n n npivot 距离头尾的距离
查找 lrange key start end O ( s + n ) O(s+n) O(s+n) s s sstart 偏移量, n n nstartend 的元素个数
查找 lindex key index O ( n ) O(n) O(n) n n n 是索引的偏移量
查找 llen key O ( 1 ) O(1) O(1)
删除 lpop key O ( 1 ) O(1) O(1)
删除 rpop key O ( 1 ) O(1) O(1)
删除 lrem key count value O ( k ) O(k) O(k) k k k 是元素个数
删除 ltrim key start end O ( k ) O(k) O(k) k k k 是元素个数
修改 lset key index value O ( n ) O(n) O(n) n n n 是索引的偏移量
阻塞操作 blpop brpop O ( 1 ) O(1) O(1)

list类型内部编码

以前版本list类型有两种内部编码,ziplist和linkedlist,现在我们主要就是使用quicklist
在这里插入图片描述

quicklist 相当于是 链表 和 压缩列表 的结合.整体还是一个链表,链表的每个节点,都是一个压缩列表。

  • 每个压缩列表,都不让它太大
  • 如果一个节点的压缩列表大小超出了阈值,我们就会将其拆分成多个,最后把多个压缩列表通过链式结构连起来

你上面说,每个节点中的压缩列表,都不能让它太大,那最大是多少呢?
用redis.conf中的list-max-ziplist-size来设置,比如下图中list-max-ziplist-size设置为-2,那么每个节点最多是8Kb

在这里插入图片描述

list类型的应用场景

使用Redis作为消息队列

生产者消费者模型

在这里插入图片描述
在上图中,谁先执行的这个brpop命令,谁就能拿到这个新来的元素。这样的设定,就能构成一个"轮询"式的效果.

  • 假设消费者执行顺序是1 2 3,当新元素到达之后,首先是消费者1拿到元素. (按照执行brpop命令的先后顺序来决定谁获取到的)。消费者1拿到元素之后,也就从brpop中返回了.(相当于这个命令就执行完了)
  • 如果消费者1还想继续消费,就需要重新再执行一句brpop.此时,三个消费者中,消费者1的brpop操作已经是最晚的了,所以分配的优先级是最低的。
  • 生产者在生产一个新元素到列表中,此时队列中最先执行brpop的是消费者2,所以由消费者2拿到该元素也从brpop中返回~~
  • 如果消费者2还想继续消费,也需要重新执行brpop.如果再来一个新元素,那么这时候队列中最先执行brpop的是消费者3,所以拿到这个元素的就是消费者3

这样就完成了一个轮询式的效果

在redis中,有很多个这样的List列表,充当消息队列,最终的生产者消费者模型大概是这样的
在这里插入图片描述

像上面这种多个列表/频道的场景,是非常常见的——比如日常使用的一些程序,抖音,有一个通道(List),来传输短视频数据,还可以有一个通道(list),来传输弹幕,还可以有频道,来传输 点赞,转发,收藏数据还可以有频道,来传输 评论 。
把数据搞成多个频道(list),就可以在某种数据发生问题的时候不会对其他数据造成影响. (解耦合)

微博 Timeline

每个用户都有属于自己的 Timeline(微博文章列表),现需要分页展示文章列表。此时可以考虑使用列表,因为列表不但是有序的,同时支持按照索引范围获取元素。
1)每篇微博使用哈希结构存储,例如微博中 3 个属性:title、timestamp、content,利用hmset不断地向Hash表中插入新的mblog

hmset mblog:1 title xx timestamp 1476536196 content xxxxx
...
hmset mblog:n title xx timestamp 1476536196 content xxxxx

2)把所有属于同一个user的blog插入到对应user的mblogs列表(List)下。向用户 Timeline 添加微博,user::mblogs 作为微博的键:

lpush user:1:mblogs mblog:1 mblog:3
...
lpush user:k:mblogs mblog:9

3)利用List中的lrangeh分页获取用户的Timeline,例如获取用户1的前10篇微博:

keylist = lrange user:1:mblogs 0 9
for key in keylist {
    hgetall key
}

此方案在实际中可能存在一个问题,就是如果key太多,那你hgetall的次数就会很多,网络请求的次数也比较多,IO次数一多了,那速度就慢下来了,这个问题应该怎么解决呢?

  • 此时可以考虑使用 pipeline(流水线)模式批量提交命令,即多条IO命令一次性全部提交,减少了网络通信的次数
  • 或者微博不采用哈希类型,而是使用序列化的字符串类型,使用 mget 获取。

获取文章时,lrange 在列表两端表现较好,获取列表中间的元素表现较差,对此我们应该如何优化?

  • 此时可以考虑将长的列表做拆分,得到多个小列表

如何将一个List做成栈或者队列?

  1. 限制一侧的存取,就是栈
    同侧存取(lpush + lpop或者rpush + rpop)为栈
  2. 限制只能一侧存,另一侧取,就是队列
    异侧存取(lpush + rpop或者rpush + lpop)为队列

网站公告

今日签到

点亮在社区的每一天
去签到