3-12{438}
{-:-}图3-12 从右侧弹出元素后numbers键中的数据
3.获取列表中元素的个数LLEN key
当键不存在时LLEN会返回0:
redis> LLEN numbers
(integer) 3
LLEN命令的功能类似SQL语句SELECT COUNT(*) FROM table_name,但是LLEN的时间复杂度为O(1),使用时Redis会直接读取现成的值,而不需要像部分关系数据库(如使用InnoDB存储引擎的MySQL表)那样需要遍历一遍数据表来统计条目数量。
4.获得列表片段LRANGE key start stop
LRANGE命令是列表类型最常用的命令之一,它能够获得列表中的某一片段。LRANGE命令将返回索引从start到stop之间的所有元素(包含两端的元素)。与大多数人的直觉相同,Redis的列表起始索引为0:
redis> LRANGE numbers 0 2
1) "2"
2) "1"
3) "0"
LRANGE命令在取得列表片段的同时不会像LPOP一样删除该片段,另外LRANGE命令与很多语言中用来截取数组片段的方法slice有一点区别是LRANGE返回的值包含最右边的元素,如在JavaScript中:
var numbers = [2, 1, 0];
console.log(numbers.slice(0, 2)); // 返回数组:[2, 1]
LRANGE命令也支持负索引,表示从右边开始计算序数,如"−1"表示最右边第一个元素,"-2"表示最右边第二个元素,依次类推:
redis> LRANGE numbers -2 -1
1) "1"
2) "0"
显然,LRANGE numbers 0 -1可以获取列表中的所有元素。另外一些特殊情况如下。
1.如果start的索引位置比stop的索引位置靠后,则会返回空列表。
2.如果stop大于实际的索引范围,则会返回到列表最右边的元素:
redis> LRANGE numbers 1 999
1) "1"
2) "0"
5.删除列表中指定的值
LREM key count value
LREM命令会删除列表中前count个值为value的元素,返回值是实际删除的元素个数。根据count值的不同,LREM命令的执行方式会略有差异。
(1)当count > 0时LREM命令会从列表左边开始删除前count个值为value的元素。
(2)当count < 0时LREM命令会从列表右边开始删除前|count|个值为value的元素。
(3)当count = 0是LREM命令会删除所有值为value的元素。例如:
redis> RPUSH numbers 2
(integer) 4
redis> LRANGE numbers 0 -1
1) "2"
2) "1"
3) "0"
4) "2"
# 从右边开始删除第一个值为"2"的元素
redis> LREM numbers -1 2
(integer) 1
redis> LRANGE numbers 0 -1
1) "2"
2) "1"
3) "0"
3.4.3 实践1.存储文章ID列表
为了解决小白遇到的问题,我们使用列表类型键posts:list记录文章ID列表。当发布新文章时使用LPUSH命令把新文章的ID加入这个列表中,另外删除文章时也要记得把列表中的文章ID删除,就像这样:LREM posts:list 1要删除的文章ID
有了文章ID列表,就可以使用LRANGE命令来实现文章的分页显示了。伪代码如下:
$postsPerPage = 10
$start = ($currentPage - 1) * $postsPerPage
$end = $currentPage * $postsPerPage - 1
$postsID = LRANGE posts:list, $start, $end
# 获得了此页需要显示的文章ID列表,我们通过循环的方式来读取文章
for each $id in $postsID
$post = HGETALL post:$id
print 文章标题:$post.title
这样显示的文章列表是根据加入列表的顺序倒序的(即最新发布的文章显示在前面),如果想让最旧的文章显示在前面,可以使用LRANGE命令获取需要的部分并在客户端中将顺序反转显示出来,具体的实现交由读者来完成。
小白的问题至此就解决了,美中不足的一点是散列类型没有类似字符串类型的MGET命令那样可以通过一条命令同时获得多个键的键值的版本,所以对于每个文章ID都需要请求一次数据库,也就都会产生一次往返时延(round-trip delay time){![4.5节中还会详细介绍这个概念。]},之后我们会介绍使用管道和脚本来优化这个问题。
另外使用列表类型键存储文章ID列表有以下两个问题。
(1)文章的发布时间不易修改:修改文章的发布时间不仅要修改post:文章ID中的time字段,还需要按照实际的发布时间重新排列posts:list中的元素顺序,而这一操作相对比较繁琐。
(2)当文章数量较多时访问中间的页面性能较差:前面已经介绍过,列表类型是通过链表实现的,所以当列表元素非常多时访问中间的元素效率并不高。
但如果博客不提供修改文章时间的功能并且文章数量也不多时,使用列表类型也不失为一种好办法。对于小白要做的博客系统来讲,现阶段的成果已经足够实用且值得庆祝了。3.6节将介绍使用有序集合类型存储文章ID列表的方法。
2.存储评论列表在博客中还可以使用列表类型键存储文章的评论。由于小白的博客不允许访客修改自己发表的评论,而且考虑到读取评论时需要获得评论的全部数据(评论者姓名,联系方式,评论时间和评论内容),不像文章一样有时只需要文章标题而不需要文章正文。所以适合将一条评论的各个元素序列化成字符串后作为列表类型键中的元素来存储。
我们使用列表类型键post:文章ID:comments来存储某个文章的所有评论。发布评论的伪代码如下(以ID为42的文章为例):
# 将评论序列化成字符串
$serializedComment = serialize($author, $email, $time, $content)
LPUSH post:42:comments, $serializedComment
读取评论时同样使用LRANGE命令即可,具体的实现在此不再赘述。
3.4.4 命令拾遗1.获得/设置指定索引的元素值LINDEX key index
LSET key index value
如果要将列表类型当作数组来用,LINDEX命令是必不可少的。LINDEX命令用来返回指定索引的元素,索引从0开始。如:
redis> LINDEX numbers 0
"2"
如果index是负数则表示从右边开始计算的索引,最右边元素的索引是−1。例如:
redis> LINDEX numbers -1
"0"
LSET是另一个通过索引操作列表的命令,它会将索引为index的元素赋值为value。例如:
redis> LSET numbers 1 7
OK
redis> LINDEX numbers 1
"7"
2.只保留列表指定片段
LTRIM key start end
LTRIM命令可以删除指定索引范围之外的所有元素,其指定列表范围的方法和LRANGE命令相同。就像这样:
redis> LRANGE numbers 0 -1
1) "1"
2) "2"
3) "7"
4) "3"
"0"
redis> LTRIM numbers 1 2
OK
redis> LRANGE numbers 0 1
1) "2"
2) "7"
LTRIM命令常和LPUSH命令一起使用来限制列表中元素的数量,比如记录日志时我们希望只保留最近的100条日志,则每次加入新元素时调用一次LTRIM命令即可:
LPUSH logs $newLog
LTRIM logs 0 99
3.向列表中插入元素
LINSERT key BEFORE|AFTER pivot value
LINSERT命令首先会在列表中从左到右查找值为pivot的元素,然后根据第二个参数是BEFORE还是AFTER来决定将value插入到该元素的前面还是后面。
LINSERT命令的返回值是插入后列表的元素个数。示例如下:
redis> LRANGE numbers 0 -1
1) "2"
2) "7"
3) "0"
redis> LINSERT numbers AFTER 7 3
(integer) 4
redis> LRANGE numbers 0 -1
1) "2"
2) "7"
3) "3"
4) "0"
redis> LINSERT numbers BEFORE 2 1
(integer) 5
redis> LRANGE numbers 0 -1
1) "1"
2) "2"
3) "7"
4) "3"
5) "0"
4.将元素从一个列表转到另一个列表
RPOPLPUSH source destination
RPOPLPUSH是个很有意思的命令,从名字就可以看出它的功能:先执行RPOP命令再执行LPUSH命令。RPOPLPUSH命令会先从source列表类型键的右边弹出一个元素,然后将其加入到destination列表类型键的左边,并返回这个元素的值,整个过程是原子的。其具体实现可以表示为伪代码:
def rpoplpush ($source, $destination)
$value = RPOP $source
LPUSH $destination, $value
return $value
当把列表类型作为队列使用时,RPOPLPUSH 命令可以很直观地在多个队列中传递数据。当source和destination相同时,RPOPLPUSH命令会不断地将队尾的元素移到队首,借助这个特性我们可以实现一个网站监控系统:使用一个队列存储需要监控的网址,然后监控程序不断地使用RPOPLPUSH命令循环取出一个网址来测试可用性。这里使用RPOPLPUSH命令的好处在于在程序执行过程中仍然可以不断地向网址列表中加入新网址,而且整个系统容易扩展,允许多个客户端同时处理队列。
3.5 集合类型博客首页,文章页面,评论页面……眼看着博客逐渐成型,小白的心情也是越来越好。时间已经到了深夜,小白却还陶醉于编码之中。不过一个他无法解决的问题最终还是让他不得不提早睡觉去:小白不知道该怎么在Redis中存储文章标签(tag)。他想过使用散列类型或列表类型存储,虽然都能实现,但是总觉得颇有不妥,再加上之前几天领略了Redis的强大功能后,小白相信一定有一种合适的数据类型能满足他的需求。于是小白给宋老师发了封询问邮件后就睡觉去了。
转天一早就收到了宋老师的回复:
你很善于思考嘛!你想的没错,Redis 有一种数据类型很适合存储文章的标签,它就是集合类型。
3.5.1 介绍集合的概念高中的数学课就学习过。在集合中的每个元素都是不同的,且没有顺序。一个集合类型(set)键可以存储至多232 −1个(相信这个数字对大家来说已经很熟悉了)字符串。
集合类型和列表类型有相似之处,但很容易将它们区分开来,如表3-4所示。
表3-4 集合类型和列表类型对比
集 合 类 型列 表 类 型
存储内容至多232 −1个字符串至多232 − 1个字符串
有序性否是
唯一性是否
集合类型的常用操作是向集合中加入或删除元素、判断某个元素是否存在等,由于集合类型在Redis内部是使用值为空的散列表(hash table)实现的,所以这些操作的时间复杂度都是O(1)。最方便的是多个集合类型键之间还可以进行并集、交集和差集运算,稍后就会看到灵活运用这一特性带来的便利。
3.5.2 命令1.增加/删除元素SADD key member [member …]
SREM key member [member …]
SADD命令用来向集合中增加一个或多个元素,如果键不存在则会自动创建。因为在一个集合中不能有相同的元素,所以如果要加入的元素已经存在于集合中就会忽略这个元素。本命令的返回值是成功加入的元素数量(忽略的元素不计算在内)。例如:
redis> SADD letters a
(integer) 1
redis> SADD letters a b c
(integer) 2
第二条SADD命令的返回值为2是因为元素“a”已经存在,所以实际上只加入了两个元素。
SREM命令用来从集合中删除一个或多个元素,并返回删除成功的个数,例如:
redis> SREM letters c d
(integer) 1
由于元素“d”在集合中不存在,所以只删除了一个元素,返回值为1。
2.获得集合中的所有元素SMEMBERS key
SMEMBERS命令会返回集合中的所有元素,例如:
redis> SMEMBERS letters
1) "b"
2) "a"
3.判断元素是否在集合中
SISMEMBER key member
判断一个元素是否在集合中是一个时间复杂度为O(1)的操作,无论集合中有多少个元素,SISMEMBER命令始终可以极快地返回结果。当值存在时SISMEMBER命令返回1,当值不存在或键不存在时返回0,例如:
redis> SISMEMBER letters a
(integer) 1
redis> SISMEMBER letters d
(integer) 0
4.集合间运算
SDIFF key [key …]
SINTER key [key …]
SUNION key [key …]
接下来要介绍的3个命令都是用来进行多个集合间运算的。
(1)SDIFF命令用来对多个集合执行差集运算。集合A与集合B的差集表示为A−_B_,代表所有属于A且不属于B的元素构成的集合(如图3-13所示),即A−_B_ = {x |x∈_A_且x∈B}。例如:
{1, 2, 3} - {2, 3, 4} = {1}
{2, 3, 4} - {1, 2, 3} = {4}
SDIFF命令的使用方法如下:
redis> SADD setA 1 2 3
(integer) 3
redis> SADD setB 2 3 4
(integer) 3
redis> SDIFF setA setB
1) "1"
redis> SDIFF setB setA
1) "4"
SDIFF命令支持同时传入多个键,例如:
redis> SADD setC 2 3
(integer) 2
redis> SDIFF setA setB setC
1) "1"
计算顺序是先计算setA - setB,再计算结果与setC的差集。
(2)SINTER命令用来对多个集合执行交集运算。集合A与集合B的交集表示为A ∩ B,代表所有属于A且属于B的元素构成的集合(如图3-14所示),即A ∩ B = {x | x ∈ A且_x_ ∈B}。例如:
{1, 2, 3} ∩ {2, 3, 4} = {2, 3}
SINTER命令的使用方法如下:
redis> SINTER setA setB
1) "2"
2) "3"
SINTER命令同样支持同时传入多个键,如:
redis> SINTER setA setB setC
1) "2"
2) "3"
(3)SUNION命令用来对多个集合执行并集运算。集合A与集合B的并集表示为A∪_B_,代表所有属于A或属于B的元素构成的集合(如图3-15所示)即A∪_B_ = {x | x∈_A_或x ∈_B_}。例如:
{1, 2, 3} ∪ {2, 3, 4} = {1, 2, 3, 4}
3-14{200}
图3-14 图中斜线部分表示A ∩ B
3-15{200}
图3-15 图中斜线部分表示A ∪ B
SUNION命令的使用方法如下:
redis> SUNION setA setB
1) "1"
2) "2"
3) "3"
4) "4"
SUNION命令同样支持同时传入多个键,例如:
redis> SUNION setA setB setC
1) "1"
2) "2"
3) "3"
4) "4"
3.5.3 实践1.存储文章标签
考虑到一个文章的所有标签都是互不相同的,而且展示时对这些标签的排列顺序并没有要求,我们可以使用集合类型键存储文章标签。
对每篇文章使用键名为post:文章ID:tags的键存储该篇文章的标签。具体操作如伪代码:
# 给ID为42的文章增加标签:
SADD post:42:tags, 闲言碎语, 技术文章, Java
# 删除标签:
SREM post:42:tags, 闲言碎语
# 显示所有的标签:
$tags = SMEMBERS post:42:tags
print $tags
使用集合类型键存储标签适合需要单独增加或删除标签的场合。如在WordPress博客程序中无论是添加还是删除标签都是针对单个标签的(如图3-16所示),可以直观地使用SADD和SREM命令完成操作。
另一方面,有些地方需要用户直接设置所有标签后一起上传修改,图3-17所示是某网站的个人资料编辑页面,用户编辑自己的爱好后提交,程序直接覆盖原来的标签数据,整个过程没有针对单个标签的操作,并未利用到集合类型的优势,所以此时也可以直接使用字符串类型键存储标签数据。
3-16{384}
{-:-}图3-16 在WordPress中设置文章标签