文章

Nest.js实现本地缓存异步续期

Nest.js实现本地缓存异步续期

场景

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

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

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

为什么增加缓存有效期?

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

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

如果过期,强行刷存。

什么时候需要自动续期?

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

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

设计思路

新的数据结构设计:

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

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

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

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

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

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

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

NestJS 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
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>;
}
本文由作者按照 CC BY 4.0 进行授权