使用缓存优化异步任务:解决复杂应用中的性能瓶颈

254 阅读4分钟

在现代的前端开发中,随着单页面应用(SPA)和动态交互界面的普及,如何有效地管理和优化异步操作(如网络请求、数据库访问等)成为了开发者们的一大挑战。尤其是在面对高频、重复执行的异步任务时,如果每次都重新执行这些任务,势必会造成性能瓶颈,浪费服务器资源,增加用户等待时间。

场景一:网络请求的重复调用

假设我们正在开发一个xx产品展示平台,用户在浏览产品详情时,每次打开页面都会触发一个请求来获取产品的规格参数或者其他附加信息,当信息量非常多时,如果没有缓存机制,那么每次用户切换产品或者刷新页面时,都会重新请求相同的数据。这不仅浪费了带宽和资源,还极大影响了用户体验——特别是在网络不稳定或者请求的延迟较高的情况下。

场景二:计算密集型任务的重复执行

又比如说,我们有一个复杂的图形渲染系统,用户进行了一次重计算,系统花费了几秒钟的时间来生成结果。如果用户在短时间内进行了多次相似的操作,系统会重复计算相同的内容,导致计算资源的浪费。我们是否可以缓存计算结果,避免重复计算,从而提高应用的响应速度?

这两个场景背后的问题,都是“相同任务的重复执行”导致了性能浪费。那么,如何在复杂的异步任务中避免这种重复执行呢?解决方案之一就是引入缓存机制,缓存已经执行过的异步任务结果,下次直接返回缓存结果,而无需重新执行。

缓存机制的基本思想

在异步任务中,缓存机制的基本原理是:对于那些可以通过相同输入获得相同输出的任务,我们可以将任务结果缓存起来。当任务再次请求时,直接从缓存中获取结果,而不是重新执行任务。这种方式在很多场景下都能大大提升性能,减少冗余操作。

然而,要实现这个功能,我们需要解决一些常见问题:

  1. 如何确定任务是否已执行过?
  2. 如何为每个任务生成唯一的标识符(key)?
  3. 如何管理缓存的生命周期?(例如,缓存是否需要过期?)
  4. 如何避免任务之间的竞争条件(race condition)?

这些问题看似复杂,但我们可以通过一定的设计模式和技巧来巧妙地解决。

解决方案:基于键值缓存的异步任务管理

下面这段代码展示了一种通过缓存来优化异步任务执行的通用方法。我们可以用它来避免重复执行相同的任务,提升应用的性能。

export function uniAsyncTaskByKey(asyncTask, getKey) {
  if (typeof getKey !== "function") {
    throw new Error("params 2 should be a funtion");
  }
  let taskIns = new Map();

  const wrap = (...args) => {
    const key = getKey(...args);
    if (!["string", "symbol", "number"].includes(typeof key)) {
      console.warn(
        `No optimization took effect! The key was expected to be a string, symbol, or number type, but got ${typeof key}`
      );
      return asyncTask(...args); // Directly invoke asyncTask if no valid key
    }

    if (!taskIns.has(key)) {
      const task = asyncTask(...args);
      taskIns.set(key, task);
      task.finally(() => {
        taskIns.delete(key); // Ensure cleanup on finish
      });
    }
    return taskIns.get(key);
  };
  return wrap;
}

export function cacheAsyncTaskByKey(asyncTask, getKey, ttl = 0) {
  if (typeof getKey !== "function") {
    throw new Error("params 2 should be a funtion");
  }

  const cache = new Map();

  const wrap = async (...args: P) => {
    const key = getKey(...args);
    if (!["string", "symbol", "number"].includes(typeof key)) {
      console.warn(
        `No optimization took effect! The key was expected to be a string, symbol, or number type, but got ${typeof key}`
      );
      return asyncTask(...args); // Directly invoke asyncTask if no valid key
    }

    let cached = cache.get(key);

    // Cache miss or cache expired
    if (!cached || (ttl > 0 && Date.now() - cached.timestamp > ttl)) {
      try {
        const result = await asyncTask(...args);
        if (result !== void 0 && key !== void 0) {
          cache.set(key, { result, timestamp: Date.now() });
        }
      } catch (e) {
        return Promise.reject(e);
      }
    }
    return cache.get(key).result;
  };
  wrap.latest = (...args: P) => {
    const key = getKey(...args);
    if (!["string", "symbol", "number"].includes(typeof key)) {
      console.warn(
        `No optimization took effect! The key was expected to be a string, symbol, or number type, but got ${typeof key}`
      );
      return asyncTask(...args);
    }
    cache.delete(key); // Manually clear cache if needed
    return wrap(...args);
  };
  return wrap;
}

export function uniCacheAsyncTaskByKey(asyncTask, getKey, ttl) {
  return cacheAsyncTaskByKey(uniAsyncTaskByKey(asyncTask, getKey), getKey, ttl);
}

代码解释

1. uniAsyncTaskByKey

功能:

uniAsyncTaskByKey 函数将异步任务(asyncTask)和一个键生成函数(getKey)封装起来,确保对相同的键只执行一次异步任务。它会根据传入的参数生成一个唯一的键,然后缓存异步任务的执行结果。对于相同的键,后续的调用会复用已经执行完成的任务,而不是重新发起任务。

关键点:

  • 去重执行:相同的键只会触发一次异步任务,避免重复调用。
  • 任务清理:任务完成后会从 taskIns 中删除,以便下次能够重新执行。

2. cacheAsyncTaskByKey

功能:

cacheAsyncTaskByKey 函数实现了异步任务的 缓存功能,可以缓存异步任务的结果。通过 ttl(缓存过期时间)来控制缓存的有效期。如果缓存未过期,则直接返回缓存中的结果;否则,重新执行异步任务并更新缓存。

关键点:

  • 缓存机制:异步任务的结果会被缓存,根据提供的 ttl 值来决定缓存何时过期。
  • 缓存失效:如果缓存过期或者不存在缓存,会重新执行异步任务,并缓存结果。

3. uniCacheAsyncTaskByKey

功能:

uniCacheAsyncTaskByKey 是一个结合了 uniAsyncTaskByKey 和 cacheAsyncTaskByKey 的函数。它首先使用 uniAsyncTaskByKey 对异步任务进行去重,然后再将结果缓存,形成一个统一的缓存机制。

关键点:

  • 组合优化:结合去重和缓存,确保异步任务既不会重复执行,也能缓存结果,进一步提升性能。

  • 灵活性:缓存的有效期由 ttl 决定,可以灵活控制缓存失效的时间。

这段代码解决了什么问题?

  1. 任务去重: 通过 Map 存储每个任务的执行状态,当任务被执行时,会首先检查该任务的 key 是否已经存在。如果已存在,直接返回缓存中的结果,而不会重新执行任务。这样就能有效避免重复执行相同任务。
  2. 缓存管理: 在 cacheAsyncTaskByKey 函数中,我们引入了 ttl(Time To Live,存活时间)机制,使得缓存的结果能够在指定时间后过期。如果缓存中的数据已经过期,则重新执行任务并更新缓存。
  3. 类型安全与灵活性: 使用了 TypeScript 的类型推导(文章最后提供了ts版本),确保每个参数和返回值的类型安全。同时,wrap.latest 方法可以手动清除缓存,这为开发者提供了更多的灵活性。

如何在实际项目中应用?

  1. 防止重复网络请求: 如果你的应用频繁向后端发送相同的请求,缓存结果将减少无效请求。例如,在产品详情页上,用户可能频繁刷新页面或切换不同的产品,使用缓存机制可以避免重复请求相同的产品数据。
  2. 避免重复计算: 如果你的应用涉及到复杂的计算(比如图形渲染、数据分析等),可以将计算结果缓存下来。如果用户进行了相同的操作,就直接返回缓存结果,而不必重新计算。
  3. 节省带宽和服务器资源: 通过减少重复请求,可以显著降低带宽消耗,减轻服务器负担,提升系统的整体性能,尤其是在高并发的场景下。
  4. 提升用户体验: 缓存机制能够减少任务的执行时间,使得用户能够更快地得到响应,特别是在网络环境不佳或者操作频繁时,能够有效减少等待时间,提升用户体验。

总结

通过这段代码,我们可以实现一个简单且高效的缓存机制,来优化异步任务的执行。它不仅能减少重复执行的开销,还能管理缓存的生命周期,保证数据的时效性。无论是在网络请求、计算任务,还是复杂的图形渲染应用中,这种缓存机制都能起到至关重要的作用。通过灵活的缓存管理,帮助开发者提升性能、节省资源,并为用户提供更流畅的体验。

在实际开发中,你可以根据具体的业务场景调整缓存过期时间,或者根据任务的特性选择合适的缓存策略。希望这篇文章能帮助你在项目中轻松实现异步任务缓存优化,让你的应用跑得更快,工作更高效!

ts版本:

type AsyncTask<P extends any[], R> = {
  (...args: P): Promise<R>;
};

type GetKeyFn<P extends any[]> = {
  (...args: P): string | number | symbol;
};

export function uniAsyncTaskByKey<P extends any[], R>(
  asyncTask: AsyncTask<P, R>,
  getKey: GetKeyFn<P>
) {
  if (typeof getKey !== "function") {
    throw new Error("params 2 should be a funtion");
  }
  let taskIns = new Map<ReturnType<typeof getKey>, Promise<R>>();

  const wrap = (...args: P) => {
    const key = getKey(...args);
    if (!["string", "symbol", "number"].includes(typeof key)) {
      console.warn(
        `No optimization took effect! The key was expected to be a string, symbol, or number type, but got ${typeof key}`
      );
      return asyncTask(...args); // Directly invoke asyncTask if no valid key
    }

    if (!taskIns.has(key)) {
      const task = asyncTask(...args);
      taskIns.set(key, task);
      task.finally(() => {
        taskIns.delete(key); // Ensure cleanup on finish
      });
    }
    return taskIns.get(key) as Promise<R>;
  };
  return wrap;
}

export function cacheAsyncTaskByKey<P extends any[], R>(
  asyncTask: AsyncTask<P, R>,
  getKey: GetKeyFn<P>,
  ttl = 0
) {
  if (typeof getKey !== "function") {
    throw new Error("params 2 should be a funtion");
  }

  const cache = new Map<
    ReturnType<typeof getKey>,
    {
      timestamp: number;
      result: Awaited<R>;
    }
  >();

  const wrap = async (...args: P) => {
    const key = getKey(...args);
    if (!["string", "symbol", "number"].includes(typeof key)) {
      console.warn(
        `No optimization took effect! The key was expected to be a string, symbol, or number type, but got ${typeof key}`
      );
      return asyncTask(...args); // Directly invoke asyncTask if no valid key
    }

    let cached = cache.get(key);

    // Cache miss or cache expired
    if (!cached || (ttl > 0 && Date.now() - cached.timestamp > ttl)) {
      try {
        const result = await asyncTask(...args);
        if (result !== void 0 && key !== void 0) {
          cache.set(key, { result, timestamp: Date.now() });
        }
      } catch (e) {
        return Promise.reject(e);
      }
    }
    return cache.get(key)!.result;
  };
  wrap.latest = (...args: P) => {
    const key = getKey(...args);
    if (!["string", "symbol", "number"].includes(typeof key)) {
      console.warn(
        `No optimization took effect! The key was expected to be a string, symbol, or number type, but got ${typeof key}`
      );
      return asyncTask(...args);
    }
    cache.delete(key); // Manually clear cache if needed
    return wrap(...args);
  };
  return wrap;
}

export function uniCacheAsyncTaskByKey<P extends any[], R>(
  asyncTask: AsyncTask<P, R>,
  getKey: GetKeyFn<P>,
  ttl?: number
) {
  return cacheAsyncTaskByKey(uniAsyncTaskByKey(asyncTask, getKey), getKey, ttl);
}