”工欲善其事,必先利其器。“—孔子《论语.录灵公》
首页 > 编程 > 使用 Fastify 和 Redis 缓存加速您的网站

使用 Fastify 和 Redis 缓存加速您的网站

发布于2024-08-27
浏览:755

Speeding Up Your Website Using Fastify and Redis Cache

不到 24 小时前,我写了一篇关于如何使用 Cloudflare 缓存加速您的网站的文章。不过,我已经将大部分逻辑转移到使用 Redis 的 Fastify 中间件中。以下是您自己执行此操作的原因以及方法。

Cloudflare 缓存问题

我遇到了 Cloudflare 缓存的两个问题:

  • 启用响应缓存后页面导航中断。不久前我在 Remix 论坛上提出了一个有关此问题的问题,但截至撰写本文时,该问题仍未解决。目前尚不清楚为什么缓存响应会导致页面导航中断,但只有当 Cloudflare 缓存响应时才会发生这种情况。
  • 我无法让 Cloudflare 在重新验证时执行服务陈旧内容,如原始帖子中所述。看起来这不是一个可用的功能。

我还遇到了一些其他问题(例如无法使用模式匹配清除缓存),但这些对我的用例来说并不重要。

因此,我决定使用 Redis 将逻辑转移到 Fastify 中间件。

[!笔记]
我将 Cloudflare 缓存留给图像缓存。在这种情况下,Cloudflare 缓存有效地充当 CDN。

Fastify 中间件

下面是我使用 Fastify 编写的用于缓存响应的中间件的带注释版本。

const isCacheableRequest = (request: FastifyRequest): boolean => {
  // Do not attempt to use cache for authenticated visitors.
  if (request.visitor?.userAccount) {
    return false;
  }

  if (request.method !== 'GET') {
    return false;
  }

  // We only want to cache responses under /supplements/.
  if (!request.url.includes('/supplements/')) {
    return false;
  }

  // We provide a mechanism to bypass the cache.
  // This is necessary for implementing the "Serve Stale Content While Revalidating" feature.
  if (request.headers['cache-control'] === 'no-cache') {
    return false;
  }

  return true;
};

const isCacheableResponse = (reply: FastifyReply): boolean => {
  if (reply.statusCode !== 200) {
    return false;
  }

  // We don't want to cache responses that are served from the cache.
  if (reply.getHeader('x-pillser-cache') === 'HIT') {
    return false;
  }

  // We only want to cache responses that are HTML.
  if (!reply.getHeader('content-type')?.toString().includes('text/html')) {
    return false;
  }

  return true;
};

const generateRequestCacheKey = (request: FastifyRequest): string => {
  // We need to namespace the cache key to allow an easy purging of all the cache entries.
  return 'request:'   generateHash({
    algorithm: 'sha256',
    buffer: stringifyJson({
      method: request.method,
      url: request.url,
      // This is used to cache viewport specific responses.
      viewportWidth: request.viewportWidth,
    }),
    encoding: 'hex',
  });
};

type CachedResponse = {
  body: string;
  headers: Record;
  statusCode: number;
};

const refreshRequestCache = async (request: FastifyRequest) => {
  await got({
    headers: {
      'cache-control': 'no-cache',
      'sec-ch-viewport-width': String(request.viewportWidth),
      'user-agent': request.headers['user-agent'],
    },
    method: 'GET',
    url: pathToAbsoluteUrl(request.originalUrl),
  });
};

app.addHook('onRequest', async (request, reply) => {
  if (!isCacheableRequest(request)) {
    return;
  }

  const cachedResponse = await redis.get(generateRequestCacheKey(request));

  if (!cachedResponse) {
    return;
  }

  reply.header('x-pillser-cache', 'HIT');

  const response: CachedResponse = parseJson(cachedResponse);

  reply.status(response.statusCode);
  reply.headers(response.headers);
  reply.send(response.body);
  reply.hijack();

  setImmediate(() => {
    // After the response is sent, we send a request to refresh the cache in the background.
    // This effectively serves stale content while revalidating.
    // Therefore, this cache does not reduce the number of requests to the origin;
    // The goal is to reduce the response time for the user.
    refreshRequestCache(request);
  });
});

const readableToString = (readable: Readable): Promise => {
  const chunks: Uint8Array[] = [];

  return new Promise((resolve, reject) => {
    readable.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
    readable.on('error', (err) => reject(err));
    readable.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
  });
};

app.addHook('onSend', async (request, reply, payload) => {
  if (reply.hasHeader('x-pillser-cache')) {
    return payload;
  }

  if (!isCacheableRequest(request) || !isCacheableResponse(reply) || !(payload instanceof Readable)) {
    // Indicate that the response is not cacheable.
    reply.header('x-pillser-cache', 'DYNAMIC');

    return payload;
  }

  const content = await readableToString(payload);

  const headers = omit(reply.getHeaders(), [
    'content-length',
    'set-cookie',
    'x-pillser-cache',
  ]) as Record;

  reply.header('x-pillser-cache', 'MISS');

  await redis.setex(
    generateRequestCacheKey(request),
    getDuration('1 day', 'seconds'),
    stringifyJson({
      body: content,
      headers,
      statusCode: reply.statusCode,
    } satisfies CachedResponse),
  );

  return content;
});

注释贯穿了代码,但这里有一些关键点:

  • 缓存标准:
    • 请求:
    • 不缓存经过身份验证的用户的响应。
    • 仅缓存 GET 请求。
    • 仅缓存包含“/supplements/”的 URL 的响应。
    • 如果请求头包含cache-control: no-cache则绕过缓存。
    • 回复:
    • 仅缓存成功的响应(statusCode为200)。
    • 不缓存已从缓存提供的响应 (x-pillser-cache: HIT)。
    • 仅缓存内容类型为text/html的响应。
  • 缓存密钥生成:
    • 使用包含请求方法、URL 和视口宽度的 JSON 表示形式的 SHA-256 哈希。
    • 在缓存键前加上“request:”前缀,以便于命名空间和清除。
  • 请求处理:
    • 挂钩 onRequest 生命周期以检查请求是否有缓存的响应。
    • 提供缓存的响应(如果可用),并使用 x-pillser-cache: HIT 进行标记。
    • 发送缓存响应后启动后台任务刷新缓存,实现“重新验证时提供陈旧内容”。
  • 响应处理:
    • 挂钩 onSend 生命周期来处理和缓存响应。
    • 将可读流转换为字符串以简化缓存。
    • 从缓存中排除特定标头(content-length、set-cookie、x-pillser-cache)。
    • 将不可缓存的响应标记为 x-pillser-cache: DYNAMIC。
    • 缓存响应的 TTL(生存时间)为一天,用 x-pillser-cache 标记新条目:MISS。

结果

我从多个位置运行了延迟测试,并捕获了每个 URL 的最慢响应时间。结果如下:

网址 国家 原点响应时间 Cloudflare 缓存响应时间 Fastify 缓存响应时间
https://pilser.com/vitamins/vitamin-b1 us-west1 240ms 16ms 40ms
https://pilser.com/vitamins/vitamin-b1 欧洲西部3 320ms 10ms 110ms
https://pilser.com/vitamins/vitamin-b1 澳大利亚-东南部1 362ms 16ms 192ms
https://pilser.com/supplements/vitamin-b1-3254 us-west1 280ms 10ms 38ms
https://pilser.com/supplements/vitamin-b1-3254 欧洲西部3 340ms 12ms 141ms
https://pilser.com/supplements/vitamin-b1-3254 澳大利亚-东南部1 362ms 14ms 183ms

与 Cloudflare 缓存相比,Fastify 缓存速度较慢。这是因为缓存的内容仍然从源提供服务,而 Cloudflare 缓存则从区域边缘位置提供服务。然而,我发现这些响应时间足以实现良好的用户体验。

版本声明 本文转载于:https://dev.to/lilouartz/speeding-up-your-website-using-fastify-and-redis-cache-4ck6?1如有侵犯,请联系[email protected]删除
最新教程 更多>
  • MySQL动态更新列使用INNER JOIN方法
    MySQL动态更新列使用INNER JOIN方法
    MySQL动态更新关联表列数据 本文介绍如何在MySQL中使用INNER JOIN动态更新目标表中的列。 我们的目标是根据共享的名称属性,将源表(tableA)中对应列的值更新到目标表(tableB)中的列。 可以使用以下UPDATE语句实现: UPDATE tableB INNER JOIN ...
    编程 发布于2025-04-19
  • 如何使用Python理解有效地创建字典?
    如何使用Python理解有效地创建字典?
    在python中,词典综合提供了一种生成新词典的简洁方法。尽管它们与列表综合相似,但存在一些显着差异。与问题所暗示的不同,您无法为钥匙创建字典理解。您必须明确指定键和值。 For example:d = {n: n**2 for n in range(5)}This creates a dicti...
    编程 发布于2025-04-19
  • 为什么使用Firefox后退按钮时JavaScript执行停止?
    为什么使用Firefox后退按钮时JavaScript执行停止?
    导航历史记录问题:JavaScript使用Firefox Back Back 此行为是由浏览器缓存JavaScript资源引起的。要解决此问题并确保在后续页面访问中执行脚本,Firefox用户应设置一个空功能。 警报'); }; alert('inline Alert')...
    编程 发布于2025-04-19
  • Python中何时用"try"而非"if"检测变量值?
    Python中何时用"try"而非"if"检测变量值?
    使用“ try“ vs.” if”来测试python 在python中的变量值,在某些情况下,您可能需要在处理之前检查变量是否具有值。在使用“如果”或“ try”构建体之间决定。“ if” constructs result = function() 如果结果: 对于结果: ...
    编程 发布于2025-04-19
  • 如何在Chrome中居中选择框文本?
    如何在Chrome中居中选择框文本?
    选择框的文本对齐:局部chrome-inly-ly-ly-lyly solument 您可能希望将文本中心集中在选择框中,以获取优化的原因或提高可访问性。但是,在CSS中的选择元素中手动添加一个文本 - 对属性可能无法正常工作。初始尝试 state)</option> < op...
    编程 发布于2025-04-19
  • 您可以使用CSS在Chrome和Firefox中染色控制台输出吗?
    您可以使用CSS在Chrome和Firefox中染色控制台输出吗?
    在javascript console 中显示颜色是可以使用chrome的控制台显示彩色文本,例如红色的redors,for for for for错误消息?回答是的,可以使用CSS将颜色添加到Chrome和Firefox中的控制台显示的消息(版本31或更高版本)中。要实现这一目标,请使用以下模...
    编程 发布于2025-04-19
  • 如何修复\“常规错误:2006 MySQL Server在插入数据时已经消失\”?
    如何修复\“常规错误:2006 MySQL Server在插入数据时已经消失\”?
    How to Resolve "General error: 2006 MySQL server has gone away" While Inserting RecordsIntroduction:Inserting data into a MySQL database can...
    编程 发布于2025-04-19
  • PHP如何在MySQL数据库中存储IP地址?
    PHP如何在MySQL数据库中存储IP地址?
    MySQL数据库IP地址使用PHP 使用PHP中存储IP地址时,使用PHP存储IP地址时,考虑到适当的字段类型和存储方法是至关重要的。 type 地址最合适的字段类型是int。尽管显然不匹配,但此选择还是由通过PHP IP2长函数从IP地址到整数的有效转换过程驱动的。要检索原始IP地址,可以使用...
    编程 发布于2025-04-19
  • 查找当前执行JavaScript的脚本元素方法
    查找当前执行JavaScript的脚本元素方法
    如何引用当前执行脚本的脚本元素在某些方案中理解问题在某些方案中,开发人员可能需要将其他脚本动态加载其他脚本。但是,如果Head Element尚未完全渲染,则使用document.getElementsbytagname('head')[0] .appendChild(v)的常规方...
    编程 发布于2025-04-19
  • 10款在线定制GIF制作工具推荐
    10款在线定制GIF制作工具推荐
    [2 在这篇文章中,我们收集了10种免费的在线GIF Maker工具,以制作您自己的自定义Ajax装载机 。易于创建自己的图像即可创建自己的自定义动画。 相关文章: 5在线加载ajax旋转器生成器工具 1。 gifmake.com 与(GIF,JPEG,PNG)构成图片,也可以分解动画gif。...
    编程 发布于2025-04-19
  • 如何使用PHP从XML文件中有效地检索属性值?
    如何使用PHP从XML文件中有效地检索属性值?
    从php $xml = simplexml_load_file($file); foreach ($xml->Var[0]->attributes() as $attributeName => $attributeValue) { echo $attributeName,...
    编程 发布于2025-04-19
  • 为什么HTML无法打印页码及解决方案
    为什么HTML无法打印页码及解决方案
    无法在html页面上打印页码? @page规则在@Media内部和外部都无济于事。 HTML:Customization:@page { margin: 10%; @top-center { font-family: sans-serif; font-weight: bo...
    编程 发布于2025-04-19
  • 如何有效地选择熊猫数据框中的列?
    如何有效地选择熊猫数据框中的列?
    在处理数据操作任务时,在Pandas DataFrames 中选择列时,选择特定列的必要条件是必要的。在Pandas中,选择列的各种选项。选项1:使用列名 如果已知列索引,请使用ILOC函数选择它们。请注意,python索引基于零。 df1 = df.iloc [:,0:2]#使用索引0和1 c...
    编程 发布于2025-04-19
  • 解决MySQL错误1153:数据包超出'max_allowed_packet'限制
    解决MySQL错误1153:数据包超出'max_allowed_packet'限制
    mysql错误1153:故障排除比“ max_allowed_pa​​cket” bytes 更大的数据包,用于面对阴谋mysql错误1153,同时导入数据capase doft a Database dust?让我们深入研究罪魁祸首并探索解决方案以纠正此问题。理解错误此错误表明在导入过程中接...
    编程 发布于2025-04-19
  • 在PHP中如何高效检测空数组?
    在PHP中如何高效检测空数组?
    在PHP 中检查一个空数组可以通过各种方法在PHP中确定一个空数组。如果需要验证任何数组元素的存在,则PHP的松散键入允许对数组本身进行直接评估:一种更严格的方法涉及使用count()函数: if(count(count($ playerList)=== 0){ //列表为空。 } 对...
    编程 发布于2025-04-19

免责声明: 提供的所有资源部分来自互联网,如果有侵犯您的版权或其他权益,请说明详细缘由并提供版权或权益证明然后发到邮箱:[email protected] 我们会第一时间内为您处理。

Copyright© 2022 湘ICP备2022001581号-3