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