Redis的数据类型和抽象概念介绍

原文链接 译者:carvin

Redis 不是一个 简单的 key-value 存储,实际上它是一个数据结构服务器,它支持不同类型的值。也就是说,在传统的key-value存储中,你将一个字符串的key关联到一个字符串的值上;而在Redis中,值不仅仅局限于简单的字符串,还同时支持其他复杂的数据结构。以下的列表是所有Redis支持的数据结构,在这篇指南中将一一介绍:

  • 二进制安全的字符串。
  • 列表[Lists]: 按照插入顺序排序的字符串元素集合。它们基于链表实现。
  • 集合[Sets]: 唯一的、无序的字符串元素集合。
  • 有序集合[Sorted sets]: 与集合类似,但是其中的每一个字符串元素都关联了一个浮点数值,称之为[score]。这些元素都是按照这个score来进行排序,所以它与集合不同,
    它可以获取一个元素范围(比如你可以说:给我最上面的10条数据,或者最下面的10条数据)。
  • 哈希[Hashes]: 由字段及相关联的值组成的maps。字段和值都是字符串。这非常类似于Ruby或者Python中的哈希。
  • 位数组(或者简单的bitmaps): 它可以使用特殊的命令,将字符串值处理为一个位的数组:你可以设置或者清空个别的位,统计所有设置为1的位,查找第一个设置或者没有设置的位等等。
  • HyperLogLogs: 这是一个基于概率的数据结构,它用于估算一个集合中的基数。不用担心,它没有看起来那么难… 稍候会在后续的HyperLogLog章节中对其进行介绍。

这些数据类型如何工作,以及通过这些命令命令参考如何去解决已有的问题,通常并不是那么容易理解的。所以这篇文档是关于Redis数据类型以及很多常见模式的一个速成教程。

所有的示例我们都将使用redis-cli 工具,它是一个简单,易使用的命令行工具,用于向Redis服务器发送指令。

Redis 的key

Redis 的key是二进制安全的,也就是说你可以使用任何二进制的序列作为key,从一个”foo”字符串到一个JPEG文件的内容都可以。空字符串也同样是一个有效的key。

一些其他的关于key的规则:

  • 使用非常长的key并不是好的主意,例如使用一个1024字节(bytes)长度的key就很烂,其不仅仅耗费内存,而且在数据集中查找这个key时也需要很高的比较成本。即使当前处理的任务是匹配存在的大值,采用哈希来处理(例如使用SHA1) 会是一个更好的主意,特别是从内存和带宽的角度来看。
  • 使用很短的key通常也不是一个好主意。将”u1000flw”作为一个key几乎是毫无意义的,如果你可以将其替换成”user:1000:followers”,则它会更具可读性,而由此增加的空间与key对象和值对象使用的空间比起来微乎其微。当简短的key将明显的消耗一小部分内存,你的工作就是寻找到一个正确的平衡点。
  • 尝试去坚持一个模式。例如使用”object-type:id”做为key就是一个好的想法,像”user:1000″。点或者虚线经常被用作多单词字段的连接符,例如”comment:1234:reply.to”或”comment:1234:reply-to”。
  • key最大可分配512MB。

Redis 的字符串类型[Strings]

Redis字符串类型是你能够与一个Redis的key关联的最简单的值类型。它也是Memcached中唯一的数据类型,所以新手可以很自然的在Redis中使用它。

Redis的key是字符串的,我们同样可以使用字符串类型来做为值,我们是将一个字符串映射到另一个字符串上。字符串数据类型在很多场景下是很有用的, 例如缓存HTML片段或者页面。

下面让我们使用 redis-cli (在这个指南中所有的示例我们都将使用redis-cli) 测试一下字符串类型。

> set mykey somevalue
OK
> get mykey
"somevalue"

如你所见,我们使用SETGET命令来设置和获取一个字符串值。需要注意的是SET将会替换存储在这个key下任何存在的值,在这个key已经存在的情况下,即使这个key关联的是一个非字符串的值,也同样会被替换。所以说 SET 执行的是一个赋值操作。

值可以是各种字符串(包括二进制数据),例如你可以在一个key中存储一个jpeg图片作为值。一个值最大不能超过512MB。

SET 命令有一些有趣的选项,它们以可选参数的方式提供。例如,我可以要求在key已经存在的情况下让SET命令执行失败;与其相反的,也可以让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 命令会将字符串解析为整数,然后对其进行自增,并将最终获得的值设置为新的值。还有一些类似的命令,比如说:INCRBY,DECRDECRBY。在内部,它们是一样的命令,只是在表现上稍有不同。

为什么说INCR是原子的呢?

这是因为即使多客户端同时向相同的key发出INCR命令,它们也不会产生竞争。例如,
客户端1读取到”10″,客户端2也同时读取到”10″,然后两个都要将其自增到11,并将新值设置为11。上面这种情况永远不会发生。因为最终的值将会是12,read-increment-set操作在执行的时候所有其他的客户端都不能同时执行这个命令。

有很多的命令可以用于字符串操作。例如,GETSET命令将为一个key设置一个新的值,并将原值作为结果返回。以下场景你可以使用这个命令,例如,如果你有这样一个系统,当你的网站有新访问者的时候, 都使用INCR来自增一个key的值;
你可能想每隔一个小时收集一次这个信息,还不想丢失每一次自增。
你就可以对这个key执行GETSET,将新值设置为”0″,并读取返回的原值。

Redis还可以通过一个命令来设置或者获取多个keys的值,这在低延迟应用中很有用。
基于这个原因,Redis提供了 MSETMGET 命令:

> 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 命令结合使用的基本部分,
TYPE 将返回指定key所存储的值的类型。

> set mykey x
OK
> type mykey
string
> del mykey
(integer) 1
> type mykey
none

Redis 的过期:key存活的时间限制

在继续介绍复杂的数据结构之前,我们需要讨论另外一个与值类型无关的特性,它叫做 Redis 的过期。基本上,你可以为一个key设置一个过期时间,也就是key的存活的期限。当达到存活的时间,这个key将会自动销毁,它跟用户在一个key上调用 DEL 命令是完全一样的。

几个关于Redis过期的快捷信息:

  • 它们可以被设置为秒或者毫秒。
  • 过期时间的精确度始终为1毫秒。
  • 过期的信息会复制和保存在硬盘上,当你的Redis服务停止后,时间也是在走的(意思是Redis保存的是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

以上的例子,是将一个key设置为一个字符串的值100,并且过期时间设置为10秒。接下来的
TTL命令是检查这个key剩余的存活时间。

如果需要采用毫秒来设置和检查过期时间,请查看 PEXPIRE
PTTL 命令,以及SET选项的完整列表。

Redis 的列表类型(Lists)

为了解释List数据类型,更好的开始是学习一点理论知识,IT人员经常以一种不当的方式在使用List。例如”Python Lists” 并不是其名称说明的那样是链表(Linked Lists),反而是数组[Arrays](相同的数据类型在Ruby中叫做Array)。

从十分笼统的角度上来看一个List就是一个有序元素的序列,例如10,20,1,2,3 就是一个list。但是一个通过数组来实现的List和一个通过链表来实现的List,在属性是有很多不同的。

Redis的Lists 是通过链表来实现的。这就意味着即使你一个list中包含数百万元素,在list的头部或者尾部新增一个新元素操作的执行时间为常数时间[in constant time]。
使用 LPUSH 命令在一个10个元素的list中添加一个新元素和在一个1000万个元素的list中添加一个新元素的速度是一样的。

那它有什么缺点呢? 使用数组实现的list中,通过索引[by index]来访问一个元素是非常快的(通过索引访问为常数时间),而使用链表实现的list中,访问速度就没有那么快(因为这个操作需要工作量和要访问元素的索引成正比)。

Redis 的List通过链表来实现,是因为对于一个数据库系统来说,能够用很快速的往一个很长的list中添加一个元素是至关重要的。另一个很大的优势,在接下来的文档将会看到,就是Redis的List可以在固定的时间内处理固定的长度。

当在大量元素中快速访问很重要时,有一种不同的数据结构可以使用,叫做有序集合[sorted sets]。它将在下面的指南中做介绍。

使用Redis 列表[Lists]的第一步

LPUSH 命令是在list的左边(也就是头部)添加一个新元素,而RPUSH是在list的右边(也就是尾部)添加一个新元素。最后LRANGE命令是从list中获取一个范围的值:

> 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表示list的倒数第二个元素,依此类推。

如你所见,RPUSH命令在list的右侧(尾部)追加元素,LPUSH命令则在list的左侧(头部)追加元素。

这两个命令都是可变参数命令[variadic commands],意思就是你可以在一次调用中自由的添加多个元素到list中。

> 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 列表[lists]中定义了一个很重要的操作就是可以弹出元素[pop elements]。
弹出元素是一个可以获取list中的元素并同时清除该元素的操作。与两侧push元素类似,你也可以从左右两侧(头部或者尾部)来pop元素。

> rpush mylist a b c
(integer) 3
> rpop mylist
"c"
> rpop mylist
"b"
> rpop mylist
"a"

我们向list中添加了三个元素以及弹出[pop]了三个元素,命令序列执行完后,list是空的,它没有更多的元素可以去pop了。如果我们继续尝试执行pop操作,结果将是:

> rpop mylist
(nil)

Redis 会返回一个NULL值用来表示list中没有元素了。

集合[lists]常见的使用场景

Lists 在很多的任务中都是很有用的,下面是两个非常典型的场景:

  • 记住社交网络中用户最近更新的帖子。
  • 使用生产者-消费者模式,来进行两个进程间的通信。生产者向list中push元素,消费者(通常是一个worker)获取这些元素并执行一些操作。Redis中有特别的list命令可以保证这个过程非常可靠和有效。

例如,两个很流行的Ruby包 resque
sidekiq 后台任务的底层都是使用Redis列表[lists]来实现的。

Twitter 获取最新的推文 用户的发帖用的是Redis列表[lists]。

下面将一步一步的描述一个常见的用例,假设你需要在一个图片分享的社交网络的主页上展示最新发布的图片,并想快速访问。

  • 每次用户提交一张新的图片,我们都用 LPUSH 命令向list中添加一个它的ID。
  • 当用户访问主页时,我们使用 LRANGE 0 9命令来获取最新的10条提交。

固定大小的列表[lists]

在很多的用户场景中,我们仅仅想要使用lists去存储 最新的项[latest items],
它们可能会是:社交网络的更新,日志或者其他的。

Redis 允许我们将list当做成一个固定集合来使用, 使用 LTRIM 命令,只会记住最新的N个元素,而会丢弃所有以前的元素。

LTRIM 命令类似于LRANGE,但是替换了显示指定元素的范围,它将设置这个范围为一个新的list值。所有超过这个范围的元素都将被删除。

通过下面这个例子将更清晰的看到:

> 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去获取list中0到2的元素,所有其他的将会被丢弃。这是一个非常简单但有效的模式:一个List push操作加一个List trim操作结合起来用作添加一个新元素并抛弃超过限制的其他元素:

LPUSH mylist <some element>
LTRIM mylist 0 999

上述联合操作添加了一个新元素,并且在list中只保留最新的1000个元素。用 LRANGE 你可以访问最顶端的元素,而不需要再去保存很老的数据。

注意:LRANGE是一个O(N)的命令,从list的头部或者尾部访问小的范围是一个常数时间的操作。

列表[lists]上的阻塞操作

Lists有一个特性适合用来实现一个队列,以及它通常也会作为一个内部进程通信系统的组成部分:阻塞操作。

假设你想要在一个进程中向一个list中push一些元素,而使用另一个进程利用这些元素来进行一些实际上的工作。这就是常见的生产者 / 消费者场景,我们可以通过下面这种简单的方式来实现:

  • 生产者调用 LPUSH 来向list中push数据项。
  • 消费者调用RPOP来获取或处理list中的数据项。

然而,很可能在一些时候list是空的,没有元素需要处理,所以 RPOP 只会返回NULL。
在这种情况下,消费者会被强制等待一段时间,然后重新执行 RPOP。这就是轮询[polling],在这种场景下,这并不是一个好的想法,因为它会有很多缺点:

  1. 它会强制Redis和客户端去执行无价值的命令(当list为空时,所有的请求都将没有实质性的工作,因为它们仅仅会返回NULL)。
  2. 为处理元素的时候添加一个延时,当一个工作进程接收到一个NULL后,让它等待一定的时间。如果延时设置很小,我们在两次调用 RPOP 之间等待的时间很短,同样会有在1中所述的问题,例如很多无价值的访问。
  3. 所以Redis实现了 BRPOPBLPOP 命令,它们是在list为空时,RPOPLPOP 命令的阻塞版本:只有当list中添加进来新元素后它们才会返回,否者一直等待;或者是达到了用户指定的过期时间。

下面是一个 BRPOP调用的例子,我们可以在工作进程中使用:

> brpop tasks 5
1) "tasks"
2) "do_something"

意思是:”等待’tasks’ list 中的元素,如果5秒后还没有可用的元素就返回。”

注意:如果你过期时间设置为0,那么它将永久等下去;你同样可以一次指定多个lists,不一定是一个,为了同时在多个lists上等待,当第一个list接收到元素后就会得到通知。

BRPOP 需要注意的几点:

  1. 顺序为客户端提供服务:第一个阻塞的客户端,当别的客户端添加进来数据后,它将会是第一个获取到数据的,依此类推。
  2. 返回值与 RPOP 命令不同:它将会返回一个二元数组,因为它需要包含key的名称,而且 BRPOPBLPOP 命令可以同时等待多个lists中的元素。
  3. 如果过期时间到了, 将会返回NULL。

如果你想要了解更多关于lists的阻塞操作,建议阅读以下内容:

  • 可以使用 RPOPLPUSH 来构建安全队列或者循环队列。
  • 还有一个阻塞的命令:BRPOPLPUSH

Keys的自动创建和删除

直到现在,在我们的例子中都没有在添加元素前去创建一个空的list,也没有在list中不再有任何元素时去删除一个空的list。当一个list为空后,Redis会自动删除这个key;或者当我们尝试向一个list中添加一个元素时,如果这个key并不存在,则Redis会自动创建一个空的list。例如:用 LPUSH 命令。

这不只是针对lists来说的,Redis中所有的多元素数据结构都适用–集合[Sets], 有序集合[Sorted Sets] 以及 哈希[Hashes]。

基本上, 我们总结以下三条规则:

  1. 当我们向一个集合数据类型中添加一个元素时,如果目标key不存在,在添加元素前,一个空的集合数据类型将会被创建。
  2. 当我们从集合数据类型中删除元素时,如果值为空后,这个key会被自动销毁。
  3. 调用一个只读的命令,例如LLEN (返回一个list的长度);或者一个写命令来删除元素,作用在一个不存在的key上和指向一个空集合类型的key上,会返回一样的结果。

规则1的例子:

> del mylist
(integer) 1
> lpush mylist 1 2 3
(integer) 3

不过,我们不能向一个已经存在key中添加错误类型的值:

> 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

当所有元素都pop完了以后,这个key将不再存在。

规则3的例子:

> del mylist
(integer) 0
> llen mylist
(integer) 0
> lpop mylist
(nil)

Redis 的哈希类型[Hashes]

Redis 的哈希跟预想中的”hash”是一样的,采用[属性-值]对:

> hmset user:1000 username antirez birthyear 1977 verified 1
OK
> 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"

哈希[hashes] 可以很方便的来描述对象[ojbects],实际上hash中的属性数量没有任何规则限制(除非是可用的内存),所以你可以在应用中以不同的方式来使用哈希[hashes]。

HMSET 命令用来设置hash中多属性的值,而 HGET 命令只能获取一个属性的值。HMGETHGET 类似,但是它会返回一个值的数组:

> 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

所有的命令你都可以在这hash命令完整列表查找到。

值得注意的是小的哈希[hashes](例如:少量的小值元素)会在内存中以一种特定的方式进行编码,将使它们非常的高效。

Redis 的集合类型[Sets]

Redis的集合[Sets] 是无序的字符串集合。SADD命令是用来向一个set中添加新元素。针对集合[Sets]还有很多其他操作,像测试一个给定的元素是否存在,在多个集合中执行相交操作[intersection],联合操作[union]或者差异操作[difference]等等。

> sadd myset 1 2 3
(integer) 3
> smembers myset
1. 3
2. 1
3. 2

这里,我向myset中添加了3个元素,并让Redis将所有元素都返回。就像你看到的那样它们是无序的 — Redis在每次调用的返回顺序是自由的,在元素顺序上没有任何约束。

Redis 有命令去测试集合中的元素。判断一个元素是否存在?

> sismember myset 3
(integer) 1
> sismember myset 30
(integer) 0

“3” 是set中的元素,而”30″却不是。

集合[Sets] 可以很好的用来表示对象间关系。
例如,我们可以很简单的使用集合[sets]来实现标签[tags]。

模拟这个问题的简单方式就是有一个set来记录每一个我们想要标记的对象。这个set包含这些对象关联的标签[tags]的IDs。

假设我们想去标记新闻。
如果我们ID为1000的新闻被标记为标签1,2,5和77,我们可以用一个set来关联这条新闻的标签的IDs:

> 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

要得到指定对象的所有标签[tags]是很简单的:

> smembers news:1000:tags
1. 5
2. 1
3. 77
4. 2

注意:在这个例子中我们假设你有一个其他的数据结构,例如Redis哈希,用来把标签的ID映射到标签的名称上。

还有一些其他的有意义的操作,使用正确的Redis命令也可以很简单的实现。例如我们可能想要获取同时标记了1,2,10和27的对象列表。我们可以是用 SINTER命令,它会在不同的set之间执行相交操作,我们可以这样使用:

> sinter tag:1:news tag:2:news tag:10:news tag:27:news
... results here ...

相交操作并不是可以执行的唯一命令,你还可以执行联合操作[unions], 差异操作[difference],获取一个随机元素等等。

获取一个元素的命令叫做 SPOP,它可以很方便的处理某些问题。例如要实现一个基于web的扑克游戏,你可能想用一个set来表示一个牌组。假设我们用一个字母的前缀来表示花色,
(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 命令会随机移除一个元素,并将它返回给客户端,所有它很适合这个场景。

然而,如果我们调用它直接返回我们的牌组,在下一局游戏时,我们又需要重新填充这些数据,这看上去不太完美。所以在开始时,我们先为key为 deck 的set创建一个副本,副本的key为 game:1:deck

这个可以通过使用 SUNIONSTORE 来完成,这通常会在多个sets间执行联合操作,并将结果存储在另一个set中。然而,因为联合操作的一个set是它自己,所以可以通过下面的命令拷贝我的牌组:

> sunionstore game:1:deck deck
(integer) 52

现在,我们准备为第一个玩家发5张牌:

> 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"

1对J,不妙啊…

现在是时候介绍set中获取其包含元素个数的命令了。在set的理论中,我们经常称这个为set的基数[cardinality of a set],所以这个Redis的命令是 SCARD

> scard game:1:deck
(integer) 47

数学计算:52 – 5 = 47。

当我们需要从set中获取随机的元素,但是又不想把它们从set中删除时,SRANDMEMBER 命令适合这样的任务。它还可以用作返回两个重复或者不重复的元素。

Redis 的有序集合类型[Sorted sets]

有序集合[Sorted sets] 是一种类似于Set和Hash混合的数据结构,像集合[sets],有序集合由唯一的,不重复的字符串元素组成,所以从某种意义上来说一个有序集合[sorted set]也是一个集合[set]。

然而,set中的元素是无序的,在有序集合中的每一个元素都会关联一个浮点数值,我们称之为分数[the score](这就是为什么说这种类型也类似于hash,因为hash中每一个元素都会映射一个值)。

此外,元素在有序集合中是有顺序的[taken in order](所以它们在请求上是无序的,排序是有序集合这种数据结构的一种特性)。它们的排序遵循如下规则:

  • 如果A和B是两个不同的元素,并且有不同的score,如果A.score > B.score,那么A > B。
  • 如果A和B有相同的score,如果A字符串在字典顺序上大于B字符串,那么A > B。A和B字符串不可能相等,因为有序集合只能包含唯一的元素。

让我们从一个简单的例子开始,添加一些精选的黑客名字作为有序集合的元素,并把它们的出生年作为”score”。

> 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,但是它会带一个附加的参数(在元素前面添加),就是score。ZADD 同样是可变参数命令,所以你可以自由的指定多个score-value对,即使上面的例子中我们没有使用这个。

通过有序集合可以非常轻松的返回一个按照出生年排序的黑客名单,因为它们本来就是排好序的

实现提示:有序集合是有两部分数据结构来实现的, 它包含一个跳跃表[skip list]和一个哈希表[hash table],所以我们每次我们添加一个元素,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"

它也可以同时返回score值,只要使用 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返回score在负无穷大到1950(包含两个极值)的所有元素。

它同样可以去移除一个范围内的元素。让我们从有序集合中移除所有出生在1940到1960之间的黑客:

> zremrangebyscore hackers 1940 1960
(integer) 4

ZREMRANGEBYSCORE 可能不是最好的命令名称,但是它非常有用,并且会返回被删除元素的数量。

另一个为有序集合元素定义的非常有用的操作是获取名次[get-rank]操作。它可以要求从一个已排序的集合元素中获取一个元素的位置。

> zrank hackers "Anita Borg"
(integer) 4

ZREVRANK 命令同样可以获取名次,不过它是就元素降序排列而言的。

字典序的scores

在Redis 2.8 最近的版本中,介绍了一个新的特性,就是它可以按照字典顺序获取范围,假设有序集合中的所有元素都插入了完全相同的score(元素都是通过C语言中的 memcmp 函数来进行比较的,这样也就保证了没有排序规则,以及每一个Redis实例都会返回相同的输出)。

操作字母范围的主要命令是 ZRANGEBYLEX, ZREVRANGEBYLEX, ZREMRANGEBYLEXZLEXCOUNT

例如,让我们重新添加一个著名黑客列表,但是这次我们使用0做为所有元素的score:

> 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位的无符号整数参数来为元素建立索引,所有你需要做的就是把这些元素添加到有序列表中,并为其设置相同的score(比如说0),但是这里是一个由8字节前缀组成的高位优先的128位数字。由于数字是高位优先的,按照字典排序(按原始字节顺序)实际上也是按照数字排序,你能够在128位空间中请求一个范围,以及在获取元素时丢弃这个前缀。

如果你想查看这个特性更高级的示例,请查看Redis 自动完成示例

更新score:排行榜

在切换到下一个主题之前关于有序集合的最后一个注意事项。有序集合的score在任何时候都可以被更新。只要对一个已经包含在有序集合中的元素调用 ZADD 就将会更新它的score(位置),这个操作的时间复杂度是O(log(N))。同样的,有序集合可以进行批量修改。

由于这个特性,有一个常见的用例就是排行榜。
典型的应用是一个Facebook 游戏,它可以根据最高分对用户进行排序,加上获取名次操作,
为了显示top-N用户,以及用户在排行榜中的名次(例如,”你是#4932这里的最好成绩”)。

位图[Bitmaps]

位图不是一种实际的数据类型,而是在字符串类型上定义的一系列面向位的操作。
由于字符串是二进制安全的二进制对象,以及它们的最大长度是512MB,它们适合设置为2^32个不同的位。

位操作分为两个组:一个是常数时间的一位操作,像设置一个位为1或者0,或者获取它的值;另一个是一组位的操作,例如计算给定一个位范围的位的数量(例如数量统计)。

位图的一个最大的好处是在存储信息时非常节省空间。例如,在一个系统中,不同的用户通过自增的用户ID来表示,记住4亿用户的一位信息(例如,记录用户是否需要接收一个简讯)可能也就仅仅使用512MB内存。

位的设置和获取使用 SETBITGETBIT 命令:

> setbit key 10 1
(integer) 1
> getbit key 10
(integer) 1
> getbit key 11
(integer) 0

SETBIT 命令的第一个参数是位数(第几位),第二个参数是需要在该位上设置的值,只能是1或者0。如果位的地址超过了当前字符串的长度,这个命令会自动扩展这个字符串。

GETBIT 只是返回指定索引位的值。超出范围的位(位的地址超出目标key存储的字符串的长度范围)总是被认为是0。

有三个命令是对一组位的操作:

  1. BITOP 执行不同字符串之间的位运算,提供的操作是与[AND], 或[OR], 异或[XOR] 和非[NOT]。
  2. BITCOUNT 执行数量统计,报告设置为1的位的数量。
  3. BITPOS 是查找第一个设置为0或者1的位。

BITPOSBITCOUNT 都能够操作字符串的字节范围,而不是运行在整个字符串上的。下面是 BITCOUNT 的简单示例:

> setbit key 0 1
(integer) 0
> setbit key 100 1
(integer) 0
> bitcount key
(integer) 2

位图常见的使用场景有:

  • 各种各样的实时分析。
  • 存储高效而且高性能的关联对象ID的布尔信息。

例如,假设你想知道你网站的用户日常访问持续时间最长的一次。你从0开始统计天数,也就是你的网站公开的那天开始,每次用户访问网站的时候你都使用 SETBIT 设置一位。作为一位索引,你只要简单的用当前系统时间减去初始的偏移后,再除以3600*24 就可以了。

这种方式,你需要为每一个用户创建一个小的字符串来包含他每天的访问信息。使用 BITCOUNT 可以很轻松的得到给定用户访问网站的天数,再加上少量的 BITPOS 调用,或者简单的获取和分析客户端的位图,就可以很简单的计算出最长持续时间了。

位图可以简单的分成多个key,例如,为了切分数据集,因为通常它可以更好的避免使用大key。通过不同的key来切分一个位图来替代将所有的位都设置到一个key中,有一个简单的策略就是一个key只存储M位,然后将key命名为 bit-number/M ,第N位的地址就包含在bit-number MOD M 的key中。

HyperLogLogs类型

HyperLogLog是一个基于概率的数据结构,用于统计东西的唯一性(严格的说这称为对集合基数的估算)。通常,统计唯一项需要使用的内存,和你想要统计项的数量成正比,因为你需要记住你已经统计过的元素,以避免重复统计。然而有一组权衡内存精度的算法:你估计测量的标准误差,在Redis的实现中,将小于1%。这个算法的神奇之处是你不再需要耗费在和统计项的数量成正比的内存,取而代之的是使用固定数量的内存! 最坏的情况占用12K字节,或者更少,如果你的HyperLogLog(从现在开始我们将称它为HLL) 只有很少的元素。

Redis 中的HLLs,从技术上讲是一种不同的数据结构,被编码为一个Redis字符串,所以你能够调用 GET 去序列化一个HLL,也可以用 SET 去反序列化它并返回给服务器。

概念上,HLL的API像是使用集合[Sets]来完成相同的任务。你可以 SADD 每一个被观察的元素到一个集合中,也可以使用 SCARD 去检查集合中包含的元素数量,唯一不同是 SADD 不能重复添加一个存在的元素。

然而,你不是真正的添加元素 到一个HLL,因为这种数据结构仅仅包含一个状态,它并不包含真正的元素,API也是一样的:

  • 每次你看到一个新元素,你可以使用 PFADD 将其添加到统计中。
  • 每次你想获取到目前为止通过 PFADD 已添加的 唯一元素的当前近似值,你可以使用 PFCOUNT

    pfadd hll a b c d
    (integer) 1
    pfcount hll
    (integer) 4

这个数据结构使用场景的示例是统计每天在一个搜索表单中用户执行的唯一性查询。

Redis 同样可以执行HLLs的合并操作,更多信息请参考:完整文档

其他值得注意的特性

有一些其他重要的Redis API在这篇文档中没有列出来,但是值得你去关注:

学习更多

这篇指南并不是完整的,它只是覆盖到了基本的API,阅读 命令参考 可以了解到更多。

感谢阅读以及使用Redis快乐。

原创文章,转载请注明: 转载自并发编程网 – ifeve.com本文链接地址: Redis的数据类型和抽象概念介绍

FavoriteLoading添加本文到我的收藏
  • Trackback are closed
  • Comments (0)
  1. No comments yet.

You must be logged in to post a comment.

return top