不能只用 Map 实现 LRU + TTL,因其不支持自动过期且手动清理破坏 O(1) 复杂度;LRU 需访问顺序感知,单纯属性或数组易在高并发下错乱;setTimeout 单独设定时器会导致内存与句柄泄漏。

为什么不能只用 Map 实现 LRU + TTL?
因为 Map 本身不支持自动过期,而手动遍历清理会破坏 O(1) 时间复杂度;同时 LRU 需要访问顺序感知,单纯靠对象属性或数组维护顺序,在高并发或频繁读写时容易错乱。常见错误是:用 setTimeout 为每个 key 单独设定时器——内存和句柄泄漏风险极高,尤其 key 量大时。
LRUMap 基础结构怎么搭才兼顾顺序与过期?
核心是把「访问时间」和「插入顺序」解耦:用 Map 存数据 + 过期时间戳,另用一个 Map(或数组)维护 key 的最近访问时间,但更轻量的做法是——每次 get 时更新时间戳,并在 set 或 get 时惰性清理过期项。实际只需两个字段:this.cache = new Map() 存 { value, expiresAt },this.maxSize 控制容量。
-
set(key, value, ttlMs):计算expiresAt = Date.now() + ttlMs,存入;若超容,调用evict()删除最久未用(即最早插入且未被get刷新过的)项 -
get(key):先检查expiresAt ,过期则delete并返回undefined;否则更新该 key 的访问时间(可选:写回expiresAt延长有效期,按需) - 「最久未用」靠插入顺序 + 访问标记实现:每次
get后,delete再set该 key,让其排到Map末尾——Map的迭代顺序就是插入顺序,所以this.cache.keys().next().value就是最老的 key
如何避免 Date.now() 在高频场景下成为瓶颈?
Date.now() 调用本身不重,但若每读写都调用两次(get 检查 + set 计算),在十万级 QPS 下会有微小开销。更关键的是时钟漂移问题:比如服务刚启动、系统时间被 NTP 回拨,会导致大量缓存误判过期。稳妥做法是:所有时间比较统一用相对时间,例如记录 this.startTime = Date.now(),存 expiresInMs 而非绝对时间戳,检查时用 Date.now() - this.startTime > expiresInMs。不过要注意,如果进程运行超 24.8 天(Number.MAX_SAFE_INTEGER / 1000 / 60 / 60 / 24),相对时间差可能溢出,一般业务无需考虑。
删除策略里「惰性清理」和「主动轮询」怎么选?
惰性清理(只在 get/set 时顺手删过期项)实现简单、无额外线程开销,适合中小流量;但它无法释放已过期但长期无人访问的内存。主动轮询(如每秒 setInterval 扫描并清理)能及时回收,但需注意:扫描不能阻塞主线程,建议用 setTimeout 分批处理(每次最多删 100 个),且必须加锁防止并发修改 Map。生产环境更推荐混合策略:惰性为主,辅以低频(如每分钟一次)轻量扫描,扫描范围限制在 Math.min(100, this.cache.size / 10)。
真正容易被忽略的是:Map.prototype.delete() 不会触发 GC 立即回收,尤其是 value 是大对象(如 JSON 字符串、Buffer)时,务必确保没有外部引用残留——比如你把缓存值又传给了某个事件监听器,那它就永远不会被回收。









