用Set配合定时器实现自动过期缓存的关键是查得快、删得准、不卡主线程:Set仅存token用于O(1)存在判断,另用时间队列记录插入顺序,由单个定时器周期扫描清理过期项。

用 Set 配合定时器实现自动过期缓存,关键不在于“存得快”,而在于“查得快、删得准、不卡主线程”。Set 本身只存键(如 token 字符串),不带时间信息,所以必须搭配额外结构记录插入时间,并用独立定时器周期性扫描清理。
✅ 为什么选 Set 做存在判断?
Set 提供 O(1) 时间复杂度的 has() 查询,比数组遍历或对象属性检查更轻量、更可靠(避免原型链干扰或字符串转数字等隐式转换)。适合高频验证 token、session ID 是否有效。
但它不能直接存时间戳——你需要一个并行结构来维护时间顺序。
✅ 核心结构:Set + 时间队列
推荐组合方式:
-
tokenSet = new Set():纯用于has(token)判断,不存任何元数据 -
tokenQueue = []:数组形式的时间队列,每个元素是{ token, timestamp },按插入顺序排列 -
单个
setInterval定期扫描:比如每 5~10 分钟执行一次清理,只处理已过期项
这样既利用了 Set 的查询优势,又通过队列保留了时间线索,避免为每个 token 单独开 setTimeout(否则 1000 个 token 就 1000 个定时器,内存和事件循环压力陡增)。
✅ 清理逻辑怎么写才安全?
清理函数要满足三个要求:不阻塞主线程、不误删未过期项、清理后同步更新 Set 和 Queue。
示例代码片段:
const EXPIRY_MS = 20 * 60 * 1000; // 20 分钟
const CLEAN_INTERVAL = 5 * 60 * 1000; // 每 5 分钟扫一次
<p>function addToken(token) {
const now = Date.now();
tokenSet.add(token);
tokenQueue.push({ token, timestamp: now });
}</p><p>function cleanupExpired() {
const now = Date.now();
const expiredTokens = [];</p><p>// 从前向后扫描,找到第一个未过期的就停(队列有序,可提前终止)
for (let i = 0; i < tokenQueue.length; i++) {
if (now - tokenQueue[i].timestamp >= EXPIRY_MS) {
expiredTokens.push(tokenQueue[i].token);
} else {
break; // 后面都还没到期,不用继续
}
}</p><p>// 批量清理:先从 Set 删除,再从 Queue 截断
expiredTokens.forEach(token => tokenSet.delete(token));
tokenQueue = tokenQueue.slice(expiredTokens.length);
}</p><p>// 启动定期清理
setInterval(cleanupExpired, CLEAN_INTERVAL);
✅ 实际使用注意点
- 插入时务必同时操作
Set和Queue,缺一不可 - 清理函数里不要用
splice在原数组中逐个删除——会频繁移动元素,性能差;改用slice一次性截取有效段更高效 - 如果业务允许一定误差(比如过期后最多延迟 5 分钟才清理),这个方案比“每个 token 绑一个
setTimeout”省资源得多 - 需要精确到毫秒级自动删除?那不适合用此方案,应换用 Map + 优先队列或专用库(如
lru-cache的 TTL 支持)


















