如何使用缓存提高请求性能?

1,962 阅读5分钟

什么是缓存?

在计算机科学中,缓存是一种在计算机内存中暂存数据的技术,以加快对这些数据的访问速度。通过缓存,可以把经常用到的数据暂存在高速缓存中,以便下次访问时可以更快地获取。这种技术被广泛应用于计算机的各个领域,包括数据库、网页浏览器等。

为什么需要缓存?

当我们的系统需要频繁地访问同一个数据时,每次都从磁盘或者网络获取数据显然效率很低。因此,我们需要将这些数据暂存到内存中,以提高访问速度。这就是缓存的作用。

需求

1、有白名单,标记仅对哪些接口做缓存处理
2、在5秒对同一个接口的请求,只会发起一次,但是其他重复的请求的响应体依旧有效(并且在5秒后清除相应缓存)

同一个 url ,一样的入参,一样的请求方式为同一个接口

代码实现

下面是一个使用缓存的例子:

const cache = {}; // 缓存对象

function fetchData(url) {

  if (cache[url]) { // 如果缓存中存在该数据

    return cache[url]; // 直接返回缓存中的数据

  } else { // 如果缓存中不存在该数据

    const data = fetchDataFromDiskOrNetwork(url); // 从磁盘或者网络中获取数据

    cache[url] = data; // 将数据存入缓存

    return data; // 返回数据

  }

}

这段代码中,cache 是一个对象,用来存储数据的缓存。fetchData 函数接收一个 url 参数,用来指定要获取数据的位置。首先,它会检查 cache 中是否已经存在该数据,如果存在就直接返回缓存中的数据;如果不存在,则从磁盘或者网络中获取数据,并将数据存入 cache 中,然后返回数据。

这段代码的实现非常简单,但也有一些缺陷。缓存的数据是不会过期的,如果服务端的数据更新,就会导致缓存中的数据过期,从而访问到旧的数据。

为了解决这些问题,我们需要对缓存进行优化。下面是一个更加完善的缓存实现,它支持设置缓存过期时间,同时还支持清理缓存和白名单等功能。

// 缓存对象
const cache = {};

const whiteList = ["app/appPage/getPageVersionById"]; // 白名单

// 缓存配置,可以在调用 cacheFetch 时传入
const defaultConfig = {
  cacheTime: 5 * 1000, // 缓存时间(毫秒)
};

export function cacheFetch(url, options = {}, config = {}) {

  // 合并默认配置和传入配置
  const trueConfig = Object.assign({}, defaultConfig, config);

  // 如果不需要缓存,则直接调用 fetch
  if (!shouldCache(url)) {
    return window.fetch(url, options);
  }

  const key = getCacheKey(url, options);
  const cached = cache[key];

  // 如果缓存存在且未过期,则直接返回缓存
  if (cached && !isExpired(cached, trueConfig.cacheTime)) {
    return Promise.resolve(cached.data);
  }

  // 如果缓存不存在或已过期,则调用 fetch,并将结果加入缓存
  const promise = window.fetch(url, options)
    .then((data) => {
      // 缓存时间到期后自动清除缓存
      setTimeout(() => {
        delete cache[key];
      }, trueConfig.cacheTime);
      // 将数据存入缓存
      cache[key] = { data, timestamp: Date.now()     };
      return data;
    });

  // 将 promise 存入缓存,以便多次调用时能返回同一个 promise
  cache[key] = { promise, timestamp: Date.now() };
  return promise;
}

// 判断是否需要缓存
function shouldCache(url) {
  // 只对在白名单中的接口进行缓存
  for (let i = 0; i < whiteList.length; i++) {
    if (url.includes(whiteList[i])) {
      return true;
    }
  }
  return false;
}

// 获取缓存键名
function getCacheKey(url, options) {
  return JSON.stringify({ url, options });
}

// 判断缓存是否过期
function isExpired(cached, cacheTime) {
  return Date.now() - cached.timestamp > cacheTime;
}

不难发现,上述的代码在网络响应较慢的时候,我们在 return Promise.resolve(cached.data); 的时候,data 可能是 undefined 。所以我们可以给请求加一个队列,如果同时发起5个请求,那么我们只发起一次,其他的 4 次都等同一个 Promise 返回。

// 缓存对象
const cache = {};
const whiteList = ['app/xxx']; // 白名单
// 队列
const pendingRequests = {};
// 缓存配置,可以在调用 fetchWithCache 时传入
const defaultConfig = {
  cacheTime: 5 * 1000, // 缓存时间(毫秒)
  capacity: 50, // 缓存容量
};

// 获取缓存键名
function getCacheKey(url, options) {
  return JSON.stringify({ url, options });
}

// 判断缓存是否过期
function isExpired(cached, cacheTime) {
  return !!(Date.now() - cached.timestamp > cacheTime);
}
// 判断是否需要缓存
function shouldCache(url) {
  // 只对在白名单中的接口进行缓存
  // eslint-disable-next-line no-plusplus
  for (let i = 0; i < whiteList.length; i++) {
    if (url.includes(whiteList[i])) {
      return true;
    }
  }
  return false;
}

// 清理缓存
function cleanCache(count) {
  const entries = Object.entries(cache);
  // 按缓存时间排序
  entries.sort((a, b) => a[1].timestamp - b[1].timestamp);
  // 删除旧的缓存
  // eslint-disable-next-line no-plusplus
  for (let i = 0; i < count && i < entries.length; i++) {
    delete cache[entries[i][0]];
  }
}

export function fetchWithCache(url, options = {}, config = {}) {
  // 合并默认配置和传入配置
  const finalConfig = Object.assign({}, defaultConfig, config);

  // 如果缓存不在容量范围内,则清理一部分缓存
  if (Object.keys(cache).length >= finalConfig.capacity) {
    cleanCache(finalConfig.capacity / 2);
  }

  // 如果不需要缓存,则直接调用 fetch
  if (!shouldCache(url)) {
    return window.fetch(url, options);
  }

  const key = getCacheKey(url, options);
  const cached = cache[key];

  // 如果缓存存在且未过期,则直接返回缓存
  if (cached && !isExpired(cached, finalConfig.cacheTime)) {
    return Promise.resolve(cached.data);
  }

  // 检查是否有等待的请求
  if (pendingRequests[key]) {
    return pendingRequests[key].then(data => {
      // 缓存时间到期后自动清除缓存
      setTimeout(() => {
        delete cache[key];
      }, finalConfig.cacheTime);
      // 将数据存入缓存
      cache[key] = { data, timestamp: Date.now() };
      return data.clone();
    });
  }

  // 如果缓存不存在或已过期,则调用 fetch,并将结果加入缓存
  const promise = window.fetch(url, options).then(data => {
    // 缓存时间到期后自动清除缓存
    setTimeout(() => {
      delete cache[key];
    }, finalConfig.cacheTime);
    // 将数据存入缓存
    cache[key] = { data: data.clone(), timestamp: Date.now() };
    return data.clone();
  });

  // 将请求添加到等待列表中
  pendingRequests[key] = promise;
  // 将 promise 存入缓存,以便多次调用时能返回同一个 promise
  // cache[key] = { promise, timestamp: Date.now() };
  return promise;
}

总结

以上就是本篇博客的全部内容。我们通过这段代码的解析,了解了缓存的一些基本知识和实现方式,以及如何进行缓存的清理和管理。缓存虽然可以提升性能,但也有一些需要注意的地方。比如,需要控制缓存容量和缓存时间,避免缓存过期或者过多占用内存等问题。

最后,需要注意的是,缓存并不是万能的。在某些情况下,缓存可能会导致错误的结果。因此,在使用缓存时,需要权衡利弊,避免产生不必要的问题。