Redis【有与无】【UR6】Redis server-assisted 客户端缓存

本文基于Redis 6.0.9版本,前提至少 Redis 3.0或更高版本。

目录

1.Redis server-assisted 客户端缓存

1.1.在计算机科学中只有两个大问题

1.2.Redis的实现客户端缓存

1.3.两种连接方式

1.4.什么是跟踪轨道

1.5.Opt-in缓存

1.6.广播模式

1.7.NOLOOP选项

1.8.避免竞争条件

1.9.与服务器断开连接时该怎么办

1.10.缓存什么

1.11.有关客户端库实施的其他提示

1.12.限制Redis使用的内存量


1.Redis server-assisted 客户端缓存

客户端缓存是一种用于创建高性能服务的技术。 它利用应用程序服务器中的可用内存(通常是与数据库节点相比是不同的计算机),以便将数据库信息的某些子集直接存储在应用程序端。

通常,当需要一些数据时,应用程序服务器将向数据库询问此类信息,如下图所示:

+-------------+                                +----------+
|             | ------- GET user:1234 -------> |          |
| Application |                                | Database |
|             | <---- username = Alice ------- |          |
+-------------+                                +----------+

使用客户端缓存时,应用程序将直接将流行查询的回复存储在应用程序内存中,以便以后可以重用此类回复,而无需再次联系数据库。

+-------------+                                +----------+
|             |                                |          |
| Application |       ( No chat needed )       | Database |
|             |                                |          |
+-------------+                                +----------+
| Local cache |
|             |
| user:1234 = |
| username    |
| Alice       |
+-------------+

尽管用于本地缓存的应用程序内存可能不会很大,但是与请求诸如数据库之类的网络服务相比,访问本地计算机内存所需的时间要小几个数量级。

由于经常非常频繁地访问相同百分比的数据,因此该模式可以极大地减少应用程序获取数据的延迟,同时减少数据库侧的负载。

此外,在许多数据集中,项目很少更改。 例如,社交网络中的大多数用户帖子都是不可变的,或者很少由用户编辑。 再加上通常一小部分帖子非常受欢迎的事实,要么是因为一小群用户拥有大量 and/or 关注者, 因为最近的帖子具有更高的知名度,那么很明显,为什么这样的模式会非常受欢迎有用。

通常,客户端缓存的两个主要优点是:

  • 可用的数据延迟非常短。
  • 数据库系统接收的查询较少,从而可以使用更少的节点来提供相同的数据集。

1.1.在计算机科学中只有两个大问题

上述模式的问题是如何使应用程序持有的信息无效,以避免向用户显示陈旧数据。例如,上述应用程序在本地缓存了user:1234信息后,Alice可以将其用户名更新为Flora。但是应用程序可能会继续为用户1234提供旧的用户名。

有时,根据我们正在建模的确切应用程序,这个问题并不重要,因此客户端将只使用固定的最大“生存时间”来缓存信息。一旦经过给定的时间量,该信息将不再被视为有效。使用Redis时,更复杂的模式会利用发布/订阅系统,以便向侦听的客户端发送无效消息。从使用的带宽的角度来看,这是可行的,但却是棘手且昂贵的,因为这种模式通常涉及向应用程序中的每个客户端发送无效消息,即使某些客户端可能没有无效数据的任何副本。此外,每个更改数据的应用程序查询都需要使用PUBLISH命令,从而使数据库花费更多的CPU时间来处理此命令。

无论使用哪种模式,都有一个简单的事实:许多大型应用程序都实现某种形式的客户端缓存,因为这是拥有快速存储或快速缓存服务器的下一个逻辑步骤。 因此,Redis 6实现了对客户端缓存的直接支持,以使此模式更易于实现,更易于访问,可靠且高效。

1.2.Redis的实现客户端缓存

Redis客户端缓存支持称为跟踪,并具有两种模式:

  • 在默认模式下,服务器会记住给定客户端访问了哪些键,并在修改了相同的键时发送无效消息。 这会在服务器端消耗内存,但是仅针对客户端可能在内存中包含的一组键发送无效消息。
  • 相反,在广播模式下,服务器不会尝试记住给定客户端访问了哪些键,因此该模式在服务器端根本不会花费任何内存。 相反,客户端订阅键前缀(例如object:或user :),并且每次触摸与该前缀匹配的键都会收到通知消息。

回顾一下,现在让我们暂时忘记广播模式,重点关注第一种模式。 我们将在后面详细介绍广播。

  • 客户可以根据需要启用跟踪。 连接开始时未启用跟踪。
  • 启用跟踪后,服务器会记住每个客户端在连接生存期内请求的键(通过发送有关此类键的读取命令)。
  • 当某个键修改了某个键,或者由于键具有关联的到期时间而将其驱逐,或者由于最大内存策略而将其驱逐时,将向所有启用了跟踪且可能已缓存了键的客户端发出无效消息。
  • 当客户端收到无效消息时,要求它们删除相应的键,以避免提供过时的数据。

这是协议的示例:

  • Client 1 -> Server: CLIENT TRACKING ON
  • Client 1 -> Server: GET foo
  • 服务器记住Client 1 可能已缓存了键"foo"
  • Client 1 可能会记住其本地内存中的"foo"值
  • Client 2 -> Server: SET foo SomeOtherValue
  • Server -> Client 1: INVALIDATE "foo"

从表面上看,这看起来很棒,但是如果您认为10k个已连接的客户端在每个长期连接的场景中都要求数百万个密钥,则服务器最终将存储过多的信息。 因此,Redis使用两个关键思想来限制服务器端使用的内存量以及处理实现该功能的数据结构的CPU成本:

  • 服务器会记住可能已在单个全局表中缓存给定键的客户端列表。 该表称为无效表。 这种无效表可以包含最大数量的条目,如果插入了新键,则服务器可以通过假装已修改(即使没有修改)该键并将其发送到客户端来驱逐旧条目。 这样做,它可以回收用于此键的内存,即使这将迫使拥有该键本地副本的客户端将其逐出。
  • 在失效表内部,我们实际上不需要存储指向客户端结构的指针,这将在客户端断开连接时强制执行垃圾回收过程:相反,我们要做的只是存储客户端ID(每个Redis客户端都有唯一的数字ID)。 如果客户端断开连接,则由于缓存插槽无效,将逐步收集垃圾信息。
  • 只有一个键名称空间,不除以数据库编号。 因此,如果客户端在数据库2中缓存密钥foo,而其他客户端在数据库3中更改了密钥foo的值,则仍然会发送无效消息。 这样,我们可以忽略数据库编号,从而减少了内存使用量和实现复杂性。

1.3.两种连接方式

使用Redis 6支持的新版本的Redis协议RESP3,可以在同一连接中运行数据查询并接收无效消息。 但是,许多客户端实现可能更喜欢使用两个独立的连接来实现客户端缓存:一个用于数据,另一个用于无效消息。 因此,当客户端启用跟踪时,它可以通过指定不同连接的“客户端ID”来指定将无效消息重定向到另一个连接。 许多数据连接可以将无效消息重定向到同一连接,这对于实现连接池的客户端很有用。 两个连接模型是RESP2唯一支持的模型(缺乏在同一连接中复用不同类型信息的能力)。

这次,我们将通过在旧的RRESP2模式下使用实际的Redis协议展示一个示例,如何完成一个完整的会话,包括以下步骤:启用跟踪重定向到另一个连接,请求键以及在收到键被修改的消息后获得无效消息 。

首先,客户端打开第一个用于失效的连接,请求连接ID,并通过Pub/Sub订阅专用通道,该通道在RESP2模式下用于获得失效消息(请记住,RESP2是通常的Redis。 协议,而不是您可以使用的更高级的协议(可选,使用HELLO命令与Redis 6一起使用):

Connection 1 -- used for invalidations)
CLIENT ID
:4
SUBSCRIBE __redis__:invalidate
*3
$9
subscribe
$20
__redis__:invalidate
:1

现在,我们可以从数据连接中启用跟踪:

(Connection 2 -- data connection)
CLIENT TRACKING on REDIRECT 4
+OK

GET foo
$3
bar

客户端可以决定在本地内存中缓存“foo” =>“ bar”。

现在,另一个客户端将修改“foo”键的值:

(Some other unrelated connection)
SET foo bar
+OK

结果,失效连接将收到一条消息,该消息使指定的键失效。

(Connection 1 -- used for invalidations)
*3
$7
message
$20
__redis__:invalidate
*1
$3
foo

客户端将检查在此缓存槽中是否有缓存的键,并将驱逐不再有效的信息。

请注意,Pub / Sub消息的第三个元素不是单个键,而是只有一个元素的Redis数组。 因为我们发送一个数组,所以如果有成组的键使它们失效,我们可以在一条消息中做到这一点。

关于与RESP2一起使用的客户端缓存以及为了读取无效消息而进行的Pub / Sub连接的理解,非常重要的一点是,为了重用旧的客户端,使用Pub / Sub完全是一个技巧。

实现,但是实际上消息并没有真正发送到某个频道,也没有被所有订阅该频道的客户端接收到。 只有我们在CLIENT命令的REDIRECT参数中指定的连接才会实际收到Pub / Sub消息,从而使此功能具有更大的可伸缩性。

如果改用RESP3,则将无效消息作为推送消息发送(在同一连接中,或者在使用重定向时在辅助连接中)(请参阅RESP3规范以获取更多信息)。

1.4.什么是跟踪轨道

如您所见,默认情况下,客户端不需要告诉服务器它们正在缓存哪些键。 服务器会跟踪只读命令上下文中提到的每个键,因为它可能会被缓存。

这具有明显的优点,即不需要客户端告诉服务器它正在缓存什么。 此外,在许多客户端实现中,这就是您想要的,因为一个好的解决方案可能是使用先进先出的方法仅缓存尚未缓存的所有内容:我们可能希望缓存固定数量的对象,每个对象 我们检索到新数据后,可以对其进行缓存,丢弃最早的缓存对象。 更高级的实现可能会删除最不常用的对象或类似对象。

请注意,无论如何,如果服务器上有写流量,则缓存插槽将在一段时间内失效。 通常,当服务器假设我们得到的东西也缓存时,我们就要进行权衡:

  1. 当客户端倾向于使用欢迎新对象的策略来缓存许多内容时,这样做会更有效率。
  2. 服务器将被迫保留有关客户端密钥的更多数据。
  3. 客户端将收到有关其未缓存的对象的无效消息。

因此,下一节将介绍另一种方法。

1.5.Opt-in缓存

客户端实现可能只想缓存选定的键,并明确地与服务器通信它们将缓存的内容和不缓存的内容:缓存新对象时,这将需要更多的带宽,但同时会减少服务器不得不记住的的数据量 ,以及客户端收到的无效消息数量。

为此,必须使用OPTIN选项启用跟踪:

CLIENT TRACKING on REDIRECT 1234 OPTIN

在这种模式下,默认情况下,不应缓存读取查询中提到的键,而是当客户端要缓存某些内容时,它必须在实际命令检索数据之前立即发送一个特殊命令:

CLIENT CACHING YES
+OK
GET foo
"bar"

CACHING命令会影响紧随其后执行的命令,但是,如果下一个命令是MULTI,则将跟踪事务中的所有命令。 同样,对于Lua脚本,将跟踪该脚本执行的所有命令。

1.6.广播模式

到目前为止,我们描述了Redis实现的第一个客户端缓存模型。 还有一个称为广播的广播,它从另一个折衷的角度看问题,它不消耗服务器端的任何内存,而是向客户端发送更多的无效消息。 在这种模式下,我们有以下主要行为:

  • 客户端使用BCAST选项启用客户端缓存,并使用PREFIX选项指定一个或多个前缀。 例如:CLIENT TRACKING on REDIRECT 10 BCAST PREFIX object: PREFIX user: 。 如果根本没有指定前缀,则假定该前缀为空字符串,因此客户端将为每个被修改的键接收无效消息。 相反,如果使用一个或多个前缀,则仅在失效消息中发送与指定前缀之一匹配的键。
  • 服务器未在失效表中存储任何内容。 相反,它仅使用不同的前缀表,其中每个前缀都与客户端列表相关联。
  • 每次修改与任何前缀匹配的键时,所有订阅该前缀的客户端都将收到无效消息。
  • 服务器将消耗CPU与注册前缀数量成正比。 如果只有几个,几乎看不到任何区别。 使用大量前缀,CPU成本可能变得非常高。
  • 在这种模式下,服务器可以优化为订阅给定前缀的所有客户端创建单个回复的过程,并将相同的回复发送给所有客户端。 这有助于降低CPU使用率。

1.7.NOLOOP选项

默认情况下,客户端跟踪甚至会向修改键的客户端发送无效消息。 有时客户端会希望这样做,因为它们实现了非常基本的逻辑,该逻辑不涉及在本地自动缓存写操作。 但是,更高级的客户端甚至可能希望将其正在执行的写入缓存在本地内存表中。 在这种情况下,在写操作之后立即接收到无效消息是一个问题,因为这将迫使客户端退出其刚刚缓存的值。

在这种情况下,可以使用NOLOOP选项:它在正常模式和广播模式下均可工作。 使用此选项,客户端可以告诉服务器他们不想接收由自己修改的键的无效消息。

1.8.避免竞争条件

在实施客户端缓存以将无效消息重定向到其他连接时,您应该意识到存在竞争状况。 请参见下面的交互示例,我们将数据连接称为“D”,并将失效连接称为“I”:

[D] client -> server: GET foo
[I] server -> client: Invalidate foo (somebody else touched it)
[D] server -> client: "bar" (the reply of "GET foo")

如您所见,由于对GET的回复较慢,无法到达客户端,因此在已经不再有效的实际数据之前,我们收到了无效消息。 因此,我们将继续提供陈旧版本的foo键。 为避免此问题,当我们使用占位符发送命令时,填充缓存是一个好主意:

Client cache: set the local copy of "foo" to "caching-in-progress"
[D] client-> server: GET foo.
[I] server -> client: Invalidate foo (somebody else touched it)
Client cache: delete "foo" from the local cache.
[D] server -> client: "bar" (the reply of "GET foo")
Client cache: don't set "bar" since the entry for "foo" is missing.

当对数据和无效消息使用单个连接时,这种竞争条件是不可能的,因为在这种情况下消息的顺序始终是已知的。

1.9.与服务器断开连接时该怎么办

同样,如果丢失了与用于获取无效消息的套接字的连接,则可能以陈旧数据结尾。 为了避免此问题,我们需要做以下事情:

  1. 确保如果连接丢失,则刷新本地缓存。
  2. 将RESP2与Pub/Sub一起使用,或者将RESP3与RESP3一起使用时,都定期ping无效通道(即使连接处于Pub / Sub模式,您也可以发送PING命令!)。 如果连接看起来断开了,我们无法收到ping回复,则在经过最长时间后,请关闭连接并刷新缓存。

1.10.缓存什么

客户可能希望运行有关给定缓存键实际在请求中服务的次数的内部统计信息,以了解将来对缓存有什么好处。 一般来说:

  • 我们不想缓存许多不断变化的键。
  • 我们不想缓存很多很少被请求的键。
  • 我们希望缓存经常需要的键并以合理的速率进行更改。 有关键没有以合理的速度更改的示例,请考虑一个连续递增的全局计数器。

但是,更简单的客户端可能只是使用一些随机采样来驱出数据,只是记住上一次提供给定的缓存值,从而试图驱逐最近未提供的键。

1.11.有关客户端库实施的其他提示

  • 处理TTL:如果要支持带TTL的缓存键,请确保还请求键TTL并在本地缓存中设置TTL。
  • 即使没有TTL,在每个键中都放置一个最大TTL是个好主意。 这是很好的保护措施,可避免可能导致客户端在本地副本中包含旧数据的错误或连接问题。
  • 绝对需要限制客户端使用的内存量。 添加新键时,必须有一种方法可以将旧键逐出。

1.12.限制Redis使用的内存量

只需确保为Redis可以记住的最大键数配置一个合适的值,或者使用BCAST模式,该模式在Redis端根本不占用任何内存。 请注意,当不使用BCAST时,Redis消耗的内存与跟踪的键数量以及请求此类键的客户端数量成正比。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

琴 韵

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值