*使用 Redis 进行二级索引
Redis 不完全是一个键值存储,因为值可以是复杂的数据结构。然而它有一个外部的键值外壳:在 API 级别,数据通过键名来寻址。可以说,原生地,Redis 只提供 主键访问。然而由于 Redis 是一个数据结构服务器,它的能力可以用于索引,以便创建不同类型的二级索引,包括复合(多列)索引。
本文档解释了如何使用以下数据结构在 Redis 中创建索引:
- 有序集合用于按 ID 或其他数字字段创建二级索引。
- 带有字典序范围的有序集合用于创建更高级的二级索引、复合索引和图遍历索引。
- 集合用于创建随机索引。
- 列表用于创建简单的可迭代索引和最近 N 项索引。
用 Redis 实现和维护索引是一个高级主题,因此需要对数据执行复杂查询的大多数用户应该了解关系存储是否更适合他们。然而很多时候,特别是在缓存场景中,有明确的需要将索引数据存储到 Redis 中,以便加速需要某种形式的索引才能执行的常见查询。
*使用有序集合的简单数字索引
你可以使用 Redis 创建的最简单的二级索引是使用有序集合数据类型,它是一种数据结构,表示按浮点数排序的元素集合,该浮点数是每个元素的 分数。元素按分数从小到大排序。
由于分数是双精度浮点数,你可以用普通有序集合构建的索引仅限于索引字段是某个范围内数字的情况。
构建此类索引的两个命令是 ZADD 和 ZRANGEBYSCORE,分别用于添加项目和检索指定范围内的项目。
例如,可以通过向有序集合添加元素来按年龄索引一组人名。元素将是人的名字,分数将是年龄。
ZADD myindex 25 Manuel
ZADD myindex 18 Anna
ZADD myindex 35 Jon
ZADD myindex 67 Helen
为了检索所有年龄在 20 到 40 岁之间的人,可以使用以下命令:
ZRANGEBYSCORE myindex 20 40
1) "Manuel"
2) "Jon"
通过使用 ZRANGEBYSCORE 的 WITHSCORES 选项,还可以获得与返回元素关联的分数。
ZCOUNT 命令可用于检索给定范围内的元素数量,而无需实际获取元素,这也很有用,特别是考虑到无论范围大小如何,该操作都在对数时间内执行。
范围可以是包含的或不包含的,详情请参阅 ZRANGEBYSCORE 命令文档。
注意:使用 ZREVRANGEBYSCORE 可以按相反顺序查询范围,当数据按给定方向(升序或降序)索引但我们希望以另一种方式检索信息时,这通常很有用。
*使用对象 ID 作为关联值
在上面的示例中,我们将名字与年龄关联。然而一般来说,我们可能想要索引存储在其他地方的某个对象的字段。我们可以不在有序集合值中直接存储与索引字段关联的数据,而是只存储对象的 ID。
例如,我可能有代表用户的 Redis 哈希。每个用户由一个可直接通过 ID 访问的单个键表示:
HMSET user:1 id 1 username antirez ctime 1444809424 age 38
HMSET user:2 id 2 username maria ctime 1444808132 age 42
HMSET user:3 id 3 username jballard ctime 1443246218 age 33
如果我想创建一个索引以便按年龄查询用户,我可以这样做:
ZADD user.age.index 38 1
ZADD user.age.index 42 2
ZADD user.age.index 33 3
这次在有序集合中与分数关联的值是对象的 ID。因此一旦我用 ZRANGEBYSCORE 查询索引,我还必须用 HGETALL 或类似的命令检索我需要的信息。明显的优势是对象可以在不触碰索引的情况下更改,只要我们不更改索引字段即可。
在接下来的示例中,我们几乎总是使用 ID 作为与索引关联的值,因为这通常是更合理的设计,只有几个例外。
*更新简单的有序集合索引
通常我们索引的是随时间变化的事物。在上面的示例中,用户的年龄每年都会变化。在这种情况下,使用出生日期作为索引而不是年龄本身会更有意义,但还有其他情况,我们只是希望某个字段不时变化,并且索引反映这种变化。
ZADD 命令使更新简单索引成为一个非常简单的操作,因为用不同的分数和相同的值重新添加元素只会更新分数并将元素移动到正确的位置,所以如果用户 antirez 年满 39 岁,为了更新代表用户的哈希中的数据以及索引中的数据,我们需要执行以下两个命令:
HSET user:1 age 39
ZADD user.age.index 39 1
该操作可以用 MULTI/EXEC 事务包装,以确保两个字段都被更新或都不更新。
*将多维数据转换为线性数据
使用有序集合创建的索引只能索引单个数值。因此你可能会认为不可能使用这种索引来索引具有多个维度的东西,但实际上这并非总是如此。如果你能有效地以线性方式表示多维的东西,那么通常可以使用简单的有序集合进行索引。
例如,Redis 地理索引 API 使用有序集合通过一种称为 Geo hash 的技术按纬度和经度索引地点。有序集合分数代表经度和纬度的交替位,因此我们将有序集合的线性分数映射到地球表面的许多小 方块。通过进行 8+1 样式的中心加邻域搜索,可以按半径检索元素。
*分数的限制
有序集合元素的分数是双精度浮点数。这意味着它们可以用不同的精度表示不同的小数或整数值,因为它们内部使用指数表示。然而对于索引目的来说有趣的是,分数始终能够在没有任何误差的情况下表示 -9007199254740992 到 9007199254740992 之间的数字,即 -/+ 2^53。
当表示更大的数字时,你需要一种不同形式的索引,能够以任意精度索引数字,称为字典序索引。
*字典序索引
Redis 有序集合有一个有趣的属性。当添加相同分数的元素时,它们按字典序排序,使用 memcmp() 函数将字符串作为二进制数据进行比较。
对于不了解 C 语言或 memcmp 函数的人来说,这意味着具有相同分数的元素通过比较其字节的原始值进行排序,逐字节比较。如果第一个字节相同,则检查第二个字节,依此类推。如果两个字符串的公共前缀相同,则认为较长的字符串更大,因此 "foobar" 大于 "foo"。
有诸如 ZRANGEBYLEX 和 ZLEXCOUNT 之类的命令,能够以字典序方式查询和计数范围,假设它们用于所有元素都具有相同分数的有序集合。
这个 Redis 特性基本上等同于 b-tree 数据结构,传统数据库经常使用它来实现索引。正如你所猜到的,正因为如此,可以使用此 Redis 数据结构来实现非常花哨的索引。
在深入了解如何使用字典序索引之前,让我们检查一下有序集合在这种特殊操作模式下的行为。由于我们需要添加具有相同分数的元素,我们将始终使用特殊的零分数。
ZADD myindex 0 baaa
ZADD myindex 0 abbb
ZADD myindex 0 aaaa
ZADD myindex 0 bbbb
从有序集合中获取所有元素立即显示它们是按字典序排序的。
ZRANGE myindex 0 -1
1) "aaaa"
2) "abbb"
3) "baaa"
4) "bbbb"
现在我们可以使用 ZRANGEBYLEX 来执行范围查询。
ZRANGEBYLEX myindex [a (b
1) "aaaa"
2) "abbb"
注意,在范围查询中,我们用特殊字符 [ 和 ( 作为标识范围的 min 和 max 元素的前缀。这些前缀是强制性的,它们指定范围的元素是包含的还是不包含的。因此范围 [a (b 意味着给我所有在字典序上介于包含 a 和不包含 b 之间的元素,也就是所有以 a 开头的元素。
还有两个特殊字符表示无限负字符串和无限正字符串,分别是 - 和 +。
ZRANGEBYLEX myindex [b +
1) "baaa"
2) "bbbb"
基本上就是这样。让我们看看如何使用这些特性来构建索引。
*第一个示例:自动补全
索引的一个有趣应用是自动补全。自动补全是指当你开始在搜索引擎中输入查询时发生的情况:用户界面会预测你可能在输入什么,提供以相同字符开头的常见查询。
自动补全的一个简单方法是将我们从用户那里获得的每个查询都添加到索引中。例如,如果用户搜索 banana,我们只需执行:
ZADD myindex 0 banana
对每个曾经遇到的搜索查询都如此。然后当我们想要补全用户输入时,我们使用 ZRANGEBYLEX 执行范围查询。想象用户在搜索表单中输入 "bit",我们想要提供以 "bit" 开头的可能搜索关键词。我们向 Redis 发送这样的命令:
ZRANGEBYLEX myindex "[bit" "[bit\xff"
基本上我们使用用户当前输入的字符串作为范围的开始,相同的字符串加上一个设置为 255 的尾随字节,即示例中的 \xff,作为范围的结束。这样我们就得到了所有以用户输入的字符串开头的字符串。
注意我们不希望返回太多项目,因此我们可以使用 LIMIT 选项来减少结果数量。
*将频率加入混合
上面的方法有点简单,因为所有用户搜索都是以这种方式相同的。在真实系统中,我们希望根据频率来补全字符串:非常流行的搜索将以更高的概率被建议,相比之下很少输入的搜索字符串。
为了实现依赖于频率的东西,同时自动适应未来的输入,通过清除不再流行的搜索,我们可以使用一个非常简单的 流算法。
首先,我们修改索引以便不仅存储搜索词,还存储与该词关联的频率。因此我们不只添加 banana,而是添加 banana:1,其中 1 是频率。
ZADD myindex 0 banana:1
我们还需要逻辑来递增索引,如果搜索词已经存在于索引中,所以我们实际会做的是类似这样的事情:
ZRANGEBYLEX myindex "[banana:" + LIMIT 1 1
1) "banana:1"
如果存在,这将返回 banana 的单个条目。然后我们可以递增关联的频率并发送以下两个命令:
ZREM myindex 0 banana:1
ZADD myindex 0 banana:2
注意,因为可能存在并发更新,所以上述三个命令应该通过 Lua 脚本 发送,以便 Lua 脚本能够原子地获取旧计数并用递增的分数重新添加项目。
因此结果是,每次用户搜索 banana 时,我们都会更新我们的条目。
还有更多:我们的目标是只保留搜索非常频繁的项目。所以我们需要某种形式的清除。当我们实际查询索引以补全用户输入时,我们可能会看到这样的情况:
ZRANGEBYLEX myindex "[banana:" + LIMIT 1 10
1) "banana:123"
2) "banahhh:1"
3) "banned user:49"
4) "banning:89"
显然没有人为 "banahhh" 进行搜索,例如,但该查询执行了一次,所以我们最终将其呈现给用户。
这就是我们可以做的。在返回的项目中,我们随机挑选一个,将其分数减一,并用新分数重新添加它。然而如果分数达到 0,我们就简单地从列表中删除该项目。你可以使用更高级的系统,但想法是索引在长期运行中将包含热门搜索,如果热门搜索随时间变化,它将自动适应。
对此算法的一个改进是根据权重挑选列表中的条目:分数越高,条目被挑选以递减其分数或驱逐它们的可能性就越小。
*针对大小写和重音进行字符串规范化
在完成示例中,我们总是使用小写字符串。然而现实比这复杂得多:语言有大写的名字、重音等等。
处理这些问题的一种简单方法是实际规范化用户搜索的字符串。无论用户搜索 "Banana"、"BANANA" 还是 "Ba'nana",我们都可以将其转换为 "banana"。
然而有时我们可能喜欢向用户呈现原始输入的项目,即使我们对索引进行规范化。为了做到这一点,我们所做的是改变索引的格式,以便不只存储 term:frequency,而是存储 normalized:frequency:original,如下例所示:
ZADD myindex 0 banana:273:Banana
基本上我们添加了另一个字段,我们只在可视化时提取和使用。范围将始终使用规范化字符串计算。这是一个有多种应用的常见技巧。
*在索引中添加辅助信息
当我们以直接方式使用有序集合时,每个对象有两个不同的属性:分数,我们用作索引,以及关联的值。当使用字典序索引时,分数始终设置为 0,基本上完全不被使用。我们只剩下一个字符串,即元素本身。
就像我们在之前的完成示例中所做的那样,我们仍然能够使用分隔符存储关联数据。例如我们使用冒号来添加频率和用于完成的原始单词。
一般来说我们可以将任何类型的关联值添加到我们的索引键中。为了使用字典序索引来实现一个简单的键值存储,我们只将条目存储为 key:value:
ZADD myindex 0 mykey:myvalue
并用以下方式搜索键:
ZRANGEBYLEX myindex [mykey: + LIMIT 1 1
1) "mykey:myvalue"
然后我们提取冒号后面的部分来检索值。然而在这种情况下需要解决的一个问题是冲突。冒号字符可能是键本身的一部分,所以必须选择它以确保永远不会与添加的键冲突。
由于 Redis 中的字典序范围是二进制安全的,你可以使用任何字节或任何字节序列。然而如果你接收不受信任的用户输入,最好使用某种形式的转义以保证分隔符永远不会碰巧是键的一部分。
例如如果你使用两个空字节作为分隔符 "\0\0",你可能希望始终将空字节转义为字符串中的两个字节序列。
*数字填充
字典序索引看起来只适合处理字符串的问题。实际上使用这种索引来索引任意精度的数字非常简单。
在 ASCII 字符集中,数字按从 0 到 9 的顺序出现,因此如果我们用前导零填充数字,结果是将它们作为字符串进行比较将按它们的数值排序。
ZADD myindex 0 00324823481:foo
ZADD myindex 0 12838349234:bar
ZADD myindex 0 00000000111:zap
ZRANGE myindex 0 -1
1) "00000000111:zap"
2) "00324823481:foo"
3) "12838349234:bar"
我们有效地创建了一个使用可以任意大的数字字段的索引。这也适用于任何精度的浮点数,只要确保我们用前导零填充数字部分,并用尾随零填充小数部分,如下面的数字列表所示:
01000000000000.11000000000000
01000000000000.02200000000000
00000002121241.34893482930000
00999999999999.00000000000000
*使用二进制形式的数字
以十进制存储数字可能会使用太多内存。一种替代方法是以二进制形式直接存储数字,例如 128 位整数。然而为了使其工作,你需要以大端格式存储数字,以便最重要的字节存储在最低有效字节之前。这样当 Redis 用 memcmp() 比较字符串时,它将有效地按值对数字进行排序。
请记住,以二进制格式存储的数据不太便于调试观察,更难解析和导出。所以这绝对是一种权衡。
*复合索引
到目前为止,我们探索了索引单个字段的方法。然而我们都知道 SQL 存储能够使用多个字段创建索引。例如,我可能在一个非常大的商店中按房间号和价格索引产品。
我需要运行查询来检索给定房间中具有给定价格范围的所有产品。我可以按以下方式索引每个产品:
ZADD myindex 0 0056:0028.44:90
ZADD myindex 0 0034:0011.00:832
这里的字段是 room:price:product_id。我在示例中使用了四位数字填充以简化。辅助数据(产品 ID)不需要任何填充。
有了这样的索引,要获取房间 56 中价格在 10 到 30 美元之间的所有产品非常容易。我们只需运行以下命令:
ZRANGEBYLEX myindex [0056:0010.00 [0056:0030.00
上面称为复合索引。它的有效性取决于字段的顺序和我要运行的查询。例如上面的索引不能有效地用于检索具有特定价格范围而不管房间号的产品。然而我可以使用主键来运行不管价格的查询,比如 给我房间 44 中的所有产品。
复合索引非常强大,在传统存储中用于优化复杂查询。在 Redis 中,它们既可以用于实现对存储在传统数据存储中的某些内容的非常快速的内存 Redis 索引,也可以用于直接索引 Redis 数据。
*更新字典序索引
字典序索引中的索引值可能变得非常复杂,难以或缓慢地从我们存储的关于对象的内容中重建。因此,一种简化索引处理的方法,以使用更多内存为代价,是同时获取一个表示索引的有序集合和一个将对象 ID 映射到当前索引值的哈希。
因此,例如,当我们索引时,我们也添加到哈希中:
MULTI
ZADD myindex 0 0056:0028.44:90
HSET index.content 90 0056:0028.44:90
EXEC
这并不总是需要的,但简化了更新索引的操作。为了删除我们为对象 ID 90 索引的旧信息,不管对象的 当前 字段值如何,我们只需按对象 ID 检索哈希值并用 ZREM 将其从有序集合视图中删除。
*使用六元存储表示和查询图
复合索引的一个很酷的事情是,它们可以方便地用于表示图,使用一种称为 Hexastore 的数据结构。
六元存储提供了对象之间关系的表示,由 主语、谓语 和 宾语 组成。 对象之间的简单关系可以是:
antirez is-friend-of matteocollina
为了表示这种关系,我可以在字典序索引中存储以下元素:
ZADD myindex 0 spo:antirez:is-friend-of:matteocollina
注意我用字符串 spo 作为我的项目前缀。这意味着该项目表示一个 subject,predicate,object(主语、谓语、宾语)关系。
我可以为同一关系再添加 5 个条目,但顺序不同:
ZADD myindex 0 sop:antirez:matteocollina:is-friend-of
ZADD myindex 0 ops:matteocollina:is-friend-of:antirez
ZADD myindex 0 osp:matteocollina:antirez:is-friend-of
ZADD myindex 0 pso:is-friend-of:antirez:matteocollina
ZADD myindex 0 pos:is-friend-of:matteocollina:antirez
现在事情开始变得有趣,我可以以许多不同的方式查询图。例如,antirez 的 所有朋友 是谁?
ZRANGEBYLEX myindex "[spo:antirez:is-friend-of:" "[spo:antirez:is-friend-of:\xff"
1) "spo:antirez:is-friend-of:matteocollina"
2) "spo:antirez:is-friend-of:wonderwoman"
3) "spo:antirez:is-friend-of:spiderman"
或者,antirez 和 matteocollina 之间有哪些关系,其中第一个是主语,第二个是宾语?
ZRANGEBYLEX myindex "[sop:antirez:matteocollina:" "[sop:antirez:matteocollina:\xff"
1) "sop:antirez:matteocollina:is-friend-of"
2) "sop:antirez:matteocollina:was-at-conference-with"
3) "sop:antirez:matteocollina:talked-with"
通过组合不同的查询,我可以提出复杂的问题。例如:
我所有的朋友中,哪些喜欢啤酒、住在巴塞罗那,而且 matteocollina 也认为是朋友?
为了获得这些信息,我首先使用 spo 查询来找到我所有的朋友。然后对于每个我得到的结果,我执行一个 spo 查询来检查他们是否喜欢啤酒,删除我找不到此关系的人。我再次这样做以按城市过滤。最后我执行一个 ops 查询来找出,在我获得的列表中,matteocollina 认为谁是朋友。
请务必查看 Matteo Collina 关于 Levelgraph 的幻灯片 以更好地理解这些想法。
*多维索引
一种更复杂的索引类型是允许你执行查询,其中两个或更多变量同时被查询特定范围。例如我可能有一个代表人的年龄和工资的数据集,我想要检索所有年龄在 50 到 55 岁之间且工资在 70000 到 85000 之间的人。
此查询可以用多列索引执行,但这需要我们先选择第一个变量然后扫描第二个,这意味着我们可能做了比需要更多的工作。可以使用不同的数据结构来执行涉及多个变量的这类查询。例如,多维树如 k-d 树 或 r-树 有时被使用。在这里我们将描述一种不同的方式来索引多维数据,使用一种表示技巧,使我们能够使用 Redis 字典序范围非常高效地执行查询。
让我们从可视化问题开始。在这张图片中,我们在空间中有一些点,代表我们的数据样本,其中 x 和 y 是我们的坐标。两个变量的最大值都是 400。
图片中的蓝色框代表我们的查询。我们想要所有 x 在 50 到 100 之间且 y 在 100 到 300 之间的点。

为了表示使这种查询能够快速执行的数据,我们首先用 0 填充我们的数字。例如假设我们想要将点 10,25(x,y)添加到我们的索引中。鉴于示例中的最大范围是 400,我们可以只填充到三位数,因此我们得到:
x = 010
y = 025
现在我们所做的是交错数字,取 x 的最左边一位数字和 y 的最左边一位数字,依此类推,以便创建一个单一的数字:
001205
这就是我们的索引,然而为了更容易重建原始表示,如果我们想(以空间为代价),我们也可以将原始值作为额外的列添加:
001205:10:25
现在,让我们思考一下这种表示为什么对范围查询有用。例如让我们取蓝色框的中心,它在 x=75 和 y=200。我们可以像之前一样通过交错数字来编码这个数字,得到:
027050
如果我们分别用 00 和 99 替换最后两位数字会发生什么?我们得到一个字典序连续的范围:
027000 到 027099
这映射到什么?它映射到代表所有 x 变量在 70 到 79 之间且 y 变量在 200 到 209 之间的值的方块。我们可以在这个区间内写入随机点,以便识别这个特定区域:

因此上面的字典序查询允许我们轻松地查询图片中特定方块内的点。然而方块对于我们正在搜索的框来说可能太小了,因此可能需要太多查询。因此我们可以做同样的事情,但不是用 00 和 99 替换最后两位数字,我们可以对最后四位数字做同样的事情,得到以下范围:
020000 029999
这次范围代表所有 x 在 0 到 99 之间且 y 在 200 到 299 之间的点。在这个区间内绘制随机点向我们展示了这片更大的区域:

哎呀,现在我们的区域对于我们的查询来说太大了,而且我们的搜索框仍然没有完全包含在内。我们需要更细的粒度,但我们可以通过用二进制形式表示我们的数字来轻松获得它。这次,当我们替换数字时,我们得到的不是十倍大的方块,而是仅两倍大的方块。
我们的数字以二进制形式,假设每个变量只需要 9 位(为了表示值高达 400 的数字)将是:
x = 75 -> 001001011
y = 200 -> 011001000
因此通过交错数字,我们在索引中的表示将是:
000111000011001010:75:200
让我们看看当我们用 0 和 1 替换最后 2、4、6、8、... 位时,我们的范围是什么:
2 位: x 介于 70 和 75 之间, y 介于 200 和 201 之间 (范围=2)
4 位: x 介于 72 和 75 之间, y 介于 200 和 203 之间 (范围=4)
6 位: x 介于 72 和 79 之间, y 介于 200 和 207 之间 (范围=8)
8 位: x 介于 64 和 79 之间, y 介于 192 和 207 之间 (范围=16)
依此类推。现在我们有了更好的粒度!
如你所见,从索引中替换 N 位会给我们边长为 2^(N/2) 的搜索框。
所以我们所做的是检查搜索框更小的维度,并检查最接近该数字的 2 的幂。我们的搜索框是 50,100 到 100,300,所以它的宽度是 50,高度是 200。我们取两者中较小的,50,并检查最接近的 2 的幂,即 64。64 是 26,所以我们会在交错表示中替换最后 12 位来工作(这样我们最终只替换每个变量的 6 位)。
然而单个方块可能无法覆盖我们的整个搜索范围,所以我们可能需要更多。我们所做的是从搜索框的左下角开始,即 50,100,并通过将每个数字的最后 6 位替换为 0 来找到第一个范围。然后我们对右上角做同样的事情。
通过两个简单的嵌套 for 循环,我们只递增有效位,就可以找到这两者之间的所有方块。对于每个找到的方块,我们将两个数字转换为交错表示,并使用转换后的表示作为范围的开始,以及相同表示但最后 12 位为 1 的作为结束范围来创建范围。
对于找到的每个方块,我们执行查询并获取其中的元素,删除搜索框外的元素。
将其转换为代码很简单。这里是一个 Ruby 示例:
def spacequery(x0,y0,x1,y1,exp)
bits=exp*2
x_start = x0/(2**exp)
x_end = x1/(2**exp)
y_start = y0/(2**exp)
y_end = y1/(2**exp)
(x_start..x_end).each{|x|
(y_start..y_end).each{|y|
x_range_start = x*(2**exp)
x_range_end = x_range_start | ((2**exp)-1)
y_range_start = y*(2**exp)
y_range_end = y_range_start | ((2**exp)-1)
puts "#{x},#{y} x from #{x_range_start} to #{x_range_end}, y from #{y_range_start} to #{y_range_end}"
# 将其转换为交错形式以进行 ZRANGEBYLEX 查询。
# 我们假设每个整数需要 9 位,因此最终的
# 交错表示将是 18 位。
xbin = x_range_start.to_s(2).rjust(9,'0')
ybin = y_range_start.to_s(2).rjust(9,'0')
s = xbin.split("").zip(ybin.split("")).flatten.compact.join("")
# 现在我们有了范围的开始,通过将指定数量的位从 0 替换为 1 来计算结束。
e = s[0..-(bits+1)]+("1"*bits)
puts "ZRANGEBYLEX myindex [#{s} [#{e}"
}
}
end
spacequery(50,100,100,300,6)
虽然非立即显而易见,但这是一个非常有用的索引策略,将来可能会以原生方式在 Redis 中实现。目前,好处是复杂性可以很容易地封装在库内,用于执行索引和查询。此类库的一个示例是 Redimension,一个使用此处描述的技术在 Redis 中索引 N 维数据的概念验证 Ruby 库。
*带有负数或浮点数的多维索引
表示负值的最简单方法是只使用无符号整数,并使用偏移量表示它们,以便在索引时,在将数字转换为索引表示之前,添加最小负整数的绝对值。
对于浮点数,最简单的方法可能是通过将整数乘以与要保留的小数位数成比例的十的幂来将它们转换为整数。
*非范围索引
到目前为止,我们检查了用于按范围或单个项目查询的索引。然而其他 Redis 数据结构如集合或列表可以用于构建其他类型的索引。它们非常常用,但也许我们并不总是意识到它们实际上是一种索引形式。
例如,我可以将对象 ID 索引到集合数据类型中,以便使用 SRANDMEMBER 的 获取随机元素 操作来检索一组随机对象。集合还可以在我只需要测试给定项目是否存在或不存在或是否具有单个布尔属性时用于检查存在性。
类似地,列表可以用于将项目索引到固定顺序中。我可以将所有项目添加到 Redis 列表中,并使用 RPOPLPUSH 旋转列表,使用相同的键名作为源和目标。这当我想要一遍又一遍地以相同顺序处理一组项目时很有用。想想一个 RSS 提要系统需要定期刷新本地副本。
另一种常与 Redis 一起使用的流行索引是 有上限的列表,其中使用 LPUSH 添加项目并使用 LTRIM 修剪,以便创建一个只包含最近遇到的 N 个项目的视图,顺序与它们被看到的顺序相同。
*索引不一致
保持索引更新可能具有挑战性,在几个月或几年的过程中,由于软件错误、网络分区或其他事件,可能会引入不一致。
可以使用不同的策略。如果索引数据在 Redis 外部,读取修复 可能是一个解决方案,其中数据在请求时以惰性方式修复。当我们在 Redis 本身中索引数据时,SCAN 系列命令可用于验证、更新或从头开始增量地重建索引。