我们现在就是Redis

了解更多

缓存、承诺和锁



回到博客

Instagram最近在他们的工程博客上发布了一篇文章,介绍了缓存值的概念。其思想是,在缓存未命中时,获取丢失的值需要一段时间,这可能会导致缓存应该保护的底层DBMS发生踩踏。踩踏包括多个并行请求,在缓存未命中时,这些请求会触发同一工作的多个实例来填充缓存(见下文)

Instagram的Nick Cooper在其帖子中展示了在缓存中存储一个虚拟值(即承诺)的想法,以向竞争对手的请求者发出信号,表示其他人正在准备数据,以便他们知道等待,而不是将DBMS击死。这是文章我指的是,它也收到了一些评论在黑客新闻。

这个想法并不新鲜(这是通过读/写缓存现成的特性之一,因为它们透明地处理数据库),但我认为它非常好,值得讨论。在这篇文章中,我将分享这种方法的一个简单实现,以及您如何从中获得比通常使用(r/w)-通过缓存提供的更多好处。

你好,Redis

在深入我的实现工作的细节之前,这里是一个概述,在将这个用例映射到Redis操作之后,我能够实现什么。我的实现:

  1. 跨不同语言工作。客户机只需要一个Redis客户机库和所用密钥命名方案的知识。然后,它可以与任何其他客户生成/解决承诺,跨越流程和网络边界
  2. 在集群部署中有效扩展。Redis可以独特地提供这种好处,而无需轮询或其他浪费模式。
  3. 有效地平衡了避免无用工作和保持可伸缩性之间的权衡。作为分布式系统的一部分,更强有力的保障需要更多的协调。

我的实现主要依赖于三个Redis特性:密钥过期(TTL)、原子操作(setnx)和Pub/Sub在以前的职位上:

共享状态导致协调,反之亦然。

Redis非常理解这一点,这就是为什么使用它可以很容易地构建这种抽象。在这种情况下,Pub/Sub消息传递功能帮助我们将一个锁定机制绑定在一起,从而轻松创建一个真正的跨网络承诺系统。

介绍记忆锁

我将此模式的实现称为Redis MemoLock。在高层次上,它的工作原理如下:

  1. 作为服务实例,当我们需要获取喷火,我们在Redis中寻找它。如果它在那里,我们就完了。
  2. 如果密钥不存在,我们尝试创建一个锁:福使用SETNX键。NX选项确保如果存在并发请求,则只有一个能够成功设置密钥(该值不重要)。
  3. 如果我们能拿到锁,我们的工作就是去取回价值。一旦我们完成了,我们将它保存到Redis,并在一个Pub/Sub频道上发送消息诺蒂夫:福通知所有其他可能等待的客户端该值现在可用。
  4. 如果我们能够获得锁,我们只需订阅的Pub/Sub频道调用诺蒂夫:福并等待成功执行此操作的实例通知我们该值现在可用(如前一步所述)。

实际上,该算法稍微复杂一些,因此我们可以获得正确的并发性并处理超时(在分布式环境中,无法过期的锁/承诺几乎毫无用处)。我们的命名方案也稍微复杂一些,因为我选择为每个资源提供一个名称空间,以便允许多个独立的服务使用同一个集群,而不冒密钥名冲突的风险。除此之外,解决这个问题并不需要太多的复杂性。

的代码

你可以在GitHub上找到我的代码。我已经发布了一个Go实现,很快我将结合我即将在NDC奥斯陆的演讲(未来可能会有更多的语言)。下面是Go版本的一个代码示例:

如果您在5秒内运行此程序的两个实例,“Cache miss!”将只显示一次,而不管您的第一次执行是否已结束(即值已缓存)或是否仍在计算(在本示例代码中,是休眠而不是执行有用的工作)。

为什么这比开箱即用的解决方案更好?

两大原因:

  1. MemoLock不仅可以保护DBMS,还可以保护任何一种生成成本较高的资源。想想PDF生成(你可以将文件存储在CDN中,并使用MemoLock将链接保存在Redis中),运行人工智能模型,或者任何你可能拥有的昂贵的在线计算。
  2. 即使我们想限制自己只使用queryset缓存,CQRS告诉我们,在DBMS中存储数据的方式不一定是对给定查询最有用的格式。您不希望为每个请求重新转换数据,执行转换的代码应该是服务的一部分,而不是存储过程的一部分,除非您有充分的理由这样做。

这就引出了我将我的解决方案称为MemoLock的原因。如果您将承诺的缓存思想推广到不仅缓存查询,而且缓存(纯)函数调用的任何(可序列化的)输出,那么您讨论的是记忆(而不是记忆)。如果你从没听说过记忆,阅读更多相关信息-这是一个有趣的概念。

总之

Instagram描述的模式并不新鲜,但值得讨论。更好的缓存可以帮助大量的用例,作为一个行业,我们并不总是完全熟悉承诺的概念,尤其是当它超出单个流程的范围时。在GitHub上试试代码请让我知道如果你喜欢的话。

Baidu