10.5 小项目:如何用 JavaScript 实现一个高效的时间+Token过期容器?
在开发中,我们经常会遇到需要临时存储数据并定时过期的场景,比如:
- 用户登录后的临时 Token 缓存
- 短信验证码的有效期管理
- 接口限流中的请求记录
- 临时会话状态存储
这类需求通常具备以下特点:
- 存储
token值和其生成时间 - 时间可能重复(多个 token 同时生成)
- 需要 快速判断某个 token 是否存在
- 数据在 20 分钟后自动过期并清除
一、需求拆解
我们明确一下核心需求:
| 需求 | 描述 |
|---|---|
| ✅ 存储结构 | 每条记录包含:token(字符串) + timestamp(时间戳) |
| ✅ 时间可重复 | 允许多个 token 在同一时间生成 |
| ✅ 快速查询 | 能在 O(1) 时间内判断某个 token 是否存在 |
| ✅ 自动过期 | 所有数据在插入后 20 分钟自动失效 |
| ✅ 高效清理 | 定期清理过期数据,不影响主线程性能 |
常见实现方式对比
方案 1:仅用数组存储(不推荐)
const tokens = [];
tokens.push({ token: 'abc', timestamp: Date.now() });- ❌ 查询 token:需遍历数组,O(n)
- ❌ 清理过期:每次都要全量扫描
- ❌ 性能差,不适合高频查询
方案 2:Map + setTimeout(精确但开销大)
为每个 token 设置一个 setTimeout,20 分钟后删除。
- ✅ 删除精确到毫秒
- ❌ 内存开销大:每个 token 一个定时器
- ❌ 大量 token 时可能导致事件循环阻塞
✅ 方案 3:Set + 时间队列 + 定时扫描(推荐)
- 使用
Set快速判断 token 是否存在(O(1)) - 使用数组(队列)按插入顺序存储
{token, timestamp} - 启动一个定时器,定期扫描并清理过期数据
⭐ 这是本文推荐的平衡性能与资源消耗的最佳实践。
✅ 核心设计思路
- 快速查找 token:使用
Set或Map来存储 token,实现 O(1) 时间复杂度的查找。 - 处理重复时间 & 存储时间:将时间戳和 token 一起存储,并通过一个列表或队列维护插入顺序,以便按时间清理。
- 定时清除过期数据:使用一个定时器(
setInterval)每隔几分钟扫描一次,清除超过 20 分钟的数据。
✅ 推荐实现:使用 Set + 时间队列
class TokenExpiryContainer {constructor() {this.tokenSet = new Set(); // 用于 O(1) 快速判断 token 是否存在this.tokenQueue = []; // 存储 { token, timestamp },按插入时间排序this.EXPIRY_MS = 20 * 60 * 1000; // 20分钟过期this.CLEAN_INTERVAL = 10 * 60 * 1000; // 每10分钟检查一次过期数据(可调)// 启动定时清理this.startCleaner();}/*** 添加 token 和时间* @param {string} token* @param {number} timestamp - 时间戳(毫秒),默认为当前时间*/add(token, timestamp = Date.now()) {// 如果 token 已存在,无需重复添加if (this.has(token)) return;this.tokenSet.add(token);this.tokenQueue.push({ token, timestamp });}/*** 判断 token 是否存在(未过期)* @param {string} token* @returns {boolean}*/has(token) {return this.tokenSet.has(token);}/*** 获取所有未过期的 token* @returns {string[]}*/getAllTokens() {const now = Date.now();return this.tokenQueue.filter(item => now - item.timestamp < this.EXPIRY_MS).map(item => item.token);}/*** 启动定时清理任务*/startCleaner() {setInterval(() => {this.cleanupExpired();}, this.CLEAN_INTERVAL);}/*** 清理过期数据*/cleanupExpired() {const now = Date.now();const expiryTime = now - this.EXPIRY_MS;let removedCount = 0;// 由于 tokenQueue 是按时间顺序插入的,我们可以从头部开始清理while (this.tokenQueue.length > 0) {const firstItem = this.tokenQueue[0];if (firstItem.timestamp <= expiryTime) {// 移除过期 tokenthis.tokenQueue.shift();this.tokenSet.delete(firstItem.token);removedCount++;} else {// 队列是有序的,第一个没过期,后面的都不会过期,提前退出break;}}if (removedCount > 0) {console.log(`清理了 ${removedCount} 个过期 token`);}}
}// === 使用示例 ===
const container = new TokenExpiryContainer();// 添加一些 token(时间可能重复)
container.add('token1', 1700000000000); // 模拟时间
container.add('token2', 1700000000000); // 相同时间
container.add('token3', Date.now()); // 当前时间console.log(container.has('token1')); // true
console.log(container.has('token999')); // false// 20分钟后,token1 和 token2 会被自动清理✅ 为什么这个设计高效?
| 需求 | 实现方式 | 时间复杂度 |
|---|---|---|
| 快速判断 token 是否存在 | 使用 Set 存储 token | O(1) |
| 存储时间 + token | tokenQueue 数组存储 {token, timestamp} | O(1) 插入 |
| 清除过期数据 | 定时扫描 + 队列有序性(提前退出) | O(k),k 是过期数量,通常很小 |
✅ 总结
- ✅ 时间可重复:使用独立的
timestamp字段,不作为键。 - ✅ 快速判断 token:
Set提供 O(1) 查找。 - ✅ 定时清除:
setInterval+ 有序队列,高效清理。
性能优化建议
| 优化点 | 说明 |
|---|---|
| 扫描频率 | CLEAN_INTERVAL 不宜太短(如 1 秒),避免频繁扫描;也不宜太长(如 30 分钟),导致过期数据滞留。5到10 分钟较合理。 |
| 内存优化 | 如果 token 数量极大,可考虑使用 Redis。 |
| 精确删除 | 若需精确删除,可用 Map 存储 token -> timeoutId,但需权衡内存。 |
| 持久化 | Node.js 重启后数据丢失?可将数据持久化到文件或数据库。 |
| 批量清理 | 生产环境可结合 Redis 的 ZSET + EXPIRE 实现更强大的过期管理。 |
适用场景
- 🔐 临时 Token 缓存:OAuth2 临时凭证、JWT 刷新 Token
- 📱 验证码管理:短信、邮箱验证码的时效控制
- 🛑 接口限流:记录用户请求时间,防止刷接口
- 🧩 会话管理:轻量级 session 存储,无需引入 Redis
- 🔄 任务去重:防止重复提交、重复处理
本文实现了一个高效、低延迟的“时间+Token”过期容器,具备以下优势:
- ✅ O(1) 查询:使用
Set实现快速存在性判断 - ✅ 支持重复时间:独立存储时间戳,互不影响
- ✅ 自动清理:定时扫描 + 有序队列,高效清除过期数据
- ✅ 轻量无依赖:纯 JavaScript 实现,适合嵌入任何项目
💡 最佳实践建议:
对于小规模应用,本文方案完全够用;
对于高并发、大规模场景,建议使用 Redis 配合EXPIRE或ZSET实现更可靠的过期机制。
