# Redis 数据类型教程

学习基本的 Redis 数据类型以及如何使用它们

以下是使用 Redis CLI 教授核心 Redis 数据类型的动手教程。有关数据类型的一般概述,请参阅 数据类型介绍

# Keys

Redis keys是二进制安全的,这意味着您可以使用任何二进制序列作为key,从“foo”之类的字符串到 JPEG 文件的内容。空字符串也是一个有效的键。

关于键的其他一些规则:

  • 很长的keys不是一个好主意。例如,1024 字节的键不仅在内存方面是一个坏主意,而且因为在数据集中查找键可能需要多次昂贵的键比较。即使手头的任务是匹配一个大值的存在,散列它(例如使用 SHA1)也是一个更好的主意,尤其是从内存和带宽的角度来看。
  • 非常短的键通常不是一个好主意。如果您可以改为写“user:1000:followers”,那么将“u1000flw”写为键几乎没有意义。后者更具可读性,与键对象本身和值对象使用的空间相比,增加的空间很小。虽然短键显然会消耗更少的内存,但您的工作是找到正确的平衡点。
  • 尝试坚持使用模式。例如“object-type:id”是个好主意,如“user:1000”。点或破折号通常用于多词字段,如“comment:4321:reply.to”或“comment:4321:reply-to”。
  • 允许的最大key大小为 512 MB。

# 字符串

Redis String 类型是可以与 Redis 键关联的最简单的值类型。它是 Memcached 中唯一的数据类型,所以新手在 Redis 中使用它也很自然。

由于 Redis 的键是字符串,所以当我们也使用字符串类型作为值时,我们是在将一个字符串映射到另一个字符串。字符串数据类型可用于许多用例,例如缓存 HTML 片段或页面。

让我们玩一下字符串类型,使用redis-cli(所有示例将redis-cli在本教程中执行)。

> set mykey somevalue
OK
> get mykey
"somevalue"

如您所见,使用 SETGET 命令是我们设置和检索字符串值的方式。请注意, SET 如果键已经存在,即使键与非字符串值相关联,也会替换已存储到键中的任何现有值。所以 SET 执行一个任务。

值可以是各种字符串(包括二进制数据),例如,您可以将 jpeg 图像存储在值中。值不能大于 512 MB。

SET 命令具有有趣的选项,这些选项作为附加参数提供。例如, SET 如果key已经存在,我可能会要求失败,或者相反,只有当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 命令将字符串值解析为整数,将其加一,最后将获得的值设置为新值。还有其他类似的命令,如 INCRBYDECRDECRBY。在内部,它始终是相同的命令,作用方式略有不同。

INCR 是原子的是什么意思?即使多个客户端针对同一个key发出 INCR,也永远不会进入竞争状态。例如,永远不会发生客户端 1 读取“10”,客户端 2 同时读取“10”,两者都递增到 11,并将新值设置为 11。最终值将始终为 12,并且读取-在所有其他客户端未同时执行命令时执行增量设置操作。

有许多用于对字符串进行操作的命令。例如,该 GETSET 命令将键设置为新值,并返回旧值作为结果。您可以使用此命令,例如,如果您的系统会在 INCR 您的网站每次接收新访问者时增加 Redis key。您可能希望每小时收集一次此信息,而不会丢失一个增量。您可以 GETSET 键,为其分配新值“0”并读回旧值。

在单个命令中设置或检索多个键的值的能力对于减少延迟也很有用。出于这个原因,有 MSET and MGET 命令:

> mset a 10 b 20 c 30
OK
> mget a b c
1) "10"
2) "20"
3) "30"

使用时 MGET,Redis 返回一个值数组。

# 更改和查询key空间

有些命令没有在特定类型上定义,但对于与键空间交互很有用,因此可以与任何类型的键一起使用。

例如,该 EXISTS 命令返回 1 或 0 来表示给定键是否存在于数据库中,而该 DEL 命令删除一个键和关联的值,无论值是什么。

> set mykey hello
OK
> exists mykey
(integer) 1
> del mykey
(integer) 1
> exists mykey
(integer) 0

从示例中,您还可以看到它 DEL 本身如何返回 1 或 0,具体取决于key是否被删除(它存在)或不(没有具有该名称的此类key)。

与键空间相关的命令有很多,但以上两个是必不可少的 TYPE 命令,它返回存储在指定键处的值的种类:

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

GET 由于第二次呼叫延迟了 5 秒以上,因此key在两次呼叫之间消失了。在上面的示例中,我们用于 EXPIRE 设置过期时间(它也可以用于为已经有过期时间的key设置不同的过期时间,例如 PERSIST 可以用于删除过期时间并使key永久存在)。但是,我们也可以使用其他 Redis 命令创建具有过期时间的键。例如使用 SET 选项:

> set key 100 ex 10
OK
> ttl key
(integer) 9

上面的示例使用字符串 value 设置一个键,100过期时间为 10 秒。稍后 TTL 调用该命令以检查key的剩余生存时间。

为了设置和检查以毫秒为单位的过期时间,请检查 PEXPIREPTTL 命令,以及 SET 选项的完整列表。

# 列表

为了解释 List 数据类型,最好从一点理论开始,因为信息技术人员经常以不正确的方式使用*List一词。*例如,“Python 列表”不是名称所暗示的(链接列表),而是数组(实际上,相同的数据类型在 Ruby 中称为数组)。

从非常普遍的角度来看,List 只是一个有序元素的序列:10,20,1,2,3 是一个列表。但是使用 Array 实现的 List 的属性与使用 Linked List实现的 List 的属性非常不同。

Redis 列表是通过链表实现的。这意味着即使列表中有数百万个元素,在列表的头部或尾部添加新元素的操作也是在恒定时间内执行的。用命令向一个有 10 个元素的列表的头部添加一个新元素的速度与向一个 LPUSH 有 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,将元素附加在列表的右侧,而 final 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 库 resquesidekiq 都在后台使用 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 的列表元素,其他所有元素都将被丢弃。这允许一个非常简单但有用的模式:一起执行 List push 操作 + List trim 操作,以添加新元素并丢弃超过限制的元素:

LPUSH mylist <some element>
LTRIM mylist 0 999

上述组合添加了一个新元素,并且仅将 1000 个最新元素放入列表中。您可以访问最重要的项目, LRANGE 而无需记住非常旧的数据。

注意:虽然 LRANGE 从技术上讲是一个 O(N) 命令,但访问列表头部或尾部的小范围是一个恒定时间操作。

# 列表上的阻塞操作

列表有一个特殊的特性,使它们适合实现队列,并且通常作为进程间通信系统的构建块:阻塞操作。

想象一下,您想通过一个进程将项目推送到列表中,并使用不同的进程来实际对这些项目进行某种工作。这是通常的生产者/消费者设置,可以通过以下简单方式实现:

  • 要将项目推送到列表中,生产者调用 LPUSH.
  • 要从列表中提取/处理项目,消费者调用 RPOP.

然而,有时列表可能是空的并且没有什么要处理的,所以 RPOP 只返回 NULL。在这种情况下,消费者被迫等待一段时间并使用 重试 RPOP。这称为轮询,在这种情况下不是一个好主意,因为它有几个缺点:

  1. 强制 Redis 和客户端处理无用的命令(列表为空时的所有请求都不会完成任何实际工作,它们只会返回 NULL)。
  2. 为项目的处理添加延迟,因为在工作人员收到 NULL 后,它会等待一段时间。为了使延迟更小,我们可以在调用 之间减少等待,从而 RPOP 放大问题 1,即更多无用的 Redis 调用。

因此,Redis 实现了被调用的命令 BRPOP,这些命令是列表为空时能够阻塞 BLPOP 的版本:它们只会在将新元素添加到列表中或达到用户指定的超时时返回给调用者。 RPOP LPOP

BRPOP 这是我们可以在 worker 中使用的调用示例:

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

这意味着:“等待列表中的元素tasks,但如果 5 秒后没有可用元素则返回”。

请注意,您可以使用 0 作为超时来永久等待元素,您还可以指定多个列表而不仅仅是一个,以便同时等待多个列表,并在第一个列表接收到元素时得到通知。

需要注意的几点 BRPOP

  1. 客户端以有序的方式提供服务:第一个阻塞等待列表的客户端,当某个元素被其他客户端推送时首先提供服务,依此类推。
  2. 返回值与 相比不同 RPOP :它是一个二元素数组,因为它还包括键的名称,因为 BRPOP 并且 BLPOP 能够阻止等待来自多个列表的元素。
  3. 如果达到超时,则返回 NULL。

您应该了解更多有关列表和阻塞操作的信息。我们建议您阅读以下内容:

  • 可以使用 LMOVE.
  • 该命令还有一个阻塞变体,称为 BLMOVE.

# 自动创建和删除key

到目前为止,在我们的示例中,我们不必在推送元素之前创建空列表,或者在它们内部不再有元素时删除空列表。Redis 有责任在列表为空时删除键,或者如果键不存在并且我们正在尝试向其中添加元素,则创建一个空列表,例如使用 LPUSH.

这并不特定于列表,它适用于由多个元素组成的所有 Redis 数据类型——Streams、Sets、Sorted Sets 和 Hashes。

基本上我们可以用三个规则来总结行为:

  1. 当我们将元素添加到聚合数据类型时,如果目标键不存在,则在添加元素之前创建一个空的聚合数据类型。
  2. 当我们从聚合数据类型中删除元素时,如果值保持为空,则键会自动销毁。Stream 数据类型是此规则的唯一例外。
  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 哈希看起来与人们期望的“哈希”完全一样,带有字段-值对:

> 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

您可以 在文档中找到完整的哈希命令列表

值得注意的是,小散列(即一些具有小值的元素)在内存中以特殊方式编码,这使得它们非常节省内存。

#

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 命令删除一个随机元素,将其返回给客户端,因此在这种情况下它是完美的操作。

但是,如果我们直接调用它来对抗我们的牌组,那么在下一场游戏中,我们将需要再次填充牌组,这可能并不理想。因此,首先,我们可以将存储在deckkey中的集合复制到game:1:deckkey中。

这是使用 来完成的 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 适合该任务的命令。它还具有返回重复和非重复元素的能力。

# 排序集

排序集是一种数据类型,类似于 Set 和 Hash 的混合。与集合一样,有序集合由唯一的、不重复的字符串元素组成,因此在某种意义上,有序集合也是一个集合。

然而,虽然集合中的元素没有排序,但有序集合中的每个元素都与一个浮点值相关联,称为分数 (这就是为什么该类型也类似于散列,因为每个元素都映射到一个值)。

此外,有序集合中的元素是按顺序排列的(因此它们不是按要求排序的,顺序是用于表示有序集合的数据结构的特性)。它们按照以下规则排序:

  • 如果 B 和 A 是具有不同分数的两个元素,则 A > B 如果 A.score > B.score。
  • 如果 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 命令也可用于获取排名。

# 词典分数

在 Redis 2.8 的最新版本中,引入了一个新功能,允许按字典顺序获取范围,假设排序集中的元素都以相同的相同分数插入(元素与 C memcmp函数进行比较,因此可以保证没有排序规则,并且每个 Redis 实例都会回复相同的输出)。

操作字典范围的主要命令是 ZRANGEBYLEXZREVRANGEBYLEX 和。 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)但具有 16 字节前缀的排序集中,其中包含128大端序中的位数。由于大端序中的数字,当按字典顺序(按原始字节顺序)排序时,实际上也是按数字排序的,因此您可以要求 128 位空间中的范围,并获取元素的值而丢弃前缀。

如果您想在更严肃的演示中查看该功能,请查看 Redis 自动完成演示

# 更新分数:排行榜

在切换到下一个主题之前,只是关于排序集的最后一点说明。排序集的分数可以随时更新。只需调用 ZADD 已包含在排序集中的元素,就会以 O(log(N)) 时间复杂度更新其分数(和位置)。因此,当有大量更新时,排序集是合适的。

由于这个特性,一个常见的用例是排行榜。典型的应用程序是一个 Facebook 游戏,您可以在其中结合按用户的高分排序的能力,加上 get-rank 操作,以显示前 N 个用户,以及用户在排行榜中的排名(例如,“你是这里的#4932 最好成绩”)。

# 位图

位图不是实际的数据类型,而是在 String 类型上定义的一组面向位的操作。由于字符串是二进制安全 blob,它们的最大长度为 512 MB,因此它们适合设置最多 2^32 个不同的位。

位操作分为两组:恒定时间单个位操作,例如将位设置为 1 或 0,或获取其值,以及对位组的操作,例如计算给定位范围内设置位的数量(例如,人口计数)。

位图的最大优势之一是它们在存储信息时通常可以极大地节省空间。例如,在不同用户由增量用户 ID 表示的系统中,仅使用 512 MB 内存就可以记住 40 亿用户的单个位信息(例如,知道用户是否想要接收新闻通讯)。

SETBIT 使用and GETBIT 命令设置和检索位:

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

SETBIT 命令的第一个参数是位数,第二个参数是设置该位的值,即 1 或 0。如果寻址位超出当前字符串长度,该命令会自动放大字符串。

GETBIT 只返回指定索引处的位值。超出范围的位(寻址超出存储在目标键中的字符串长度的位)始终被视为零。

对一组位进行操作的命令有 3 个:

  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 相关的空间高效但高性能的布尔信息。

例如,假设您想知道您的网站用户的最长每日访问次数。您从零开始计算天数,即您公开网站的日期,并在 SETBIT 每次用户访问该网站时设置一点。作为位索引,您只需获取当前的 unix 时间,减去初始偏移量,然后除以一天中的秒数(通常为 3600*24)。

这样,对于每个用户,您都有一个包含每天访问信息的小字符串。 BITCOUNT 可以轻松获取给定用户访问网站的天数,同时通过几次调用 BITPOS,或者简单地获取和分析客户端的位图,可以轻松计算最长的连续性。

位图很容易拆分为多个键,例如为了对数据集进行分片,并且通常最好避免使用大键。要将位图拆分到不同的键上,而不是将所有位设置为一个键,一个简单的策略就是每个键存储 M 位并获得键名bit-number/M和第 N 位以在键内寻址bit-number MOD M

# HyperLogLogs

HyperLogLog 是一种概率数据结构,用于对独特事物进行计数(从技术上讲,这称为估计集合的基数)。通常计算唯一项目需要使用与您要计算的项目数量成比例的内存量,因为您需要记住过去已经看到的元素以避免多次计算它们。然而,有一组算法可以用内存来换取精度:你以一个带有标准误差的估计度量结束,在 Redis 实现的情况下,这个误差小于 1%。该算法的神奇之处在于您不再需要使用与计数的项目数成正比的内存量,而是可以使用恒定量的内存!最坏情况下为 12k 字节,如果您的 HyperLogLog(我们的

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 进行黑客攻击!

Last Updated: 5/25/2023, 2:35:11 PM