简体   繁体   English

进行缓存的 Ajax 调用时如何避免竞争条件

[英]How can I avoid race condition when making cached Ajax calls

Since many public APIs such as GitHub public API has a request limit, so it makes sense for us to implement some cache mechanism to avoid unnecessary request calls.由于 GitHub public API 等许多公共 API 都有请求限制,因此我们有必要实现一些缓存机制以避免不必要的请求调用。 However I discovered that this might incur race condition.但是我发现这可能会导致竞争条件。

I coded up an example to demonstrate the situation https://codesandbox.io/s/race-condition-9kynm?file=/src/index.js我编写了一个示例来演示情况https://codesandbox.io/s/race-condition-9kynm?file=/src/index.js

Here I first implement a cachedFetch ,这里我首先实现一个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;
  });
};

It uses sessionStorage to cache the results.它使用sessionStorage来缓存结果。

And I am making the requests to Github API.我正在向 Github API 提出请求。 The idea is simple, there is a Input and a p tag, and the Input has a event listener to listen for input changes and uses the input value to get the github user's name and the p will render the name on the page.思路很简单,有一个Input和一个p标签,而Input有一个事件监听器来监听输入变化,并使用输入值来获取github用户的名字, p会在页面上渲染这个名字。

The race condition might occur in the following situation:竞争条件可能发生在以下情况:

  1. User types jack in the input field, since this is the first time the user types jack so the result is not cached.用户在输入字段中输入jack ,因为这是用户第一次输入jack ,所以结果不会被缓存。 The request will be made to fetch this jack user's Github profile将请求获取此jack用户的 Github 配置文件
  2. Then the user types david in the input field, since this is also the first time the user types david so the result is not cached.然后用户在输入字段中输入david ,因为这也是用户第一次输入david ,所以结果不会被缓存。 The request will be made to fetch this david user's Github profile将请求获取此david用户的 Github 配置文件
  3. Finally the user types jack in the input field for the second time, since the result is already in the cache.最后,用户第二次在输入字段中键入jack ,因为结果已经在缓存中。 The no request will be made and we can read the user profile from sessionStorage and render the result immediately.将发出 no 请求,我们可以从 sessionStorage 读取用户配置文件并立即呈现结果。

Then you can image that, if the second request, ie request to fetch david 's profile takes too long, user will see david end up being the final result rendered on the page even if his/her last search was for jack .然后你可以想象,如果第二个请求,即获取david个人资料的请求花费太长时间,用户将看到david最终成为页面上呈现的最终结果,即使他/她的最后一次搜索是对jack This is because jack 's result got overridden by the david 's result which takes much longer to get back.这是因为jack的结果被david的结果覆盖,这需要更长的时间才能返回。

In my example, I used this function to simulate the user typing在我的示例中,我使用了这个 function 来模拟用户打字

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"));
}

the sleep function is defined as sleep function 定义为

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

Right now what I can think of is using debounce to avoid the situation when the user is typing too fast.现在我能想到的是使用 debounce 来避免用户输入速度过快的情况。 However it doesn't solve the problem in the fundamental level.但是,它并没有从根本上解决问题。

Also we can use some global variable to keep track of the latest input value, and use that to check if the result we are about to render is from the latest input value.我们也可以使用一些全局变量来跟踪最新的输入值,并使用它来检查我们即将渲染的结果是否来自最新的输入值。 Somehow I just don't think this is an elegant solution to this problem.不知何故,我只是不认为这是解决这个问题的一个优雅的解决方案。

Any suggestions are appreciated.任何建议表示赞赏。

You can save the current e.target.value in a variable inside the input handler.您可以将当前e.target.value保存在输入处理程序内的变量中。 Then, once the cachedFetch response comes back, check if the same value is still in the input field.然后,一旦cachedFetch响应返回,检查输入字段中是否仍然存在相同的值。 Only set the input field if the values match.仅当值匹配时才设置输入字段。

(If the values don't match, like if the input is a , then b , then a , and it takes the b request a longer time to finish, then b will be stored in the cache, but it won't be displayed to the user) (如果值不匹配,例如输入是a ,然后是b ,然后a ,并且b请求需要更长的时间才能完成,则b将存储在缓存中,但不会显示给用户)

Also, make sure to only display the result to the user when an error does not occur:此外,请确保仅在未发生错误时向用户显示结果:

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);
});

You might be able to use the AbortController.您也许可以使用 AbortController。 It's experimental and not added to all browsers yet (missing in IE).它是实验性的,尚未添加到所有浏览器中(在 IE 中缺失)。

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

Create an AbortController instance.创建一个 AbortController 实例。

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

And connect it to your fetch.并将其连接到您的获取。

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

And then cancel the request when you return something from the cache.然后当您从缓存中返回某些内容时取消请求。

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM