List-列表类型:L&R
列表类型:有序、可重复
Arraylist和linkedlist的区别
Arraylist是使用数组来存储数据,特点:查询快、增删慢
Linkedlist是使用双向链表存储数据,特点:增删快、查询慢,但是查询链表两端的数据也很快。
Redis的list是采用来链表来存储的,所以对于redis的list数据类型的操作,是操作list的两端数据来操作的。
命令
向列表左边增加元素:lpush key:val1 val2
127.0.0.1:6379>lpush list:1 1 2 3
(integer) 3
向列表右边增加元素:rpush key:val1 val2
127.0.0.1:6379>rpush list:1 4 5 6
(integer) 3
查看列表:LRANGE key start stop
LRANGE命令是列表类型最常用的命令之一,获取列表中的某一片段,将返回start、stop之间的所有元素(包含两端的元素),索引从0开始。索引可以是负数,如:“-1”代表最后边的一个元素。
语法:LRANGE key start stop
获取下标0-2的元素 前闭后闭
127.0.0.1:6379> lrange list1 0 2
1) "2"
2) "1"
3) "4"
这里需要注意的是,list的索引从0开始大家知道,但是取全部list的值时,结束索引需要设置为-1,也可以设置成list长度减1
127.0.0.1:6379> lrange l:list 0 -1
1) "6"
2) "5"
3) "2"
4) "2"
从列表两端弹出元素:LPOP key&RPOP key
LPOP命令从列表左边弹出一个元素,会分两步完成:
第一步是将列表左边的元素从列表中移除
第二步是返回被移除的元素值。
语法:
LPOP key
RPOP key
127.0.0.1:6379> lpop list:1
"3“
127.0.0.1:6379> rpop list:1
"6“
获取列表中元素的个数LLEN key
语法:LLEN key
127.0.0.1:6379> llen list:1
(integer) 2
删除列表中指定的值 LREM key count value
LREM命令会删除列表中前count个值为value的元素,返回实际删除的元素个数。根据count值的不同,该命令的执行方式会有所不同:
当count>0时, LREM会从列表左边开始删除。
当count lrem list1 2 1
(integer) 2
获得/设置指定索引的元素值LINDEX key index
获得指定索引的元素值
语法:LINDEX key index
127.0.0.1:6379> lindex list 2
"1"
设置指定索引的元素值
语法:LSET key index value
127.0.0.1:6379> lset list 2 2
OK
只保留列表指定范围元素:LTRIM key start stop
只保留start - stop 之间的元素
语法:LTRIM key start stop
127.0.0.1:6379> lrange l:list 0 -1
1) "6"
2) "5"
3) "0"
4) "2"
127.0.0.1:6379> ltrim l:list 0 2
OK
127.0.0.1:6379> lrange l:list 0 -1
1) "6"
2) "5"
3) "0"
向列表中插入元素LINSERT key BEFORE|AFTER pivot value
该命令首先会在列表中从左到右查找值为pivot的元素,然后根据第二个参数是BEFORE还是AFTER来决定将value插入到该元素的前面还是后面。
语法:LINSERT key BEFORE|AFTER pivot value
127.0.0.1:6379> lrange list 0 -1
1) "3"
2) "2"
3) "1"
将值为4的元素插入到3后面
127.0.0.1:6379> linsert list after 3 4
(integer) 4
127.0.0.1:6379> lrange list 0 -1
1) "3"
2) "4"
3) "2"
4) "1"
将A列表最后一个元素移到B列表RPOPLPUSH source destination
用于移除列表的最后一个元素,并将该元素添加到另一个列表并返回。
语法:RPOPLPUSH source destination
127.0.0.1:6379> lrange list 0 -1
1) "3"
2) "4"
3) "2"
4) "1"
127.0.0.1:6379> rpoplpush list newlist
"1"
127.0.0.1:6379> lrange newlist 0 -1
1) "1"
127.0.0.1:6379> lrange list 0 -1
1) "3"
2) "4"
3) "2"
移出并获取列表的最后一个元素(阻塞):Brpop
移出并获取列表的最后一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。
redis 127.0.0.1:6379> BRPOP LIST1 LIST2 .. LISTN TIMEOUT
假如在指定时间内没有任何元素被弹出,则返回一个 nil 和等待时长。 反之,返回一个含有两个元素的列表,第一个元素是被弹出元素所属的 key ,第二个元素是被弹出元素的值。
redis> DEL list1 list2
(integer) 0
redis> RPUSH list1 a b c
(integer) 3
redis> BRPOP list1 list2 0
1) "list1"
2) "c"
将A列表最后一个元素移到B列表(阻塞)Brpoplpush
命令从列表中取出最后一个元素,并插入到另外一个列表的头部; 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。假如在指定时间内没有任何元素被弹出,则返回一个 nil 和等待时长。 反之,返回一个含有两个元素的列表,第一个元素是被弹出元素的值,第二个元素是等待时长。
redis 127.0.0.1:6379> BRPOPLPUSH LIST1 ANOTHER_LIST TIMEOUT
# 非空列表
redis> BRPOPLPUSH msg reciver 500
"hello moto" # 弹出元素的值
(3.38s) # 等待时长
redis> LLEN reciver
(integer) 1
redis> LRANGE reciver 0 0
1) "hello moto"
# 空列表
redis> BRPOPLPUSH msg reciver 1
(nil)
(1.34s)
应用场景:大容量&更新频率不高&的静态列表
商品评论列表
思路
在Redis中创建商品评论列表
用户发布商品评论,将评论信息转成json存储到list中。
用户在页面查询评论列表,从redis中取出json数据展示到页面。
定义商品评论列表key
商品编号为1001的商品评论key【items: comment:1001】
每日排行榜:某个时间段数据固定榜单
list类型的lrange命令可以分页查看队列中的数据。可将每隔一段时间计算一次的排行榜存储在list类型中,如京东每日的手机销量排行、学校每次月考学生的成绩排名、斗鱼年终盛典主播排名等,下图是酷狗音乐“K歌擂台赛”的昨日打擂金曲排行榜,每日计算一次,存储在list类型中,接口访问时,通过page和size分页获取打擂金曲。
list类型的lpush命令和lrange命令能实现最新列表的功能,每次通过lpush命令往列表里插入新的元素,然后通过lrange命令读取最新的元素列表,如朋友圈的点赞列表、评论列表。
但是,并不是所有的最新列表都能用list类型实现,因为对于频繁更新的列表,list类型的分页可能导致列表元素重复或漏掉,举个例子,当前列表里由表头到表尾依次有(E,D,C,B,A)五个元素,每页获取3个元素,用户第一次获取到(E,D,C)三个元素,然后表头新增了一个元素F,列表变成了(F,E,D,C,B,A),此时用户取第二页拿到(C,B,A),元素C重复了。可以尾插头拿 即 左边插入从右边拿。
只有不需要分页(比如每次都只取列表的前5个元素)或者更新频率低(比如每天凌晨更新一次)的列表才适合用list类型实现。
对于需要分页并且会频繁更新的列表,需用使用有序集合sorted set类型实现。
另外,需要通过时间范围查找的最新列表,list类型也实现不了,也需要通过有序集合sorted set类型实现,如以成交时间范围作为条件来查询的订单列表。之后在介绍有序集合sorted set类型的应用场景时会详细介绍sorted set类型如何实现最新列表。
那么问题来了,对于排行榜和最新列表两种应用场景,list类型能做到的sorted set类型都能做到,list类型做不到的sorted set类型也能做到,那为什么还要使用list类型去实现排行榜或最新列表呢,直接用sorted set类型不是更好吗?
原因是sorted set类型占用的内存容量是list类型的数倍之多(之后会在容量章节详细介绍),对于列表数量不多的情况,可以用sorted set类型来实现,比如上文中举例的打擂金曲排行榜,每天全国只有一份,两种数据类型的内存容量差距可以忽略不计,但是如果要实现某首歌曲的翻唱作品地区排行榜,数百万的歌曲,300多个地区,会产生数量庞大的榜单,或者数量更加庞大的朋友圈点赞列表,就需要慎重地考虑容量的问题了。
底层数据结构压缩列表和双向链表
在版本3.2之前,Redis 列表list使用两种数据结构作为底层实现:
- 压缩列表ziplist
- 双向链表linkedlist
- 因为双向链表占用的内存比压缩列表要多, 所以当创建新的列表键时, 列表会优先考虑使用压缩列表, 并且在有需要的时候, 才从压缩列表实现转换到双向链表实现。
ziplist
ziplist 是一个特殊的双向链表 特殊之处在于:没有维护双向指针:prev next;而是存储上一个 entry的长度和 当前entry的长度,通过长度推算下一个元素在什么地方。 牺牲读取的性能,获得高效的存储空间,因为(简短字符串的情况)存储指针比存储entry长度 更费内存。这是典型的“时间换空间”。
ziplist使用局限性 字段、值比较小,才会用ziplist。
压缩列表ziplist存储结构 ziplist使用连续的内存块,每一个节点(entry)都是连续存储的;
1.ziplist可以通过data header计算出当前entry的结束位置,也就能得到下一个entry的起始位置,正向遍历
2.ziplist可以通过length of previous entry计算出上一个entry的起始位置,反向遍历
压缩列表转化成双向链表条件
创建新列表时 redis 默认使用 redis_encoding_ziplist 编码, 当以下任意一个条件被满足时, 列表会被转换成 redis_encoding_linkedlist 编码:
- 试图往列表新添加一个字符串值,且这个字符串的长度超过 server.list_max_ziplist_value (默认值为 64字节)。
- ziplist 包含的节点超过 server.list_max_ziplist_entries (默认值为 512 )。
注意:这两个条件是可以修改的,在 redis.conf 中:
list-max-ziplist-value 64
list-max-ziplist-entries 512
linkedlist
当链表entry数据超过512、或单个value 长度超过64字节,底层就会转化成linkedlist编码; linkedlist是标准的双向链表,Node节点包含prev和next指针,可以进行双向遍历; 还保存了 head 和 tail 两个指针,因此,对链表的表头和表尾进行插入的复杂度都为 (1) —— 这是高效实现 LPUSH 、 RPOP、 RPOPLPUSH 等命令的关键。