文章目录加载中

缓存设计-基于LRU的异步缓存续期

# 场景

基于 LRU 的缓存是否失效的指标是:是否为最近使用。

在此基础上,新增了 maxAge 字段,表示缓存有效期,数据结构:

maxAge: number // 有效期
data: any // 缓存

为什么增加缓存有效期?

有些数据被频繁访问,按照 LRU 策略,不会失效。

但是数据需要刷新,否则会失去实效性,因此新增一个有效期。

如果过期,强行刷存。

什么时候需要自动续期?

当缓存过期后,去请求接口,更新缓存。如果接口失效,那么需要自动续期。

这种情况一般后端接口挂了,不自动续期,会导致雪崩,降低可用率。

# 设计思路

新的数据结构设计:

maxAge: number // 有效期
data: any // 缓存
finalExpiration: number // 最终过期时间

在当前时间~有效期之间:缓存有效无需刷新。

在有效期~最终过期时间:缓存失效,可以刷新,自动续期。

在最终过期时间后:不能自动续期。

对于有效期~最终过期,支持两种刷新:

  • 同步刷新:阻塞等待接口返回,成功,更新缓存,返回最新结果;失败,返回最新结果。
  • 异步刷新:非阻塞,直接返回旧缓存;异步获取请求结果,成功则更新缓存。

对于「刷新」操作,需要从外界传入回调函数。

# NestJS 实现

import { Injectable, Scope } from '@nestjs/common';
import QuickLRU from 'quick-lru';

@Injectable({ scope: Scope.TRANSIENT })
export class CacheService {
  private _cache: QuickLRU<string, CacheData>;
  private _ttl: number; // 缓存有效期,默认为1分钟

  constructor(ttl = 60 * 1000) {
    this._cache = new QuickLRU({ maxSize: 1000 });
    this._ttl = ttl;
  }

  /**
   * 设置缓存
   *
   * @param {any} key 缓存标识
   * @param {any} value 缓存的值
   * @param {number} finalExpiration 缓存最终过期时间,默认为 Infinity
   */
  public set(key, value, finalExpiration?: number) {
    const ts = Date.now();
    // 最终过期时间 >= 过期时间
    finalExpiration =
      typeof finalExpiration === 'number' && finalExpiration >= ts + this._ttl
        ? finalExpiration
        : Infinity;

    this._cache.set(key, {
      ts,
      value,
      finalExpiration
    });
  }

  /**
   * 读取缓存
   *
   * @param {any} key 缓存标识
   */
  public get(key) {
    const data = this._cache.get(key);
    if (!data) {
      // 没缓存
      return;
    }
    const { ts, value } = data;
    const now = new Date().getTime();
    if (now > ts + this._ttl) {
      // 有缓存,但是已经超过 TTL,应该把缓存清掉
      this._cache.delete(key);
      return;
    } else {
      return value;
    }
  }

  /**
   * 读取缓存,缓存过期自动回源,回源成功则自动续期
   *
   * @param {any} key 缓存标识
   * @param {Function} fn 数据回源函数,返回一个 Promise 对象
   * @param {number} finalExpiration 回源获得的缓存的最终过期时间,默认为 Infinity
   * @param {boolean} isAsync 是否异步回源,默认异步
   */
  public async getWithBack(key, fn: IFunction<any>, finalExpiration?: number, isAsync = true) {
    const data = this._cache.get(key);
    if (!data) {
      return;
    }

    const now = Date.now();
    // 情况1: 缓存未过期
    if (now <= data.ts + this._ttl) {
      return data.value;
    }

    // 情况2: 缓存过期,并且超过了最大过期时间
    if (now > data.finalExpiration) {
      this._cache.delete(key);
      return;
    }

    // 情况3: 缓存过期,但是没有超过最大过期时间
    if (isAsync) {
      // 异步回源续期
      fn()
        .then(value => this.set(key, value, finalExpiration))
        .catch(error => {
          // ignore error
        });

      return data.value;
    } else {
      // 同步回源续期
      try {
        const value = await fn();
        this.set(key, value, finalExpiration);
        return value;
      } catch (error) {
        // ignore error
        return data.value;
      }
    }
  }
}

interface CacheData {
  ts: number; // 生成时间
  finalExpiration: number; // 最终过期时间
  value: any; // 存储的值
}

interface IFunction<T> {
  (...args: any): Promise<T>;
}
本文来自心谭博客:xin-tan.com,经常更新web和算法的文章笔记,前往github查看目录归纳:github.com/dongyuanxin/blog