dot Redis 8 已发布——它是开源的

了解更多

使用 Redis 进行查询缓存

了解您的缓存是否达到企业级水平,并学习如何:在全球范围内扩展同时保持低延迟,以及更有效地缓存以降低成本

当我开始搭建网站时,我并没有过多关注性能。性能是一种奢侈品,只有在你掌握了诸如 HTML 和 CSS 或后端使用的任何编程语言等基础知识后,你才会去担心它。初学者的目标是让网站上线,实现页面间的跳转,并确保它在各种设备上看起来都不错。缓存和性能是我们稍后才会考虑的事情。

有很多充分的理由去研究缓存解决方案。从 SEO 的角度来看,谷歌会惩罚加载慢的网站。如果你的网站加载时间很长,你的排名可能会受到影响,这当然会对收入产生严重影响。谷歌的 John Mueller 表示,页面加载时间超过两秒会“影响”你网站的“抓取”,并且他期望“[小于] 2-3 秒”。另一个因素是加载慢的网站对用户体验的影响。当我开始写代码时,完全加载一个页面大约需要八秒是可以接受的。只有当时间超过这个阈值时,你才会担心用户会放弃页面。如今,这个时间可能在两秒左右,甚至可能更短,具体取决于你所在的行业

总而言之:你的网站越快,它的排名就越好,访客停留和互动的时间就越长。但这个问题对我来说并没有变得紧急,直到我的客户打电话说他们的新网站“太慢了”。公平地说,网站上有一些很重的图片,而且他们使用的是一家糟糕的托管公司。那也是在过去,10 美元的托管费用意味着你的网站在一个共享服务器上。他们对服务器优化的想法是安装最新版本的 CPanel(天哪)。经过一番研究,我构建了一个基本的页面缓存系统,效果很好。客户很高兴,我也开始学习一项新技能,一切都很顺利。但很快我就意识到,虽然页面级缓存是一个很棒的工具,但这只是其中一种工具。

当页面缓存不足时

页面缓存的工作原理如下:请求进来后,服务器处理请求,然后存储生成的 HTML。为了本篇博文的目的,我们以 Redis 为例。下次有人请求我们的页面时,系统会先检查缓存。如果页面在缓存中,系统就会使用缓存中的页面,而不是在服务器层面重新处理请求。

这种情况非常适合那些变化不大的页面,例如条款和条件或隐私政策。这些页面的流量与主应用程序不同,主应用程序通常包含基于用户的动态数据。

让我们想象一下,我们有一个应用程序,它充当用户目录,并允许根据某种活动进行过滤。用户可以查看人员列表,并看到他们的电子邮件地址和电话号码。我们的应用程序使用 SQL 数据库,因此要获取这些数据,我们需要一个简单的 select 查询
SELECT username, email, phone_number FROM users WHERE activity='baseball';

在我们的应用程序中,假设活动是用户注册时设置的一个列表。因此,人们可以看到的活动类型因用户而异。我们会将活动列表存储在另一个表中,并且此页面也需要该信息。

SELECT name, id FROM activity_list WHERE user_id=1;

在这种情况下,我们需要针对当前用户运行一个查询。该用户将看到的数据是整个列表的一个子集,并且是独一无二的。鉴于此,我们无法缓存整个页面,因此需要转而缓存每个单独的查询。

缓存查询,而非页面

现在我们需要考虑何时以及如何缓存查询。一个简单的解决方案是使用 Redis Hashes 来存储我们的结果集。我们可以将数据存储为 JSON 编码的字符串,准备好后,只需将其取出并在代码库中使用即可。

那么缓存看起来像什么呢?这里是一些伪代码
1. > HGET user-activity-list cache
2. 如果结果不为 nil,则返回结果集,否则转到步骤 3
3. 运行查询并将数据库结果集保存到变量中
4. 如果数据库结果大小 > 0
5. 将结果集数据转换为 JSON 字符串
6. > HSET user-activity-list cache JSON string result set
7. 否则,如果结果集 <= 0,则抛出错误

关于上面的伪代码,我应该指出,当然我们忽略了这实际上是一种会话存储模式。在现实世界中,你不会希望在 Redis 中永远缓存这些查询,你只会在用户登录并活跃在你的网站上时创建和存储它们。我创建这个通用的 user-activity-list 键在 Redis 中,只是为了演示理论并激发你如何在自己的代码中实现这些功能的想法。

除此之外,我们的键名也不是最好的。user-activity-list 这个名称也只是为了这个非常通用且非常狭窄的例子而用。对于一个实际的应用程序,你会希望以一种更可预测的方式命名你的键。如果我是真正实现它,键名中的某个位置会包含用户名,并且这都将是用户会话的一部分,这样就很容易检索和使用。

继续,我们应该解决几个问题。首先,我们正在获取 user-activity-list,但在我们运行一次此查询后,我们应该使查询过期,以免向用户展示相当陈旧的数据。有几种不同的方法可以解决此问题。

写入时过期

假设这个活动列表是用户自己更新的。她会去设置页面,调整一些东西,然后保存更改。她可能每周或每月才做一次。在这种情况下,我们可以保留缓存,直到用户更改某些内容
1. 用户将 "sportsball" 添加到他们的列表并保存
2. 响应用户保存操作,我们 UNLINK user-activity-list

你可以用任何你喜欢的方式来完成第一和第二部分。对于这个系统,我假设我们有一种注册/推送事件的方式。那一部分不是很重要,重要的是我们的系统在每次更新时都会删除缓存。我们不需要担心重新缓存数据,因为那是原始函数的工作。

按时间过期

写入时过期可能不适用于所有场景。在某些情况下,我们只想在设定的时间内缓存查询。对于这些情况,我们可以使用 Redis 来使我们的键过期。

让我们看看我们的第一个缓存解决方案,并在其中加入 Redis EXPIRE 会是什么样子
1. > HGET user-activity-list cache
2. 如果结果不为 nil,则返回缓存值,否则转到步骤 3
3. 运行查询并将数据库结果集保存到变量中
4. 如果数据库结果大小 > 0
5. 将结果集数据转换为 JSON 字符串
HSET user-activity-list cache JSON string results
EXPIRE user-activity-list 100

这里的技巧在于步骤五。设置键后,我们使用 Redis 为该键设置过期时间(以秒为单位)。Redis 会替我们删除键,因此我们无需在代码中管理它。

结论

在页面级缓存无效或效果不佳的情况下,缓存数据库查询是一个很好的替代方案。我们可以使用 Redis 设置和获取带有已保存查询结果的哈希值,并返回这些值,而不是访问数据库。这将极大地加快我们的网站速度。想一下:从数据库访问并向用户返回信息的标准时间是 100 毫秒,而从 Redis 获取的平均时间仅为 2 毫秒。这是一个巨大的性能提升。

每小时页面浏览量每页数据库查询次数每次查询耗时数据库查询次数每小时性能影响
1,0002-5100 毫秒2,000-5,0003.33-8.33 分钟

在我们虚构的应用程序中,如果用户每小时访问这个活动页面 1000 次,并且我们每次都必须访问数据库,那么这将累积起来(这还只是针对一个查询)。想象一下,如果这个页面有 2-5 个查询呢?如果这些查询需要与多个表进行复杂的连接呢?在一个页面上执行三个同步查询,仅仅从数据库返回数据就很容易花费 300 毫秒以上。在一个我们只有 2-3 秒来吸引用户注意力的世界里,为什么要让自己处于这种不利地位呢?

现在,想象一下我们 90% 的时间都可以从缓存中检索数据。

每小时页面浏览量每页数据库查询次数每次查询耗时每次缓存命中耗时缓存命中数据库查询次数Redis 调用次数每小时性能影响
1,0002-5100 毫秒2 毫秒90%200-5001,800-4,50023.5-59 秒

在 Redis 中缓存查询可以将单页上的 300 毫秒变成仅仅 6 毫秒。在整个网站范围内,这将把每小时花费在获取数据上的许多分钟缩短到几秒钟。这是一个性能提升,应该会让客户非常满意。

在我们虚构的应用程序中,如果用户每小时访问这个活动页面 1000 次,并且我们每次都必须访问数据库,那么这将累积起来(这还只是针对一个查询)。想象一下,如果这个页面有 2-5 个查询呢?如果这些查询需要与多个表进行复杂的连接呢?在一个页面上执行三个同步查询,仅仅从数据库返回数据就很容易花费 300 毫秒以上。在一个我们只有 2-3 秒来吸引用户注意力的世界里,为什么要让自己处于这种不利地位呢?

现在,想象一下我们 90% 的时间都可以从缓存中检索数据。

在 Redis 中缓存查询可以将单页上的 300 毫秒变成仅仅 6 毫秒。在整个网站范围内,这将把每小时花费在获取数据上的许多分钟缩短到几秒钟。这是一个性能提升,应该会让客户非常满意。