结合不同数据结构实现高效缓存机制
- 使用Map和数组实现LRU缓存
- 原理:LRU(Least Recently Used)缓存策略是当缓存满时,优先淘汰最久未使用的数据。
- 代码示例:
class LRUCache {
constructor(capacity) {
this.capacity = capacity;
this.cache = new Map();
this.order = [];
}
get(key) {
if (this.cache.has(key)) {
this.order.splice(this.order.indexOf(key), 1);
this.order.push(key);
return this.cache.get(key);
}
return -1;
}
put(key, value) {
if (this.cache.has(key)) {
this.cache.set(key, value);
this.order.splice(this.order.indexOf(key), 1);
} else if (this.cache.size >= this.capacity) {
const leastUsed = this.order.shift();
this.cache.delete(leastUsed);
}
this.cache.set(key, value);
this.order.push(key);
}
}
- 说明:这里使用
Map
来存储键值对,因为Map
的查找和删除操作时间复杂度为O(1)。数组
用来记录访问顺序,通过数组操作来维护LRU顺序。
- 使用Set和对象实现缓存去重
- 原理:在缓存数据时,有时需要确保缓存的数据不重复。
Set
可以高效地检查元素是否存在,而对象可以存储具体的数据。
- 代码示例:
const cacheSet = new Set();
const cacheObject = {};
function addToCache(key, value) {
if (!cacheSet.has(key)) {
cacheSet.add(key);
cacheObject[key] = value;
}
}
function getFromCache(key) {
if (cacheSet.has(key)) {
return cacheObject[key];
}
return null;
}
- 说明:
Set
用于快速判断某个键是否已经在缓存中,对象用于存储实际的缓存值。
内存管理问题
- 避免内存泄漏
- 及时清理过期数据:在缓存中设置过期时间,定期检查并删除过期的数据。例如,在LRU缓存中,可以在
put
操作时记录每个键值对的时间戳,在get
和put
操作时检查并删除过期数据。
class ExpiringLRUCache {
constructor(capacity, ttl) {
this.capacity = capacity;
this.ttl = ttl;
this.cache = new Map();
this.order = [];
this.timestamps = new Map();
}
get(key) {
if (this.cache.has(key)) {
const now = Date.now();
if (now - this.timestamps.get(key) > this.ttl) {
this.cache.delete(key);
this.order.splice(this.order.indexOf(key), 1);
this.timestamps.delete(key);
return -1;
}
this.order.splice(this.order.indexOf(key), 1);
this.order.push(key);
this.timestamps.set(key, now);
return this.cache.get(key);
}
return -1;
}
put(key, value) {
const now = Date.now();
if (this.cache.has(key)) {
this.cache.set(key, value);
this.order.splice(this.order.indexOf(key), 1);
this.timestamps.set(key, now);
} else if (this.cache.size >= this.capacity) {
const leastUsed = this.order.shift();
this.cache.delete(leastUsed);
this.timestamps.delete(leastUsed);
}
this.cache.set(key, value);
this.order.push(key);
this.timestamps.set(key, now);
}
}
- 解除引用:当从缓存中删除数据时,确保所有对该数据的引用都被解除,特别是当缓存的数据是复杂对象时。例如,在
LRUCache
的delete
操作中,不仅要从Map
中删除键值对,还要确保没有其他地方引用该值。
- 根据业务需求调整缓存策略
- 读多写少场景:可以使用更复杂的缓存策略,如LFU(Least Frequently Used),这种策略记录每个数据的访问频率,优先淘汰访问频率最低的数据。
- 写多读少场景:可以适当降低缓存容量,减少内存占用,因为数据变化频繁,缓存的命中率可能较低。
Node.js环境下Set和Map在高并发场景的挑战及解决方案
- 挑战
- 线程安全问题:Node.js是单线程的,但在高并发场景下,多个异步操作可能同时访问和修改
Set
或Map
,导致数据不一致。例如,在一个Web应用中,多个HTTP请求可能同时尝试更新缓存(Set
或Map
)。
- 性能瓶颈:虽然
Set
和Map
在单个操作上性能较好,但在高并发大量操作时,可能会因为单线程执行而出现性能瓶颈。
- 解决方案
- 使用锁机制:可以使用
mutex
(互斥锁)来确保同一时间只有一个操作可以访问和修改Set
或Map
。例如,使用async - mutex
库:
const Mutex = require('async - mutex').Mutex;
const mySet = new Set();
const mutex = new Mutex();
async function addToSet(value) {
const release = await mutex.acquire();
try {
mySet.add(value);
} finally {
release();
}
}
async function getSetSize() {
const release = await mutex.acquire();
try {
return mySet.size;
} finally {
release();
}
}
- 分区策略:将
Set
或Map
分成多个部分(分区),不同的操作可以并行处理不同的分区,减少竞争。例如,根据键的哈希值将缓存数据分配到不同的Map
实例中,每个实例由独立的异步任务管理。