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

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

Tags
Nest.js
缓存设计
LRU算法
CreatedTime
Aug 23, 2022 10:16 AM
Slug
2020-08-23-cache-lru
UpdatedTime
Last updated August 23, 2022

场景

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