在一些业务中,要从redis获取一个超大的字符串。为了避免频繁从redis获取数据,需要在内存中实现对数据的缓存。
实现
1. 从内存缓存读取数据,没有发现数据,则从redis获取数据并设置过期时间。
2. 从内存缓存读取数据,发现数据而没有过期,直接返回。
3. 从内存缓存读取数据,发现数据过期,重新从redis获取数据并设置过期时间。注意:需要比较从redis的数据时候和缓存数据一致,不一致才替换缓存数据,否则会出现2份内容一样的数据。即之前拿过的数据和内存缓存的数据是内容一样的,由于redis的数据没有变化,但是内存缓存的数据过期,重新创建一个一模一样的数据。
4. 定时检查过期数据,发现数据过期,直接从内存缓存中删除。防止缓存数据一直在内存中。
5. 从内存缓存读取数据,需要加锁,防止异步读取redis数据时,Nodejs再次切换函数,再次发现数据不存在或者数据过期,再次异步读取redis数据。避免频繁读取redis数据。这里需要使用async-lock,保证读取redis是按队列一个个读取,即判断数据不存在或者数据过期,需要之前读取redis并设置到内存缓存后,才能去判断数据不存在或者数据过期的情况。
/**
* 简单地通过计数来实现共享缓存对象
*/
import { DeviceValidator } from './device_validator'
import * as AsyncLock from 'async-lock'
import { isDeepStrictEqual } from 'util';
interface CacheInfo {
data: any
expire: number
}
const cache = new Map<string, CacheInfo>();
// 定时删除过期的键。
setInterval(() => {
let count = 0;
const now = Date.now()
for (const [key, info] of cache) {
// 发现过期要清理
if (info.expire <= now) {
cache.delete(key);
count++;
// 每次只删除20个键,减少出现undefined的情况。
if (count >= 20) {
break;
}
}
}
}, 5000);
function getOrCreateCacheInfo(key: string, expire: number) {
let cacheInfo = cache.get(key);
if (!cacheInfo) {
cacheInfo = { data: undefined, expire };
cache.set(key, cacheInfo);
}
return cacheInfo
}
export function deleteCache(key: string) {
cache.delete(key);
}
export function updateCacheExpire(key: string, expire: number) {
const cacheInfo = cache.get(key);
if (cacheInfo) {
cacheInfo.expire = expire;
}
}
const lock = new AsyncLock({maxPending: 100000000})
export function devtypeCacheKey(owner: string, devtype: string) {
return `${owner}.${devtype}`
}
export async function getDevtypeCache(owner: string, devtype: string) {
const key = devtypeCacheKey(owner, devtype);
// 虽然JS是单线程,但是await后,getDevtypeCache会调用多次。这里需要锁住代码
return lock.acquire<any>(key, async () => {
const now = Date.now();
const expire = now + 30000;
const cacheInfo = getOrCreateCacheInfo(key, expire);
if (cacheInfo.data != undefined && cacheInfo.expire > now) {
// 这个data指向的引用没有变化,后续拿的对象都是指向同一个对象。
return cacheInfo.data;
}
// 设备类型变化不大,不使用redis的订阅发布来监听设备类型的变化。
const devTypeInfo = await DeviceValidator.getDevtype(owner, devtype);
if (!isDeepStrictEqual(devTypeInfo, cacheInfo.data)) {
cacheInfo.data = devTypeInfo;
}
cacheInfo.expire = expire;
return cacheInfo.data;
})
}
export function deviceCacheKey(owner: string, devtype: string, devid: string) {
return `${owner}.${devtype}.${devid}`
}
export async function getDeviceCache(owner: string, devtype: string, devid: string) {
const key = deviceCacheKey(owner, devtype, devid);
return lock.acquire<any>(key, async () => {
const now = Date.now();
const expire = now + 10000;
const cacheInfo = getOrCreateCacheInfo(key, expire)
if (cacheInfo.data != undefined && cacheInfo.expire > now) {
return cacheInfo.data;
}
const deviceInfo = await DeviceValidator.getDevice(owner, devtype, devid);
if (!isDeepStrictEqual(deviceInfo, cacheInfo.data)) {
cacheInfo.data = deviceInfo;
}
cacheInfo.expire = expire;
return cacheInfo.data;
})
}