Redis最佳做法,第2部分

Redis实验室的Redis最佳实践翻译周期的第二部分,讨论了交互模式和数据存储模式。

第一部分在这里

互动模式


Redis不仅可以充当传统的DBMS,还可以使用其结构和命令在微服务或进程之间交换消息。Redis客户端的广泛使用,服务器和协议的速度和效率以及内置的经典结构使您可以创建自己的工作流程和事件机制。在本章中,我们将介绍以下主题:

  • 事件队列;
  • 用Redlock阻止;
  • Pub / Sub;
  • 分布式事件。

事件队列


Redis中的列表是有序行列表,与您可能熟悉的链接列表非常相似。将值添加到列表(推送)和从列表中删除值(弹出)是非常轻量级的操作。可以想象,这是管理队列的一个很好的结构:将元素添加到开头并从末尾读取它们(FIFO)。 Redis还提供了其他功能,使此模式更加有效,可靠和易于使用。

列表具有命令的子集,可让您执行“阻止”行为。术语“阻止”是指仅与一个客户端的连接。实际上,这些命令不允许客户端执行任何操作,直到值出现在列表中或超时到期为止。这样就无需轮询Redis,而是等待结果。由于客户端无法在期望值的同时做任何事情,因此我们需要两个开放的客户端来说明这一点:
客户1客户2
1个
> BRPOP my-q 0
[期望值]
2
> LPUSH my-q hello
(integer) 1
1) "my-q"
2) "hello"
[客户端已解锁,准备接受命令]
3
> BRPOP my-q 0
[期望值]

在此示例中,在步骤1中,我们看到被阻止的客户端不会立即返回任何内容,因为它不包含任何内容。最后一个参数是等待时间。这里0表示永恒的期望。在第二行中my-q中输入一个值,并且第一个客户端立即退出阻塞状态。第三行,再次调用BRPOP(您可以在应用程序中循环执行此操作),并且客户端也等待下一个值。通过按“ Ctrl + C”,您可以解除锁定并退出客户端。

让我们反转示例,看看BRPOP如何与非空列表一起工作:
客户1客户2
1个
> LPUSH my-q hello
(integer) 1
2
> LPUSH my-q hej
(integer) 2
3
> LPUSH my-q bonjour
(integer) 3
4
> BRPOP my-q 0
1) "my-q"
2) "hello"
5
> BRPOP my-q 0
1) "my-q"
2) "hej"
6
> BRPOP my-q 0
1) "my-q"
2) "bonjour"
7
> BRPOP my-q 0
[期望值]

在步骤1-3中,我们将3个值添加到列表中,然后看到答案在增长,表明列表中元素的数量。尽管调用了BRPOP,第4步仍会立即返回该值。这是因为仅当队列中没有值时才会发生阻塞行为。我们可以在步骤5-6中看到相同的即时响应,因为这是针对队列中的每个项目完成的。在步骤7中,BRPOP在队列中找不到任何内容,并阻塞了客户端,直到添加了某些内容为止。

队列通常代表一些需要在另一个流程(工作人员)中完成的工作。在这种类型的工作负载中,重要的是,如果工作人员在执行过程中由于某种原因摔倒,工作也不会消失。Redis支持这种类型的队列。为此,请使用BRPOPLPUSH命令而不是BRPOP。她希望在一个列表中有一个值,一旦它出现在其中,就将其放在另一个列表中。这是原子完成的,因此两个工人不可能更改相同的值。让我们看看它是如何工作的:
客户1客户2
1个
> LINDEX worker-q 0
(nil)
2[如果结果不为零,则以某种方式对其进行处理,然后转到步骤4]
3
> LREM worker-q -1 [   1]
(integer) 1
[返回步骤1]
4
> BRPOPLPUSH my-q worker-q 0
[期望值]
5
> LPUSH my-q hello
"hello"
[客户端已解锁,准备接受命令]
6[打招呼]
7
> LREM worker-q -1 hello
(integer) 1
8[返回步骤1]

在步骤1-2中,我们什么也不做,因为worker-q为空。如果返回了某些内容,则我们将其处理并删除,然后再次返回到步骤1,以检查是否有任何内容进入队列。因此,我们首先清除工作人员的队列并执行现有工作。在第4步中,我们等到该值出现在my-q中,然后将其原子地转移到worker-q。然后,我们以某种方式处理“ hello”,之后将其从worker-q中删除并返回到步骤1。如果该过程在步骤6中终止,则该值仍保留在worker-q中。重新启动过程后,我们将立即删除在步骤7中未删除的所有内容。

这种模式极大地减少了失业的可能性,但前提是工人在步骤2和3或5和6之间死亡,这是不可能的,但是最佳实践将在工人的逻辑中考虑到这一点。

重锁锁定


有时在系统中,有必要阻塞一些资源。为了应用在竞争环境中无法解决的重要更改,这可能是必需的。封锁目标:

  • 只允许一名工人捕获资源;
  • 能够可靠地释放锁定对象;
  • 不要紧锁资源(一定时间后必须解锁)。

Redis具有一个简单的基于密钥的数据模型,并且每个分片都是单线程的,而且运行速度相当快,因此它是实现阻塞的一个不错的选择。使用Redis的优秀锁实现称为Redlock。
Redlock客户端几乎适用于每种语言,但是,重要的是要了解Redlock的工作方式,以便安全有效地使用它。

首先,您需要了解Redlock旨在在至少3台具有独立Redis实例的计算机上运行。这消除了锁定机制中的单点故障,后者可能导致所有资源的死锁。要理解的另一点是,尽管机器上的时钟不应100%同步,但它们应以相同的方式工作-时间以相同的速度移动:机器上的时间为一秒,机器B上的时间为一秒。

使用Redlock设置锁定对象首先需要获得毫秒精度的时间戳。您还必须提前指出阻塞时间。然后,通过将密钥设置为随机值(仅当此密钥尚不存在时)并设置密钥的超时时间来设置阻止对象。对于每个独立实例重复此操作。如果实例掉落,则将立即跳过该实例。如果在超时到期之前已在大多数实例上成功安装了锁定对象,则将其视为已捕获。安装或更新锁定对象的时间是达到锁定状态所花费的时间,减去预定义的锁定时间。如果发生错误或超时,请解锁所有实例,然后重试。

要释放锁对象,最好使用Lua脚本,该脚本将检查预期的随机值是否在键集中。如果存在,则可以将其删除,否则最好保留密钥,因为这些密钥可能是较新的锁定对象。

if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

Redlock进程提供了良好的保证,并且没有单点故障,因此您可以完全确定将分配单个锁对象,并且不会发生互锁。

酒馆/酒馆


除了数据存储,Redis还可以用作Pub / Sub平台(发布者/订阅者)。在这种模式下,发布者可以向任何数量的频道订阅者发布消息。这些是基于“即发即弃”原理的消息,也就是说,如果释放了消息并且订户不存在,则消息将消失而无法恢复。
订阅频道后,客户端进入订户模式,无法再调用命令-它变为只读状态。发布者没有这样的限制。

您可以订阅多个频道。我们首先使用SUBSCRIBE命令订阅两个天气和体育频道:

> SUBSCRIBE weather sports
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "weather"
3) (integer) 1
1) "subscribe"
2) "sports"
3) (integer) 2

在一个单独的客户端(例如,另一个终端窗口)中,我们可以使用PUBLISH命令在以下任一通道中发布消息:

> PUBLISH sports oilers/7:leafs/1
(integer) 1

第一个参数是通道的名称,第二个参数是消息。该消息可以是任何消息,在这种情况下,它是游戏中的一个已编码帐户。该命令返回将向其传递消息的客户端的数量。在订户客户端中,我们立即看到以下消息:

1) "message"
2) "sports"
3) "oilers/7:leafs/1"

响应包含三个元素:这是一条消息的指示,订阅频道,实际上是一条消息。客户端在收到后立即返回到收听频道。

返回发布者,我们可能会发布另一条消息:

> PUBLISH weather snow/-4c
(integer) 1

在订户中,我们将看到相同的格式,但消息的通道不同:

1) "message"
2) "weather"
3) "snow/-4c"

让我们向没有订阅者的频道发布消息:

> PUBLISH currency CADUSD/0.787
(integer) 0

由于没有人收听货币通道,因此答案将为0。此消息已发送,以后订阅此通道的客户将不会收到有关此消息的通知-该消息已被发送并被忘记。

除了订阅单个频道外,Redis还允许通过掩码订阅频道。全局样式的掩码传递给PSUBSCRIBE命令:

> PSUBSCRIBE sports:*

客户将从体育开始从所有渠道接收消息在另一个客户端中,调用以下命令:

> PUBLISH sports:hockey oilers/7:leafs/1
(integer) 1
> PUBLISH sports:basketball raptors/33:pacers/7
(integer) 1
> PUBLISH weather:edmonton snow/-4c
(integer) 0

请注意,前两支球队返回1,而最后两支球队返回0。尽管我们没有直接订阅“ 体育:曲棍球”或“ 体育:篮球”,但客户会通过掩码进行订阅来接收消息。在客户-订户窗口中,我们可以看到只有匹配掩码的通道才有结果。

1) "pmessage"
2) "sports:*"
3) "sports:hockey"
4) "oilers/7:leafs/1"
1) "pmessage"
2) "sports:*"
3) "sports:basketball"
4) "raptors/33:pacers/7"

此输出与SUBSCRIBE命令的输出略有不同,因为它包含掩码本身以及通道的真实名称。

分布式事件


Redis的发布/订阅消息传递方案可以扩展以创建有趣的分布式事件。假设我们有一个存储在哈希表中的结构,但是我们只想在单个字段超过订户设置的数值时更新客户端。我们将通过遮罩收听频道并提取status中的哈希值在此示例中,我们对具有值5-9的update_status感兴趣

> PSUBSCRIBE update_status:[5-9]
1) "psubscribe"
2) "update_status:[5-9]"
3) (integer) 1
...

要更改status / error_level值,我们需要两个可以依次执行或在MULTI / EXEC块中执行的命令。第一个命令设置级别,第二个命令发布一个通知,该通知具有在通道本身中编码的值。

> HSET status error_level 5
(integer) 1
> PUBLISH update_status:5 0
(integer) 1

在第一个窗口中,我们看到已收到该消息,然后您可以切换到另一个客户端并调用HGETALL命令:

...
1) "pmessage"
2) "update_status:[5-9]"
3) "update_status:5"
4) "0"

> HGETALL status
1) "error_level"
2) "5"

我们还可以使用此方法来更新某些冗长过程的局部变量。这可以允许同一进程的多个实例实时交换数据。

为什么这种模式比使用Pub / Sub更好?进程重新启动时,它可以获取整个状态并开始侦听。更改将在任意数量的进程之间同步。

数据存储方式


在Redis中有几种​​存储结构化数据的模式。在本章中,我们将考虑以下内容:

  • JSON中的数据存储;
  • 储存设施。

JSON数据存储


在Redis中有几种​​存储JSON数据的选项。最常见的形式是预先序列化对象并将其保存在特殊键下:

> SET car "{\"colour\":\"blue\",\"make\":\"saab\",\"model\":93,\"features\":[\"powerlocks\",\"moonroof\"]}"
OK
> GET car
"{\"colour\":\"blue\",\"make\":\"saab\",\"model\":93,\"features\":[\"powerlocks\",\"moonroof\"]}"

它看起来似乎很简单,但是它有一些非常严重的缺点:

  • 序列化需要客户端计算资源来读写。
  • JSON格式会增加数据大小;
  • Redis只有间接的方式来处理JSON中的数据。

前两点在少量数据上可以忽略不计,但是成本会随着数据的增长而增加。但是,第三点是最关键的。

在Redis 4.0之前,在Redis中使用JSON的唯一方法是在cjson模块中使用Lua脚本。尽管仍然存在瓶颈,并且在学习Lua方面又增加了麻烦,但这部分解决了问题。此外,许多应用程序只是接收了整个JSON字符串,将其反序列化,使用数据,进行序列化,然后再次保存。这是一种反模式。以这种方式丢失数据的风险很大。

应用程序实例1应用程序实例2
1个
> GET my-car
2[反序列化,更改机器颜色,然后再次序列化]
> GET my-car
3
> SET my-car

[实例1的新值]
[反序列化,更改机器型号,然后再次序列化]
4
> SET my-car

[实例2的新值]
5
> GET my-car

第5行的结果将仅显示实例2的更改,实例1的颜色更改将丢失。

Redis 4.0及更高版本具有使用模块的能力。ReJSON是一个模块,提供了一种特殊的数据类型和用于与其直接交互的命令。ReJSON以二进制格式保存数据,从而减小了存储数据的大小,提供了对元素的更快访问,而无需花费时间进行反序列化。

要使用ReJSON,您需要将其安装在Redis服务器上或在Redis Enterprise中启用它。

前面使用ReJSON的示例如下所示:

应用程序实例1应用程序实例2
1个
> JSON.SET car2 . '{"colour": "blue",  "make":"saab", "model":93,  "features": ["powerlocks",  "moonroof"]}‘
OK
2
> JSON.SET car2 colour '"red"'
OK
3
> JSON.SET car2 model '95'
OK
> JSON.GET car2 .
"{\"colour\":\"red",\"make\":\"saab\",\"model\":95,\"features\":[\"powerlocks\",\"moonroof\"]}"

ReJSON提供了一种更安全,更快速,更直观的方式来在Redis中使用JSON数据,尤其是在需要对嵌套元素进行原子更改的情况下。

对象存储


乍一看,标准Redis“哈希表”数据类型似乎与JSON对象或其他类型非常相似。使字段成为字符串或数字并防止嵌套结构要容易得多。但是,在计算了每个字段的“路径”之后,您可以“展平”对象并将其保存在Redis哈希表中。

{
    "colour": "blue",
    "make": "saab",
    "model": {
        "trim": "aero",
        "name": 93
    },
    "features": ["powerlocks", "moonroof"]
}

使用JSONPath(JSON的XPath),我们可以在哈希表的同一层上表示每个元素:

> HSET car3 colour blue
> HSET car3 make saab
> HSET car3 model.trim aero
> HSET car3 model.name 93
> HSET car3 features[0] powerlocks
> HSET car3 features[1] moonroof

为了清楚起见,这些命令是单独列出的,但是许多参数可以传递给HSET。

现在,您可以请求整个对象或其单个字段:

> HGETALL car3
 1) "colour"
 2) "blue"
 3) "make"
 4) "saab"
 5) "model.trim"
 6) "aero"
 7) "model.name"
 8) "93"
 9) "features[0]"
10) "powerlocks"
11) "features[1]"
12) "moonroof"

> HGET car3 model.trim
"aero"

尽管这提供了一种快速有效的方法来检索Redis中的存储对象,但它也有缺点:

  • 在不同的语言和库中,JSONPath的实现可能会不同,从而导致不兼容。在这种情况下,值得使用一种工具对数据进行序列化和反序列化。
  • 阵列支持:
    • 稀疏数组可能有问题;
    • 无法执行许多操作,例如在数组中间插入元素。

  • JSONPath密钥中不必要的资源消耗。

此模式与ReJSON几乎相同。如果ReJSON可用,则在大多数情况下最好使用它。但是,以上述方式存储对象比ReJSON具有一个优势:与Redis SORT团队集成。但是,此命令在计算上很复杂,并且是此模式范围之外的单独复杂主题。

下一个结论部分将介绍时间序列模式,限速模式,布隆过滤器模式,计数器以及Redis中Lua的使用。

PS:我试图将这些文章的文字尽可能以“野蛮的”英语改成俄语,但是,如果您认为这个主意难以理解或不正确,请在评论中纠正我。

Source: https://habr.com/ru/post/undefined/


All Articles