💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
An introduction to Redis data types and abstractions ==================================================== __摘要__: 1. 原文地址: [http://redis.io/topics/data-types-intro](http://redis.io/topics/data-types-intro) Redis 并不是一个简单的键值对存储工具,事实上它是一个数据结构服务器,支持不同类型的值。这意味着,在传统的键值对存储工具上你可以将字符串键关联到字符串的值上,而在 Redis 上,值并不仅限于简单的字符串,也能是更复杂的数据结构。下面列出了 Redis 支持的所有数据结构,它将会在这篇教程中分别地介绍。 + Binary-safe String + List: 根据插入顺序排序的字符串元素的集合,它们是基础的链表。 + Sets: 独一无二的,未排序的字符串元素的集合。 + Sorted sets: 和 Sets 相似,但是每个字符串元素关联到了一个浮点整数值,这个值叫做`Score`。它总是按照`Score`来排序,所以不像 Sets 那样,它能够取出一定范围的值(例如取出头10个元素,尾10个元素)。 + Hashes, 它映射了域和值关联的组合。每个域和值都是字符串。这个非常像 Ruby 或者 Python 中的哈希类型 + Bit arrays(或者简单的 bitmaps ): 可以使用特殊的命令像处理位数组那样来处理字符串:你可以设置和清除独立的位,统计所有被设置成1的位,寻找第一个被设置或者未被设置的位,等等。 + HyperLogLogs: 这是一个概率数据结构,它被使用是为了估算一个集合的基数。不要害怕,它比它看起来的要简单,请参考这个教程的随后的 HyperLogLogs 部分 了解这些数据结构是如何工作并且知道如何使用这些数据结构去解决 [command reference](http://redis.io/commands) 给出的问题并不总是不重要的,所以这篇文档是一个 Redis 数据结构和他们最通用的模式的崩溃课程。 下面所有的例子我们将会使用 redis-cli 组件,一个简单但是便利的命令行组件,它用来向 Redis 服务器发送命令。 ## Redis keys Redis 键是二进制安全的,这意味着你可以使用任何二进制序列作为键,从一个字符串(比如 "foo")到一个 JPEG 文件的内容。空字符串也是有效的键。 下面是一些关于键的其他规则: + 非常长的键并不是一个好主意。例如1024字节的键是一个坏主意,这不仅在内存方面,也因为在数据集中这些键的查找可能需要一些代价高昂的键比较。即使手头的任务是比较一些大值的存在,使用它的哈希值是一个更好的注意(例如使用 SHA1 哈希算法),尤其是从内存和带宽的方面来考虑。 + 非常短的键通常也不是一个好主意,如果你可以把键写成 "user:1000:followers",那么使用 "u1000flw" 作为键是无意义的。前者的可读性更好,而且与键对象和值对象本身消耗的空间相比,这里添加的内存空间很小。虽然短的键看起来会使用更少的空间,而你的工作是去寻找一个平衡。 + 尝试坚持去使用模式。例如 "object-type:id" 这种方式就是一个好主意,就比如 "user:1000"。点和破折号经常在多个单词域中使用,比如 "comment:1234:reply:to" 或者 "comment:1234:reply-to" + 允许的键的最大长度为 512MB。 ## Redis Strings Redis 字符串类型是你使用键可以访问的值的最简单的类型。它是一种仅仅在内存中的数据类型,所以让新手在 Redis 中首先使用它就很自然了。 因为 Redis 的键是一个字符串,当我们也在值上使用字符串类型的时候,我们就把一个字符串映射到了另一个字符串。字符串类型在许多情况下都是很有用的,例如缓存 HTML 片段或者 Redis 中的页。 让我们来玩一点字符串类型,通过使用 redis-cli (这个教程中的所有例子将会通过 redis-cli 来执行)。 ``` > set mykey somevalue OK > get mykey "somevalue" ``` 就像你所看到的那样,你可以使用`SET`和`GET`命令,这是我们设置和检索字符串的方式。注意`SET`将会替代任何在那个键上存储的已经存在的值,甚至键关联的是一个非字符串类型的值。所以`SET`执行的是一个分配任务。 值可以是任意类型的字符串(包括二进制数据),例如你可以在一个键中存储 JPEG 图片,一个值可以大于 512 MB。 `SET`命令有一些有趣的选项,它们是作为额外的参数来提供的。例如,我可以要求`SET`命令在键已经存在的时候分配失败(nx),或者相反,仅仅当键已经存在的时候才会分配成功(xx)。 ``` > 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`命令是原子化的,这意味着即使多个客户端在同一个键上发出了`INCR`命令,也不会进入竞争状态。例如,客户端1读出一个值是10,客户端2读出的这个值也是10,它们同时发出了一个`INCR`命令,但是新值却被设置成为了11,这种情况永远不会发生。最终的值总是12,而且读取-自增-设值这三个操作执行的时候,其他的客户端并不能在这个键上执行命令。 这里有许多在客户端上操作的命令。例如`GETSET`命令将键设值为一个新的值,但是返回旧的值作为结果。你可以使用这个命令,例如,你有一个系统。在你的的网站上每有一个新的拜访者到来的时候就使用`INCR`命令进行自增。你可能想每隔一个小时就收集这个信息,且不会丢失自增操作。你可以使用`GETSET`命令来给这个键赋一个新的值0,同时返回旧的值。 在一个命令中设值或者检索多个值的能力也可以用在降低延迟上。在这里你可以使用`MSET`和`MGET`命令: ``` > mset a 10 b 20 c 30 OK > mget a b c 1) "10" 2) "20" 3) "30" ``` 当`MGET`使用的时候,Redis 将会返回一个值的数组。 ## 修改和查询键空间 这里有一些命令没有在特殊的类型中定义,但是用来去和键空间进行交互,因此,它们能够被使用在任何类型的键上。 例如,`EXIST`命令返回0或者1用来去标识一个给定的键是否存在于数据库中,而`DEL`命令用来去删除一个键和它关联的值,无论它的值是什么类型。 ``` > set mykey hello OK > exists mykey (integer) 1 > del mykey (integer) 1 > exists mykey (integer) 0 ``` 从上述的命令中也可以看到,`DEL`命令返回返回0或者1取决于这个键是否被删除了(既这个键存在或者不存在)。 这里有许多键空间相关的命令,但是以上两个和`TYPE`命令是很重要的,`TYPE`命令返回了存储在特殊的键中的值的类型: ``` > set mykey x OK > type mykey string > del mykey (integer) 1 > type mykey none ``` ## Redis expires:具有生存时间限制的键 在讨论更复杂的数据结构之前,我们需要讨论任何值类型都有的另外一个特征,叫做 __Redis exipres__。 基本上你可以设置一个键的超时时间,这个时间限制了这个键的生存周期。当这个超时时间到达以后,这个键将会被自动地销毁,就好像用户通过调用`DEL`命令来进行手动销毁的一样。 下面是一些关于 Redis expires 的快速浏览的信息: + 它们可以使用秒或者毫秒的精度 + 但是过期时间的精确度始终是1毫秒 + 有关过期的信息被复制并持久地存储到了磁盘上,当 Redis 服务器停止的时候,时间仍然虚拟地流逝(这意味着 Redis 保存了键将要过期的日期)。 设置一个过期时间是很容易的: ``` > set key some-value OK > expire key 5 (integer) 1 > get key (immediately) "some-value" > get key (after some time) (nil) ``` 键在两次`GET`调用之间消失了,因为第二次调用的延时超过了5秒钟。在上面的例子中我们使用`EXPIRE`命令来设置了一个过期时间(它也能用于给一个已经存在过期时间的键设置另外一个不同的过期时间,就像`PRESIST`命令可以用来删除过期时间,使这个键永远存在),然而我们也可以通过其他的 Redis 命令在创建键的时候设置过期时间。例如通过`set`命令的选项: ``` > set key 100 ex 10 OK > ttl key (integer) 9 ``` 上面的例子中,将一个键的值设置为100,同时有一个过期时间为10秒钟。随后的`TTL`命令用来去检查这个键还能存活多长时间。 如果要在毫秒级别上设置和检查过期时间,请参考`PEXPIRE`命令,`PTTL`命令,和`SET`命令的所有选项的列表。 ## Redis Lists 要去解释一个 Redis 的列表数据类型最好以一点理论来开始,因为术语列表经常被信息技术人员不当地使用。例如,"Python Lists"并不是建议的名字(可能被理解成 Linked Lists),而是应该叫做数组(同样的数据结构在 Ruby 中叫做数组)。 从一个非常通用的点去查看一个列表它仅仅是一系列的排序过的元素: 10,20,1,2,3 是一个列表。但是一个使用数组实现原型的列表和一个使用链表实现原型的列表是非常不同的。Redis 列表是通过链表来实现的。这意味着即使你有一个数百万元素的列表,在列表的头部或者尾部添加一个新的数据,使用的都是恒定的时间,向一个有10个元素的列表的头部添加新元素的时间和向一个有一千万元素的列表的头部添加新元素的时间是相同的。 接下来聊什么呢?在使用数组实现的列表中通过下标来访问元素是非常迅速的(使用了索引访问的恒定时间),而在链表实现的列表中通过下标来访问元素就没那么迅速了(需要执行的操作的工作量与被访问的元素索引成正比)。 Redis 通过链表来实现列表是因为对于一个数据库系统来说,关键是能够以非常快的方式向一个非常长的列表添加元素。另一个很重要的优点是,就像你随后将要看到的那样,Redis 列表能够以恒定时间获取恒定的长度。 能够快速访问一个大型集合的中间部分是很重要的,这里能够采用一个不同的数据结构,叫做 sorted sets。排序集将会在这篇教程的后面介绍。 ## 操作 Redis Lists 的第一步 `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 元素的能力,pop 元素的操作既能够从列表中检索元素,也能在同时把它从列表中消除。你能够从左边或者右边来 pop 元素,就像你能够向链表的两端 push 元素一样。 ``` > rpush mylist a b c (integer) 3 > rpop mylist "c" > rpop mylist "b" > rpop mylist "a" ``` 我们添加了三个元素到列表中,然后又把他们 pop 掉了。所以在上面一连串的命令操作过后,列表是空的,而且也没有更多的元素可以被 pop 了。如果我们尝试继续去 pop 元素,我们将会得到如下的结果: ``` > rpop mylist (nil) ``` Redis 返回一个 NULL 值表示这个列表中没有更多的元素了。 ## List 的通常用例 List 在许多任务中都是很有用的,两个非常具有代表性的用例是: + 记住用户发布到社交网络的最近的更新 + 使用生产者-消费者的模式在进程间通信,生产者将元素 push 到 list 中,而消费者(通常是一个worker)消费这些元素并执行相应的动作。针对这种情况,Redis 有特殊的命令来让执行变得更可靠和高效。 例如,流行的 Ruby 库 resque 和 sidekiq 使用 Redis 列表来实现后台作业。 流行的社交网络 Twitter 将获取用户提交的最近的微博到 Redis list 中。 要一步一步地描述一个通常用例,可以想象你的主页要显示提交到共享社交网络的最近的照片,并且你希望加速访问。 + 每次提交新的照片,我们使用`LPUSH`命令来将图片的ID添加到一个列表中。 + 当用于访问主页的时候,我们通过`LRANGE 0 9`命令获取到最近提交的10个项目。 ## 封顶的列表 在许多用例中,我们使用列表仅仅想存储最近的元素,而不管他们是社交网络的更新,日志,还是其他的什么东西。 Redis 允许我们使用列表去做一个封顶的集合,仅仅需要记住通过`LTRIM`命令可以获取到最近的N个元素同时丢弃所有更旧的元素。 `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之间的所有列表的元素,其他的东西都会被丢弃。这样就可以允许一个非常简单但是很有用的模式,对一个列表做一个 push 操作和一个 trim 操作,在添加新元素的同时丢弃所有超出限制的元素。 ``` LPUSH mylist <some element> LTRIM mylist 0 999 ``` 上述两个命令的集合添加一个新元素到列表,同时获取最近的1000个元素。通过`LRANGE`命令你可以访问最近的元素不需要记住任何旧的数据。 注意:由于`LRANGE`是一个`O(N)`的命令,所以访问列表头或者尾的小范围的元素都是一个固定时间消耗操作。 ## 列表上的阻塞操作 列表有非常特殊的特点以至于让它们很适合去实现队列,并且通常作为进程间通信系统的构建模块:阻塞操作。 想象你想要在一个进程中添加数据到一个列表中,并且使用另外一个不同的进程以便实际地去使用这些数据实际做另外一些工作。这实际上是一个普通的生产者消费者体系,可以用下面简单的方式来实现。 + 去推送一些数据到列表中,生产者调用`LPUSH`命令 + 从列表中提取并处理这些数据,消费者调用`RPOP`命令 然而,有时会出现列表为空没有数据去处理的情况,所以`RPOP`命令就返回`NULL`。在这个例子中消费者被迫去等待一些时间,然后再来用`RPOP`命令去尝试。这个过程叫做 *polling*,在这种上下文环境中这并不是一个好的主意,因为它有如下的几个缺点: + 强迫 Redis 和客户端去处理无用的命令(当列表为空的时候,所有的请求实际上并不会做什么工作,它们仅仅会返回 NULL )。 + 为处理数据添加了一定的延时,因为当工作进程收到了 NULL 以后,它将会等待一定的时间。如果想要使延时更小的话,我们可以在两次`RPOP`调用的中间,使用更少的时间间隔。但这样就会放大1号问题的效果。例如,Redis 将会调用更多的无效的命令。 所以 Redis 实现了叫做`BRPOP`和`BLPOP`的命令,他们是`RPOP`和`LPOP`的变异版本,当读取的列表为空的时候,它们将会阻塞,直到新的元素到达列表的时候,他们才会返回给调用者,或者当用户定义的时延到达的时候也会返回。 下面是一个我们可以在工作线程中使用的`BRPOP`调用的例子: ``` > brpop tasks 5 1) "tasks" 2) "do_something" ``` 它意味着,在列表任务中等待元素,但是如果5秒内没有元素可用的话就返回。 注意你可以使用0作为时延来永远等待元素,你可以指定多个列表而不仅仅是一个,以便同时在多个列表上等待,且当第一个列表接收到一个元素的时候获得提醒。 关于`BRPOP`命令有一下几点需要注意: + 客户端是按顺序获得服务的,第一个阻塞等待列表的客户端,在其他客户端推送元素到列表中的时候,将会第一个获得服务,如此等等。 + 和`RPOP`命令比起来返回值并不相同:它是一个两个元素的数组,因为`BRPOP`和`BLPOP`命令能够在多个列表上阻塞等待元素,所以它返回结果中也包括了键(列表)的名字。 + 如果规定的时延到达的话,将会返回 NULL 。 关于列表和阻塞操作这里有一些更多的东西你需要知道。我们建议你根据如下的提示阅读更多的内容: + 也可以使用[`RPOPLPUSH`](https://redis.io/commands/rpoplpush)来建立安全队列或者旋转队列 + 这里也有一个阻塞版的命令变体,叫做[`BRPOPLPUSH`](https://redis.io/commands/brpoplpush) ## 自动地创建和移除键 到目前为止,在我们的例子中,我们没有过自动地在向列表中添加元素之前创建空列表,也没有在列表中元素为空的时候删除空列表。当列表为空的时候,删除空列表,和当我们试着向一个不存在的列表中添加元素的时候(例如使用`LPUSH`命令),自动创建空列表,这些都是 Redis 的责任。 这不仅针对列表,它应用到了 Redis 的所有的由多个元素组成的复合数据类型,集合,有序集合,哈希表等。 基本上我们可以用三条规则来汇总这些行为: 1. 在我们添加元素到一个集合数据类型之前,如果目标集合数据类型不存在,在我们将元素加入其中之前,一个空的集合数据类型将会被创建。 2. 在我们从一个集合数据类型移除元素的时候,如果这个集合数据类型的值变成了空,那么这个键就会被自动销毁。 3. 调用一个只读的命令,比如`LLEN`(这个命令返回列表的长度),或者写一个命令移除元素,这个元素有一个空的键,这些都会产生相同的结果,如果键入的命令期望找到一个空的集合类型,而键正好拥有。 规则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 ``` 当所有的元素都弹出以后,这个键就不存在了。 规则3的例子: ``` # 注:就是针对一个空的不存在的列表求长度或者取值的时候,都是返回相同的结果。 > del mylist (integer) 0 > llen mylist (integer) 0 > lpop mylist (nil) ``` ## Redis 哈希类型 一个哈希类型看起来期望一个值去执行哈希,来使用一个键值对: ``` > 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" ``` 哈希类型是为了方便代表对象,事实上许多域你都可以放置到一个哈希里面,并没有特殊的限制(除了可用的内存之外),所以你可以在你的应用程序里面使用多种不同的方式来使用哈希。命令`HMSET`设置哈希值的多个域,`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 ``` 你可以在[这个文档](http://redis.io/commands#hash)中找到关于哈希数据类型的命令的列表。 值得注意的是,小哈希(例如,一些拥有小值的元素)在内存中以特殊的方式编码存储,以让内存存储变得更有效。 ## Redis 集合类型 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 。 一个例子是标签化一篇新的文章。如果文章 ID 是1000,拥有标签1,2,4和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`,对于某些问题的建模很方便。例如,为了去实现一个基于 Web 的扑克游戏,你可以想要用一个集合来表示一套牌。想象我们使用了一个字符的前缀来表示牌,(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`命令随机地删除一个元素,并把它返回给客户端,所以它在这种情况下执行了这个操作。 然而,如果我们在我们的牌上再次直接调用这个命令,在下一次游戏中我们可能需要再次填充卡牌,这可能不太理想。所以一开始,我们可以对存储卡牌的集合的键做一份复制,复制成键 game:1:deck 键。 这是一个使用`SUNIONSTORE`完成的命令,它通常在多个集合之间求并集,然后将结果存储到另外一个集合中。然而,因为单个集合的并集就是它自己,所以我们通过这个命令为原来的牌的集合做了一份备份。 ``` > 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" ``` 这里有一对"J",再好不过了。。。 现在很适合去介绍一下提供集合中元素的个数的命令。这个在集合的上下文中通常叫做集合的 "cardinality" ,所以 Redis 的这个命令就叫做`SCARD`。 ``` > scard game:1:deck (integer) 47 ``` 数学算式是: 52 - 5 = 47。 当你仅需要随机地从集合中获取一个元素而不需要删除它的时候,`SRANDMEMBER`命令很适合这个任务。它还具有返回重复的和非重复的元素的能力。 ## Redis 有序集合 有序集合是一种类似于集合和哈希类型的混合体的数据类型。就像集合一样,有序集合由独一无二的,不重复的字符串元素组成,所以在某种意义上,有序集合也是一种集合。 然而,当集合内的元素是无序的时候,有序集合内部的每个元素都和一个浮点数关联着,叫做 *score* (这就是为什么这种类型也相似于哈希类型,因为每个元素都映射到一个值上)。 此外,有序集合中的元素是有顺序的(所以它们不是按照请求排序的,顺序是一种用来代表有序集合的数据结构的特性),它们按照如下的规则进行排序。 + 如果 A 和 B 是拥有两个不同分数的元素,如果 A 的分数 > B 的分数,那么 A > B。 + 如果 A 和 B 确实拥有同样的分数,如果 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`也是可变的,所以你可以自由地指定多个分数-值对,甚至这没有在上面的例子中进行使用。 对于有序集合来说,根据黑客的年龄排序返回一个黑客的列表是不重要的,因为实际上他们已经进行排序了。 实现注意事项:排序集合是通过一个包含了跳跃链表和一个哈希表实现的双端口数据结构,所以每次添加元素 Redis 执行的都是一个 O(log(N)) 的操作,那是很好的,但同时我们不需要要求排序集合做任何的事情,因为它已经是排序的了。 ``` > 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年(包括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 ``` `ZREVRANK`命令也可以用来获取其他顺序的排序,例如让元素以降序排列。 ## 词典分数 在 Redis 最近的2.8版本中,一个新特性被添加进来了,那就是允许以词典序范围地获取元素,假设有序集合中的元素是以相同的分数插入的(元素是通过C语言的`memcmp`函数来比较的,所以它用来保证没有整理,而每个 Redis 实例都会有相同的输出)。 以字典序范围操作的命令主要有`ZRANGEBYLEX`,`ZREVRANGEBYLEX`,`ZREMRANGEBYLEX`,和`zlexcount`。 例如,让我们再次添加我们的著名黑客集合,但是这次使用0作为所有元素的分数: ``` > 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)添加进有序集合,同时有一个128位的大端数字组成的8字节前缀。因为数字是按大端存放的,所以以字典排序(原始字节顺序排序)的时候实际上是按照数字顺序进行排序的。你可以获取128位空间范围内的元素,通过丢弃前缀来获取元素。 如果你想要看关于这个上下文中提到的特点的更严谨的Demo,请参考 [Redis autocomplete demo](http://autocomplete.redis.io/)。 ## 更新分数,排行榜 在切换到下一个主题之前,需要注意一下有序集合的一些东西。有序集合的分数能够在任何时候更新。仅仅需要对一个有序集合已经包括的元素再次调用`ZADD`命令,它的分数(包括位置)就会在 O(log(N)) 个时间复杂度内进行更新。因此,有序集合适合进行大量的更新。 由于这个特征,常见的用例就是排行榜。典型的应用是 Facebook 的游戏,你可以结合获取用户高分的能力,加上获取排行的能力,去展示分数最高的 N 个用户,同时在排行榜中显示这 N 个用户的排名(例如,你最好的成绩是4932名)。 ## 位图(Bitmaps) 位图不再是一个真实的数据类型,而是一组定义在字符串类型上的面向位的操作的集合。因为字符串是二进制安全的 BLOB 类型,它的最大长度为512MB,所以它们适合去设置 2<sup>32</sup> 种不同的位。 位操作被分成了两组,恒定时间的位操作,就像设置一个位位1或者0,或者是获得它的值,和在一组位上的操作。例如计算给定范围的位上的设置了的位的数量(比如,人口计算)。 位图的最大一个优点就是当它们存储信息的适合会提供临界的空间。例如在一个系统中,不同的用户由增长的用户ID来代表,它可以使用512MB的内存来记住4亿用户的单个位信息(比如用一个位来代表用户是否想要接收一个通讯信息)。 位可以通过`SETBIT`命令和`GETBIT`命令来设置和检索: ``` > setbit key 10 1 (integer) 1 > getbit key 10 (integer) 1 > getbit key 11 (integer) 0 ``` `SETBIT`命令获取它的第一个参数来作为一个位数,而第二个参数来作为要设置的位的值,它只能是0或者1。这个命令会自动扩大字符串,如果这个位地址超出了这个字符串的长度的话。 `GETBIT`命令仅仅返回制定了索引的位的值。超出范围的位(标识一个超出了目标键存储的字符串的长度的一个位)总是被认为是0。 这里有三个操作一组位的命令: 1. `BITOP`在不同的字符串之间执行逐位的操作。提供的操作是 AND, OR, XOR 和 NOT。 2. `BITCOUNT`,执行人口计数,汇报被设置成1的位的个数。 3. `BITPOS`寻找第一个有指定的0或者1的位。 `bitops`和`bitcount`都能够操作字节范围的字符串,而不需要在整个长度的字符串上进行操作。下面是`BITCOUNT`调用获取值的例子: ``` > setbit key 0 1 (integer) 0 > setbit key 100 1 (integer) 0 > bitcount key (integer) 2 ``` 通常位图的用户使用情况是这样的: + 实时分析所有类型 + 存储空间高效利用的且高性能的二进制信息,同时和对象 ID 关联起来。 例如你想要知道你网站用户的访问你网站的最长的连续天数。你开始在你使你的网站公开的那天开始算起,从0开始计算你的天数,每次用户访问网站的适合,就用`SETBIT`命令设置一个位。作为位索引,你只需要简单地获取当前的 UNIX 时间,减去初始的偏移,同时除以3600 * 24。 这个方式是每个用户拥有一个小的字符串包含每天的访问信息。通过`BITCOUNT`命令可以去很容易地获得一个给定用户的访问网站的天数,只需要通过一点`BITOPS`调用即可,或者简单地获取和分析客户端的位图,很容易去计算最长登录天数。 位图可以分成多个键,例如为了去切分数据集成片,应因为通常情况下避免使用巨大的键来工作更好一点。去根据不同的键来切分位图,而不是设置所有的位到一个键中。一个通用的策略就是每个键存储 M 位,同时通过 位数/M 来获取键的名字,而键内的第 N 位标识则通过位数对 M 求模获得。 ## HyperLogLogs HyperLogLogs 是一个概率数据结构用来统计独一无二的事情(技术上,这被称为估计集合的基数)。通常统计唯一元素需要使用和你想统计的元素数量成比例的内存数量,因为你需要在记住你在过去见过的元素从而避免来多次统计他们。然而这里有一组算法来精确地转换内存,你可以以一个标准误差的估计测量值来结束,在 Redis 的实现例子中,这个小于1%。这个算法的神奇之处就是你不再需要使用和你需要统计的元素成比例的内存数量,而是可以使用不变的内存。最坏的情况下也只需要12k字节,或者如果你的 HyperLogLogs(下文中我们将会称呼它为HLL) 已经见过了一些元素之后,会使用的更少。 Redis 中的HLL,他会被编码成 Redis String类型(但是它们技术上是不同的数据结构)。所以你可以用`GET`命令来序列化一个 HLL 数据,或者通过`SET`命令将它反序列化回服务器。 概念上 HLL 的 API 就像使用集合来完成同样的任务一样。你可以将看到的每个元素`SADD`到集合中,或者使用`SCARD`命令来获得集合内的元素个数,而且集合内的元素也是唯一的,因为`SADD`不会添加一个已经存在的元素。 而实际上你并不需要真的把一个元素添加到 HLL 中,因为这个数据结构它只包含状态,而不包含真正的元素,API 和集合也是相同的: + 每次你看到一个新的元素,可以通过`PFADD`命令添加到集合中。 + 每次你想要检索当前为止使用`PFADD`添加到集合中的唯一元素的近似值,你可以使用`PFCOUNT`命令: ``` > pfadd hll a b c d (integer) 1 > pfcount hll (integer) 4 ``` 这个数据结构的一个使用场景就是统计每天某个用户通过搜索表单执行的唯一的查询。 Redis 也能够去执行 HLL 的联合,参考[完整文档](https://redis.io/commands#hyperloglog)来获取更多的信息。 ## 其他值得注意的特点 在 Redis 的API中还有一些重要的东西,但不能在这篇文档的上下文中探索了,但是绝对值得你去注意: + [可以在大的集合上增量迭代键空间](https://redis.io/commands/scan) + 可以在服务端上运行[ Lua 脚步](https://redis.io/commands/eval)来获得更好的延时和带宽 + Redis 也是一个[发布/订阅](https://redis.io/topics/pubsub)服务器 ## 学习更多 这篇教程并没有完整地覆盖到 Redis 的所有基础 API。可以通过阅读[命令索引](https://redis.io/commands)来发现更多的 API。感谢阅读,同时祝您能够愉快地使用 Redis 。