缓存明明在工作,却依然导致了重复的 API 调用

缓存并没有损坏。

然而,针对同一个用户名的三个并发请求却向 GitHub 发送了三次请求。

这种情况发生在 CommitPulse 中,这是一个将 GitHub 数据转换为 SVG 徽章的 Next.js API。当一个 README 走红时,成千上万的人会同时查看该徽章。这会产生巨大的流量。

缓存对串行请求有效,但对并发请求失效了。

问题在于:

  • 请求 A 检查缓存。缓存未命中。请求 A 开始从 GitHub 获取数据。
  • 请求 B 在 5ms 后到达。它检查缓存。由于请求 A 尚未完成,缓存仍然未命中。请求 B 开始进行第二次获取。
  • 请求 C 在 10ms 后到达。它也看到了缓存未命中,并开始进行第三次获取。

这就是“惊群效应”(thundering herd problem)。高负载下的缓存未命中会触发大量向你的上游供应商发送的相同调用。如果你使用的是像 GitHub 这样有频率限制的 API,这可能会瞬间耗尽你的配额。

解决方案是请求合并(request coalescing)。

你必须将待处理的请求与已完成的缓存条目分开进行跟踪。我实现了一个 in-flight Map 来管理这一点:

  • 当请求开始时,将其 Promise 存储在 Map 中。
  • 如果针对同一个键(key)到达了第二个请求,不要启动新的获取操作。
  • 相反,直接从 Map 中返回现有的 Promise。
  • 一旦请求完成,将其从 Map 中移除,并将结果保存到缓存中。

这确保了无论有多少人同时请求相同的数据,都只有一次调用会命中 API。

在修复这个问题的同时,我也解决了同一文件中的另外三个边缘情况 Bug:

  • Token 缺失错误:系统将 "undefined" 作为凭据发送。我将其更新为在发起请求前验证 Token。
  • 内存泄漏:重试逻辑在 AbortSignals 上留下了过时的事件监听器。我添加了清理逻辑以防止泄漏。
  • URL 注入:带有特殊字符的用户名破坏了 API 路径。我添加了编码处理以保护 URL 结构。

仅有缓存是不够的。你还需要对当前正在进行的(in flight)请求进行去重。

Source: https://dev.to/eshaanagrawal/the-cache-was-working-and-still-causing-duplicate-api-calls-3n51