dot 快速的未来即将在您所在的城市举办的活动中到来。

加入我们参加 Redis 发布会

使用 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 哈希来存储我们的结果集。我们可以将数据存储为 JSON 编码的字符串,并在准备好时将其拉出来并在我们的代码库中使用。

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

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

除此之外,我们的密钥名称也不是最好的。名称 user-activity-list 同样只适用于这个非常通用且非常狭窄的示例。对于实际应用程序,您需要以更可预测的方式命名密钥。如果我真正这样做,密钥中会包含用户名,并且所有这些都是用户会话的一部分,因此可以轻松检索和使用。

继续前进,我们应该解决几个问题。首先,我们获得了 user-activity-list,但我们应该在运行一次此查询后将其过期,这样就不会冒着向用户显示一些陈旧数据的风险。我们可以通过几种不同的方式解决此问题。

写入时过期

假设此活动列表是用户自己更新的。她会转到设置页面,修改一些东西,然后保存更改。她可能每个星期或每个月只做一次。在这种情况下,我们可以将缓存保留到用户更改内容为止
1. 用户将“sportsball”添加到他们的列表并保存
2. 响应用户的保存操作,我们 UNLINK user-activity-list

您可以以任何您喜欢的方式进行第一部分和第二部分。对于此系统,我假装我们有一种注册/推送事件的方法。那部分并不重要,但重要的是我们的系统在每次更新时都会删除缓存。我们不必担心重新缓存数据,因为这是原始函数的工作。

按时间过期

写入时过期可能不适用于所有场景。在某些情况下,我们希望仅在一定时间内缓存查询。对于这些情况,我们可以使用 Redis 来使密钥过期。

让我们看一下第一个缓存解决方案,看看它在 Redis EXPIRE 中是什么样子
1. > HGET user-activity-list cache
2. 如果结果不为空,则返回缓存的值,否则转到步骤 3
3. 运行查询并将 DB 结果集保存到一个变量中
4. 如果 DB 结果大小 > 0
5. 将结果集数据转换为 JSON 字符串
HSET user-activity-list cache JSON 字符串结果
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 毫秒。在整个网站范围内,这将从每小时花费的数分钟时间减少到仅仅几秒钟,用于获取数据。这种性能提升将使客户非常满意。