UNPKG

@zenweb/cache

Version:
270 lines (269 loc) 8.92 kB
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(); } }