Redis 不是一个普通的键值存储,它实际上是一个数据结构服务器,支持不同类型的值。这意味着,虽然在传统的键值存储中,您将字符串键与字符串值相关联,但在 Redis 中,值不仅限于简单的字符串,还可以保存更复杂的数据结构。以下是 Redis 支持的所有数据结构的列表,本教程将单独介绍:
二进制安全字符串。
列表:根据插入顺序排序的字符串元素的集合。它们基本上是链表。
集合:唯一的、未排序的字符串元素的集合。
排序集,类似于 Sets,但其中每个字符串元素都与一个浮点数相关联,称为score。元素总是按其分数排序,因此与 Set 不同,它可以检索一系列元素(例如,您可能会问:给我前 10 个或后 10 个)。
哈希,它是由与值关联的字段组成的映射。字段和值都是字符串。这与 Ruby 或 Python 哈希非常相似。
位数组(或简单的位图):可以使用特殊命令来处理字符串值,如位数组:您可以设置和清除单个位,将所有设置为 1 的位计数,找到第一个设置或未设置的位,等等。
HyperLogLogs:这是一种概率数据结构,用于估计集合的基数。不要害怕,它比看起来更简单......请参阅本教程的 HyperLogLog 部分的后面部分。
Streams:提供抽象日志数据类型的类似地图条目的仅附加集合。它们在Redis Streams 简介中进行了深入 介绍。
从命令参考中掌握这些数据类型如何工作以及使用什么来解决给定问题并非易事,因此本文档是 Redis 数据类型及其最常见模式的速成课程。
对于所有示例,我们将使用该redis-cli实用程序,一个简单但方便的命令行实用程序,向 Redis 服务器发出命令。
Redis 键
Redis 密钥是二进制安全的,这意味着您可以使用任何二进制序列作为密钥,从“foo”之类的字符串到 JPEG 文件的内容。空字符串也是一个有效的键。
关于键的其他一些规则:
- 很长的密钥不是一个好主意。例如,1024 字节的键不仅在内存方面是一个坏主意,而且因为在数据集中查找键可能需要多次昂贵的键比较。即使手头的任务是匹配一个大值的存在,散列它(例如使用 SHA1)也是一个更好的主意,尤其是从内存和带宽的角度来看。
- 非常短的键通常不是一个好主意。如果您可以改为写“user:1000:followers”,那么将“u1000flw”写为键几乎没有意义。后者更具可读性,与键对象本身和值对象使用的空间相比,增加的空间很小。虽然短键显然会消耗更少的内存,但您的工作是找到正确的平衡点。
- 尝试坚持使用模式。例如“object-type:id”是个好主意,如“user:1000”。点或破折号通常用于多词字段,如“comment🔢reply.to”或“comment🔢reply-to”。
- 允许的最大密钥大小为 512 MB。
Redis 字符串
Redis String 类型是可以与 Redis 键关联的最简单的值类型。它是 Memcached 中唯一的数据类型,所以新手在 Redis 中使用它也很自然。
由于 Redis 的键是字符串,所以当我们也使用字符串类型作为值时,我们是在将一个字符串映射到另一个字符串。字符串数据类型可用于许多用例,例如缓存 HTML 片段或页面。
让我们玩一下字符串类型,使用redis-cli(所有示例将redis-cli在本教程中执行)。
> set mykey somevalue
OK
> get mykey
"somevalue"
如您所见,使用SET和GET命令是我们设置和检索字符串值的方式。请注意,如果键已经存在,即使键与非字符串值相关联, SET将替换已存储到键中的任何现有值。所以SET执行一个赋值。
值可以是各种字符串(包括二进制数据),例如,您可以将 jpeg 图像存储在值中。值不能大于 512 MB。
SET命令有一些有趣的选项,作为附加参数提供。例如,如果密钥已经存在,我可能会要求SET失败,或者相反,只有在密钥已经存在时才会成功:
> 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、 DECR和DECRBY。在内部,它始终是相同的命令,作用方式略有不同。
INCR 是原子的是什么意思?即使多个客户端针对同一个密钥发出 INCR,也永远不会进入竞争状态。例如,永远不会发生客户端 1 读取“10”,客户端 2 同时读取“10”,两者都递增到 11,并将新值设置为 11。最终值将始终为 12,并且读取-在所有其他客户端未同时执行命令时执行增量设置操作。
有许多用于对字符串进行操作的命令。例如,GETSET命令将键设置为新值,并返回旧值作为结果。您可以使用此命令,例如,如果您的系统在 每次网站接收新访问者时使用INCR递增 Redis 密钥。您可能希望每小时收集一次此信息,而不会丢失一个增量。您可以GETSET键,为其分配新值“0”并读回旧值。
在单个命令中设置或检索多个键的值的能力对于减少延迟也很有用。出于这个原因,有MSET和MGET命令:
> mset a 10 b 20 c 30
OK
> mget a b c
1) "10"
2) "20"
3) "30"
当使用MGET时,Redis 返回一个值数组。
更改和查询密钥空间
有些命令没有在特定类型上定义,但对于与键空间交互很有用,因此可以与任何类型的键一起使用。
例如,EXISTS命令返回 1 或 0 来表示给定键是否存在于数据库中,而DEL命令删除键和关联的值,无论值是什么。
> set mykey hello
OK
> exists mykey
(integer) 1
> del mykey
(integer) 1
> exists mykey
(integer) 0
从示例中,您还可以看到DEL本身如何返回 1 或 0,具体取决于密钥是否被删除(它存在)或不(没有具有该名称的此类密钥)。
与键空间相关的命令有很多,但以上两个与TYPE命令一起是必不可少的,它返回存储在指定键处的值的种类:
> set mykey x
OK
> type mykey
string
> del mykey
(integer) 1
> type mykey
none
Redis 过期:存活时间有限的密钥
在继续更复杂的数据结构之前,我们需要讨论另一个与值类型无关的功能,称为Redis expires。基本上你可以为一个键设置一个超时,这是一个有限的生存时间。当生存时间过去时,密钥会自动销毁,就像用户使用密钥调用DEL命令一样。
关于 Redis 的一些快速信息过期:
- 它们可以使用秒或毫秒精度进行设置。
- 但是,过期时间分辨率始终为 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来设置过期时间(它也可以用于为已经拥有的密钥设置不同的过期时间,例如可以使用PERSIST来删除过期时间并使密钥永久存在)。但是,我们也可以使用其他 Redis 命令创建具有过期时间的键。例如使用SET选项:
> set key 100 ex 10
OK
> ttl key
(integer) 9
上面的示例使用字符串 value 设置一个键,100过期时间为 10 秒。稍后调用TTL命令以检查密钥的剩余生存时间。
为了设置和检查以毫秒为单位的过期时间,请检查PEXPIRE和PTTL命令,以及SET选项的完整列表。
Redis 列表
要解释 List 数据类型,最好从一点理论开始,因为信息技术人员经常以不正确的方式使用List一词。例如,“Python 列表”不是名称所暗示的(链接列表),而是数组(实际上,相同的数据类型在 Ruby 中称为数组)。
从非常普遍的角度来看,List 只是一个有序元素的序列:10,20,1,2,3 是一个列表。但是使用 Array 实现的 List 的属性与使用 Linked List实现的 List 的属性非常不同。
Redis 列表是通过链表实现的。这意味着即使列表中有数百万个元素,在列表的头部或尾部添加新元素的操作也是在恒定时间内执行的。使用LPUSH命令向具有 10 个元素的列表头部添加一个新元素的速度与向具有 1000 万个元素的列表头部添加一个元素的速度相同。
有什么缺点?在使用 Array 实现的列表中按索引访问元素非常快(恒定时间索引访问),而在使用链表实现的列表中则不那么快(其中操作需要与被访问元素的索引成比例的工作量)。
Redis 列表是使用链表实现的,因为对于数据库系统而言,能够以非常快速的方式将元素添加到非常长的列表中至关重要。稍后您会看到,另一个强大的优势是 Redis 列表可以在恒定的时间内以恒定的长度获取。
当快速访问大量元素的中间很重要时,可以使用一种不同的数据结构,称为排序集。排序集将在本教程后面介绍。
Redis 列表的第一步
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 列表上定义的一项重要操作是弹出元素的能力。弹出元素是同时从列表中检索元素并从列表中删除它的操作。您可以从左右弹出元素,类似于在列表两侧推送元素的方式:
> 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 列表。
为了逐步描述一个常见的用例,假设您的主页显示在照片共享社交网络中发布的最新照片,并且您希望加快访问速度。
- 每次用户发布新照片时,我们都会使用LPUSH将其 ID 添加到列表中。
- 当用户访问主页时,我们使用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 的列表元素,其他所有内容都将被丢弃。这允许一个非常简单但有用的模式:一起执行 List push 操作 + List trim 操作,以添加新元素并丢弃超过限制的元素:
LPUSH mylist <some element>
LTRIM mylist 0 999
上述组合添加了一个新元素,并且仅将 1000 个最新元素放入列表中。使用LRANGE,您无需记住非常旧的数据即可访问热门项目。
注意:虽然LRANGE在技术上是一个O(N)命令,但访问列表头部或尾部的小范围是一个恒定时间操作。
列表上的阻塞操作
列表有一个特殊的特性,使它们适合实现队列,并且通常作为进程间通信系统的构建块:阻塞操作。
想象一下,您想通过一个进程将项目推送到列表中,并使用不同的进程来实际对这些项目进行某种工作。这是通常的生产者/消费者设置,可以通过以下简单方式实现:
- 要将项目推送到列表中,生产者调用LPUSH。
- 为了从列表中提取/处理项目,消费者调用RPOP。
然而,有时列表可能是空的并且没有要处理的内容,因此RPOP只返回 NULL。在这种情况下,消费者被迫等待一段时间并使用RPOP重试。这称为轮询,在这种情况下不是一个好主意,因为它有几个缺点:
- 强制 Redis 和客户端处理无用的命令(列表为空时的所有请求都不会完成任何实际工作,它们只会返回 NULL)。
- 为项目的处理添加延迟,因为在工作人员收到 NULL 后,它会等待一段时间。为了使延迟更小,我们可以在对RPOP的调用之间等待更少,从而放大问题 1,即更多无用的 Redis 调用。
因此,Redis 实现了称为BRPOP和BLPOP的命令,它们是 RPOP 和 LPOP 的版本,如果列表为空,则能够阻塞:它们只会在将新元素添加到列表中或用户指定的超时时返回给调用者到达。
这是我们可以在 worker 中使用的BRPOP调用示例:
> brpop tasks 5
1) "tasks"
2) "do_something"
这意味着:“等待列表中的元素tasks,但如果 5 秒后没有可用元素则返回”。
请注意,您可以使用 0 作为超时来永久等待元素,您还可以指定多个列表而不仅仅是一个,以便同时等待多个列表,并在第一个列表接收到元素时得到通知。
关于BRPOP的一些注意事项:
- 客户端以有序的方式提供服务:第一个阻塞等待列表的客户端,当某个元素被其他客户端推送时首先提供服务,依此类推。
- 与RPOP相比,返回值不同:它是一个二元素数组,因为它还包含键的名称,因为BRPOP和BLPOP能够阻止等待来自多个列表的元素。
- 如果达到超时,则返回 NULL。
您应该了解更多有关列表和阻塞操作的信息。我们建议您阅读以下内容:
- 可以使用LMOVE构建更安全的队列或旋转队列。
- 该命令还有一个阻塞变体,称为BLMOVE。
自动创建和删除密钥
到目前为止,在我们的示例中,我们不必在推送元素之前创建空列表,或者在它们内部不再有元素时删除空列表。Redis 有责任在列表为空时删除键,或者如果键不存在并且我们正在尝试向其中添加元素,例如使用LPUSH ,则创建一个空列表。
这并不特定于列表,它适用于由多个元素组成的所有 Redis 数据类型——Streams、Sets、Sorted Sets 和 Hashes。
基本上我们可以用三个规则来总结行为:
- 当我们将元素添加到聚合数据类型时,如果目标键不存在,则在添加元素之前创建一个空的聚合数据类型。
- 当我们从聚合数据类型中删除元素时,如果值保持为空,则键会自动销毁。Stream 数据类型是此规则的唯一例外。
- 调用只读命令,例如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 哈希
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
您可以在文档中找到完整的哈希命令列表。
值得注意的是,小散列(即一些具有小值的元素)在内存中以特殊方式编码,这使得它们非常节省内存。
Redis 集
Redis Set 是无序的字符串集合。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、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)ears、(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 命令的好时机,该命令提供集合内元素的数量。在集合论的上下文中,这通常称为集合的基数 ,因此 Redis 命令称为SCARD。
> scard game:1:deck
(integer) 47
数学有效:52 - 5 = 47。
当您只需要获取随机元素而不将它们从集合中删除时,可以使用适合该任务的SRANDMEMBER命令。它还具有返回重复和非重复元素的能力。
Redis 排序集
排序集是一种数据类型,类似于 Set 和 Hash 的混合。与集合一样,有序集合由唯一的、不重复的字符串元素组成,因此在某种意义上,有序集合也是一个集合。
然而,虽然集合内的元素没有排序,但已排序集合中的每个元素都与一个浮点值相关联,称为分数 (这就是该类型也类似于散列的原因,因为每个元素都映射到一个值)。
此外,有序集合中的元素是按顺序排列的(因此它们不是按要求排序的,顺序是用于表示有序集合的数据结构的特性)。它们按照以下规则排序:
- 如果 A 和 B 是具有不同分数的两个元素,则 A > B 如果 A.score > B.score。
- 如果 A 和 B 具有完全相同的分数,则如果 A 字符串在字典顺序上大于 B 字符串,则 A > B。A 和 B 字符串不能相等,因为排序集只有唯一元素。
让我们从一个简单的例子开始,添加一些选定的黑客姓名作为排序的集合元素,他们的出生年份作为“分数”。
> 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命令也可用于获取排名,考虑到元素按降序排序。
词典分数
在 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"
由于 sorted sets 排序规则,它们已经按字典顺序排序:
> 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组成的 16 字节前缀的排序集中大端序中的位数。由于大端序中的数字,当按字典顺序(按原始字节顺序)排序时,实际上也是按数字排序的,因此您可以要求 128 位空间中的范围,并获取元素的值而丢弃前缀。
如果您想在更严肃的演示中查看该功能,请查看Redis 自动完成演示。
更新分数:排行榜
在切换到下一个主题之前,只是关于排序集的最后一点说明。排序集的分数可以随时更新。只需对已包含在排序集中的元素调用ZADD将更新其分数(和位置),时间复杂度为O(log(N)) 。因此,当有大量更新时,排序集是合适的。
由于这个特性,一个常见的用例是排行榜。典型的应用程序是一个 Facebook 游戏,您可以在其中结合按用户的高分排序的能力,加上 get-rank 操作,以显示前 N 个用户,以及用户在排行榜中的排名(例如,“你是这里的#4932 最好成绩”)。
位图
位图不是实际的数据类型,而是在 String 类型上定义的一组面向位的操作。由于字符串是二进制安全 blob,它们的最大长度为 512 MB,因此它们适合设置最多 2 32 个不同的位。
位操作分为两组:恒定时间单个位操作,例如将位设置为 1 或 0,或获取其值,以及对位组的操作,例如计算给定位范围内设置位的数量(例如,人口计数)。
位图的最大优势之一是它们在存储信息时通常可以极大地节省空间。例如,在不同用户由增量用户 ID 表示的系统中,仅使用 512 MB 内存就可以记住 40 亿用户的单个位信息(例如,知道用户是否想要接收新闻通讯)。
使用SETBIT和GETBIT命令设置和检索位:
> setbit key 10 1
(integer) 1
> getbit key 10
(integer) 1
> getbit key 11
(integer) 0
SETBIT命令的第一个参数是位数,第二个参数是设置该位的值,即 1 或 0。如果寻址位超出当前字符串长度,该命令会自动放大字符串。
GETBIT只返回指定索引处的位值。超出范围的位(寻址超出存储在目标键中的字符串长度的位)始终被视为零。
对一组位进行操作的命令有 3 个:
- BITOP在不同的字符串之间执行逐位操作。提供的操作是 AND、OR、XOR 和 NOT。
- BITCOUNT执行人口计数,报告设置为 1 的位数。
- BITPOS查找具有指定值 0 或 1 的第一位。
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调用,或者简单地获取和分析客户端的位图,可以轻松计算最长的连续访问。
位图很容易拆分为多个键,例如为了对数据集进行分片,并且通常最好避免使用大键。要将位图拆分到不同的键上,而不是将所有位设置为一个键,一个简单的策略就是每个键存储 M 位并获得键名bit-number/M和第 N 位以在键内寻址bit-number MOD M。
HyperLogLogs
HyperLogLog 是一种概率数据结构,用于对独特事物进行计数(从技术上讲,这称为估计集合的基数)。通常计算唯一项目需要使用与您要计算的项目数量成比例的内存量,因为您需要记住过去已经看到的元素以避免多次计算它们。然而,有一组算法可以用内存来换取精度:你以一个带有标准误差的估计度量结束,在 Redis 实现的情况下,这个误差小于 1%。该算法的神奇之处在于您不再需要使用与计数的项目数成正比的内存量,而是可以使用恒定数量的内存!在最坏的情况下为 12k 字节,或者如果您的 HyperLogLog(我们从现在将它们称为 HLL)看到的元素很少,则更少。
Redis 中的 HLL,虽然在技术上是一种不同的数据结构,但被编码为 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 还能够执行 HLL 的联合,请查看 完整文档以获取更多信息。
其他显着特点
Redis API 中还有其他一些重要的东西无法在本文档的上下文中探讨,但值得您注意:
可以增量地迭代大型集合的键空间。
可以在服务器端运行 Lua 脚本以改善延迟和带宽。
Redis 也是一个Pub-Sub 服务器。
了解更多
本教程并不完整,仅涵盖了 API 的基础知识。阅读命令参考以发现更多信息。
感谢您的阅读,祝您愉快地使用 Redis 进行黑客攻击!