尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

边缘与服务端双层缓存:基于 Node.js Memory LRU 与 HTTP 响应头的极致调优

边缘与服务端双层缓存:基于 Node.js Memory LRU 与 HTTP 响应头的极致调优 边缘与服务端双层缓存基于 Node.js Memory LRU 与 HTTP 响应头的极致调优在轻量级 API 服务中高频重复的数据库查询与昂贵的 CPU 运算是拖慢响应时间的主因。单纯依靠 Redis 往往增加了额外的网络跳数Network Hop与部署复杂度。本文设计一套优雅的“双层缓存架构”Two-Tier Caching在 Node.js 进程内部使用 Memory LRU 作为第一层超高速缓存在 HTTP 响应头配置标准的Cache-Control与ETag作为第二层边缘 CDN 缓存。flowchart TD A[HTTP API Request] -- B{第一层: 浏览器 / CDN 边缘缓存} B -- Cache-Control 304 Not Modified -- C[浏览器极速读取本地缓存 0ms] B -- 边缘未命中 (Pass to Server) -- D{第二层: Node.js 内存 LRU 缓存} D -- Hit in Memory -- E[返回 LRU 内存快照 1ms] D -- Miss in Memory -- F[底层数据库查询 SQLite / Compute] F -- G[更新 Node.js LRU 内存缓存] G -- H[计算 ETag / s-maxage Header] H -- I[返回 HTTP 200 给客户端]一、双层缓存的设计哲学与优势单体轻量应用的最优缓存设计应当遵循离数据使用者越近越好的原则第一层客户端与 CDN 边缘缓存Edge Tier利用标准的 HTTP 协议头s-maxage、stale-while-revalidate让 CDN 节点或浏览器直接截断请求请求甚至不需要真正到达你的源站服务器。第二层Node.js 进程内 Memory LRU 缓存In-Memory Tier对于必须到达源站的请求直接从 Node.js 堆内存的 LRU 散列表中读取避免发起 TCP / 本地磁盘 I/O 去查询 SQLite 或 PostgreSQL。这种双层架构可以在不引入额外 Redis 实例的前提下将轻量 API 的吞吐性能提升上百倍。二、基于 Node.js 的 LRU 内存缓存组件实现LRULeast Recently Used最近最少使用算法可以在内存达到上限时自动剔除最老未被访问的节点防止 Node.js 堆内存崩溃。// lib/memoryLruCache.ts export interface CacheNodeK, V { key: K; value: V; expiresAt: number; prev: CacheNodeK, V | null; next: CacheNodeK, V | null; } export class MemoryLruCacheK, V { private capacity: number; private ttlMs: number; private cache: MapK, CacheNodeK, V new Map(); private head: CacheNodeK, V | null null; private tail: CacheNodeK, V | null null; constructor(capacity: number 500, ttlMs: number 60000) { this.capacity capacity; this.ttlMs ttlMs; } /** * 从 LRU 缓存读取数据 */ public get(key: K): V | null { const node this.cache.get(key); if (!node) return null; // 校验 TTL 过期时间 if (Date.now() node.expiresAt) { this.remove(key); return null; } // 将被访问的节点提升至双向链表头部 (代表最新使用) this.moveToHead(node); return node.value; } /** * 写入数据到 LRU 缓存 */ public set(key: K, value: V, customTtlMs?: number): void { const ttl customTtlMs || this.ttlMs; const expiresAt Date.now() ttl; if (this.cache.has(key)) { const node this.cache.get(key)!; node.value value; node.expiresAt expiresAt; this.moveToHead(node); return; } // 内存满载时剔除尾部最老未使用的节点 if (this.cache.size this.capacity) { if (this.tail) { this.cache.delete(this.tail.key); this.removeNode(this.tail); } } const newNode: CacheNodeK, V { key, value, expiresAt, prev: null, next: null, }; this.cache.set(key, newNode); this.addToHead(newNode); } public remove(key: K): void { const node this.cache.get(key); if (node) { this.cache.delete(key); this.removeNode(node); } } private addToHead(node: CacheNodeK, V): void { node.next this.head; node.prev null; if (this.head) this.head.prev node; this.head node; if (!this.tail) this.tail node; } private removeNode(node: CacheNodeK, V): void { if (node.prev) node.prev.next node.next; else this.head node.next; if (node.next) node.next.prev node.prev; else this.tail node.prev; } private moveToHead(node: CacheNodeK, V): void { this.removeNode(node); this.addToHead(node); } }三、整合 HTTP Cache-Control 与 ETag 的无感中间件在 Express 或 Fastify 中封装一个极简的双层缓存中间件// middleware/twoTierCache.ts import { Request, Response, NextFunction } from express; import crypto from node:crypto; import { MemoryLruCache } from ../lib/memoryLruCache; // 实例化全局单例的 Node.js 堆内存 LRU 缓存 (最多缓存 1000 个 API 结果) const lruCache new MemoryLruCachestring, { body: any; etag: string }(1000, 30000); export function twoTierCacheMiddleware(ttlSeconds: number 30) { return (req: Request, res: Response, next: NextFunction) { // 仅针对 GET 请求应用缓存 if (req.method ! GET) { return next(); } const cacheKey ${req.originalUrl || req.url}; const clientETag req.headers[if-none-match]; // 1. 尝试从 Node.js 第一层内存 LRU 中读取 const cached lruCache.get(cacheKey); if (cached) { // 校验客户端带过来的 ETag 协商头 if (clientETag clientETag cached.etag) { // 第二层: 浏览器 / CDN 协商缓存命中直接返回 304 Not Modified0 带宽消耗 res.setHeader(Cache-Control, public, max-age0, s-maxage${ttlSeconds}, stale-while-revalidate15); res.setHeader(ETag, cached.etag); return res.status(304).end(); } // 第一层: 内存 LRU 命中直接返回数据 res.setHeader(Cache-Control, public, max-age0, s-maxage${ttlSeconds}, stale-while-revalidate15); res.setHeader(ETag, cached.etag); res.setHeader(X-Cache-Status, HIT-MEMORY); return res.json(cached.body); } // 2. 内存未命中拦截 res.json 原生方法自动捕获响应并写入双层缓存 const originalJson res.json.bind(res); res.json (body: any): Response { // 计算确定性的 MD5 ETag const etag ${crypto.createHash(md5).update(JSON.stringify(body)).digest(hex)}; // 写入内存 LRU 缓存 lruCache.set(cacheKey, { body, etag }, ttlSeconds * 1000); // 设置标准的 HTTP Cache-Control 协议头 res.setHeader(Cache-Control, public, max-age0, s-maxage${ttlSeconds}, stale-while-revalidate15); res.setHeader(ETag, etag); res.setHeader(X-Cache-Status, MISS); return originalJson(body); }; next(); }; }四、stale-while-revalidate 策略的优雅体现在Cache-Control中配置stale-while-revalidate15是一种极其优雅的平滑降级策略当缓存未过期时CDN 节点瞬间返回缓存数据。当缓存在 15 秒内刚刚过期时CDN依然瞬间向用户返回旧的缓存数据0 延迟但会在后台异步发下一个请求给源站 Node.js 服务去刷新缓存。任何用户都不会感到因缓存失效而产生的突发卡顿五、架构考量与主动失效Invalidation防线双层缓存大幅提升了性能但也必须处理好主动失效问题写操作触发的缓存清除当用户提交了一个POST /api/articles新建文章请求时必须在中间件或 Controller 中显式调用lruCache.remove(/api/articles)清除旧的内存缓存防止出现数据不一致。多节点平滑同步如果未来扩展为了多台物理 VPS 节点第一层内存 LRU 仅能覆盖当前节点第二层 HTTP CDN 缓存依然能够保障全球用户的统一一致性。用标准的 HTTP 缓存协议控制边缘用极速的 Memory LRU 保护数据库是独立产品极低开销的极致性能实践。
返回列表