Redis6客户端缓存的相关设计

Posted by LB on Mon, Mar 16, 2020

这篇文章翻译自Redis官方博客,这篇文章阐述了Redis6中将如何支持客户端缓存功能。

纽约Redis一天结束了,我于5:30在酒店起床,仍然与意大利时区保持同步,并立即走在曼哈顿的街道上,完全爱上了风景和美好的生活感觉。 但是我在Redis 6发行版中的感觉是,可能是最重要的功能,即新版本的Redis协议(RESP3)的采用曲线将非常缓慢,这是有充分理由的: 明智的人会在没有充分理由的情况下避免使用工具。 毕竟我为什么要这么严重地改进协议?主要有两个原因,即为客户提供更多的语义答复,并开放使用旧协议难以实现的新功能。 对我来说,最重要的功能之一就是客户端缓存

让我们回到一年前。我来到旧金山的Redis Conf 2018,当时我坚信客户端缓存是Redis未来最重要的事情。 如果我们需要快速存储和高速缓存,那么我们需要在客户端中存储信息的子集。这是对延迟较小且规模较大的数据提供服务的想法的自然扩展。事实上,几乎所有的大公司都已经这样做了,因为这是唯一的生存之道。然而,Redis无法在此过程中协助客户。 一个幸运的巧合希望Ben Malec在Redis Conf上确切地谈论客户端缓存[1],仅使用Redis提供的工具和许多非常聪明的想法。

[1] https://www.youtube.com/watch?v=kliQLwSikO4

本采取的方法确实打开了我的想象。 Ben为了使他的设计工作而使用了两个关键思想。首先是使用Redis Cluster的“哈希槽”概念,以将key分为16k组。这样,客户端将无需跟踪每个key的有效性,但可以将单个元数据条目用于一组key。Ben使用Pub / Sub来更改键时发送通知,因此他需要应用程序各个部分的帮助,但是该架构非常可靠。 修改key?同时发布一条使它无效的消息。 在客户端,您是否在缓存key?记住缓存每个key的时间戳,并且在接收到无效消息时,还要记住每个插槽的无效时间。 当使用给定的缓存key时,通过检查缓存的key是否具有比该key所属的插槽接收到的失效时间戳更旧的时间戳,来进行懒惰驱逐:在这种情况下,该key是陈旧数据, 必须再次询问服务器。

看完演讲之后,我意识到这是在服务器内部使用的好主意,以便允许Redis为客户端完成部分工作,并让客户端缓存更简单、更有效,所以我回家后,写了一个文档描述设计[2]。

[2] https://groups.google.com/d/msg/redis-db/xfcnYkbutDw/kTwCozpBBwAJ

但是,要使我的设计正常工作,我必须专注于将Redis协议切换到更好的协议,因此我开始编写规范,然后编写RESP3的代码,以及其他Redis 6之类的东西,例如ACL等,并且客户端缓存加入了 由于缺乏时间,我以某种方式放弃了Redis的许多构想的巨大空间。

但是我还是在纽约街头思考这个想法。 后来和会议的朋友一起去吃午餐和喝咖啡休息时间。 当我回到酒店房间时,剩下的整个晚上都是在飞机起飞前的第二天,所以我开始遵循我一年前写给小组的建议,开始编写Redis 6客户端缓存的实现。 看起来仍然很棒。

Redis服务器辅助的客户端缓存,最终称为跟踪(但我可能会改变想法),是一个非常简单的功能,由几个关键的想法组成。

key空间被划分为“缓存槽”,但它们比Ben使用的哈希槽大得多。 我们使用CRC64输出的24位,因此有超过1600万个不同的插槽。为什么这么多?因为我认为您希望有一个拥有1亿key的服务器,而一条无效消息应该只影响客户端缓存中的几个key。Redis中无效表的内存开销是130mb:一个8字节的数组,指向16M个条目。这对我来说是可以的,如果你想要这个功能,你就要充分利用你在客户端的所有内存,所以使用130MB的服务器端是可以的;您所赢得的是一个更细粒度的失效。

客户端通过简单的命令以opt方式启用该特性:

1    CLIENT TRACKING on

服务器会回复旧的+ OK,从那一刻开始,命令表中标记为“只读”的每个命令不仅会把键返回给调用者,而且还会产生副作用 客户端到目前为止请求的所有键的缓存插槽(但只有使用只读命令的键才是,这是服务器与客户端之间的协议)。Redis存储此信息的方法很简单。每个Redis客户端都有一个唯一的ID,因此,如果客户端ID 123执行有关将key散列到插槽1、2和5的MGET,我们将获得带有以下条目的无效表:

11 -> [123]
22 -> [123]
35 -> [123]

但是稍后客户端ID 444也会询问插槽5中的key,因此该表将如下所示:

15 -> [123, 444]

现在,其他一些客户端更改了插槽5中的某些key。发生的事情是Redis将检查Invalidation Table,以发现客户端123和444都可能在该插槽上缓存了key。我们将向这两个客户端发送无效消息,因此他们可以自由地以任何形式处理该消息:要么记住上一次插槽无效的时间戳记,然后以懒惰的方式检查时间戳记(或者 如果您更喜欢此渐进式“时期”:它比较安全),然后根据比较结果将其逐出。否则,客户端可以通过获取其在此特定插槽中缓存的内容的表来直接直接回收对象。这种具有24位哈希函数的方法不是问题,因为即使缓存了数千万个key,我们也不会有很长的列表。发送无效消息后,我们可以从无效表中删除条目,这样,我们将不再向这些客户端发送无效消息,直到它们不再读取该插槽的key为止。

请注意,客户端不必真正使用hash函数的所有24位。例如,他们可能只使用20位,然后也会转移Redis发送给他们的无效消息槽。不确定这样做是否有很多好的理由,但在内存受限的系统中可能是一个想法。

如果您严格按照我所说的进行操作,您会认为相同的连接同时接收到正常的客户端响应和无效消息。对于RESP3,这是可能的,因为无效消息是作为“推送”消息类型发送的。 但是,如果客户端是阻塞客户端,而不是事件驱动的客户端,则这将变得很复杂:应用程序需要某种方式不时读取新数据,并且看起来复杂而脆弱。 在这种情况下,最好使用另一个应用程序线程和另一个客户端连接,以便接收无效消息。 因此,您可以执行以下操作:

1    CLIENT TRACKING on REDIRECT 1234

基本上,我们可以说通过当前连接获得的所有key,我们希望将无效消息发送给客户端1234。例如,在连接池的情况下,多个客户端可能会要求将无效消息重定向到单个客户端。 您需要做的就是创建此特殊连接以接收无效消息,调用CLIENT ID知道此客户端连接具有哪个ID,然后启用跟踪。

还有一个问题:如果我们失去了与服务器的连接,会发生什么呢? 我们可能会遇到麻烦,因为无效消息将不再被接收。 通常,应用程序会检测到链接断开,并重新连接,刷新当前缓存(或采用更多软分辨率,例如将所有时隙的所有时间戳记在未来几秒钟,以便有一些时间在服务时填充缓存 可能过时的数据)。 但是,如果无效线程不时ping通该连接以确保该连接处于活动状态,则可能是一个更好的主意。但是,为了减少陈旧数据的风险,Redis还将开始使用特殊的推式消息通知已将无效消息重定向到其他已断开连接的其他客户端的情况,仅使用特殊的推送消息:在下一个查询中执行 客户会知道的。

我所描述的只是合并到Redis不稳定中。 可能这不是硬道理,但我们距离第一个Redis 6候选发布版本还有几个月的时间,现在有时间更改所有内容:请将您的反馈发送给我。 我也在寻找启用RESP2功能的方法。 仅当启用了重定向时,这才起作用,并且侦听消息的客户端可能应该进入发布/订阅模式,以便我们可以发送某种发布/订阅消息。 这样,可以完全重用旧客户端。

我希望这足以激发您的胃口:如果我们在Redis内很好地执行此操作,然后将其记录下来以使客户作者知道如何提供支持,那么即使在运行应用程序的情况下,数据也可能比以往更接近应用程序 由迄今为止尚未尝试实现客户端缓存的小型团队组成。 对于已经这样做的大型团队和非常大型的应用程序,可以减少开销以及实现的复杂性。

原文链接:http://antirez.com/news/130 [[1]]: https://www.youtube.com/watch?v=kliQLwSikO4 “[1]”