从 Memcache 切换到 Redis 以及有关缓存的一些提示为什么我们要切换移动键的策略全部或全部翻转开关缓存 Gotcha 快乐缓存!

2025-05-25

从 Memcache 切换到 Redis 以及一些缓存技巧

我们为何选择

移动密钥的策略

要么全有,要么全无

扳动开关

缓存陷阱

快乐缓存!

上个月,我们在 DEV 上从 Memcache 切换到了 Redis。这篇文章探讨了切换的原因、方法,以及一些需要注意的陷阱,以便您能够充分利用所使用的缓存解决方案。

我们为何选择

说到缓存,Memcache 有点像个黑匣子。很难知道里面有什么,以及它是如何被使用的。我刚加入 DEV 时,收到的是一个 75GB 的 Memcache 实例,我们为此花了不少钱。考虑到我们的实际使用量,75GB 的缓存似乎有点大,所以我立即开始尝试如何查看它的内容。经过几个小时的 Google 搜索,我发现除非你知道缓存的键,否则你无法真正看到 Memcache 里的内容。无法查看数据存储是 SRE 的噩梦,它会让你感到非常无助,无法胜任你的工作。

除了可见性差之外,Memcache 与 Rails 的兼容性也不太好。为了使用它,你必须使用Dalli gem,我最近了解到,Dalli gem 会覆盖 Rails 中的许多核心缓存方法,才能让 Memcache 正常工作。如果你需要添加额外的 gem 来让你的缓存解决方案正常工作,这可不是什么好事,只会增加整体的复杂性。

最后,Memcache 支持的数据结构非常有限。如果您只需要简单的键值对(例如字符串和数字),那么一开始这并不是什么大问题。但是,随着数据的增长,您可能需要更多数据存储选项,而这正是 Redis 的优势所在。它提供了许多不同的数据结构,您可以使用它们来更高效地存储数据。

出于以上所有原因,我们选择用 Redis 替换 Memcache 作为应用程序缓存。除了上面提到的所有优点之外,Redis 还具有一些不错的功能,例如异步删除Lua 脚本,可以处理复杂的逻辑。此外,它还允许我们使用速度更快的后台工作服务,例如我们正在从DelayedJob 迁移到 Sidekiq。

一旦我们决定要进行转换,下一个问题就是我们该如何去做。

移动密钥的策略

为了迁移到 Redis,我们决定逐步迁移,一次只迁移一个或几个键。这样做的目的是确保大型冷缓存不会拖慢速度。同时,这也让我们能够查看缓存的所有内容,并评估是否真的需要缓存它们。

我们的做法是使用 Redis 创建一个新的 Rails 缓存客户端,并将其命名为 RedisRailsCache。这个新客户端的行为与 Redis 类似,Rails.cache但它指向的不是 Memcache,而是 Redis。

RedisRailsCache = ActiveSupport::Cache::RedisCacheStore.new(url: redis_url, expires_in: DEFAULT_EXPIRATION)
Enter fullscreen mode Exit fullscreen mode

单个键的迁移过程非常顺利,我们一路上没有遇到任何意外,也没有遇到任何卡顿。接下来,我们遇到了 Rails 片段缓存迁移的难题。最初的计划是像处理Rails.cache键一样,用指向 Redis 的某种客户端逐个替换片段缓存的调用。

我们尝试过的一种方法是设置双 Rails 缓存DualRailsStore作为我们的 Rails cache_store。我对自己创造的这个东西非常自豪。然而,当我们将其投入生产环境时,我们发现 Memcache dalli gem 的存储方式与 Rails 缓存存储预期的方式不符,这导致了很多问题。我们很快撤回了 PR,重新回到了设计阶段。

要么全有,要么全无

在研究了所有选项并评估了它们的风险之后,我决定在 Slack 中提出一个疯狂的想法:

Molly:“因为 dalli_store 这个 gem,我们在 Memcache 中缓存数据的方式和 Rails 的缓存方式完全不同,所以尝试并行执行这些操作真是太麻烦了。所以我又有一个疯狂的想法:我们非常依赖 Fastly 进行缓存,所以我在想,如果直接切换到 Redis 会有多糟糕?我们能不能安排一两个小时的“维护”(基本上是预期的性能下降时间)然后直接做?”

这是我第一次在 DEV 上抛出如此冒险的想法,我以为人们肯定会驳斥它并回应说“不可能,你疯了!” 但实际上,我得到的回应是这样的:

本:“有可能。我当然有办法让它变得更顺畅。”

我在现实生活中的反应是“哦,感谢上帝,他们不认为我疯了!”
男人擦着前额,说着“呼”

之后,我想着可以安排在周末或晚上,等事情比较缓和的时候。但出乎意料的是,本问我下午想不想做。我当然回答“好!”我宁愿撕掉创可贴,也不愿焦急地等待。

扳动开关

为了转向 Redis,我们对应用程序做了一些修改,以减轻我们预期的额外负载

  • 添加了额外的测功机(服务器)来帮助负载。
  • 注释掉主页和标签页的缓存清除功能,这些页面可能会提供更陈旧的服务
  • 通过 Fastly 切断“附加内容框”和“社交预览”控制器的流量,因为它们有大量缓存,我们可以暂时不用它们
  • 暂时删除了帖子下方和侧边栏上的“其他文章”

完成所有这些后,我们添加了一个 ENV 变量,用于切换到 Redis。一切就绪后,我们设置了 ENV 变量,然后静待观察。我和 Ben 都预料到 Redis 的流量会激增,负载会压垮我们的服务器。我们做好了最坏的打算。但最坏的情况始终没有发生。

两名飞行员驾驶飞机,图片说明

我们一度怀疑是 ENV 变量出了问题,于是我打开 Redis 看看片段键有没有进来。果然,真的进来了!Redis 流量略有上升,但随着用户在网站上的浏览,键的数量缓慢而稳定地攀升。没有出现大幅飙升或急剧下降的情况。说实话,我们的警报甚至都没响过。我和 Ben 观察并等待了将近两个小时,才确信一切可能都正常,结果确实如此。

我把 Redis 的存储空间调到了 25GB,因为之前 Memcache 的容量是 75GB,我完全不知道会有什么问题。结果发现 Memcache 里有很多垃圾数据,因为目前 Redis 的缓存数据大小大约只有 3GB。

缓存陷阱

在整个过程中,我对 DEV 平台上的缓存进行了大量的研究和评估,甚至在切换到 Redis 之前就做了很多修改。以下是我观察到的一些常见问题,在使用任何类型的缓存存储(例如 Redis 或 Memcache)时都应该注意。

创建不必要的外部请求

Redis 和 Memcache 都速度超快,因此非常适合使用。然而,当你与服务器通信时,你仍然需要向服务器发出外部请求,而该外部请求需要时间。

有几次我看到一些 HTML 代码块被缓存了,但这些代码块在渲染时并没有发出任何外部请求。换句话说,我们不是从服务器提供 HTML,而是向 Redis 发出外部请求。Redis 的速度可能很快,但它比不上内存的速度。如果一段代码没有发出任何外部请求,就不要通过不必要的缓存来增加请求。这样做只会降低应用的运行速度。以下是我们完全移除缓存的一个示例。

密钥不过期

尤其是在刚开始开发应用程序时,很容易将内容塞进未设置有效期的外部缓存中。这很容易失控,导致 75GB 的缓存里充满了你实际上并不需要的垃圾。

在代码中设置缓存存储客户端时,请务必设置默认过期时间。每次传入键时,都需要设置过期时间。您可以尝试依靠开发人员来确保设置,但根据我的经验,我们都非常不可靠。最简单的方法是,当未明确设置过期时间时,让代码默认设置过期时间。这样就不会出现任何疏漏。

低缓存 ROI

添加缓存会增加复杂性和开销。现在您必须确保密钥在必要时已过期,否则最终会向用户显示过时的信息。现在,您在测试和开发中还需要考虑另一个变量。您是否在这些环境中缓存数据?您是否对缓存进行了存根处理?等等。由于增加了复杂性,您需要确保在使用缓存时确实能够从中获得收益。

缓存的目的是通过获取通常需要很长时间才能检索的数据、记住它并提供数据,从而提高速度,而无需承担从原始来源检索数据的所有开销。然而,有时缓存并不值得。

一种情况是,如果您正在缓存一个已经非常快的请求。例如,如果您正在缓存一个非常快的User.find()命令,您需要考虑的是,缓存带来的微小速度提升是否值得您为了确保每次用户更改时都必须破坏该缓存而不得不这样做?

另一种情况是,如果您不经常请求数据,缓存可能就不理想了。假设您为用户缓存了一次页面浏览,这样当用户重新加载页面时,一切都会变得非常快。如果用户只是坐在那里反复加载页面,这当然很棒。但如果他们浏览完页面后就离开了,那么缓存就没有任何用处了。

找到这些场景可能比较棘手,但一种方法是查看命中率。大多数缓存数据库会以百分比的形式显示命中率,例如 75%。这意味着在 75% 的请求时间内,缓存中都有该键。命中率越高,缓存的使用率就越高,处理速度也就越高。

唯一键太多

这和我上面提到的 ROI 场景有点类似。需要注意的一点是不要出现太多唯一键。让我解释一下。假设你有一段用于缓存的代码:

Rails.cache.fetch("user-follow-count-#{id}-#{updated_at.rfc3339}", expires_in: 1.hour) do 
  followers.count
end
Enter fullscreen mode Exit fullscreen mode

该代码缓存了用户的关注者数量,我们添加了updated_at时间戳,以确保用户更新后,数据不会过期。然而,有很多方法可以更新用户,并且其关注者数量不会改变。例如,我们可以更新姓名、邮箱或其他信息,每次更新都会创建一个新的缓存键,而关注者数量保持不变。

更好的方法是使用更具体的名称(例如last_followed_at时间戳)来命名缓存键:

Rails.cache.fetch("user-follow-count-#{id}-#{last_followed_at.rfc3339}", expires_in: 1.hour) do 
  followers.count
end
Enter fullscreen mode Exit fullscreen mode

这样,只有当用户被关注时,缓存键才会被重置,这是合理的,因为这意味着关注者数量会发生变化。请注意类似这样的情况,你可能使用了updated_at过于激进的过期方法,从而降低了缓存的有效性。理想情况下,缓存键应该只在缓存值发生变化时才更改。

快乐缓存!

希望这篇文章对您有所帮助,并且我们在 DEV 缓存存储切换方面的经验可以帮助您决定是否适合进行切换。如果您想了解更多详细信息,或者想查看完成此切换所需的所有 PR,请查看此 Github 问题。如果您有任何疑问或文章中任何部分不清楚,请告诉我!

文章来源:https://dev.to/molly/switching-from-memcache-to-redis-and-some-tips-on-caching-4g1l
PREV
没有“正确”的方法:Git Rebase 与 Merge Rebase 和 Merge 的工作原理 合并 Rebase 我的 Rebase 与 Merge 策略 没有“正确”的方法
NEXT
提升您的 Ruby 技能:使用数组 each map flat_map select detect rejection partition count with_index chaining 您成功了!!!