文章目录加载中
缓存设计-基于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