列表( list) 类型是用来存储多个有序的字符串, 如图2-18所示, a、b、 c、 d、 e五个元素从左到右组成了一个有序的列表, 列表中的每个字符串称为元素( element) , 一个列表最多可以存储2的23次方-1个元素。 在Redis中, 可以对列表两端插入( push) 和弹出( pop) , 还可以获取指定范围的元素列表、 获取指定索引下标的元素等( 如图2-18和图2-19所示) 。 列表是一种比较灵活的数据结构, 它可以充当栈和队列的角色, 在实际开发上有很多应用场景。
这两个特点在后面介绍集合和有序集合后, 会显得更加突出, 因此在考虑是否使用该数据结构前, 首先需要弄清楚列表数据结构的特点。
2.4.1 命令
下面将按照对列表的5种操作类型对命令进行介绍, 命令如表2-4所示。
1.添加操作
( 1) 从右边插入元素
rpush key value [value ...]
下面代码从右向左插入元素c、 b、 a:
127.0.0. 1:6379> rpush listkey c b a
(integer) 3
lrange0-1命令可以从左到右获取列表的所有元素:
127.0.0.1:6379> lrange listkey 0 -1
1) "c"
2) "b"
3) "a"
( 2) 从左边插入元素
lpush key value [value ...]
使用方法和rpush相同, 只不过从左侧插入, 这里不再赘述。
( 3) 向某个元素前或者后插入元素
linsert key before|after pivot value
linsert命令会从列表中找到等于pivot的元素, 在其前( before) 或者后( after) 插入一个新的元素value, 例如下面操作会在列表的元素b前插入java:
127.0.0.1:6379> linsert listkey before b java
(integer) 4
返回结果为4, 代表当前命令的长度, 当前列表变为:
127.0.0.1:6379> lrange listkey 0 -1
1) "c"
2) "java"
3) "b"
4) "a"
2.查找
( 1) 获取指定范围内的元素列表
lrange key start end
1) "java"
2) "b"
3) "a"
( 2) 获取列表指定索引下标的元素
lindex key index
例如当前列表最后一个元素为a:
127.0.0.1:6379> lindex listkey -1
"a"
( 3) 获取列表长度
llen key
例如, 下面示例当前列表长度为4:
127.0.0.1:6379> llen listkey
(integer) 4
3.删除
( 1) 从列表左侧弹出元素
lpop key
如下操作将列表最左侧的元素c会被弹出, 弹出后列表变为java、 b、a:
127.0.0.1:6379>lpop listkey
"c"
127.0.0.1:6379> lrange listkey 0 -1
1) "java"
2) "b"
3) "a"
( 2) 从列表右侧弹出
rpop key
它的使用方法和lpop是一样的, 只不过从列表右侧弹出, 这里不再赘述。
( 3) 删除指定元素
lrem key count value
lrem命令会从列表中找到等于value的元素进行删除, 根据count的不同分为三种情况:
·count>0, 从左到右, 删除最多count个元素。
·count<0, 从右到左, 删除最多count绝对值个元素。
·count=0, 删除所有。
例如向列表从左向右插入5个a, 那么当前列表变为“a a a a a java b a”,下面操作将从列表左边开始删除4个为a的元素:
127.0.0.1:6379> lrem listkey 4 a
(integer) 4
127.0.0.1:6379> lrange listkey 0 -1
1) "a"
2) "java"
3) "b"
4) "a"
( 4) 按照索引范围修剪列表
ltrim key start end
例如, 下面操作会只保留列表listkey第2个到第4个元素:
127.0.0.1:6379> ltrim listkey 1 3
OK
127.0.0.1:6379> lrange listkey 0 -1
1) "java"
2) "b"
3) "a"
4.修改
修改指定索引下标的元素:
lset key index newValue
下面操作会将列表listkey中的第3个元素设置为python:
127.0.0.1:6379> lset listkey 2 python
OK
127.0.0.1:6379> lrange listkey 0 -1
1) "java"
2) "b"
3) "python"
5.阻塞操作
阻塞式弹出如下:
blpop key [key ...] timeout
brpop key [key ...] timeout
blpop和brpop是lpop和rpop的阻塞版本, 它们除了弹出方向不同, 使用方法基本相同, 所以下面以brpop命令进行说明, brpop命令包含两个参数:
·key[key...]: 多个列表的键。
·timeout: 阻塞时间( 单位: 秒) 。
1) 列表为空: 如果timeout=3, 那么客户端要等到3秒后返回, 如果timeout=0, 那么客户端一直阻塞等下去:
127.0.0.1:6379> brpop list:test 3
(nil)
(3.10s)
127.0.0.1:6379> brpop list:test 0
...阻塞...
如果此期间添加了数据element1, 客户端立即返回:
127.0.0.1:6379> brpop list:test 3
1) "list:test"
2) "element1"
(2.06s)
2) 列表不为空: 客户端会立即返回。
127.0.0.1:6379> brpop list:test 0
1) "list:test"
2) "element1"
在使用brpop时, 有两点需要注意。
第一点, 如果是多个键, 那么brpop会从左至右遍历键, 一旦有一个键能弹出元素, 客户端立即返回:
127.0.0.1:6379> brpop list:1 list:2 list:3 0
..阻塞..
此时另一个客户端分别向list: 2和list: 3插入元素:
client-lpush> lpush list:2 element2
(integer) 1
client-lpush> lpush list:3 element3
(integer) 1
客户端会立即返回list: 2中的element2, 因为list: 2最先有可以弹出的元素:
127.0.0.1:6379> brpop list:1 list:2 list:3 0
1) "list:2"
2) "element2_1"
第二点, 如果多个客户端对同一个键执行brpop, 那么最先执行brpop命令的客户端可以获取到弹出的值。
客户端1:
client-1> brpop list:test 0
...阻塞...
客户端2:
client-2> brpop list:test 0
...阻塞...
客户端3:
client-3> brpop list:test 0
...阻塞...
此时另一个客户端lpush一个元素到list: test列表中:
client-lpush> lpush list:test element
(integer) 1
那么客户端1最会获取到元素, 因为客户端1最先执行brpop, 而客户端2和客户端3继续阻塞:
client> brpop list:test 0
1) "list:test"
2) "element"
有关列表的基础命令已经介绍完了, 表2-5是这些命令的时间复杂度,开发人员可以参考此表选择适合的命令。
列表类型的内部编码有两种。
·ziplist( 压缩列表) : 当列表的元素个数小于list-max-ziplist-entries配置( 默认512个), 同时列表中每个元素的值都小于list-max-ziplist-value配置时( 默认64字节) , Redis会选用ziplist来作为列表的内部实现来减少内存的使用。
·linkedlist( 链表) : 当列表类型无法满足ziplist的条件时, Redis会使用linkedlist作为列表的内部实现。
下面的示例演示了列表类型的内部编码, 以及相应的变化。
1) 当元素个数较少且没有大元素时, 内部编码为ziplist:
127.0.0.1:6379> rpush listkey e1 e2 e3
(integer) 3
127.0.0.1:6379> object encoding listkey
"ziplist"
2.1) 当元素个数超过512个, 内部编码变为linkedlist:
127.0.0.1:6379> rpush listkey e4 e5 ...忽略... e512 e513
(integer) 513
127.0.0.1:6379> object encoding listkey
"linkedlist"
2.2) 或者当某个元素超过64字节, 内部编码也会变为linkedlist:
127.0.0.1:6379> rpush listkey "one string is bigger than 64 byte................................"
(integer) 4
127.0.0.1:6379> object encoding listkey
Redis3.2版本提供了quicklist内部编码, 简单地说它是以一个ziplist为节点的linkedlist, 它结合了ziplist和linkedlist两者的优势, 为列表类型提供了一种更为优秀的内部编码实现, 它的设计原理可以参考Redis的另一个作者Matt Stancliff的博客: https://matt.sh/redis-quicklist。有关列表类型的内存优化技巧将在8.3节详细介绍。
2.4.3 使用场景
1.消息队列
如图2-21所示, Redis的lpush+brpop命令组合即可实现阻塞队列, 生产者客户端使用lrpush从列表左侧插入元素, 多个消费者客户端使用brpop命令阻塞式的“抢”列表尾部的元素, 多个客户端保证了消费的负载均衡和高可用性。
2.文章列表
每个用户有属于自己的文章列表, 现需要分页展示文章列表。 此时可以考虑使用列表, 因为列表不但是有序的, 同时支持按照索引范围获取元素。
1)每篇文章使用哈希结构存储, 例如每篇文章有3个属性title、timestamp、 content:
hmset acticle:1 title xx timestamp 1476536196 content xxxx
...
hmset acticle:k title yy timestamp 1476512536 content yyyy
...
2) 向用户文章列表添加文章, user: {id}: articles作为用户文章列表的键:
lpush user:1:acticles article:1 article:3
...
lpush user:k:acticles article:5
...
3) 分页获取用户文章列表, 例如下面伪代码获取用户id=1的前10篇文章:
articles = lrange user:1:articles 0 9
for article in {articles}
hgetall {article}
实际上列表的使用场景很多, 在选择时可以参考以下口诀:
·lpush+lpop=Stack(栈)
·lpush+rpop=Queue(队列)
·lpsh+ltrim=Capped Collection(有限集合)
·lpush+brpop=Message Queue(消息队列)
这两个特点在后面介绍集合和有序集合后, 会显得更加突出, 因此在考虑是否使用该数据结构前, 首先需要弄清楚列表数据结构的特点。
2.4.1 命令
下面将按照对列表的5种操作类型对命令进行介绍, 命令如表2-4所示。
1.添加操作
( 1) 从右边插入元素
rpush key value [value ...]
下面代码从右向左插入元素c、 b、 a:
127.0.0. 1:6379> rpush listkey c b a
(integer) 3
lrange0-1命令可以从左到右获取列表的所有元素:
127.0.0.1:6379> lrange listkey 0 -1
1) "c"
2) "b"
3) "a"
( 2) 从左边插入元素
lpush key value [value ...]
使用方法和rpush相同, 只不过从左侧插入, 这里不再赘述。
( 3) 向某个元素前或者后插入元素
linsert key before|after pivot value
linsert命令会从列表中找到等于pivot的元素, 在其前( before) 或者后( after) 插入一个新的元素value, 例如下面操作会在列表的元素b前插入java:
127.0.0.1:6379> linsert listkey before b java
(integer) 4
返回结果为4, 代表当前命令的长度, 当前列表变为:
127.0.0.1:6379> lrange listkey 0 -1
1) "c"
2) "java"
3) "b"
4) "a"
2.查找
( 1) 获取指定范围内的元素列表
lrange key start end
lrange操作会获取列表指定索引范围所有的元素。 索引下标有两个特点:
第一, 索引下标从左到右分别是0到N-1, 但是从右到左分别是-1到-N。
第二, lrange中的end选项包含了自身, 这个和很多编程语言不包含end不太相同, 例如想获取列表的第2到第4个元素, 可以执行如下操作:
127.0.0.1:6379> lrange listkey 1 31) "java"
2) "b"
3) "a"
( 2) 获取列表指定索引下标的元素
lindex key index
例如当前列表最后一个元素为a:
127.0.0.1:6379> lindex listkey -1
"a"
( 3) 获取列表长度
llen key
例如, 下面示例当前列表长度为4:
127.0.0.1:6379> llen listkey
(integer) 4
3.删除
( 1) 从列表左侧弹出元素
lpop key
如下操作将列表最左侧的元素c会被弹出, 弹出后列表变为java、 b、a:
127.0.0.1:6379>lpop listkey
"c"
127.0.0.1:6379> lrange listkey 0 -1
1) "java"
2) "b"
3) "a"
( 2) 从列表右侧弹出
rpop key
它的使用方法和lpop是一样的, 只不过从列表右侧弹出, 这里不再赘述。
( 3) 删除指定元素
lrem key count value
lrem命令会从列表中找到等于value的元素进行删除, 根据count的不同分为三种情况:
·count>0, 从左到右, 删除最多count个元素。
·count<0, 从右到左, 删除最多count绝对值个元素。
·count=0, 删除所有。
例如向列表从左向右插入5个a, 那么当前列表变为“a a a a a java b a”,下面操作将从列表左边开始删除4个为a的元素:
127.0.0.1:6379> lrem listkey 4 a
(integer) 4
127.0.0.1:6379> lrange listkey 0 -1
1) "a"
2) "java"
3) "b"
4) "a"
( 4) 按照索引范围修剪列表
ltrim key start end
例如, 下面操作会只保留列表listkey第2个到第4个元素:
127.0.0.1:6379> ltrim listkey 1 3
OK
127.0.0.1:6379> lrange listkey 0 -1
1) "java"
2) "b"
3) "a"
4.修改
修改指定索引下标的元素:
lset key index newValue
下面操作会将列表listkey中的第3个元素设置为python:
127.0.0.1:6379> lset listkey 2 python
OK
127.0.0.1:6379> lrange listkey 0 -1
1) "java"
2) "b"
3) "python"
5.阻塞操作
阻塞式弹出如下:
blpop key [key ...] timeout
brpop key [key ...] timeout
blpop和brpop是lpop和rpop的阻塞版本, 它们除了弹出方向不同, 使用方法基本相同, 所以下面以brpop命令进行说明, brpop命令包含两个参数:
·key[key...]: 多个列表的键。
·timeout: 阻塞时间( 单位: 秒) 。
1) 列表为空: 如果timeout=3, 那么客户端要等到3秒后返回, 如果timeout=0, 那么客户端一直阻塞等下去:
127.0.0.1:6379> brpop list:test 3
(nil)
(3.10s)
127.0.0.1:6379> brpop list:test 0
...阻塞...
如果此期间添加了数据element1, 客户端立即返回:
127.0.0.1:6379> brpop list:test 3
1) "list:test"
2) "element1"
(2.06s)
2) 列表不为空: 客户端会立即返回。
127.0.0.1:6379> brpop list:test 0
1) "list:test"
2) "element1"
在使用brpop时, 有两点需要注意。
第一点, 如果是多个键, 那么brpop会从左至右遍历键, 一旦有一个键能弹出元素, 客户端立即返回:
127.0.0.1:6379> brpop list:1 list:2 list:3 0
..阻塞..
此时另一个客户端分别向list: 2和list: 3插入元素:
client-lpush> lpush list:2 element2
(integer) 1
client-lpush> lpush list:3 element3
(integer) 1
客户端会立即返回list: 2中的element2, 因为list: 2最先有可以弹出的元素:
127.0.0.1:6379> brpop list:1 list:2 list:3 0
1) "list:2"
2) "element2_1"
第二点, 如果多个客户端对同一个键执行brpop, 那么最先执行brpop命令的客户端可以获取到弹出的值。
客户端1:
client-1> brpop list:test 0
...阻塞...
客户端2:
client-2> brpop list:test 0
...阻塞...
客户端3:
client-3> brpop list:test 0
...阻塞...
此时另一个客户端lpush一个元素到list: test列表中:
client-lpush> lpush list:test element
(integer) 1
那么客户端1最会获取到元素, 因为客户端1最先执行brpop, 而客户端2和客户端3继续阻塞:
client> brpop list:test 0
1) "list:test"
2) "element"
有关列表的基础命令已经介绍完了, 表2-5是这些命令的时间复杂度,开发人员可以参考此表选择适合的命令。
列表类型的内部编码有两种。
·ziplist( 压缩列表) : 当列表的元素个数小于list-max-ziplist-entries配置( 默认512个), 同时列表中每个元素的值都小于list-max-ziplist-value配置时( 默认64字节) , Redis会选用ziplist来作为列表的内部实现来减少内存的使用。
·linkedlist( 链表) : 当列表类型无法满足ziplist的条件时, Redis会使用linkedlist作为列表的内部实现。
下面的示例演示了列表类型的内部编码, 以及相应的变化。
1) 当元素个数较少且没有大元素时, 内部编码为ziplist:
127.0.0.1:6379> rpush listkey e1 e2 e3
(integer) 3
127.0.0.1:6379> object encoding listkey
"ziplist"
2.1) 当元素个数超过512个, 内部编码变为linkedlist:
127.0.0.1:6379> rpush listkey e4 e5 ...忽略... e512 e513
(integer) 513
127.0.0.1:6379> object encoding listkey
"linkedlist"
2.2) 或者当某个元素超过64字节, 内部编码也会变为linkedlist:
127.0.0.1:6379> rpush listkey "one string is bigger than 64 byte................................"
(integer) 4
127.0.0.1:6379> object encoding listkey
"linkedlist"
Redis3.2版本提供了quicklist内部编码, 简单地说它是以一个ziplist为节点的linkedlist, 它结合了ziplist和linkedlist两者的优势, 为列表类型提供了一种更为优秀的内部编码实现, 它的设计原理可以参考Redis的另一个作者Matt Stancliff的博客: https://matt.sh/redis-quicklist。有关列表类型的内存优化技巧将在8.3节详细介绍。
2.4.3 使用场景
1.消息队列
如图2-21所示, Redis的lpush+brpop命令组合即可实现阻塞队列, 生产者客户端使用lrpush从列表左侧插入元素, 多个消费者客户端使用brpop命令阻塞式的“抢”列表尾部的元素, 多个客户端保证了消费的负载均衡和高可用性。
2.文章列表
每个用户有属于自己的文章列表, 现需要分页展示文章列表。 此时可以考虑使用列表, 因为列表不但是有序的, 同时支持按照索引范围获取元素。
1)每篇文章使用哈希结构存储, 例如每篇文章有3个属性title、timestamp、 content:
hmset acticle:1 title xx timestamp 1476536196 content xxxx
...
hmset acticle:k title yy timestamp 1476512536 content yyyy
...
2) 向用户文章列表添加文章, user: {id}: articles作为用户文章列表的键:
lpush user:1:acticles article:1 article:3
...
lpush user:k:acticles article:5
...
3) 分页获取用户文章列表, 例如下面伪代码获取用户id=1的前10篇文章:
articles = lrange user:1:articles 0 9
for article in {articles}
hgetall {article}
使用列表类型保存和获取文章列表会存在两个问题。 第一, 如果每次分页获取的文章个数较多, 需要执行多次hgetall操作, 此时可以考虑使用Pipeline( 第3章会介绍) 批量获取, 或者考虑将文章数据序列化为字符串类型, 使用mget批量获取。 第二, 分页获取文章列表时, lrange命令在列表两端性能较好, 但是如果列表较大, 获取列表中间范围的元素性能会变差, 此时可以考虑将列表做二级拆分, 或者使用Redis3.2的quicklist内部编码实现,它结合ziplist和linkedlist的特点, 获取列表中间范围的元素时也可以高效完成。
实际上列表的使用场景很多, 在选择时可以参考以下口诀:
·lpush+lpop=Stack(栈)
·lpush+rpop=Queue(队列)
·lpsh+ltrim=Capped Collection(有限集合)
·lpush+brpop=Message Queue(消息队列)