Redis的数据类型教程
学习基本的Redis数据类型,以及如何使用它们
下面是一个使用Redis CLI核心Redis数据类型的实践教程。关于数据类型的一般概述,请参见Redis的数据类型。
Key
Redis的Key是二进制安全的,这意味着你可以使用任何二进制序列作为Key,从"foo"这样的字符串到JPEG文件的内容。 空字符串也是一个有效的Key。
关于Key的其他几条规则:
- 非常长的Key不是一个好主意。例如,1024字节的Key不仅在内存上是个坏主意,而且还因为在数据集中查找该Key可能需要几次昂贵的Key比较。即使手头的任务是匹配一个大数值的存在,散列它(例如用SHA1)也是一个更好的主意,特别是从内存和带宽的角度来看。
- 非常短的Key通常不是一个好主意。如果你可以改为写"user:1000:followers",那么写"u1000flw"作为一个Key就没有什么意义。与Key对象本身和值对象所使用的空间相比,后者更具可读性并且增加的空间较小。虽然短Key显然会消耗更少的内存,但你的工作是找到正确的平衡。
- 试着坚持使用一个模式。例如,"object-type:id"是一个好主意,如"user:1000"。点或破折号通常用于多字段,如在"comment:4321:reply.to"或"comment:4321:reply-to"。
- 允许的最大Key大小为512MB。
字符串
Redis字符串类型是你能与Redis Key关联的最简单的值类型。它是Memcached中唯一的数据类型,所以对于新人来说,在Redis中使用它也是非常自然的。
由于Redis的Key是字符串,当我们把字符串类型也作为一个值时,我们是把一个字符串映射到另一个字符串。字符串数据类型对许多用例都很有用,比如缓存HTML片段或页面。
让我们用redis-cli
来玩一下字符串类型(在本教程中,所有的例子都将通过redis-cli
来进行)。
> set mykey somevalue
OK
> get mykey
"somevalue"
正如你所看到的,使用SET
和GET
命令是我们设置和检索一个字符串值的方式。请注意,如果Key已存在,即使Key与非字符串值关联,SET
将替换任何已经存储到Key中的现有值,在Key已经存在的情况下,即使该Key与非字符串值相关。所以SET
执行了一个赋值。
值可以是各种字符串(包括二进制数据),例如你 可以在值中存储 jpeg 图像。值不能大于 512 MB。
SET
命令有一些有趣的选项,它们作为附加参数提供。例如,如果Key已经存在,我可能会要求 SET
失败,或者相反,它只有在Key已经存在时才会成功:
> set mykey newval nx
(nil)
> set mykey newval xx
OK
即使字符串是Redis的基本值,你也可以对其进行一些有趣的操作。例如,一个是原子增量:
> set counter 100
OK
> incr counter
(integer) 101
> incr counter
(integer) 102
> incrby counter 50
(integer) 152
INCR命令将字符串值解析为整数,将其递增1,最后将得到的值设置为新值。 还有其他类似的命令,如INCRBY、DECR和DECRBY。在内部,它始终是同一个命令,以稍微不同的方式行事。
INCR是原子的,这意味着即使是多个客户端对同一个Key发出INCR,也不会出现竞赛状态。例如,绝不会发生客户端1读取"10",客户端2同时读取"10",两者都递增到11,并将新值设置为11。最终的值将永远是12,并且在所有其他客户端没有同时执行命令的情况下进行read-increment-set操作。
有许多对字符串进行操作的命令。例如,GETSET
命令将一个Key设置为一个新值,并将旧值作为结果返回。你可以使用这个命令,例如,如果你有一个系统,每当你的网站收到一个新的访问者时,就用INCR
增加一个Redis Key。你可以GETSET
Key,给它分配"0"的新值,并读回旧值。
在一条命令中设置或检索多个Key值的能力对于减少延迟也很有用。为此,有MSET
和MGET
命令:
> mset a 10 b 20 c 30
OK
> mget a b c
1) "10"
2) "20"
3) "30"
当使用MGET
时,Redis会返回一个数组的值。
更改和查询Key空间
有些命令未针对特定类型进行定义,但对于与Key空间进行交互非常有用,因此可以与任何类型的Key一起使用。
例如,EXISTS
命令返回1或0,以示数据库中是否存在一个给定的Key,而DEL
命令删除一个Key和相关的值,不管这个值是什么。
> set mykey hello
OK
> exists mykey
(integer) 1
> del mykey
(integer) 1
> exists mykey
(integer) 0
从例子中你也可以看到DEL
本身是如何返回1或0的,这取决于该Key是否被删除(它存在)或不存在(没有这样的Key,没有这个名字)。
有许多与Key空间相关的命令,但以上两个是基本的命令,还有TYPE
命令,它返回存储在指定Key上的值的种类:
> set mykey x
OK
> type mykey
string
> del mykey
(integer) 1
> type mykey
none
Key过期
在继续之前,我们应该看一下Redis的一个重要功能,无论你存储的是什么类型的值,它都能发挥作用:Key过期。Key过期让你为一个Key设置一个超时时间,也被称为 "生存时间",或 "TTL"。当生存时间过期时,Key会自动销毁。
关于Key过期的几个重要说明:
- 它们可以用秒或毫秒的精度来设置。
- 然而,过期时间的始终是1毫秒为最小单位。
- 有关过期的信息会被复制并保留在磁盘上,当你的Redis服务器保持停止状态时,时间实际上已经过去了(这意味着 Redis 会保存Key过期的日期)。
使用EXPIRE
命令来设置Key的过期时间:
> set key some-value
OK
> expire key 5
(integer) 1
> get key (immediately)
"some-value"
> get key (after some time)
(nil)
Key在两次GET
调用之间消失了,因为第二次调用延迟了5秒以上。在上面的例子中,我们用EXPIRE
来设置过期时间(也可以使用它来为已有的Key设置不同的过期时间,比如PERSIST
可以用来删除过期时间,并使Key永远持久)。然而,我们也可以使用其他Redis命令来创建带有过期时间的Key。例如,使用SET
选项:
> set key 100 ex 10
OK
> ttl key
(integer) 9
上面的例子设置了一个字符串值为100
的钥,有效期为10秒。之后调用TTL
命令,以检查该Key的剩余有效期。
为了设置和检查以毫秒为单位的过期时间,请检查PEXPIRE
和PTTL
命令,以及SET
选项的完整列表。
列表
为了解释列表数据类型,最好从一些理论开始,因为List这个术语经常被信息技术人员以不恰当的方式使用。例如,"Python List"并不像它的名字所暗示的那样(链接列表),而是数组(实际上同样的数据类型在Ruby中被称为Array)。
从一个非常普遍的角度来看,列表只是一个有序元素的序列:10、20、1、2、3就是一个列表。但是使用数组实现的List的属性与使用链接List实现的List的属性有很大不同。
Redis的列表是通过链接列表实现的。这意味着,即使你在一个列表中拥有数百万个元素,在列表头部或尾部添加新元素的操作也会在恒定的时间内进行。用LPUSH
命令在一个有10个元素的列表头部添加一个新元素的速度,与在有1000万个元素的列表头部添加一个元素的速度相同。
有什么坏处呢?在用数组实现的列表中,访问一个元素通过索引是非常快的(恒定时间的索引访问),而在用链表实现的列表中就不那么快了(操作需要的工作量与访问元素的索引成正比)。
Redis 列表是用链接列表实现的,因为对于数据库系统来说,能够以非常快的方式向一个很长的列表添加元素是非常关键的。 另一个强大的优势,正如你马上就会看到的,就是 Redis 列表可以在恒定的时间内以恒定的长度进行。
当快速访问一个大的元素集合的中间部分很重要时,可以使用一种不同的数据结构,称为排序集。 排序集将在本教程的后半部分介绍。
使用Redis List的第一个步骤
LPUSH
命令在左边(头部)的列表中添加一个新元素,而RPUSH
命令在右边(尾部)的列表中添加一个新元素。最后,LRANGE
命令从列表中提取元素的范围:
> rpush mylist A
(integer) 1
> rpush mylist B
(integer) 2
> lpush mylist first
(integer) 3
> lrange mylist 0 -1
1) "first"
2) "A"
3) "B"
请注意,LRANGE需要两个索引,即要返回的范围的第一个和最后一个元素。两个索引都可以是负数,告诉 Redis 从末尾开始计算:所以 -1 是最后一个元素,-2 是列表的倒数第二个元素,以此类推。
你可以看到RPUSH
将元素附加到列表的右侧,而最后的LPUSH
将元素附加在左侧。
这两个命令都是可变参数命令,这意味着您可以在一次调用中自由地将多个元素推送到列表中:
> rpush mylist 1 2 3 4 5 "foo bar"
(integer) 9
> lrange mylist 0 -1
1) "first"
2) "A"
3) "B"
4) "1"
5) "2"
6) "3"
7) "4"
8) "5"
9) "foo bar"
Redis 列表上定义的一个重要操作是pop元素 的能力。弹出元素是同时从列表中检索元素并从列表中删除元素的操作。您可以从左侧和右侧弹出元素,类似于在列表两侧推送元素:
> rpush mylist a b c
(integer) 3
> rpop mylist
"c"
> rpop mylist
"b"
> rpop mylist
"a"
我们添加了三个元素,弹出了三个元素,所以在这一序列命令的最后,列表是空的,没有更多的元素可以弹出。如果我们试图再弹出一个元素,我们会得到这样的结果:
> rpop mylist
(nil)
Redis返回了一个NULL值,表示列表中没有任何元素。
列表的常见使用情况
列表在许多任务中都很有用,以下是两个非常有代表性的用例:
- 记住用户在社交网络中发布的最新更新。
- 进程之间的通信,使用消费者-生产者模式,生产者将项目推入列表,而消费者(通常是一个工作者)消费这些项目并执行行动。Redis有特殊的列表命令,使这种用例更加可靠和高效。
例如,流行的Ruby库resque和sidekiq都在引擎盖下使用Redis列表,以实现后台作业。
流行的Twitter社交网络将用户发布的最新推文录入Redis列表。
为了逐步描述一个常见的用例,假设您的主页显示共享社交网络中发布的最新照片,并且您希望加快访问速度。
- 每次用户发布新的照片,我们就把它的ID添加到一个列表中,用
LPUSH
来表示。 - 当用户访问主页时,我们使用
LRANGE 0 9
,以便获得最新的10条发布的信息。
上限列表
在许多用例中,我们只想用列表来存储最新的项目,不管它们是什么:社交网络更新、日志或其他任何东西。
Redis允许我们将列表作为一个有上限的集合,只记住最新的N个项目,并使用LTRIM
命令丢弃所有最旧的项目。
LTRIM
命令与LRANGE
类似,但它不显示指定的元素范围,而是将这个范围设置为新的列表值。所有超出指定范围的元素都会被删除。
举个例子会更清楚地说明问题:
> rpush mylist 1 2 3 4 5
(integer) 5
> ltrim mylist 0 2
OK
> lrange mylist 0 -1
1) "1"
2) "2"
3) "3"
上面的LTRIM
命令告诉Redis只提取索引0到2的列表元素,其他的都将被丢弃。这允许一个非常简单但有用的模式:做一个列表推送操作+一个列表修剪操作,以便添加一个新的元素并丢弃超过限制的元素:
LPUSH mylist <some element>
LTRIM mylist 0 999
上面的组合增加了一个新的元素,并且只把1000个最新的元素带入列表。通过LRANGE
,你可以访问最上面的项目,而不需要记住非常老的数据。
注意:虽然LRANGE
在技术上是一个O(N)命令,但向列表的头部或尾部访问小范围是一个恒定的时间操作。
阻止对列表进行操作
列表有一个特殊的特点,使其适合于实现队列,一般来说,作为进程间通信系统的构建块:阻塞操作。
想象一下,您想用一个进程将项目推送到一个列表中,然后用另一个进程对这些项目进行实际操作。这就是通常的生产者/消费者设置,可以通过以下简单的方法实现:
但是有可能有时候列表是空的,没有东西可以处理,所以RPOP
只是返回NULL。在这种情况下,消费者不得不等待一段时间,然后用RPOP
再次重试。这被称为polling,在这种情况下不是一个好主意,因为它有几个缺点:
- 强迫Redis和客户端处理无用的命令(当列表为空时,所有的请求都不会得到任何实际的工作,它们只会返回NULL)。
- 会增加处理项目的延迟,因为工作进程在收到 NULL 后会等待一段时间。为了减少延迟,我们可以减少调用
RPOP
之间的等待时间,但这样做的结果是扩大了问题 1,即增加了对 Redis 的无用调用。
因此Redis实现了名为BRPOP
和BLPOP
的命令,这些命令是RPOP
和LPOP
的版本,能够在列表为空时进行阻塞:只有当有新元素被添加到列表中,或达到用户指定的超时时,它们才会返回给调用者。
这是一个BRPOP
调用的例子,我们可以在worker中使用:
> brpop tasks 5
1) "tasks"
2) "do_something"
它的意思是:"等待列表中的元素tasks
,但如果5秒后没有元素,则返回"。
注意,你可以用0作为超时来永远等待元素,你也可以指定多个列表,而不是只有一个,以便同时等待多个列表,当第一个列表收到一个元素时,就会得到通知。
关于BRPOP
,有几件事情需要注意:
- 客户端的服务是有序的:当某个元素被其他客户端推送时,最先阻止等待列表的客户端最先得到服务,以此类推。
- 与
RPOP
相比,返回值是不同的:它是一个双元素数组,因为它还包括key的名称,因为BRPOP
和BLPOP
能够阻止等待来自多个列表的元素的。 - 如果超时,则返回NULL。
关于列表和拦截操作,您应该知道更多。我们建议您阅读以下内容:
自动创建和删除Key
到目前为止,在我们的示例中,我们从未在推送元素前创建过空列表,也从未在空列表中不再有元素时将其删除。 当列表留空时,是Redis负责删除Key,或者在Key不存在而我们试图向其添加元素时创建一个空列表,例如,用LPUSH
来创建。
这不是专门针对列表的,它适用于所有由多个元素组成的Redis数据类型--流、集、排序集和哈希值。
基本上,我们可以用三条规则来总结这种行为:
- 当我们向聚合数据类型添加元素时,如果目标Key不存在,则在添加元素之前创建一个空的聚合数据类型。
- 当我们从聚合数据类型中删除元素时,如果值仍然为空,则Key将自动销毁。流数据类型是唯一的例外。
- 调用只读命令,如
LLEN
(返回列表的长度),或使用空Key删除元素的写命令,总是产生相同的结果,就好像该Key持有命令期望查找的类型的空聚合类型一样。
第1条规则的例子:
> del mylist
(integer) 1
> lpush mylist 1 2 3
(integer) 3
然而,如果键存在的话,我们就不能对错误的类型进行操作:
> set foo bar
OK
> lpush foo 1 2 3
(error) WRONGTYPE Operation against a key holding the wrong kind of value
> type foo
string
第2条规则的例子:
> lpush mylist 1 2 3
(integer) 3
> exists mylist
(integer) 1
> lpop mylist
"3"
> lpop mylist
"2"
> lpop mylist
"1"
> exists mylist
(integer) 0
在所有元素被弹出后,Key就不再存在了。
第3条规则的例子:
> del mylist
(integer) 0
> llen mylist
(integer) 0
> lpop mylist
(nil)
哈希
Redis的哈希看起来和人们期望的哈希完全一样,都是字段-值对的:
> hset user:1000 username antirez birthyear 1977 verified 1
(integer) 3
> hget user:1000 username
"antirez"
> hget user:1000 birthyear
"1977"
> hgetall user:1000
1) "username"
2) "antirez"
3) "birthyear"
4) "1977"
5) "verified"
6) "1"
虽然哈希表示对象很方便,但实际上你可以放在哈希中的字段数量并没有实际限制(除了可用的内存),所以你可以在你的应用程序中以许多不同的方式使用哈希。
命令HSET
设置哈希的多个字段,而HGET
检索单个字段。HMGET
与HGET
类似,但返回一个数组的值:
> hmget user:1000 username birthyear no-such-field
1) "antirez"
2) "1977"
3) (nil)
有一些命令也能够对单个字段进行操作,比如HINCRBY
:
> hincrby user:1000 birthyear 10
(integer) 1987
> hincrby user:1000 birthyear 10
(integer) 1997
你可以在文档中找到哈希命令的完整列表。
值得注意的是,小的哈希(即具有小数值的几个元素)是以特殊的方式在内存中编码的,这使得它们的内存效率非常高。
集(Set)
Redis集合是无序的字符串集合。SADD
命令向一个集合添加新的元素。也可以对集合进行其他一些操作,比如测试一个给定的元素是否已经存在,执行多个集合之间的交集、并集或差集,等等。
> sadd myset 1 2 3
(integer) 3
> smembers myset
1. 3
2. 1
3. 2
在这里,我向我的集合添加了三个元素,并告诉Redis返回所有的元素。正如你所看到的,它们没有被排序 -- Redis可以在每次调用时以任何顺序返回元素,因为没有与用户约定元素的顺序。
Redis有测试成员的命令。例如,检查一个元素是否存在:
> sismember myset 3
(integer) 1
> sismember myset 30
(integer) 0
"3"是一个集合的成员,而"30"则不是。
集合适合于表达对象之间的关系。 例如,我们可以很容易地使用集合来实现标签。
对这个问题进行建模的一个简单方法是,为每个我们想要标记的对象建立一个集合。这个集合包含与该对象相关的标签的ID。
一个例子是给新闻文章打标签。 如果文章ID1000被打上了标签1、2、5和77,一套可以将这些标签ID与新闻项目联系起来:
> sadd news:1000:tags 1 2 5 77
(integer) 4
我们可能还希望具有逆关系:带有给定标签的所有新闻的列表:
> sadd tag:1:news 1000
(integer) 1
> sadd tag:2:news 1000
(integer) 1
> sadd tag:5:news 1000
(integer) 1
> sadd tag:77:news 1000
(integer) 1
要获得一个给定对象的所有标签并不难:
> smembers news:1000:tags
1. 5
2. 1
3. 77
4. 2
注意:在这个例子中,我们假设你有另一个数据结构,例如Redis哈希,它将标签ID映射到标签名称上。
使用正确的 Redis 命令,还可以轻松实现其他非琐碎操作。例如,我们可能想要一个所有标签为1、2、10和27的对象的列表。我们可以使用SINTER
命令来实现,它可以执行不同集合之间的交集。我们可以使用:
> sinter tag:1:news tag:2:news tag:10:news tag:27:news
... results here ...
除了交集之外,您还可以执行并集、差集、提取随机元素等。
提取元素的命令被称为SPOP
,在对某些问题进行建模时非常方便。例如,为了实现一个基于网络的扑克游戏,你可能想用一个集合来表示你的牌。想象一下,我们用一个字符前缀来表示(C)lubs, (D)iamonds, (H)earts, (S)pades:
> sadd deck C1 C2 C3 C4 C5 C6 C7 C8 C9 C10 CJ CQ CK
D1 D2 D3 D4 D5 D6 D7 D8 D9 D10 DJ DQ DK H1 H2 H3
H4 H5 H6 H7 H8 H9 H10 HJ HQ HK S1 S2 S3 S4 S5 S6
S7 S8 S9 S10 SJ SQ SK
(integer) 52
现在我们想为每个玩家提供5张牌。SPOP
命令删除了一个随机元素,将其返回给客户端,所以在这种情况下,它是完美的操作。
然而,如果我们直接针对我们的牌组调用它,在下一次游戏中,我们将需要再次填充牌组,这可能不是很理想。因此,我们可以先将存储在deck
键中的牌组复制到game:1:deck
键中。
这是用SUNIONSTORE
完成的,它通常在多个集合之间进行联合,并将结果存储到另一个集合中。 然而,由于单个集合的联合就是它自己,我可以用以下方式复制我的牌:
> sunionstore game:1:deck deck
(integer) 52
现在,我准备为第一个玩家提供五张牌:
> spop game:1:deck
"C6"
> spop game:1:deck
"CQ"
> spop game:1:deck
"D1"
> spop game:1:deck
"CJ"
> spop game:1:deck
"SJ"
一对插孔,不是很好...
这是一个介绍set命令的好时机,它提供了一个集合内部元素的数量。在集合理论中,这通常被称为集合的基数(cardinality),所以Redis的命令被称为SCARD
。
> scard game:1:deck
(integer) 47
计算结果是:52 - 5 = 47.
当你需要只获得随机元素而不把它们从集合中删除时,有一个SRANDMEMBER
命令适合这个任务。它还具有返回重复和非重复元素的功能。
排序集合(Sorted set)
排序集是一种数据类型,类似于Set和Hash的混合体。像集合一样,排序集由唯一的、不重复的字符串元素组成,所以在某种意义上,排序集也是一个集合。
然而,虽然集合内的元素不是有序的,但排序后的集合中的每个元素都与一个浮点值相关联,称为分数(这就是为什么类型也类似于哈希,因为每个元素都被映射到一个值)。
此外,排序集中的元素是按顺序取的(所以它们不是按要求排序的,排序是用于表示排序集的数据结构的一个特殊性)。它们是根据以下规则来排序的:
- 如果B和A是两个具有不同分数的元素,那么如果A.score是>B.score,则A>B。
- 如果 B 和 A 的得分完全相同,那么如果 A 字符串的词性大于 B 字符串的词性,则 A > B。B 和 A 字符串不可能相等,因为排序集只有唯一元素。
让我们从一个简单的例子开始,把一些选定的黑客名字作为排序的集合元素,把他们的出生年月作为"得分"。
> zadd hackers 1940 "Alan Kay"
(integer) 1
> zadd hackers 1957 "Sophie Wilson"
(integer) 1
> zadd hackers 1953 "Richard Stallman"
(integer) 1
> zadd hackers 1949 "Anita Borg"
(integer) 1
> zadd hackers 1965 "Yukihiro Matsumoto"
(integer) 1
> zadd hackers 1914 "Hedy Lamarr"
(integer) 1
> zadd hackers 1916 "Claude Shannon"
(integer) 1
> zadd hackers 1969 "Linus Torvalds"
(integer) 1
> zadd hackers 1912 "Alan Turing"
(integer) 1
正如你所看到的ZADD
与SADD
相似,但需要一个额外的参数(放在要添加的元素之前),这就是分数。 ZADD
也是可变的,所以你可以自由指定多个分数-值对,即使上面的例子中没有使用这个方法。
通过排序集,返回一个按出生年月排序的黑客列表是很简单的,因为实际上他们已经被排序了。
实现说明:排序集是通过一个包含跳过列表和哈希表的双端口数据结构实现的,所以每次我们添加一个元素,Redis都会执行一个 O(log(N)) 的操作。这很好,是当我们要求排序的元素时,Redis根本不需要做任何工作,它已经全部排序了:
> zrange hackers 0 -1
1) "Alan Turing"
2) "Hedy Lamarr"
3) "Claude Shannon"
4) "Alan Kay"
5) "Anita Borg"
6) "Richard Stallman"
7) "Sophie Wilson"
8) "Yukihiro Matsumoto"
9) "Linus Torvalds"
注意:0和-1意味着从元素索引0到最后一个元素(-1在这里的作用就像在LRANGE
命令的情况一样)。
如果我想以相反的方式排序,从最年轻的到最老的,该怎么办? 使用ZREVRANGE,而不是ZRANGE:
> zrevrange hackers 0 -1
1) "Linus Torvalds"
2) "Yukihiro Matsumoto"
3) "Sophie Wilson"
4) "Richard Stallman"
5) "Anita Borg"
6) "Alan Kay"
7) "Claude Shannon"
8) "Hedy Lamarr"
9) "Alan Turing"
使用WITHSCORES
参数,也可以返回分数:
> zrange hackers 0 -1 withscores
1) "Alan Turing"
2) "1912"
3) "Hedy Lamarr"
4) "1914"
5) "Claude Shannon"
6) "1916"
7) "Alan Kay"
8) "1940"
9) "Anita Borg"
10) "1949"
11) "Richard Stallman"
12) "1953"
13) "Sophie Wilson"
14) "1957"
15) "Yukihiro Matsumoto"
16) "1965"
17) "Linus Torvalds"
18) "1969"
在范围内操作
排序集比这更强大。让我们得到所有在1950年以前出生的人。我们使用ZRANGEBYSCORE
命令来做这件事:
> zrangebyscore hackers -inf 1950
1) "Alan Turing"
2) "Hedy Lamarr"
3) "Claude Shannon"
4) "Alan Kay"
5) "Anita Borg"
我们要求Redis返回所有得分在负无穷大和1950之间的元素(两个极端都包括在内)。
也可以删除某一范围的元素。让我们从排序的集合中删除所有出生于1940年和1960年之间的黑客:
> zremrangebyscore hackers 1940 1960
(integer) 4
ZREMRANGEBYSCORE
也许不是最好的命令名称,但它可以非常有用,并返回被移除元素的数量。
另一个为排序集合元素定义的极其有用的操作是get-rank操作。可以查找一个元素在排序集合中的位置。
> zrank hackers "Anita Borg"
(integer) 4
ZREVRANK
命令也是可用的,以便获得排名,考虑到元素以降序方式排序。
字典顺序score
在最近的Redis 2.8版本中,引入了一个新的功能,允许按词法获取范围,假设一个排序的集合中的元素都是以相同的分数插入的(元素是用C memcmp
函数比较的,所以可以保证没有整理,每个Redis实例都会以相同的输出来回复)。
操作词典范围的主要命令是ZRANGEBYLEX
,ZREVRANGEBYLEX
,ZREMRANGEBYLEX
和ZLEXCOUNT
。
例如,让我们再次添加我们的黑客名单,但这次对所有的元素都使用了零分:
> zadd hackers 0 "Alan Kay" 0 "Sophie Wilson" 0 "Richard Stallman" 0
"Anita Borg" 0 "Yukihiro Matsumoto" 0 "Hedy Lamarr" 0 "Claude Shannon"
0 "Linus Torvalds" 0 "Alan Turing"
由于排序集的排序规则,它们已经是按词汇表排序的:
> zrange hackers 0 -1
1) "Alan Kay"
2) "Alan Turing"
3) "Anita Borg"
4) "Claude Shannon"
5) "Hedy Lamarr"
6) "Linus Torvalds"
7) "Richard Stallman"
8) "Sophie Wilson"
9) "Yukihiro Matsumoto"
使用ZRANGEBYLEX
,我们可以查询字典范围:
> zrangebylex hackers [B [P
1) "Claude Shannon"
2) "Hedy Lamarr"
3) "Linus Torvalds"
范围可以是包容的,也可以是排他的(取决于第一个字符),也可以用+
和-
字符串分别指定无限字符串和负无限字符串。更多信息,请参阅文档。
这个功能很重要,因为它允许我们使用排序集作为通用索引。例如,如果你想通过一个128位的无符号整数参数来索引元素,你所需要做的就是将元素添加到一个具有相同分数(例如0)的排序集中,但有一个16字节的前缀,由大端的128位数字组成。由于大端的数字在按字母顺序排列时(按原始字节顺序)实际上也是按数字顺序排列的,所以你可以要求在128位空间内的范围,并得到元素的值,而放弃前缀。
如果您想在一个更严肃的演示中看到该功能,请查看Redis自动完成演示。
更新分数:排行榜
在转到下一个话题之前,我想对排序集做最后的说明。 排序集的分数可以在任何时候更新。只要针对一个已经包含在排序集中的元素调用ZADD
,就会以O(log(N))的时间复杂度更新其分数(和位置)。因此,当有大量的更新时,排序集是合适的。
由于这个特点,一个常见的用例是排行榜。 典型的应用是一个Facebook游戏,你把用户按高分排序的能力,加上get-rank操作,以显示前N名的用户,以及用户在排行榜中的排名(例如,"你是这里的#4932最佳得分者")。
位图
位图不是一个实际的数据类型,而是一组定义在String类型上的面向位的操作。由于字符串是二进制安全的blob,其最大长度为512MB,所以它们适合设置2^32个不同的位。
位操作分为两组:恒定时间的单个位操作,如将一个位设置为1或0,或获得其值,以及对位组的操作,如在给定的位范围内计算被设置位的数量(如人口计数)。
位图的最大优势之一是在存储信息时可以极大地节省空间。例如,在一个用增量用户 ID 表示不同用户的系统中,只需使用 512 MB 内存,就可以记住 40 亿用户的单个位信息(例如,了解用户是否愿意接收时事通讯)。
> setbit key 10 1
(integer) 0
> getbit key 10
(integer) 1
> getbit key 11
(integer) 0
SETBIT
命令的第一个参数是位号,第二个参数是要设置的位值,即1或0。 如果被寻址的位在当前字符串长度之外,该命令会自动扩大字符串的长度。
GETBIT
只是返回指定索引处的位的值。 超出范围的位(寻址超出目标Key存储字符串长度的位)总是被认为是零。
有三条命令对位组进行操作:
BITPOS
和BITCOUNT
都能对字符串的字节范围进行操作,而不是对字符串的整个长度进行操作。下面是一个调用BITCOUNT
的简单例子:
> setbit key 0 1
(integer) 0
> setbit key 100 1
(integer) 0
> bitcount key
(integer) 2
位图的常见使用情况是:
- 各种实时分析。
- 存储与对象 ID 相关的布尔信息,空间利用率高,但性能好。
例如,您想知道网站用户最长的连续日访问量。您可以从零开始计算天数,也就是您公开网站的那一天,并在每次用户访问网站时用 SETBIT
设置一个位。作为位索引,只需用当前的 unix 时间减去初始偏移量,再除以一天中的秒数(通常为 3600*24)即可。
这样,每个用户每天都有一个包含访问信息的小字符串。通过 BITCOUNT
,可以轻松获得特定用户访问网站的天数,而通过调用几次 BITPOS
,或只需在客户端获取并分析位图,就可以轻松计算出最长的连访天数。
将位图分割成多个Key是非常简单的,例如,为了对数据集进行分片,以及在一般情况下最好避免处理巨大的Key。要将位图分割到不同的Key中,而不是将所有位都设置到一个Key中,一个简单的方法是每个Key存储 M 位,然后用bit-number/M
获得Key的名称,用bit-number MOD M
获得Key内的第N位地址。
HyperLog日志
HyperLogLog是一个概率数据结构,用来计算唯一的项(技术上这是指估计一个集合的基数)。通常,计算唯一项需要使用的内存量与要计算的项数成正比,因为你需要记住过去已经看到过的元素,以避免重复计算。不过,有一套算法可以用内存换取精确度:你最终得到的是带有标准误差的估计值,在 Redis 实现中,标准误差小于 1%。这种算法的神奇之处在于,你不再需要使用与计算项数成正比的内存量,而是可以使用恒定的内存量!在最坏的情况下,只需使用 12k 字节,如果你的HyperLogLog(我们从现在开始就叫他们HLL)中的元素很少,那么使用的内存就会少得多。
Redis 中的 HLL 虽然在技术上是一种不同的数据结构,但被编码为 Redis 字符串,因此您可以调用 GET
来序列化一个 HLL,并调用 SET
来将其反序列化回服务器。
从概念上讲,HLL API就像使用集合来完成同样的任务。您可以将每个观察到的元素 SADD
到一个集合中,然后使用SCARD
来检查集合内的元素数量,这些元素是唯一的,因为SADD
不会重新添加一个现有的元素。
由于数据结构只包含状态而不包含实际元素,因此你并不能真正将 项目添加到 HLL,但 API 是一样的:
-
每看到一个新元素,就用
PFADD
将其添加到计数中。 -
每次你想检索迄今为止使用
PFADD
添加的唯一元素added的近似值时,你都会使用PFCOUNT
。> pfadd hll a b c d (integer) 1 > pfcount hll (integer) 4
这种数据结构的一个用例是计算用户每天在搜索表单中进行的唯一查询。
Redis也能够执行HLL的联合,请查看完整的文档以了解更多信息。
其他值得注意的特点
Redis API中还有其他一些重要的东西,在本文中无法探讨,但值得你注意:
- 可以增量地遍历一个大型集合的Key空间。
- 可以运行 Lua 脚本服务器端 来改善延迟和带宽。
- Redis 也是一个Pub-Sub 服务器。
了解更多信息
本教程绝不是完整的,它只涵盖了API的基础知识。 阅读命令参考,以发现更多的内容。
谢谢你的阅读,祝你在使用Redis的过程中获得乐趣!