Redis 6.0客户端缓存--服务器辅助缓存

目录

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

客户端缓存的Redis实现

两种连接模式

Tracking跟踪什么

带Opt-in选项的缓存

广播模式

NOLOOP选项

避免竞争条件

与服务器断开连接时执行的操作

要缓存的内容

关于客户端库实现的其他要点

限制Redis内存使用量


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

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

+-------------+                                +----------+

|             | ------- GET user:1234 -------> |          |

| Application |                                | Database |

|             | <---- username = Alice ------- |          |

+-------------+                                +----------+

使用客户端缓存时,应用程序会将热门查询的结果直接存储在应用程序内存中,以便以后可以重用此类查询结果,而无需再次关联数据库。

+-------------+                                +----------+

|             |                                |          |

| Application |       ( No chat needed )       | Database |

|             |                                |          |

+-------------+                                +----------+

| Local cache |

|             |

| user:1234 = |

| username    |

| Alice       |

+-------------+

尽管用于本地缓存的应用程序内存可能不是很大,但与请求诸如数据库这样的网络服务相比,访问本地计算机内存所需的时间要几个数量级。由于一小部分热点数据被频繁访问,因此该模式可以极大地减少应用程序获取数据的延迟,同时减少数据库端的负载。

此外,在许多数据集中的项很少会进行变更。例如,社交网络中的大多数用户帖子要么不能变更,要么很少被用户编辑。再加上通常只有一小部分帖子非常受欢迎,要么因为一小群用户拥有大量关注者,要么因为优先显示最近的帖子这样,为什么这模式会有用就一目了然了

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

  1. 数据可使用并且延迟非常短。
  2. 数据库系统收到的查询更少,从而可以用更少的节点数来提供相同的数据集。

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

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

有时,对于我们要建模的具体应用程序,这个问题可能不是什么大问题,因此客户端只需要使用固定的最大“生存时间”来缓存信息就可以了。一旦经过了设置的时间,该信息将不再有效。使用Redis时,更复杂的方式可以利用发布/订阅系统,向听的客户端发送效消息。从使用带宽的角度来看,这是可行的,不过却很困难开销也会很大,因为这种方式即使其中某些客户端没有效数据的任何副本通常也会向应用程序中的每个客户端发送效消息。此外,更数据的每个应用程序请求都需要使用PUBLISH命令,这会使数据库花费更多的CPU时间来处理该命令

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

客户端缓存的Redis实现

Redis客户端缓存支持称为跟踪(Tracking),它有两种模式:

  • 在默认模式下,服务器会记住某个客户端访问了哪些键,并在相同键被修改时发送效消息。这会消耗服务器端的内存,但仅对客户端可能在内存中拥有的一组发送效消息。
  • 广播模式下,服务器不会尝试记住某个客户端访问了哪些键,因此该模式在服务器端不消耗内存。与之相对应,客户端订阅object:user:这样的前缀并且每次匹配到响应前缀的键接收通知消息。

现在广播模式先放一放先来看一下第一种模式。我们将在后面详细介绍广播。

  1. 客户可以根据需要启用跟踪。连接开始时默认未启用跟踪。
  2. 跟踪启用后,服务器会记住每个客户端在连接期请求的(通过发送这些键的读类型的命令)。
  3. 当某个客户端修改了某个如果由于设置的过期时间将其剔除,或者由于最大内存策略而将其剔除时,所有启用了跟踪且可能已缓存了该键的客户端都会收到失效消息通知。
  4. 当客户端收到失效消息时,需要删除相应的,以避免提供过时的数据。

以下是协议的示例:

  • 客户端1 ->服务器:客户端跟踪启用
  • 客户端1 ->服务器:GET foo
  • (服务器记住客户端1可能已缓存键“ foo”)
  • (客户端1可能会记住其本地内存中的“ foo”值)
  • 客户端2 ->服务器:SET foo SomeOtherValue
  • 服务器->客户端1:INVALIDATE "foo"

乍看之下,这似乎不错不过你可以想象一下,如果1万长期保持连接的客户端都请求上百万个,服务器会由于存储太多信息而宕机。因此,Redis用两个关键思想来用来实现该特性服务器端内存使用量以及处理数据结构的CPU消耗

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

两种连接模式

使用Redis 6支持的新版Redis协议RESP3,可以在同一连接中运行数据查询接收效消息。不过,许多客户端实现可能倾向于使用两个单独的连接来实现客户端缓存:一个用于数据,另一个用于效消息。因此,当客户端启用跟踪时,它可以通过设置不同连接的“客户端ID”指定失消息重定向到另一个连接。许多数据连接可以将效消息重定向到同一连接,这对于实现连接池的客户端很有用。这两个连接模式是仅有的同样适用于RESP2协议(缺乏在同一连接中多路传输不同类型信息的能力)。

下面,我们将举一个例子,先通过Redis的RRESP2协议,完成一个完整的会话,包括以下步骤:启用跟踪重定向到另一个连接,并发起键的请求以及在修改键后获取失效信息

首先,客户端打开第一个用于失效的连接,请求连接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规范)。

Tracking跟踪什么

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

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

请注意,无论如何,如果服务器上有写流量,缓存插槽将在写的这段时间内失效。通常,当服务器认为我们获取数据会缓存时,我们要进行权衡:

  1. 客户端倾向于使用欢迎新对象的策略(先进先出)来缓存信息会更有效率
  2. 服务器将被迫保留更多客户端的数据。
  3. 客户端将收到其未缓存的对象的无用的失效消息。

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

带Opt-in选项的缓存

(注:这部分开展中,尚未在Redis中实现)

客户端实现可能只想缓存某些特定的,并它们将缓存什么而不缓存什么显式地与服务器进行通信:会在缓存新对象时,需要更多的带宽,不过同时会减少服务器需要记录的数据量,也会减少客户端收到的效消息量。

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

CLIENT TRACKING on REDIRECT 1234 OPTIN

在这种模式下,默认情况下,读请求中涉及的键会被缓存,而是当客户端要缓存某些内容时,在实际检索数据命令之前发送一个特命令:

CACHING

+OK

GET foo

"bar"

为了充分发挥协议的效率可以CACHING命令带上NOREPLY选项 :在这种情况下,它将完全静默

CACHING NOREPLY

GET foo

"bar"

    CACHING命令的作用范围是紧随其后执行的命令,但是如果下一命令是复合命令(MULTI),那么将跟踪事务中的所有命令。Lua脚本也类似,脚本执行的所有命令都会被跟踪

广播模式

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

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

NOLOOP选项

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

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

避免竞争条件

在实施客户端缓存效消息重定向到其他连接时,应注意可能存在竞争况。请参见以下交互示例其中的数据连接“ 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. 确保如果连接丢失清空本地缓存。
  2. 将RESP2与Pub/Sub组合使用或者使用RESP3都会定期对失效通道进行ping操作(即使连接处于Pub / Sub模式,也可以发送PING命令!)。如果连接看起来断开并且在设置的最长连接超时时间我们仍没有收到ping操作的响应就会关闭连接并清空缓存。

缓存的内容

客户可能希望运行关于所缓存的键在实际请求中提供服务的次数的内部统计信息,以便将来更好地对数据进行缓存。一般来说:

  • 我们不想缓存那些不断变化的
  • 我们不想缓存那些很少被请求的
  • 我们希望缓存经常请求并以合理的率更改关于键没有以合理的频率来更改的示例,请参考不断递增的全局计数器。

不过,更简单的客户端可能只是通过一些随机采样来剔除数据,记住上一次提供服务的缓存值,来剔除最近未用到那些键。

客户端库实的其他要点

  • 处理TTL:如果要缓存带TTL的,请确保同时获取键的TTL并在本地缓存中设置TTL。
  • 即使没有TTL,在每个键中都设置一个最大TTL是个好主意。这是一种很好的保护措施,可预防可能导致客户端在本地副本中包含旧数据的错误或连接问题。
  • 一定要限制客户端使用的内存量。添加新时,一定要有一种方法可以将旧键剔除

限制Redis内存使用

只需确保为Redis需要保存键的最大数设置一个合适的值,或者使用BCAST模式,该模式在Redis端不占用内存。注意,当不使用BCAST时,Redis消耗的内存与跟踪的数以及请求这些键的客户端数成正比。


本文翻译自Redis官网https://redis.io/topics/client-side-caching,水平所限,翻译难免有所不足,欢迎大家一起探讨。如需转载,请注明出处。

你可能感兴趣的:(Redis)