仅仅做个笔记,自己经常用此缓存,做了大部分封装支持缓存时间以及过期自动删除
此篇博客会持续更新
//---------------------------------------------2022-10-05-----------------------------------------------------------
class Cache {
constructor({ defaultTtl = 60, scanFrequency = 300 }) {
this.TIME_UNITS = {
SECOND: 1,
MINUTE: 60,
HOUR: 60 * 60,
DAY: 24 * 60 * 60,
YEAR: 365 * 24 * 60 * 60,
};
this.defaultTtl = defaultTtl;
this.scanFrequency = scanFrequency;
this.cacheData = {};
this.loadCache();
if (this.scanFrequency > 0) {
// 启动定期扫描器以清理过期项
setInterval(() => {
this.cleanExpired();
}, this.scanFrequency * 1000);
}
}
get(key) {
try {
const entry = this.cacheData[key];
if (!entry || this.isExpired(entry)) {
return null;
}
return JSON.parse(entry.value);
} catch (error) {
onCacheError(key, 'get', error);
return null;
}
}
set(key, value, ttlIn = this.defaultTtl, timeUnit = 'SECOND') {
try {
const unit = this.TIME_UNITS[timeUnit.toUpperCase()];
if (!unit) {
throw new Error(`Invalid time unit "${timeUnit}" provided.`);
}
const ttlMs = ttlIn * unit * 1000;
const expiresAt = new Date().getTime() + ttlMs;
const entry = {
value: JSON.stringify(value),
expiresAt,
lastUsed: expiresAt,
};
this.cacheData[key] = entry;
this.writeCache();
} catch (error) {
onCacheError(key, 'set', error);
}
}
delete(key) {
try {
delete this.cacheData[key];
this.writeCache();
return true;
} catch (error) {
onCacheError(key, 'delete', error);
return false;
}
}
cleanExpired() {
const currentTime = new Date().getTime();
const expiredKeys = Object.keys(this.cacheData).filter((key) => {
const entry = this.cacheData[key];
return this.isExpired(entry, currentTime);
});
expiredKeys.forEach((key) => delete this.cacheData[key]);
if (expiredKeys.length > 0) {
this.writeCache();
}
}
isExpired(entry, currentTime = new Date().getTime()) {
return entry.expiresAt <= currentTime;
}
loadCache() {
try {
const cacheStr = uni.getStorageSync('__cache');
this.cacheData = cacheStr ? JSON.parse(cacheStr) : {};
} catch (error) {
onCacheError(null, 'loadCache', error);
}
}
writeCache() {
try {
uni.setStorageSync('__cache', JSON.stringify(this.cacheData));
} catch (error) {
onCacheError(null, 'writeCache', error);
}
}
}
在上面的代码实现中,我们通过 Cache 构造函数接收了两个可选参数:defaultTtl 和 scanFrequency。这些参数分别用于设置默认的缓存时间和定期扫描器的运行频率。
我们还实现了针对单个缓存项的“最近使用”策略。具体而言,我们在每个缓存项中添加了一个 lastUsed 时间戳,并且在获取或设置缓存项时更新该时间戳。随后,我们可以使用该时间戳来确定最近使用的和很少使用的缓存项,并删除未被使用的过期项。
最后,我们添加了 loadCache 和 writeCache 实用程序方法来从本地存储中加载缓存数据并将更改写回本地存储。此外,我们还通过循环调用 cleanExpired 方法来定期扫描所有缓存项并删除过期的条目。
//---------------------------------------------继续优化-----------------------------------------------------------
-
支持增量存储。当使用 set 方法时,我们每次都会将整个缓存对象写入本地存储。对于大型缓存对象,这可能会导致性能问题。为了确定最少的数据需要更新,我们可以添加一个标记来表示哪些条目被修改,并只写入这些条目。
-
添加可配置的淘汰策略。当前,我们使用FIFO(先进先出)策略清除过期的缓存项。但由于某些项目可能比其他项目更重要,因此我们需要一种可自定义淘汰策略的方法。我们可以允许用户指定一个函数来根据缓存键或值的属性选择需要删除的键。
-
将长度限制添加到缓存。在某些情况下,我们可能希望限制缓存元素的总数以节省内存。为此,我们可以添加一个 maxEntries 参数,并在缓存条目超过最大值时自动清除旧条目。
-
缓存类实例的复用。如果我们请求相同的数据,不同的页面会创建相同的缓存类实例,从而浪费内存。我们可以使用单例模式来确保引用相同的实例,从而避免重复创建。
-
针对移动端设备的优化。考虑到移动设备的资源有限,我们应该减少每个缓存对象的占用内存,并尽可能减少 I/O 操作。为了实现这一点,我们可以使用二进制数据的形式存储缓存对象和采用异步写入方式。
class Cache {
constructor({ defaultTtl = 60, scanFrequency = 300, maxEntries = Infinity } = {}) {
this.TIME_UNITS = {
SECOND: 1,
MINUTE: 60,
HOUR: 60 * 60,
DAY: 24 * 60 * 60,
YEAR: 365 * 24 * 60 * 60,
};
this.defaultTtl = defaultTtl;
this.scanFrequency = scanFrequency;
this.maxEntries = maxEntries;
this.cacheData = {};
this.loadCache();
if (this.scanFrequency > 0) {
this.startCleanUpTask();
}
}
get(key) {
try {
const entry = this.cacheData[key];
if (!entry || this.isExpired(entry)) {
return null;
}
entry.lastUsed = new Date().getTime();
this.writeCache(key, entry);
return clone(entry.value);
} catch (error) {
console.error(`[${key}] get error: `, error);
return null;
}
}
set(key, value, ttlIn = this.defaultTtl, timeUnit = 'SECOND') {
try {
const unit = this.TIME_UNITS[timeUnit.toUpperCase()];
if (!unit) {
throw new Error(`Invalid time unit "${timeUnit}" provided.`);
}
const ttlMs = ttlIn * unit * 1000;
const expiresAt = new Date().getTime() + ttlMs;
const entry = {
value: clone(value),
expiresAt,
lastUsed: expiresAt,
};
this.cacheData[key] = entry;
this.writeCache(key, entry);
// 如果数量超出限制,清除旧的条目
if (this.getSize() > this.maxEntries) {
const keys = Object.keys(this.cacheData).sort((a, b) => {
return this.cacheData[a].lastUsed - this.cacheData[b].lastUsed;
});
this.delete(keys[0]);
}
} catch (error) {
console.error(`[${key}] set error: `, error);
}
}
delete(key) {
try {
delete this.cacheData[key];
uni.removeStorageSync(key);
return true;
} catch (error) {
console.error(`[${key}] delete error: `, error);
return false;
}
}
startCleanUpTask() {
setInterval(() => {
this.cleanExpired();
}, this.scanFrequency * 1000);
}
cleanExpired() {
const currentTime = new Date().getTime();
const expiredKeys = Object.keys(this.cacheData).filter((key) => {
const entry = this.cacheData[key];
return this.isExpired(entry, currentTime);
});
expiredKeys.forEach((key) => this.delete(key));
}
isExpired(entry, currentTime = new Date().getTime()) {
return entry.expiresAt <= currentTime;
}
loadCache() {
try {
const keys = uni.getStorageInfoSync().keys;
keys.forEach((key) => {
if (key.startsWith('__cache-')) {
const keyWithoutPrefix = key.replace('__cache-', '');
const cacheStr = uni.getStorageSync(key);
if (cacheStr) {
const cached = JSON.parse(cacheStr);
this.cacheData[keyWithoutPrefix] = cached;
}
}
});
} catch (error) {
console.error('loadCache error:', error);
}
}
writeCache(key, cache) {
try {
uni.setStorageSync(`__cache-${key}`, JSON.stringify(cache));
} catch (error) {
console.error(`[${key}] writeCache error: `, error);
}
}
getSize() {
return Object.keys(this.cacheData).length;
}
}
function clone(source) {
return JSON.parse(JSON.stringify(source));
}
在这个实现中,我们添加了 maxEntries 选项,该选项确定当条目超过给定数量时缓存将自动清理旧的条目。
我们还添加了 startCleanUpTask 方法,该方法使用 setInterval 定期扫描所有缓存对象,并删除过期的元素。loadCache 和 writeCache 方法也做了适当调整以支持新的命名规则和异步写入方式。
最后,我们添加了一个大小限制,如果当前缓存对象大于最大数量,就会删除最少使用的键。为了对原始缓存数据进行克隆(防止内存开销),我们还附加了一个小型的辅助函数 “clone”。
//----------------------------------------第三版优化------------------------------------------------------------------------
-
使用 LRU(最近最少使用)算法进行条目清理。目前,我们只是使用 FIFO 算法删除过期缓存项。但 LRU 算法能够确定哪些条目在最后一段时间内未被使用,从而更精确地删除旧的或冷门数据。
-
为了进一步降低 I/O 成本,我们可以使用 compression.js 库来压缩和解压缩缓存对象。这样可以减小缓存对象在磁盘上的空间占用,并提高读写速度。
-
将缓存数据存储到 Web Worker 中。Web Worker 是浏览器中可在后台线程中执行 JavaScript 代码的工具,可以避免耗时操作阻塞 UI 渲染。通过将缓存数据放入 web worker,我们可以在后台线程中持久化数据,而无需侵入主线程。
-
添加缓存命名空间功能。对于一些拥有大量资源,如图片或音频的 Web 应用程序,命名空间能够让用户在不同的命名空间之间切换缓存数据而不会造成数据混乱。
-
考虑使用数据库作为后端的缓存存储介质。尤其是在缓存对象非常大的情况下,可以考虑使用数据库存储仓库 / 列储存换取缓存命中更高,因为索引会比文件系统的查询速度要快很多,并且提供复杂的查询方式。
使用 LRU 算法进行条目清理
class Cache {
constructor({ ... }) {
this.orderedKeys = [];
// ...
}
set(key, value, ttlIn = this.defaultTtl, timeUnit = 'SECOND') {
try {
// ...
this.orderedKeys.push(key);
if (this.getSize() > this.maxEntries) {
const leastUsedKey = this.orderedKeys.shift();
this.delete(leastUsedKey);
}
} catch (error) {
console.error(`[${key}] set error: `, error);
}
}
get(key) {
try {
const entry = this.cacheData[key];
if (!entry || this.isExpired(entry)) {
return null;
}
this.updateKeyOrder(key);
// ...
} catch (error) {
console.error(`[${key}] get error: `, error);
return null;
}
}
delete(key) {
try {
delete this.cacheData[key];
const index = this.orderedKeys.indexOf(key);
if (index > -1) {
this.orderedKeys.splice(index, 1);
}
uni.removeStorageSync(key);
return true;
} catch (error) {
console.error(`[${key}] delete error: `, error);
return false;
}
}
updateKeyOrder(key) {
const index = this.orderedKeys.indexOf(key);
if (index > -1) {
this.orderedKeys.splice(index, 1);
}
this.orderedKeys.push(key);
}
}
在这里,我们使用了一个 orderedKeys 数组来跟踪缓存项的顺序。当一个元素被访问时,它就会被移到 orderedKeys 数组的末尾。在清理旧条目时,我们从 orderedKeys 数组的开头删除最少使用的元素。
2.使用 compression.js 库进行缓存对象压缩
import LZString from 'lz-string';
class Cache {
// ...
loadCache() {
try {
const keys = uni.getStorageInfoSync().keys;
keys.forEach((key) => {
if (key.startsWith('__cache-')) {
const keyWithoutPrefix = key.replace('__cache-', '');
const cacheStr = uni.getStorageSync(key);
if (cacheStr) {
const decompressed = LZString.decompress(cacheStr);
const cached = JSON.parse(decompressed);
this.cacheData[keyWithoutPrefix] = cached;
}
}
});
} catch (error) {
console.error('loadCache error:', error);
}
}
writeCache(key, cache) {
try {
const compressed = LZString.compress(JSON.stringify(cache));
uni.setStorageSync(`__cache-${key}`, compressed);
} catch (error) {
console.error(`[${key}] writeCache error: `, error);
}
}
}
在这里,我们使用了名为 LZString 的 JavaScript 库来执行基于 Lempel-Ziv 算法的数据压缩和解压缩操作。我们在读写缓存时使用该库,将缓存对象压缩到字符串中,以减少其磁盘空间占用。
3.将缓存数据存储到 Web Worker 中
// magic-cache.js 文件,使用于 worker 内部
class Cache {
// 这里的方式与主程序体相同,包含优化后的功能
}
let cache = null;
onmessage = function (e) {
if (!cache) {
cache = new Cache();
}
const method = e.data.method;
const args = e.data.args || [];
const res = cache[method](...args);
this.postMessage({ method, result: res });
};
// main.js 文件
const myWorker = new Worker('magic-cache.js');
myWorker.onmessage = function (event) {
console.log(`[${event.data.method}] result:`, event.data.result);
};
myWorker.postMessage({ method: 'set', args: ['foo', { bar: 'baz' }, 60] });
myWorker.postMessage({ method: 'get', args: ['foo'] });
在这里,我们将 Cache 类构造函数和实例保存在 worker 内部,并使用 onmessage 方法接收指令。主程序使用 new Worker() 构造函数创建一个新的 worker,发送指令并打印结果。
这种方法可以避免长时间耗时操作阻塞 UI 渲染,因为 Web Workers 在后台线程中运行 JavaScript 代码。
4.添加缓存命名空间功能
class Cache {
constructor({ name = '__default__', ... }) {
this.cacheName = name;
// ...
}
//-----------------------------------------闲来无事再优化一波--------------------------------------------------
使用 Map 作为缓存数据存储介质的完整封装方法
具体代码体现
function createCache({
defaultTtl = 60, // 默认过期时间为 60 秒
scanFrequency = 300, // 每隔 5 分钟扫描一次过期对象,如果为 0 则禁用自动清理功能
maxEntries = Infinity, // 缓存容量,默认无限制
}) {
const TIME_UNITS = {
SECOND: 1,
MINUTE: 60,
HOUR: 3600,
DAY: 86400,
};
function clone(data) {
return JSON.parse(JSON.stringify(data));
}
class Cache {
constructor() {
this.cacheData = new Map();
this.defaultTtl = defaultTtl;
this.scanFrequency = scanFrequency;
this.maxEntries = maxEntries;
if (this.scanFrequency > 0) {
this.startCleanUpTask();
}
}
get(key) {
try {
const entry = this.cacheData.get(key);
if (!entry || this.isExpired(entry)) {
return null;
}
entry.lastUsed = new Date().getTime();
return clone(entry.value);
} catch (error) {
console.error(`[${key}] get error: `, error);
return null;
}
}
set(key, value, ttlIn = this.defaultTtl, timeUnit = 'SECOND') {
try {
const unit = TIME_UNITS[timeUnit.toUpperCase()];
if (!unit) {
throw new Error(`Invalid time unit "${timeUnit}" provided.`);
}
const ttlMs = ttlIn * unit * 1000;
const expiresAt = new Date().getTime() + ttlMs;
const entry = {
value: clone(value),
expiresAt,
lastUsed: expiresAt,
};
this.cacheData.set(key, entry);
if (this.cacheData.size > this.maxEntries) {
// 删除最少使用的条目
const leastUsed = [...this.cacheData.entries()].sort(
([, a], [, b]) => a.lastUsed - b.lastUsed
)[0][0];
this.cacheData.delete(leastUsed);
}
} catch (error) {
console.error(`[${key}] set error: `, error);
}
}
delete(key) {
try {
const success = this.cacheData.delete(key);
return success;
} catch (error) {
console.error(`[${key}] delete error: `, error);
return false;
}
}
cleanExpired() {
const currentTime = new Date().getTime();
for (const [key, entry] of this.cacheData.entries()) {
if (this.isExpired(entry, currentTime)) {
this.cacheData.delete(key);
}
}
}
startCleanUpTask = () => {
setInterval(() => {
this.cleanExpired();
}, this.scanFrequency * 1000);
};
isExpired({ expiresAt }, currentTime = new Date().getTime()) {
return currentTime > expiresAt;
}
getSize() {
return this.cacheData.size;
}
}
return new Cache();
}
/-----------------------------------------------------------------分割线---------------------------
-
使用 LRU(Least Recently Used)算法来维护缓存条目的顺序。这种算法会根据条目的使用情况,将最近最少使用的条目排在前面,从而删除这些条目时会先尝试删除它们。
-
对于读多写少的场景,可以加入读写锁的支持,也就是说读取操作和写入操作之间只有一个操作可以执行,以避免出现并发写入时导致的数据不一致问题。
-
在高并发场景中,在写入大量数据时可能会因为缓存空间不足而影响应用程序的性能,此时可以考虑使用分布式缓存,将缓存数据分散到多个缓存服务器上。例如,可以采用 Redis 这种分布式内存数据库作为远程缓存介质,通过配置客户端连接多个 Redis 实例来构建一个集群,从而提高缓存的可用性和扩展性。
-
如果业务场景需要精确的 TTL (Time-to-live,缓存过期时间),则可以使用比较成熟的缓存产品或框架,例如 Memcached、Redis 等,这些产品对于缓存 TTL 与自动清理、LRU 等机制已经进行了优化,可以提供更加稳定和高效的缓存解决方案。
下面是一个使用 LRU 算法实现的缓存类:
class LRUCache {
constructor(capacity) {
this.cache = new Map();
this.capacity = capacity;
}
get(key) {
if (!this.cache.has(key)) {
return null;
}
const value = this.cache.get(key);
this.cache.delete(key);
this.cache.set(key, value);
return value;
}
put(key, value) {
if (this.cache.has(key)) {
this.cache.delete(key);
} else if (this.cache.size >= this.capacity) {
this.cache.delete(this.cache.keys().next().value);
}
this.cache.set(key, value);
}
}
这个类维护了一个 Map 对象,存储了缓存中的键值对数据。在读取数据时,如果缓存存在指定的 key,则将其从 Map 中删除,并再次插入到 Map 尾部,以此保证最近访问的数据始终排在 Map 最后。在写入数据时,如果容量已经满,那么会删除 Map 头部的元素,然后再将新元素插入到 Map 尾部。
这种实现方式虽然简单,但在大部分情况下可以提高缓存访问速度,因为最近访问的数据往往比较可能再次被访问到,而且 Map 对象维持了元素插入的顺序,可以快速获取最早插入或者最近插入的元素。然而,在处理大批量数据或者高并发环境下,这种实现方式也存在一定的缺陷,可能会导致频繁的 Map 增删操作,影响性能。
然后再把cache进行一次封装
class Cache {
/**
* 初始化缓存对象
* @param {number} capacity - 缓存容量
* @param {number} [cleanupIntervalMs=60000] - 清理过期缓存的时间间隔(单位:毫秒)。默认为 1 分钟。
*/
constructor(capacity, cleanupIntervalMs = 60000) {
this.capacity = capacity;
this.cache = new LRUCache(capacity);
setInterval(() => this.cleanup(), cleanupIntervalMs); // 定时清理过期缓存
}
/**
* 获取缓存中对应键的值
* @param {string} key - 缓存的键
* @returns {*} 缓存的值。如果不存在,则返回 null。
*/
get(key) {
if (!key) {
return null;
}
const item = this.cache.get(key);
if (!item || (item.expireAt && item.expireAt < Date.now())) {
this.cache.del(key);
return null;
}
return item.value;
}
/**
* 设置缓存的键值对
* @param {string} key - 缓存的键
* @param {*} value - 缓存的值
* @param {number} [ttl=0] - 过期时间(单位:秒)。默认为 0,表示不过期。
* @returns {*} 设置成功返回 true,否则返回 false。
*/
set(key, value, ttl = 0) {
if (!key) {
return false;
}
const item = { value };
if (ttl > 0) {
item.expireAt = Date.now() + ttl * 1000;
}
this.cache.put(key, item);
return true;
}
/**
* 删除缓存中指定键的值
* @param {string} key - 缓存的键
* @returns {*} 删除成功返回 true,否则返回 false。
*/
del(key) {
if (!key) {
return false;
}
return this.cache.del(key);
}
/**
* 判断缓存中是否存在指定键的值
* @param {string} key - 缓存的键
* @returns {*} 存在返回 true,否则返回 false。
*/
has(key) {
if (!key) {
return false;
}
const item = this.cache.get(key);
if (!item || (item.expireAt && item.expireAt < Date.now())) {
this.cache.del(key);
return false;
}
return true;
}
/**
* 获取缓存中指定键的过期时间
* @param {string} key - 缓存的键
* @returns {number|null} 过期时间(单位:秒)。如果不存在或不过期,则返回 null。
*/
ttl(key) {
if (!key) {
return null;
}
const item = this.cache.get(key);
if (!item || (item.expireAt && item.expireAt < Date.now())) {
this.cache.del(key);
return null;
}
if (item.expireAt) {
return Math.floor((item.expireAt - Date.now()) / 1000);
}
return null;
}
/**
* 清空缓存中所有的键值对
*/
clear() {
this.cache.clear();
}
/**
* 清理过期缓存
*/
cleanup() {
for (const [key, item] of this.cache.cache) {
if (item.expireAt && item.expireAt < Date.now()) {
this.cache.del(key);
}
}
}
}
这个封装类包含了初始化、读取、写入、删除和清空缓存等功能。其中,使用了 LRU 算法的 LRUCache 类作为底层缓存实现,可根据 capacity 参数调节缓存的容量。
在 set() 方法中,加入了 ttl 参数用于设置键值对的过期时间。如果 ttl>0,则将过期时间 expireAt 存储在 item 对象中,并在 get() 方法中判断检查缓存数据是否过期。
注意,在实际应用过程中,我们还需要考虑缓存一致性的问题,以及在不同的业务场景中可能需要针对性地选择采用各种不同的缓存方式或缓存产品,以达到更好的缓存效果
--------------------------------------//最后一版了//----------------------------------------------------------
封装的感觉差不多了 各种方法都用过了 具体用那一版看自己的选择了
class Cache {
/**
* 创建一个新的缓存对象
* @param {number} capacity - 缓存容量
*/
constructor(capacity) {
this.capacity = capacity || 100000;
this.store = new Map();
this.cleanupTimer = setInterval(() => this.deleteExpired(), 30 * 60 * 1000);
}
/**
* 获取缓存中指定键的值
* @param {string} key - 缓存的键
* @returns {*} 对应的缓存值(或 null)
*/
get(key) {
const item = this.store.get(key);
if (item && (!item.ttl || Date.now() <= item.expireAt)) {
return item.value;
}
this.del(key);
return null;
}
/**
* 设置缓存中指定键值对,并可设置过期时间
* @param {string} key - 缓存的键
* @param {*} value - 缓存的值
* @param {number} ttl - 过期时间,单位秒
* @returns {*} 是否设置成功
*/
set(key, value, ttl = 0) {
if (this.store.size >= this.capacity) {
this.deleteExpired(); // 先删除过期缓存
if (this.store.size >= this.capacity) {
const oldestKey = this.store.keys().next().value; // 删除最久未使用的缓存
this.del(oldestKey);
}
}
const expireAt = ttl > 0 ? Date.now() + ttl * 1000 : Infinity;
const item = { value, expireAt };
this.store.set(key, item);
return true;
}
/**
* 删除指定键的缓存
* @param {string} key - 缓存的键
* @returns {*} 是否删除成功
*/
del(key) {
const deleted = this.store.delete(key);
return deleted > 0;
}
/**
* 清空所有缓存
*/
clear() {
this.store.clear();
}
/**
* 获取指定键的过期时间
* @param {string} key - 缓存的键
* @returns {*} 对应的过期时间(或 null)
*/
getExpireTime(key) {
const item = this.store.get(key);
if (item && item.ttl) {
const seconds = Math.round((item.expireAt - Date.now()) / 1000);
return seconds > 0 ? seconds : 0;
}
return null;
}
/**
* 判断指定键是否存在有效缓存
* @param {string} key - 缓存的键
* @returns {boolean} 是否存在缓存
*/
has(key) {
const item = this.store.get(key);
return item && (!item.ttl || Date.now() <= item.expireAt);
}
/**
* 删除过期的缓存
*/
deleteExpired() {
const now = Date.now();
for (const [key, item] of this.store.entries()) {
if (item.ttl && now > item.expireAt) {
this.store.delete(key);
}
}
}
/**
* 销毁缓存对象,停止定时清理任务
*/
destroy() {
clearInterval(this.cleanupTimer);
this.cleanupTimer = null;
this.store.clear();
}
}