Redis中的List类型

List的介绍

列表(List)相当于数组或者顺序表

约定最左侧元素下标为0


redis的下标支持负数下标 getrange,同时支持左侧和右侧的插入和删除,也就是头插头删,尾插尾删,list内部的结果(编码方式)并非是一个简单的数组,而是更接近于双端队列

获取List中的长度,删除元素,获取元素等等

  1. 列表中的元素是有序的,有的时候是”升序降序“,有的时候是”顺序很重要“,如果把元素位置颠倒,顺序调换,此时得到的新的List和之前的List是不等价的,所以怎么解释要结合上下文来理解;例如,栈/堆在不同场景下会有不同的理解:数据结构中的栈和堆,操作系统的栈和堆,JVM的栈和堆;同步(同步和互斥的同步,还是同步和异步的同步)
  2. 获取元素和删除元素的区别:相当于栈中的pop和peek的区别,都能拿到值,但是pop就会将值删除;在List中lindex能获取到元素的值,lrem也能返回被删除元素的值
  3. 列表中的元素是允许重复的,像hash这样的类型,field是不能重复的

因为当前的List,头和尾都能高效的插入删除元素,就可以把这个List当作栈/队列来使用的

Redis中有一个典型的应用场景,作为消息队列,最早的时候就是通过List类型,后来Redis又提供了stream类型

List中的常用命令

lpush

将一个或者多个元素从左侧放入(头插)到list中,也就是头插,例如插入1 2 3 4,那么最后头元素就是4

时间复杂度为O(1),返回值为list长度

如果key已经存在,并且对应的value类型,不是list,此时lpush命令就要报错

lrange

获取从start到stop范围之间的元素,此处描述的区间也是闭区间,下标也是支持负数的

此处的这个序号,是专门给结果集合使用的序号,和list下标无关

例如使用hset就可以验证这一点,可以看到hash操作也可能会得到这种带有序号的结果,此处的序号仅仅是标识下返回元素的顺序,和下标无关,hash类型就没有下标的概念

谈到下标往往会说到超出范围的问题

C++中可能会导致程序崩溃,也可能会得到一个不合法的数据,也可能会得到一个看起来合法,但是错误的数据,也有可能恰好得到一个符合要求的数据

优点:效率是最高的

缺点:程序员不一定能第一时间发现问题

Java中,下标超出范围会抛出异常

优点:出现问题能及时发现

缺点:多出一步下标合法性的验证

两种处理方式:机器执行快还是程序员写代码写的快?当然是程序员写代码快重要

redis中并没有采取上述两种方案,如果不是合法区间就会得到空区间

  • 此处对于越界下标的处理方式,更接近于python的处理方式(python的切片)
  • 鲁棒性:你对我越粗鲁,我就表现越棒
  • 程序的容错能力更强

lpushx

如果当前key存在,将一个或者多个元素从左侧放入到list中,不存在直接返回

时间复杂度为O(1),返回值为list长度

rpush

尾部插入一个或者多个元素,时间复杂度为O(1),返回值为list长度,也就是left和right

并没有rrange,有lrange,l代表着list

rpushx

尾部插入一个或者多个元素,如果不存在,则不插入,x代表着exists中的x

lpop

从list左侧的元素删除,也就是头删,时间复杂度为O(1),返回值为去除的元素或者null

rpop

尾删,多了一个count,在redis5版本中都是没有count参数,从redis6.2版本,新增了一个count参数,count就描述了要删除几个元素,由于当前使用redis5,所以没有count参数

redis中的list是一个双端队列,从两头插入/删除元素都是非常高效的

  1. 搭配使用rpush和lpop,就相当于队列了
  2. 搭配使用llpush和rpop,就相当于栈了

lindex

给定下表,获取到对应的元素;如果下标非法,返回的是nil

linsert

在指定位置插入元素,before/after是指定在该元素之前还是之后,只能写一个,时间复杂度为O(N),N表示列表的长度

返回值是插入之后得到的新的list的长度,万一要插入的列表中有多个基准值如何?

linsert进行插入的时候,要根据基准值,找到对应的位置,从左往右找,找到的第一个基准值即可

llen

直接获取list长度

lrem

rem就是remove的缩写,count是删除的个数,element要删除的值

  1. 假设list中为 1 2 3 4 1 2 3 4 1 2 3 4,要删除的是1
  2. 如果count>0,从左往右找,count == 2 ,此时删除第一个1和第二个1
  3. 如果count<0,从右往左找,count == -2 ,此时删除第四个1和第三个1
  4. 如果count=0,删除所有的元素

ltrim

指定范围进行删除元素,保留start和stop之内的元素,区间外面两面的元素直接被删除(闭区间)

时间复杂度为O(N),ACl是access control list也就是访问控制列表(权限),redis6及其以上才开始支持

Redis有很多命令,acl这块就把每个命令都打上一些标签,打好所有的标签之后,管理员就可以给每个redis用户配置不同的权限,允许该用户可以执行某些标签执行的命令

lset

根据下标来修改元素,时间复杂度为O(1),返回值为ok

lset可以支持J以ava方式的抛出异常

阻塞版本命令

阻塞:当前的线程,代码不继续执行了,会在满足一定条件之后被唤醒

两个命令:blpop,brpop和lpop,rpop类似,b也就是blocking

阻塞队列(BlockingQueue)可以看我之前的博客

生产者消费者模型会使用队列来作为中间的”交易场所“,这个队列我们期望有两个特性:

  • 线程安全
  • 阻塞

  • 如果队列为空,尝试出队列,就会产生阻塞,直到队列不空,阻塞解除
  • 如果队列为满,尝试入队列,也会产生阻塞,直到队列不满,阻塞解除

redis中的list也相当于阻塞队列一样,线程安全是通过单线程模型支持的,阻塞,则只支持”队列为空“的情况,不考虑”队列满“

  • 如果list中存在元素,blpop和brpop就和lpop和rpop作用完全相同
  • 如果list中为空,blpop和brpop就会产生阻塞,一直阻塞到队列不空为之
  • 但阻塞版本会根据timeout,阻塞一段时间,期间Redis可以执行其他命令(此处的blpop和brpop看起来好像耗时很久,但是实际上并不会对redis服务器产生负面影响)
  • 使用brpop和blpop的时候,这里是可以显式设置阻塞时间
  • 命令中如果设置了多个键,那么会从左向右进行遍历键,一旦有一个键对应的列表中可以弹出元素,命令立即返回
  • blpop和brpop都是可以同时去尝试获取多个key的列表的元素,多个key对应多个list,这多个list哪个有元素了,就会返回哪个元素
  •  如果多个客户端针对同一个键执行pop,则最先执行命令的客户端会得到弹出的元素

可以以抢票为例,同一张票,但是有多个人抢,当票有空的时候,哪个人先抢到,那个人就能先到目的地

blpop

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

针对一个非空的列表进行操作

返回的结果相当于一个pair(二元组)

  • 一方面是告诉我们当前的数据来自哪个key
  • 一方面告诉我们取到的数据是啥

针对一个空list进行操作

同时打开两个客户端就可以看到会阻塞住一段时间直到有数据进入

3.针对多个key进行操作,和上述情况一致

brpop

效果和blpop完全一样,仅仅是功能的不同也就是尾删,此处这里两个阻塞队列用途主要就是用来作为”消息队列“,当前这俩命令虽然可以一定程度满足”消息队列“这样的需求,整体来说,这俩功能还是比较有限,blpop和brpop有些英雄迟暮~~

List命令小结

序号命令及描述
1BLPOP key1 [key2 ] timeout
移出并获取列表的第一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。
2BRPOP key1 [key2 ] timeout
移出并获取列表的最后一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。
3BRPOPLPUSH source destination timeout
从列表中弹出一个值,将弹出的元素插入到另外一个列表中并返回它; 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。
4LINDEX key index
通过索引获取列表中的元素
5LINSERT key BEFORE|AFTER pivot value
在列表的元素前或者后插入元素
6LLEN key
获取列表长度
7LPOP key
移出并获取列表的第一个元素
8LPUSH key value1 [value2]
将一个或多个值插入到列表头部
9LPUSHX key value
将一个值插入到已存在的列表头部
10LRANGE key start stop
获取列表指定范围内的元素
11LREM key count value
移除列表元素
12LSET key index value
通过索引设置列表元素的值
13LTRIM key start stop
对一个列表进行修剪(trim),就是说,让列表只保留指定区间内的元素,不在指定区间之内的元素都将被删除。
14RPOP key
移除列表的最后一个元素,返回值为移除的元素。
15RPOPLPUSH source destination
移除列表的最后一个元素,并将该元素添加到另一个列表并返回
16RPUSH key value1 [value2]
在列表中添加一个或多个值到列表尾部
17RPUSHX key value
为已存在的列表添加值

list类型的内部编码

ziplist(压缩列表):

当列表的元素个数⼩于 list-max-ziplist-entries 配置(默认 512 个),同时 列表中每个元素的⻓度都⼩于 list-max-ziplist-value 配置(默认 64 字节)时,Redis 会选⽤ ziplist 来作为列表的内部编码实现来减少内存消耗。

linkedlist(链表):

当列表类型⽆法满⾜ ziplist 的条件时,Redis 会使⽤ linkedlist 作为列表的内 部实现。

上述内容仅供参考,现在redis5并不采取上述内部编码方式

ziplist

把数据按照更紧凑的压缩形式进行表示的,节省空间,当元素个数多了,操作起来效率会降低

当元素个数多了就会采取linkedlist-双向链表

quicklist

相当于链表和压缩列表的结合,整体还是一个链表,链表的每个节点,是一个压缩列表,每个压缩列表都不让他太大,同时再把多个压缩列表通过链式结构连起来

上述两个配置项现在已经不再使用-因为现在使用quicklist编码方式

-2代表着8kb

我们要掌握其中的策略和思想,不要去背数字

list类型的应用场景

1.用list作为”数组“这样的结构,来存储多个元素

使用MySQL表示学生和班级信息

student(studentId,studentName,age,score,classId)

class(classId,className)
  • 在上述表结构中,可以很方便的实现”查询指定班级中有哪些同学“
  • redis提供的查询功能,不像MySQL那么强大
  • redis的查询是拿着key对应value进行查询;MySQL则是在某一列进行查询

2.使用redis作为消息队列

要是生产者消费者模型,brpop这个操作是阻塞操作,当列表为空的时候,brpop就会阻塞等待,一直等到其他客户端,push了元素,谁先执行的这个brpop命令,谁就能拿到这个新来的元素

像这样的设定,就能构成一个”轮询“式的效果-类似于负载均衡器

  1. 假设消费者执行顺序是123,当新元素到达之后,首先是消费者1拿到元素(按照执行brpop命令的先后顺序来决定谁获取到的),消费者1拿到元素之后,也就从brpop中返回了(相当于这个命令就执行完了)如果消费者1还想继续消费,就需要重新执行brpop
  2. 此时,再来一个新的元素过来,就是消费者2拿到该元素也从brpop中返回,如果消费者2还想继续消费,也需要重新执行brpop。再来一个新元素,就是消费者3拿到这个元素了

假如说有个包子摊,有许多人排队,我排在最末尾,买完之后发现忘了给女朋友买了,然后就重新想进去,结果后面的老哥说gun,然后我去最后排队,老哥买完了之后,也发现忘了给女神买包子了,然后又问后面的老哥,老哥说gun,然后就排我后面来排队,这就是一个简单的使用轮询的方式来完成生产者消费者模型

虽然这样的结构能完成一定的功能,但还是很简陋

Redis 分频道阻塞消息队列模型

什么时候会涉及到这种情况,我们常见的就有抖音,就有一个通道(channel/topic),来传输视频数据,还可以有一个通道,来传输数据,还可以有频道,来传输点赞,转发,收藏数据,还可以有频道,来传输,评论数据。

多个频道就可以在某种数据发生问题的时候,不会对其他数据造成影响(解耦合)

3.微博Timeline

每个⽤⼾都有属于⾃⼰的 Timeline(微博列表),现需要分⻚展⽰⽂章列表。此时可以考虑使⽤ 列表,因为列表不但是有序的,同时⽀持按照索引范围获取元素。

1)每篇微博使⽤哈希结构存储,例如微博中 3 个属性:title、timestamp、content:

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

2)向⽤⼾ Timeline 添加微博,user::mblogs 作为微博的键:

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

3)分⻚获取⽤⼾的 Timeline,例如获取⽤⼾ 1 的前 10 篇微博:

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

此⽅案在实际中可能存在两个问题:

  • 1 + n 问题。即如果每次分⻚获取的微博个数较多,需要执⾏多次 hgetall 操作,此时可以考虑使⽤ pipeline(流⽔线)模式批量提交命令,或者微博不采⽤哈希类型,⽽是使⽤序列化的字符串类 型,使⽤ mget 获取。
  • 分裂获取⽂章时,lrange 在列表两端表现较好,获取列表中间的元素表现较差,此时可以考虑将列 表做拆分。

选择列表类型时,请参考:

  • 同侧存取(lpush + lpop 或者 rpush + rpop)为栈
  • 异侧存取(lpush + rpop 或者 rpush + lpop)为队列

pipeline(流⽔线):虽然是多个redis命令,但是把这些命令合并成一个网络请求进行通信,大大降低了客户端和服务器之间的交互次数了

  • 17
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值