@zenweb/cache
Version:
Zenweb Cache module
270 lines (269 loc) • 8.92 kB
JavaScript
import { gzip, unzip } from 'node:zlib';
import { promisify } from 'node:util';
import { Locker } from './locker.js';
import { debug, sleep } from './utils.js';
const compress = promisify(gzip);
const decompress = promisify(unzip);
const GZ_HEADER = Buffer.from([0x1F, 0x8B, 0x08]);
const _getDebug = debug.extend('get');
const _setDebug = debug.extend('set');
const _lockDebug = debug.extend('lock');
/**
* 缓存结果
*/
export class CacheResult {
compressed;
data;
/**
* @param compressed 是否为压缩数据
* @param data 数据
*/
constructor(compressed, data) {
this.compressed = compressed;
this.data = data;
}
}
/**
* 对象缓存系统
*/
export class Cache {
redis;
option;
constructor(redis, option) {
this.redis = redis;
this.option = option;
}
/**
* 取得缓存剩余有效期
*/
ttl(key) {
return this.redis.ttl(key);
}
/**
* 删除缓存
*/
del(key) {
return this.redis.del(key);
}
/**
* 直接设置缓存 - 不经过序列化的数据
* @param key
* @param value
* @param ttl 缓存有效时长 (秒),不设置取默认设置
*/
setRaw(key, value, ttl) {
if (ttl && ttl > 0) {
return this.redis.set(key, value, 'EX', ttl);
}
return this.redis.set(key, value);
}
/**
* 缓存对象
* @param key 缓存key
* @param value 缓存值,除了 `Buffer` 以外的值会经过序列化
* @param ttlopt 缓存有效时长 (秒),不设置取默认设置 | 缓存选项
*/
async set(key, value, ttlopt) {
let compressed;
let data = Buffer.isBuffer(value) ? value : this.option.serializer.serialize(value);
const opt = Object.assign({}, this.option.set, typeof ttlopt === 'object' ? ttlopt : undefined);
if (typeof ttlopt === 'number') {
opt.ttl = ttlopt;
}
_setDebug('[%s] data length:', key, data.length);
if (opt.compressMinLength && data.length >= opt.compressMinLength) {
let _compressed = await compress(data, { level: opt.compressLevel });
_setDebug('[%s] compressed length: %d', key, _compressed.length);
if (_compressed.length < (data.length * (opt.compressStoreRatio || 0.95))) {
compressed = _compressed;
}
}
await this.setRaw(key, compressed || data, opt.ttl);
return {
data,
compressed,
};
}
/**
* 直接取得缓存
*/
getRaw(key) {
return this.redis.getBuffer(key);
}
async get(key, opt) {
let data = await this.getRaw(key);
let _opt = Object.assign({ parse: true, decompress: true }, opt);
if (data) {
// 解压处理
let compressed = GZ_HEADER.equals(data.subarray(0, GZ_HEADER.length));
if (compressed) {
_getDebug('[%s] is compressed', key);
if (_opt.parse || _opt.decompress) {
_getDebug('[%s] decompress', key);
data = await decompress(data);
compressed = false;
}
}
// 解析处理
if (_opt.parse) {
_getDebug('[%s] parse', key);
return this.option.serializer.deserialize(data);
}
return new CacheResult(compressed, data);
}
}
lockGet(key, fetch, opt) {
const _opt = Object.assign({}, this.option.set, this.option.lockGet, opt);
return new LockGet(this, key, fetch, _opt).get();
}
/**
* 单例执行,在并发情况下保证同一个 key 只会单独执行,防止同时处理
* @param key 锁key
* @param run 获得锁时调用
*/
async singleRunner(key, run, _opt) {
const opt = Object.assign({}, this.option.lockGet, _opt);
const locker = new Locker(this.redis, key, opt.lockTimeout);
const retryTimeout = typeof opt.retryTimeout === 'number' ? opt.retryTimeout : 0;
const retryDelay = (opt.retryDelay === undefined || opt.retryDelay < 0) ? 500 : opt.retryDelay;
let retryTime = 0;
while (true) {
// 取得锁并执行
if (await locker.acquire()) {
try {
return await run();
}
finally {
await locker.release();
}
}
// 无法取得锁是否重复尝试
if (retryTime >= retryTimeout) {
throw new Error('Unable to acquire lock');
}
else {
retryTime += retryDelay;
await sleep(retryDelay);
}
}
}
}
class LockGet {
cache;
key;
fetch;
opt;
locker;
constructor(cache, key, fetch, opt) {
this.cache = cache;
this.key = key;
this.fetch = fetch;
this.opt = opt;
}
/**
* 获取数据并设置到缓存中
*/
async fetchAndSet() {
const obj = await this.fetch();
// 本地存储
if (this.opt.localStore) {
this.opt.localStore[this.key] = obj;
}
const result = await this.cache.set(this.key, obj, this.opt);
if (this.opt.parse !== false) {
return obj;
}
return new CacheResult(!this.opt.decompress && Boolean(result.compressed), !this.opt.decompress && result.compressed || Buffer.from(result.data));
}
/**
* 预刷新处理
*/
async preRefresh(preRefresh) {
const ttl = await this.cache.ttl(this.key);
_lockDebug('[%s] preRefresh remain: %d', this.key, ttl);
if (ttl < preRefresh) {
_lockDebug('[%s] preRefresh -> fetchAndSet', this.key);
await this.fetchAndSet();
}
}
/**
* 从缓存中取得数据
*/
async getCache() {
// 本地存储
if (this.opt.localStore && this.key in this.opt.localStore) {
return this.opt.localStore[this.key];
}
// 尝试从 redis 中获取
const data = await this.cache.get(this.key, this.opt);
if (data !== undefined) {
if (!this.locker && this.opt.preRefresh && this.opt.preRefresh > 0) {
this.preRefresh(this.opt.preRefresh);
}
// 本地存储
if (this.opt.localStore && typeof this.opt.localStore === 'object') {
this.opt.localStore[this.key] = data;
}
return data;
}
}
async tryFetch() {
const retryTimeout = typeof this.opt.retryTimeout === 'number' ? this.opt.retryTimeout : 5000;
const retryDelay = (this.opt.retryDelay === undefined || this.opt.retryDelay < 0) ? 500 : this.opt.retryDelay;
let retryTime = 0;
while (true) {
if (!this.locker) {
this.locker = new Locker(this.cache.redis, `${this.key}.LOCK`, this.opt.lockTimeout);
}
else {
// 尝试从 redis 中获取
const data = await this.getCache();
if (data !== undefined) {
return data;
}
}
// 获取锁
if (await this.locker.acquire()) {
_lockDebug('[%s] acquire success', this.key);
try {
return await this.fetchAndSet();
}
finally {
await this.locker.release();
}
}
else {
// 不等待为 true 不需要重试,说明有其他请求更新数据
if (this.opt.noWait === true) {
_lockDebug('[%s] no wait', this.key);
return;
}
// 获取锁失败,有其他请求更新数据,等待后重新获取获取数据
if (retryTime >= retryTimeout) {
throw new Error('Unable to acquire lock: ' + this.key);
}
else {
retryTime += retryDelay;
await sleep(retryDelay);
}
}
}
}
async get() {
// 如果强制刷新则不需要尝试从缓存获取,直接进入锁获取并取得数据
const refresh = this.opt.refresh || false;
if (!refresh) {
// 先尝试从缓存获取
const data = await this.getCache();
if (data !== undefined) {
return data;
}
}
// 不等待为 true 直接返回 undefined
if (this.opt.noWait === true) {
this.tryFetch();
return;
}
return this.tryFetch();
}
}