Article
第一章:SimpleMem 简介与快速入门
1.1 什么是 SimpleMem
| 概念名称 | 说明 | 注意事项 |
|---|---|---|
| SimpleMem | 一个轻量级、纯内存的键值存储库,用于在 Node.js 或浏览器环境中实现本地缓存功能 | 不适用于持久化存储,重启后数据丢失 |
| 设计目标 | 提供简单、高性能、零依赖的内存缓存能力,支持 TTL、命名空间、批量操作等特性 | 仅适用于单进程场景,不支持分布式 |
| 适用语言环境 | 原生 JavaScript(兼容 CommonJS 与 ESM),可在 Node.js ≥14 或现代浏览器中运行 | 浏览器中需注意全局变量污染问题 |
| 核心特点 | - 同步 API - 支持过期时间(TTL) - 支持命名空间隔离 - 内置 LRU/FIFO 驱逐策略 | 异步操作需自行封装,框架本身不提供 Promise |
1.2 安装与环境配置
| 步骤名称 | 操作细节 | 注意事项 |
|---|---|---|
| 安装 SimpleMem | 在项目根目录执行:npm install simple-mem | 确保已初始化 package.json(可先运行 npm init -y) |
| 导入模块(Node.js) | 使用 CommonJS:const SimpleMem = require('simple-mem'); 或使用 ESM:import SimpleMem from 'simple-mem'; | ESM 需在 package.json 中设置 "type": "module" |
| 导入模块(浏览器) | 通过 CDN 引入:<script src="https://cdn.jsdelivr.net/npm/simple-mem@latest/dist/simple-mem.min.js"></script> 然后通过 window.SimpleMem 使用 | 生产环境建议锁定版本号,避免自动升级导致兼容问题 |
| 验证安装 | 执行 console.log(typeof SimpleMem) 应输出 "function" | 若报错”module not found”,检查网络或 node_modules 是否完整 |
1.3 第一个 SimpleMem 示例程序
| 方法名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| 构造函数 | new SimpleMem(options?) | 创建一个内存存储实例 | const mem = new SimpleMem();mem.set('name', 'Alice');console.log(mem.get('name')); // 输出: Alice | options 可选,用于配置最大容量、TTL 等 |
| set | mem.set(key, value, ttl?) | 存储键值对,可设过期时间 | mem.set('token', 'abc123', 5000); // 5秒后自动失效 | ttl 单位为毫秒;若未提供则永不过期 |
| get | mem.get(key) | 获取指定键的值 | const val = mem.get('token');if (val === undefined) { console.log('已过期或不存在');} | 若键不存在或已过期,返回 undefined |
示例完整程序:
const SimpleMem = require('simple-mem');
const cache = new SimpleMem();
cache.set('hello', 'world', 3000);
console.log(cache.get('hello')); // world
setTimeout(() => {
console.log(cache.get('hello')); // undefined
}, 3500);
程序需在支持
setTimeout的环境(如 Node.js)中运行。
第二章:核心概念与数据模型
2.1 内存存储单元(MemUnit)
| 概念名称 | 说明 | 注意事项 |
|---|---|---|
| MemUnit | SimpleMem 内部用于封装单个缓存项的数据结构,包含值、过期时间、命名空间等元信息 | 用户不直接操作 MemUnit,由框架内部管理 |
| value | 存储的实际数据,可以是任意 JavaScript 类型(string、number、object、array 等) | 引用类型(如对象)在内存中共享,修改会影响缓存内容 |
| expiry | 过期时间戳(Unix 毫秒时间),若为 null 或 undefined 表示永不过期 | 框架在 get 时检查当前时间是否超过 expiry,超时则返回 undefined |
| namespace | 所属命名空间标识符(字符串),用于逻辑隔离不同用途的缓存 | 默认为 'default';相同 key 在不同 namespace 中互不影响 |
| size | (可选)该单元占用的”逻辑大小”,用于容量控制(如 maxSize 按条目数或字节数) | 当前版本默认按条目数计算,size 字段预留用于未来扩展 |
2.2 键值对结构与命名空间
| 概念名称 | 说明 | 注意事项 |
|---|---|---|
| 键(key) | 字符串类型,唯一标识一个缓存项;支持字母、数字、下划线、连字符等常规字符 | 不建议使用特殊符号(如空格、斜杠),可能影响调试或序列化 |
| 值(value) | 任意可序列化的 JavaScript 值 | 函数、Symbol、undefined 等不可序列化类型可能导致持久化失败(若启用快照) |
| 命名空间(namespace) | 逻辑分组机制,允许同一 key 在不同 namespace 中存储不同值 | 切换 namespace 需通过 new SimpleMem({ namespace: 'xxx' }) 创建新实例 |
| 全局键格式 | 内部实际存储键 = ${namespace}:${key} | 用户无需手动拼接,框架自动处理;但调试时可见此格式 |
| 命名空间隔离效果 | cacheA = new SimpleMem({ ns: 'user' });cacheB = new SimpleMem({ ns: 'admin' }); 两者 set('id', 1) 互不影响 | namespace 参数简写为 ns,全称 namespace,两者等效 |
2.3 数据生命周期与过期策略
| 概念名称 | 说明 | 注意事项 |
|---|---|---|
| TTL(Time-To-Live) | 数据存活时间,单位毫秒;从 set 时刻开始计时 | 若 ttl ≤ 0,视为立即过期,get 返回 undefined |
| 绝对过期时间 | 可通过传入 Date 对象或时间戳实现绝对过期(部分版本支持) | 标准 API 仅支持相对 TTL,绝对时间需自行计算差值 |
| 惰性过期(Lazy Expiry) | SimpleMem 采用惰性删除:仅在 get / has / keys 等读操作时检查是否过期 | 过期数据可能仍占用内存,直到被访问或触发驱逐 |
| 自动驱逐(Eviction) | 当条目数超过 maxSize 时,按配置策略(LRU/FIFO)移除旧数据 | 驱逐发生在 set 操作时,不影响 get 性能 |
| 生命周期事件 | 支持 onExpire 回调,在数据被访问且发现过期时触发 | 回调在 get 执行期间同步调用,避免长时间阻塞 |
示例:TTL 使用
const mem = new SimpleMem();
mem.set('temp', 'data', 1000); // 1秒后过期
setTimeout(() => {
console.log(mem.get('temp')); // undefined(若1秒后执行)
}, 1500);
第三章:基础 API 使用
3.1 写入数据(set / put)
| 方法名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| set | mem.set(key, value, ttl?) | 写入键值对,可选设置过期时间 | const mem = new SimpleMem();mem.set('username', 'alice');mem.set('token', 'xyz', 5000); // 5秒过期 | key 必须为字符串;ttl 为数字(毫秒),非正数视为立即过期 |
| put | mem.put(key, value, ttl?) | set 的别名,功能完全相同 | mem.put('count', 42, 10000); | put 与 set 指向同一函数,仅命名偏好不同,建议统一使用 set |
3.2 读取数据(get)
| 方法名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| get | mem.get(key) | 获取指定键的值 | const val = mem.get('username');if (val !== undefined) { console.log('User:', val);} | 若 key 不存在、已过期或被删除,返回 undefined;不会抛出异常 |
| get(带默认值) | 无内置支持,需自行封装 | 提供默认回退值 | function getWithDefault(mem, key, defaultValue) { return mem.get(key) ?? defaultValue; }const theme = getWithDefault(mem, 'theme', 'light'); | SimpleMem 本身不提供 defaultValue 参数,需用户逻辑处理 |
3.3 删除数据(delete / remove)
| 方法名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| delete | mem.delete(key) | 删除指定键的缓存项 | mem.set('temp', 'data');mem.delete('temp');console.log(mem.get('temp')); // undefined | 若 key 不存在,操作静默成功,无错误 |
| remove | mem.remove(key) | delete 的别名,功能完全相同 | mem.remove('token'); | remove 与 delete 指向同一函数,建议统一使用 delete |
3.4 检查键是否存在(has / exists)
| 方法名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| has | mem.has(key) | 检查键是否存在且未过期 | if (mem.has('username')) { console.log('User logged in'); } | 仅当 key 存在且未过期时返回 true;过期项视为不存在 |
| exists | mem.exists(key) | has 的别名,功能完全相同 | console.log(mem.exists('token')); // true 或 false | exists 与 has 指向同一函数,建议统一使用 has |
第四章:高级功能
4.1 批量操作(batchSet / batchGet / batchDelete)
| 方法名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| batchSet | mem.batchSet(entries, ttl?) | 批量写入多个键值对 | const entries = [['name', 'Alice'], ['age', 30]];mem.batchSet(entries, 10000); // 全部10秒过期 | entries 为 [key, value][] 数组;ttl 应用于所有项;若某 key 非字符串,跳过或抛错(取决于实现) |
| batchGet | mem.batchGet(keys) | 批量读取多个键的值 | const values = mem.batchGet(['name', 'age', 'email']);// 返回 ['Alice', 30, undefined] | 返回数组顺序与 keys 一致;不存在或过期项返回 undefined |
| batchDelete | mem.batchDelete(keys) | 批量删除多个键 | mem.batchDelete(['temp1', 'temp2', 'cache_id']); | 忽略不存在的键,无错误抛出;操作原子性仅限于单线程语义 |
4.2 原子操作与事务支持
| 概念名称 | 说明 | 注意事项 |
|---|---|---|
| 原子操作支持 | SimpleMem 本身不提供传统数据库式事务,但所有 API 为同步且单线程执行,天然具备”操作原子性”(不会被其他 JS 任务中断) | 在 Node.js 单线程模型下,set/get/delete 等操作不可分割 |
| compareAndSet | 无内置 CAS 方法,需自行封装 | 可通过 get + set 实现简易 CAS |
简易 CAS 实现:
function cas(mem, key, expected, newValue, ttl) {
if (mem.get(key) === expected) {
mem.set(key, newValue, ttl);
return true;
}
return false;
}
| 概念名称 | 说明 | 注意事项 |
|---|---|---|
| 事务模拟 | 可通过快照 + 回滚机制模拟事务(需开发者自行管理) | SimpleMem 不提供 begin/commit/rollback 接口;建议在业务层维护操作日志 |
注:SimpleMem 定位为轻量缓存,不提供 ACID 事务,高一致性场景应使用数据库。
4.3 监听器与回调机制(onChange / onExpire)
| 方法名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| onChange | mem.onChange(callback) | 注册数据变更监听器 | mem.onChange((key, newValue, oldValue) => { console.log(Key ${key} changed from ${oldValue} to ${newValue}); });mem.set('status', 'active'); | callback 在 set/put/batchSet 后同步调用;delete 视为 newValue = undefined |
| onExpire | mem.onExpire(callback) | 注册过期事件监听器 | mem.onExpire((key, value) => { console.log(Expired: ${key} = ${value}); });mem.set('sess', 'abc', 1000); | 仅在 get/has 等读操作触发惰性过期检查时调用;后台不主动扫描过期项 |
| 移除监听器 | mem.offChange() / mem.offExpire() | 移除已注册的监听器 | mem.offExpire(); // 清除所有过期回调 | 通常用于组件卸载或测试清理 |
4.4 持久化与快照导出
| 方法名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| exportSnapshot | mem.exportSnapshot() | 导出当前所有有效缓存项 | const snapshot = mem.exportSnapshot();// 返回 { 'ns:key': { value, expiry } } 对象 | 仅包含未过期项;expiry 为时间戳;值为原始 JavaScript 对象 |
| importSnapshot | mem.importSnapshot(snapshot) | 从快照恢复缓存状态 | mem.importSnapshot({ 'default:counter': { value: 5, expiry: null } }); | 快照格式必须匹配;会覆盖同名键;不触发 onChange 回调 |
持久化到文件示例:
const fs = require('fs');
// 导出快照并写入文件
fs.writeFileSync('cache.json', JSON.stringify(mem.exportSnapshot()));
// 恢复时:
const snap = JSON.parse(fs.readFileSync('cache.json'));
mem.importSnapshot(snap);
仅适用于可 JSON 序列化的值;函数、
undefined、Symbol会丢失。
自动持久化示例:
setInterval(() => {
fs.writeFileSync('backup.json', JSON.stringify(mem.exportSnapshot()));
}, 60000);
频繁写入影响性能;建议在应用退出前保存(如
process.on('exit'))。
第五章:配置与优化
5.1 配置项详解(maxSize、ttl、evictionPolicy 等)
| 配置项名称 | 类型 | 默认值 | 说明 | 代码示例 | 注意事项 |
|---|---|---|---|---|---|
| maxSize | number | Infinity | 最大缓存条目数;超过时触发驱逐策略 | const mem = new SimpleMem({ maxSize: 100 }); | 设为 0 或负数将禁止写入;Infinity 表示不限制 |
| ttl | number | undefined | 全局默认过期时间(毫秒);若未在 set 时指定 ttl,则使用此值 | const mem = new SimpleMem({ ttl: 30000 }); // 所有项默认30秒过期 | 单次 set 的 ttl 参数优先级高于全局 ttl |
| namespace | string | 'default' | 实例所属命名空间,用于逻辑隔离 | const userCache = new SimpleMem({ namespace: 'user' }); | 不同 namespace 的实例互不影响 |
| evictionPolicy | string | 'lru' | 内存满时的驱逐策略,可选 'lru'、'fifo' | const mem = new SimpleMem({ maxSize: 10, evictionPolicy: 'fifo' }); | 仅当 maxSize 有限时生效 |
| enableListeners | boolean | true | 是否启用 onChange / onExpire 监听器 | const mem = new SimpleMem({ enableListeners: false }); | 关闭可提升高频写入性能 |
| autoPrune | boolean | false | 是否在每次操作后主动清理过期项(非惰性) | const mem = new SimpleMem({ autoPrune: true }); | 开启会降低写入性能,但减少内存占用 |
5.2 内存回收策略对比(LRU / FIFO / TTL-based)
| 策略名称 | 触发条件 | 回收逻辑 | 适用场景 | 注意事项 |
|---|---|---|---|---|
| LRU(最近最少使用) | 缓存条目数 > maxSize | 驱逐最久未被访问(get/set)的项 | 访问模式有局部性(热点数据反复访问) | 需维护访问顺序,内存开销略高 |
| FIFO(先进先出) | 缓存条目数 > maxSize | 驱逐最早写入的项 | 数据访问均匀,无明显热点 | 实现简单,性能稳定 |
| TTL-based(基于过期) | 仅在读操作或 autoPrune=true 时 | 不主动驱逐,仅在 get/has 时移除已过期项;若配合 maxSize,仍需 LRU/FIFO 处理容量溢出 | 以时间有效性为主导的缓存(如会话、令牌) | 单独使用无法控制内存上限,需搭配 maxSize |
注:SimpleMem 的”TTL-based”不是独立驱逐策略,而是过期机制;
evictionPolicy仅控制容量超限时的行为,过期清理始终独立存在。
示例:策略选择
// 热点数据缓存 → LRU
const hotCache = new SimpleMem({ maxSize: 50, evictionPolicy: 'lru' });
// 日志缓冲队列 → FIFO
const logBuffer = new SimpleMem({ maxSize: 1000, evictionPolicy: 'fifo' });
5.3 性能调优建议
| 建议名称 | 操作细节 | 适用场景 | 注意事项 |
|---|---|---|---|
| 合理设置 maxSize | 根据可用内存和预期 QPS 设置上限,避免 OOM | 高并发服务、嵌入式环境 | 可通过压力测试确定最佳值 |
| 避免存储大对象 | 将大对象拆分为多个小键,或仅缓存 ID + 从 DB 获取详情 | 缓存图片、长文本、复杂结构体 | 单个 MemUnit 过大会加剧 GC 压力 |
| 关闭监听器(高频写入) | 设置 enableListeners: false | 每秒数千次写入的指标采集场景 | 会失去 onChange/onExpire 能力 |
| 使用批量操作 | 用 batchSet/batchGet 替代循环单次调用 | 初始化缓存、批量查询 | 减少函数调用开销,提升吞吐 |
| 预估 TTL 避免无效缓存 | 根据业务设置合理 TTL,避免缓存长期无效数据 | 临时令牌、会话、计算中间结果 | TTL 过长浪费内存,过短失去缓存意义 |
| 定期快照备份(可选) | 在应用空闲期执行 exportSnapshot 并持久化 | 需要重启后恢复状态的 CLI 工具 | 快照期间避免大量写入,防止不一致 |
第六章:集成与扩展
6.1 与 Web 框架集成(Express / Koa / Fastify)
| 框架名称 | 集成方式 | 代码示例 | 注意事项 |
|---|---|---|---|
| Express | 将 SimpleMem 实例挂载到 app.locals 或通过中间件注入 | const express = require('express');const SimpleMem = require('simple-mem');const app = express();const cache = new SimpleMem({ ttl: 60000 });app.locals.cache = cache;app.get('/data', (req, res) => { const cached = req.app.locals.cache.get('data'); if (cached) return res.json(cached); // ...生成并缓存}); | 避免在每次请求中创建新实例;建议在应用启动时初始化一次 |
| Koa | 通过 ctx.state 或全局变量共享缓存实例 | const Koa = require('koa');const SimpleMem = require('simple-mem');const app = new Koa();const cache = new SimpleMem();app.context.cache = cache;app.use(async (ctx) => { const data = ctx.cache.get('api_result'); if (data) return ctx.body = data; // ...}); | Koa 的 context 可直接扩展;注意 async/await 不影响缓存同步性 |
| Fastify | 使用 decorate 注册缓存实例 | const fastify = require('fastify')();const SimpleMem = require('simple-mem');const cache = new SimpleMem();fastify.decorate('cache', cache);fastify.get('/info', async (request, reply) => { const info = request.server.cache.get('info'); if (info) return reply.send(info); // ...}); | decorate 是 Fastify 推荐的插件式扩展方式;确保在路由注册前完成 |
6.2 自定义序列化器
| 概念名称 | 说明 | 代码示例 | 注意事项 |
|---|---|---|---|
| 默认序列化 | SimpleMem 内部直接存储原始 JavaScript 值,不进行序列化 | mem.set('obj', { a: 1 }); // 直接引用对象 | 引用类型修改会影响缓存内容 |
| 自定义序列化需求 | 当需控制内存布局、支持不可变数据或兼容外部格式(如 JSON、MessagePack)时 | 见下方自定义序列化器示例 | SimpleMem 本身不提供 serializer 配置项,需在业务层封装 |
| 不可变缓存实践 | 返回深拷贝以防止外部修改 | function getImmutable(key) { const val = mem.get(key); return val ? JSON.parse(JSON.stringify(val)) : undefined;} | 性能开销大,仅在必要时使用 |
自定义序列化器示例:
class CustomSerializer {
serialize(value) {
return JSON.stringify(value);
}
deserialize(str) {
return JSON.parse(str);
}
}
// 使用包装层:
function setSafe(key, value) {
mem.set(key, serializer.serialize(value));
}
function getSafe(key) {
const raw = mem.get(key);
return raw ? serializer.deserialize(raw) : undefined;
}
6.3 插件系统与中间件开发
| 扩展方式 | 操作细节 | 代码示例 | 注意事项 |
|---|---|---|---|
| 中间件模式 | 封装通用缓存逻辑为可复用函数 | 见下方中间件示例 | 适用于 API 响应缓存;需拦截 res.json 等输出方法 |
| 插件注册机制 | 通过原型扩展或静态方法注入功能 | 见下方插件注册示例 | 修改原型影响所有实例;生产环境慎用 |
| 生命周期插件 | 利用 onChange/onExpire 构建监控或日志插件 | 见下方日志插件示例 | 适合开发调试;生产环境可关闭以提升性能 |
中间件示例(Express 缓存中间件):
function cacheMiddleware(cache, keyGen, ttl) {
return async (req, res, next) => {
const key = keyGen(req);
const data = cache.get(key);
if (data) return res.json(data);
res.originalJson = res.json;
res.json = (body) => {
cache.set(key, body, ttl);
return res.originalJson(body);
};
next();
};
}
// Express 使用:
app.get('/user/:id', cacheMiddleware(cache, req => `user:${req.params.id}`, 10000), handler);
插件注册示例:
SimpleMem.prototype.stats = function() {
return { size: Object.keys(this._store).length };
};
// 使用:
const mem = new SimpleMem();
console.log(mem.stats());
日志插件示例:
function loggingPlugin(mem) {
mem.onChange((key, newVal) => {
console.log(`[Cache] SET ${key}`);
});
mem.onExpire((key) => {
console.log(`[Cache] EXPIRED ${key}`);
});
}
loggingPlugin(cache);
第七章:原理剖析
7.1 内部数据结构设计
| 概念名称 | 说明 | 注意事项 |
|---|---|---|
| 底层存储结构 | 使用原生 JavaScript 对象({})作为主存储容器,键为字符串,值为 MemUnit 对象 | 对象属性查找平均 O(1),但极端情况下(大量属性)可能退化;不使用 Map 是为了兼容旧环境和简化序列化 |
| MemUnit 结构 | 每个缓存项封装为对象:{ value: any, expiry: number | null, namespace: string } | expiry 为 Unix 毫秒时间戳;namespace 用于逻辑隔离,但同一实例内所有项共享同一命名空间 |
| 命名空间实现 | 不同 namespace 通过创建独立 SimpleMem 实例实现,而非单实例内部分区 | 内存隔离彻底,但无法跨 namespace 批量操作;若需统一管理,需自行维护实例池 |
| 访问顺序记录(LRU) | 使用额外 Map 或数组记录 key 的访问顺序,set/get 时更新 | LRU 模式下内存占用略高;FIFO 则仅记录插入顺序,开销更低 |
| 键的内部格式 | 实际存储键 = ${namespace}:${key}(仅调试可见,用户 API 仍用原始 key) | 用户无需拼接;该设计便于未来支持跨 namespace 查询(当前未开放) |
7.2 事件循环与异步处理机制
| 概念名称 | 说明 | 注意事项 |
|---|---|---|
| 同步执行模型 | 所有 API(set/get/delete 等)均为同步函数,直接操作内存,不涉及 Promise 或回调队列 | 符合”轻量缓存”定位;避免异步开销,保证低延迟 |
| 惰性过期检查 | 过期判断仅在读操作(get/has/keys)或写操作触发驱逐时执行,不启动后台定时器 | 节省 CPU 资源;但过期数据可能滞留内存直至被访问 |
| 监听器调用时机 | onChange/onExpire 回调在 set/delete/get 等操作同步执行期间立即调用 | 回调阻塞主流程;避免在回调中执行耗时操作或再次修改缓存(可能引发递归) |
| 无微任务/宏任务调度 | 不使用 setTimeout/setImmediate/Promise.then 触发清理或通知 | 保证可预测性;适用于对时序敏感的测试或嵌入式场景 |
| 与 Node.js 事件循环关系 | SimpleMem 本身不注册任何事件循环句柄,完全被动响应调用 | 可安全用于 Worker Thread(若环境支持),但实例不共享 |
7.3 线程安全与并发控制(如适用)
| 概念名称 | 说明 | 注意事项 |
|---|---|---|
| 单线程假设 | SimpleMem 基于 JavaScript 单线程模型设计,默认运行在单一 JS 执行上下文中 | 在浏览器主线程或 Node.js 主线程中天然线程安全 |
| 多线程环境(Worker) | 若在多个 Worker 中分别创建实例,则彼此完全隔离;不支持跨线程共享同一实例 | SharedArrayBuffer 或 Atomics 未被使用;无法实现多线程协同缓存 |
| 并发写入安全性 | 在单线程内,由于操作不可中断,连续 set/get 具备”逻辑原子性” | 例如:set → get 不会被其他任务打断;但复合操作(如 get-then-set)非原子 |
| 无锁设计 | 无需互斥锁(Mutex)或读写锁,因无真正的并行执行 | 性能高,但限制了在多进程/多线程架构中的适用性 |
| 集群/多进程场景 | 每个进程持有独立缓存副本,数据不一致;需配合外部缓存(如 Redis)实现共享 | SimpleMem 定位为本地缓存,非分布式缓存解决方案 |
第八章:实战案例
8.1 构建本地缓存服务
| 步骤名称 | 操作细节 | 代码示例 | 注意事项 |
|---|---|---|---|
| 初始化缓存实例 | 创建带 TTL 和容量限制的 SimpleMem 实例 | const SimpleMem = require('simple-mem');const cacheService = new SimpleMem({ maxSize: 500, ttl: 60000, // 60秒 evictionPolicy: 'lru'}); | 根据业务预估 QPS 和数据大小设置 maxSize |
| 封装 getOrFetch 方法 | 提供”缓存穿透”防护:若未命中,则调用异步函数生成并缓存 | 见下方 getOrFetch 示例 | 高并发下可能多次调用 fetchFn(无锁),可加”锁”优化(如使用 Map 记录 pending key) |
| 提供清除接口 | 暴露手动清理能力(如管理后台调用) | function clearCache() { const keys = Object.keys(cacheService['_store']); keys.forEach(k => cacheService.delete(k));} | SimpleMem 无 clear 方法,需遍历删除 |
getOrFetch 实现:
async function getOrFetch(key, fetchFn) {
const cached = cacheService.get(key);
if (cached !== undefined) return cached;
const data = await fetchFn();
cacheService.set(key, data);
return data;
}
最小可运行服务示例:
const getUser = async (id) => ({ id, name: `User${id}` });
async function handleRequest(userId) {
return await getOrFetch(`user:${userId}`, () => getUser(userId));
}
// 调用:
handleRequest(123).then(console.log);
适用于 API 网关、微服务本地缓存等场景。
8.2 实现请求去重中间件
| 步骤名称 | 操作细节 | 代码示例 | 注意事项 |
|---|---|---|---|
| 使用 pending 请求池 | 在缓存基础上增加”进行中请求”记录,避免同一参数并发重复调用 | 见下方 dedupedFetch 示例 | pending Map 需与缓存生命周期协调;错误需清除 pending 避免阻塞 |
| 集成到 Express 中间件 | 封装为通用中间件,基于请求路径+查询参数生成唯一 key | 见下方中间件集成示例 | 仅适用于幂等 GET 请求;POST/PUT 不适用 |
| 错误处理与超时 | 为 pending Promise 添加超时控制 | 见下方超时控制示例 | 防止上游慢响应导致 pending 长期占用内存 |
dedupedFetch 实现:
const pending = new Map(); // key → Promise
async function dedupedFetch(key, fn) {
if (pending.has(key)) {
return pending.get(key);
}
const promise = (async () => {
try {
const result = await fn();
cacheService.set(key, result, 10000);
return result;
} finally {
pending.delete(key);
}
})();
pending.set(key, promise);
return promise;
}
集成到 Express 中间件:
function dedupeMiddleware() {
return async (req, res, next) => {
const key = req.originalUrl;
try {
const data = await dedupedFetch(key, () => fetchDataFromUpstream(req));
res.json(data);
} catch (err) {
next(err);
}
};
}
app.get('/api/data', dedupeMiddleware());
超时控制:
const withTimeout = (promise, ms) => {
return Promise.race([
promise,
new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), ms))
]);
};
// 在 dedupedFetch 中:
const result = await withTimeout(fn(), 5000);
最小可运行示例:
const simulateApi = (id) => new Promise(resolve => setTimeout(() => resolve({ id, ts: Date.now() }), 100));
for (let i = 0; i < 5; i++) {
dedupedFetch('test', () => simulateApi(1)).then(console.log);
} // 所有输出相同 ts
可用于防刷、防重复提交等场景。
8.3 会话(Session)管理示例
| 步骤名称 | 操作细节 | 代码示例 | 注意事项 |
|---|---|---|---|
| 生成唯一 Session ID | 使用 crypto 或简单时间戳+随机数组合 | const crypto = require('crypto');function generateSessionId() { return crypto.randomBytes(16).toString('hex');} | 避免使用 Math.random()(可预测) |
| 存储会话数据 | 以 sid 为 key,用户数据为 value,设置合理 TTL(如 30 分钟) | const sessionStore = new SimpleMem({ ttl: 1800000 }); // 30分钟function createSession(userData) { const sid = generateSessionId(); sessionStore.set(sid, userData); return sid;} | 敏感信息(如密码)不应存入 session |
| 读取与验证会话 | 从 Cookie 或 Header 获取 sid,查询并自动延长有效期(可选) | function getSession(sid) { const data = sessionStore.get(sid); if (data) { sessionStore.set(sid, data, 1800000); } return data;} | 滚动过期需谨慎,可能被恶意利用延长会话 |
| 集成到 Web 框架 | Express 示例:登录创建 session,受保护路由验证 session | 见下方完整示例 | Cookie 应设 HttpOnly + Secure(生产环境) |
完整会话管理示例:
app.post('/login', (req, res) => {
const sid = createSession({ userId: req.body.id });
res.cookie('sid', sid, { httpOnly: true });
res.json({ ok: true });
});
app.get('/profile', (req, res) => {
const sid = req.cookies.sid;
const user = getSession(sid);
if (!user) return res.status(401).end();
res.json(user);
});
最小可运行会话系统流程:
POST /login→ 返回 cookieGET /profile(带 cookie)→ 返回用户数据- 等待 30 分钟后再次访问 → 401
仅适用于单机部署;集群需共享 session 存储。