【问题标题】:How can I avoid race condition when making cached Ajax calls进行缓存的 Ajax 调用时如何避免竞争条件
【发布时间】:2020-07-23 18:43:36
【问题描述】:

由于很多公共API比如GitHub公共API都有请求限制,所以我们有必要实现一些缓存机制来避免不必要的请求调用。但是我发现这可能会导致竞争条件。

我编写了一个示例来演示这种情况 https://codesandbox.io/s/race-condition-9kynm?file=/src/index.js

这里我先实现一个cachedFetch

const cachedFetch = (url, options) => {
  // Use the URL as the cache key to sessionStorage
  let cacheKey = url;
  let cached = sessionStorage.getItem(cacheKey);
  if (cached !== null) {
    console.log("reading from cache....");
    let response = new Response(new Blob([cached]));
    return Promise.resolve(response);
  }

  return fetch(url, options).then(async response => {
    if (response.status === 200) {
      let ct = response.headers.get("Content-Type");
      if (ct && (ct.includes("application/json") || ct.includes("text"))) {
        response
          .clone()
          .text()
          .then(content => {
            sessionStorage.setItem(cacheKey, content);
          });
      }
    }
    return response;
  });
};

它使用sessionStorage 来缓存结果。

我正在向 Github API 发出请求。思路很简单,有一个Input和一个p标签,Input有一个事件监听器来监听输入变化并使用输入值获取github用户名,p会渲染页面上的名称。

竞态条件可能发生在以下情况:

  1. 用户在输入字段中输入jack,因为这是用户第一次输入jack,所以结果不会被缓存。将发出请求以获取此 jack 用户的 Github 个人资料
  2. 然后用户在输入字段中输入david,因为这也是用户第一次输入david,所以结果不会被缓存。将发出请求以获取此 david 用户的 Github 个人资料
  3. 最后用户第二次在输入字段中输入jack,因为结果已经在缓存中。将发出 no 请求,我们可以从 sessionStorage 中读取用户配置文件并立即呈现结果。

然后您可以想象,如果第二个请求,即获取david 的配置文件的请求时间过长,用户将看到david 最终成为页面上呈现的最终结果,即使他/她最后一个搜索的是jack。这是因为jack 的结果被david 的结果覆盖,这需要更长的时间才能返回。

在我的例子中,我使用这个函数来模拟用户打字

async function userTyping() {
  sessionStorage.clear();
  inputEl.value = "jack";
  inputEl.dispatchEvent(new Event("input"));

  await sleep(100);
  inputEl.value = "david";
  inputEl.dispatchEvent(new Event("input"));

  await sleep(100);
  inputEl.value = "jack";
  inputEl.dispatchEvent(new Event("input"));
}

sleep 函数定义为

const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));

现在我能想到的是使用 debounce 来避免用户输入速度过快的情况。但是它并没有解决根本层面的问题。

我们还可以使用一些全局变量来跟踪最新的输入值,并使用它来检查我们要渲染的结果是否来自最新的输入值。不知何故,我只是不认为这是解决这个问题的优雅解决方案。

欢迎提出任何建议。

【问题讨论】:

  • 返回查询和响应。然后检查查询是否与当前输入值匹配,如果匹配则只呈现响应。
  • @Barmar 您能否通过编写解决方案或解决方案的某些部分来详细说明解决方案?不确定我是否理解您所说的“将查询与响应一起返回”的意思
  • 我认为CertainPerformance 的回答与我的想法一致。

标签: javascript html http caching frontend


【解决方案1】:

您也许可以使用 AbortController。它是实验性的,尚未添加到所有浏览器中(在 IE 中缺失)。

https://developer.mozilla.org/en-US/docs/Web/API/AbortController

创建一个 AbortController 实例。

const controller = new AbortController();
const signal = controller.signal;

并将其连接到您的 fetch。

return fetch(url, { ...options, signal }).then(async response => ...

然后当你从缓存中返回一些东西时取消请求。

if (cached !== null) {
  controller.abort();
  ...
}

【讨论】:

  • 感谢您的回复。有趣的是,我正在阅读一些关于中止获取请求的文章并遇到了这个AbortController。这个AbortController 看起来真的很晦涩,我问了我当地的一些朋友,他们多年来一直担任前端工程师编码,他们之前没有见过或使用过这些东西
  • 我自己没用过。但似乎您可以将它连接到多个正在进行的获取 stackoverflow.com/questions/48998013/… 。所以它可能是您的案例的一个很好的解决方案。
  • 你在你的开发生涯中遇到过这种类型的竞态条件吗?不使用AbortController你是怎么解决的?
  • 我已经按照您之前的建议制作了去抖动功能。为了在您停止输入一段时间后才执行该操作。正如您所说,这不是一个很好的解决方案,当从缓存返回某些内容时,能够取消所有已启动的请求会很酷。
【解决方案2】:

您可以将当前的e.target.value 保存在输入处理程序内的变量中。然后,一旦cachedFetch 响应返回,检查输入字段中是否仍然存在相同的值。仅当值匹配时才设置输入字段。

(如果值不匹配,例如输入是a,然后是b,然后是a,并且b 请求需要更长的时间才能完成,那么b 将存储在缓存中,但不会显示给用户)

另外,请确保仅在未发生错误时向用户显示结果:

inputEl.addEventListener("input", e => {
  const { value } = e.target;
  if (value === "") {
    return;
  }
  const url = endpoint + value;
  cachedFetch(url)
    .then(response => response.json())
    .then((result) => {
      if (e.target.value === value) {
        resultContainer.innerHTML = result.name;
      }
    })
    .catch(errorHandler);
});

【讨论】:

  • 所以e.target.value总是反映最新的输入值,我们可以用它来检查当前输入值是否等于请求时的输入值?凉爽的。我想过类似的事情,使用某种全局变量来跟踪最新的输入值。但我只是认为这个解决方案不够优雅。
  • 我并不是说您的解决方案不优雅。我是说使用全局变量不够优雅。谢谢你的回复
猜你喜欢
  • 2019-06-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-01-30
  • 2010-09-25
相关资源
最近更新 更多