ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

undici Cache Store 深入指南:内存与 SQLite 缓存后端的设计与实现

undici Cache Store 深入指南:内存与 SQLite 缓存后端的设计与实现 后端网络通信【免费下载链接】undiciAn HTTP/1.1 client, written from scratch for Node.js项目地址https://gitcode.com/gh_mirrors/un/undici点击查看免费下载导读Cache Store 是 undici 缓存拦截器cache interceptor的存储后端负责持久化与检索缓存响应并依据请求与响应Vary头决定返回哪一份缓存。本指南基于 CacheStore.md 官方文档结合 memory-cache-store.js、sqlite-cache-store.js 与缓存拦截器源码 cache.js系统讲解MemoryCacheStore、SqliteCacheStore的构造参数、核心方法、驱逐策略与事件机制并给出自定义 Store 的完整契约让你能够在生产环境中为 undici 接入内存、SQLite 乃至 Redis 等任意缓存后端。什么是 Cache StoreA cache store 是 cache interceptor 用来持久化与检索缓存响应的存储后端。当请求到来时Store 通过把请求与响应上的Vary头进行比较来决定该为请求提供哪一份已存储的响应并且要求遵循 RFC 9111HTTP 缓存语义。该特性自 undici v7.0.0 起引入目前标记为 Stability: 2 - Stable属于稳定的公开 API。undici 内置两种 StoreMemoryCacheStore把响应保存在进程内存中适用于单进程、短生命周期或对持久化无要求的场景。SqliteCacheStore把响应持久化到 SQLite 数据库基于 Node.js 的node:sqliteAPI 实现适用于需要跨进程重启保留缓存的场景。两者都通过cacheStores导出对象暴露见 index.jsimport { cacheStores } from undici const { MemoryCacheStore, SqliteCacheStore } cacheStores需要特别说明的是SqliteCacheStore依赖node:sqliteAPI。该类始终会被导出但在不支持node:sqlite的 Node.js 版本中构造它会直接抛错。因此在使用 SQLite 后端前请先确认运行环境满足要求。与缓存拦截器的接入方式Cache Store 由缓存拦截器消费。构造一个 Store 后通过interceptors.cache({ store })注入到 Dispatcher 组合中即可生效import { interceptors, cacheStores, Agent, setGlobalDispatcher } from undici const store new cacheStores.MemoryCacheStore({ maxSize: 50 * 1024 * 1024 }) setGlobalDispatcher( new Agent().compose(interceptors.cache({ store })) )从 cache.js 的源码可以看到拦截器默认配置为const { store new MemoryCacheStore(), methods [GET], cacheByDefault undefined, type shared, origins undefined } opts即不传store时默认使用MemoryCacheStore默认只缓存GET请求缓存类型为共享缓存shared。拦截器内部会调用assertCacheStore(store, opts.store)见 util/cache.js校验 Store 是否实现了get、createWriteStream、delete三个方法否则直接抛出TypeError。Class:MemoryCacheStoreMemoryCacheStore继承自EventEmitter在内存中保存缓存响应。它同时约束三个上限缓存响应总数maxCount、全部响应总大小maxSize、单条响应大小maxEntrySize。任一上限被突破时Store 会按最久未使用LRU优先驱逐大约一半的条目并触发maxSizeExceeded事件。import { interceptors, cacheStores, Agent, setGlobalDispatcher } from undici const store new cacheStores.MemoryCacheStore({ maxSize: 50 * 1024 * 1024 }) setGlobalDispatcher( new Agent().compose(interceptors.cache({ store })) )new MemoryCacheStore([options])构造参数均为可选选项类型默认值说明maxCountnumber1024最多可存储的响应条数maxSizenumber104857600100 MiB所有存储响应体的总大小上限字节maxEntrySizenumber52428805 MiB单条响应体的大小上限字节超过则不入缓存errorCallbackfunction无接收 Store 内部错误回调参数为errErrormaxCount、maxSize、maxEntrySize均必须是非负整数否则抛出TypeError。从 memory-cache-store.js 的实现可以看到三个上限分别用私有字段保存#maxCount 1024 #maxSize 104857600 // 100MB #maxEntrySize 5242880 // 5MB且每个字段都经过严格校验——必须是number类型、必须是整数、且不小于 0违反任一条件即抛TypeErrorthrow new TypeError(MemoryCacheStore options.maxCount must be a non-negative integer)内部使用Map保存条目以${key.origin}:${key.path}作为顶层键同一 URL 下可以按Vary差异保存多条变体。memoryCacheStore.size类型number。当前所有已存储响应体的总字节数。该值在写入、替换、驱逐、删除时同步增减见 memory-cache-store.js。memoryCacheStore.isFull()返回 boolean当 Store 已触及maxSize或maxCount上限时返回true否则返回false。实现非常直观isFull () { return this.#size this.#maxSize || this.#count this.#maxCount }memoryCacheStore.get(key)key{CacheKey}要查找的请求返回{GetResult|undefined}命中则返回匹配的缓存响应无新鲜条目则返回undefined查找命中需要同时满足三个条件条目method与key.method相同、条目deleteAt时间在未来未过期、且条目vary表中列出的每个请求头都与key.headers中对应头相等。核心匹配逻辑位于 memory-cache-store.jsfunction findEntry (key, entries, now) { for (let i 0; i entries.length; i) { const entry entries[i] if ( entry.deleteAt now entry.method key.method varyMatches(key, entry) ) { return entry } } }值得注意的一个实现细节命中后Store 会把该 URL 的条目列表从Map中先删除再重新写入#entries.delete(topLevelKey)后再set以此在插入顺序上把最近命中的条目移动到末尾为 LRU 驱逐提供依据——这正是最久未使用优先驱逐的实现基础。memoryCacheStore.createWriteStream(key, value)key{CacheKey}响应对应的请求value{CacheValue}要存储的响应元数据返回{Writable|undefined}用于写入响应体的可写流响应不可缓存时返回undefined写入流的实现见 memory-cache-store.js。每个 chunk 写入时累计entry.size一旦超过maxEntrySize就直接this.destroy()销毁流不落库流结束时final把条目提交进Map。提交时如果已存在匹配条目则替换并扣除旧条目大小否则新增条目并让#count 1。提交后立即检查是否超限if (store.#size store.#maxSize || store.#count store.#maxCount) { // 触发 maxSizeExceeded 事件若未触发过 store.emit(maxSizeExceeded, {...}) store.#evict(entry) }这里体现了关键机制写入新条目本身不拒绝而是先写入、再驱逐保证刚写入的条目在驱逐时也能被保护见下文#evict。memoryCacheStore.delete(key)key{CacheKey}要移除缓存的请求返回undefined删除该key.origin与key.path对应的所有缓存条目即整个顶层键并同步扣减#size与#count。若key不是对象则抛出TypeErrordelete (key) { if (typeof key ! object) { throw new TypeError(expected key to be object, got ${typeof key}) } ... }驱逐策略与maxSizeExceeded事件驱逐逻辑位于#evictmemory-cache-store.js。当超限时Store 以maxSize / 2与maxCount / 2为目标从Map头部即最久未命中的 URL开始逐个驱逐条目直到总大小与总条数都降到一半以下驱逐时跳过刚写入的keep条目。若最终仍未降到上限以内最后才把keep自身也驱逐确保内存安全。事件maxSizeExceeded自 v7.10.0 起在超限时、驱逐发生前立即触发负载字段字段类型说明sizenumber当前所有响应总大小字节maxSizenumber配置的maxSize上限countnumber当前存储的响应条数maxCountnumber配置的maxCount上限该事件每次溢出只触发一次由#hasEmittedMaxSizeEvent标志控制直到 Store 重新降到两个上限以下才会再次允许触发。典型用途监控缓存压力、上报指标或主动扩容。store.on(maxSizeExceeded, ({ size, maxSize, count, maxCount }) { console.warn(cache store overflow: ${count}/${maxCount} entries, ${size}/${maxSize} bytes) })Class:SqliteCacheStoreSqliteCacheStore使用node:sqlite的同步 APIDatabaseSync把缓存响应持久化到 SQLite 数据库。构造时若当前 Node.js 版本没有node:sqlite会直接抛出异常。import { interceptors, cacheStores, Agent, setGlobalDispatcher } from undici const store new cacheStores.SqliteCacheStore({ location: ./cache.db }) setGlobalDispatcher( new Agent().compose(interceptors.cache({ store })) )从 sqlite-cache-store.js 可见内部常量VERSION 3用于表名版本管理当前建表为cacheInterceptorV3单条响应上限为 2 GB。new SqliteCacheStore([options])构造参数均为可选选项类型默认值说明locationstring:memory:SQLite 数据库文件路径传:memory:使用纯内存数据库maxCountnumberInfinity最多可存储的响应条数maxEntrySizenumber20000000002 GB单条响应体大小上限字节不能超过 2 GBmaxCount、maxEntrySize必须是非负整数且maxEntrySize不得大于 2 GB否则抛TypeError见 sqlite-cache-store.js。与内存版不同SQLite 版没有maxSize总大小上限因为磁盘空间由数据库自行管理。SQLite 底层结构构造时执行的建表语句见 sqlite-cache-store.js值得关注它揭示了持久化的数据模型PRAGMA journal_mode WAL; PRAGMA synchronous NORMAL; PRAGMA temp_store memory; PRAGMA optimize; CREATE TABLE IF NOT EXISTS cacheInterceptorV3 ( id INTEGER PRIMARY KEY AUTOINCREMENT, url TEXT NOT NULL, method TEXT NOT NULL, body BUF NULL, deleteAt INTEGER NOT NULL, statusCode INTEGER NOT NULL, statusMessage TEXT NOT NULL, headers TEXT NULL, cacheControlDirectives TEXT NULL, etag TEXT NULL, vary TEXT NULL, cachedAt INTEGER NOT NULL, staleAt INTEGER NOT NULL ); CREATE INDEX IF NOT EXISTS idx_cacheInterceptorV3_getValuesQuery ON cacheInterceptorV3(url, method, deleteAt); CREATE INDEX IF NOT EXISTS idx_cacheInterceptorV3_deleteByUrlQuery ON cacheInterceptorV3(deleteAt);几点实现细节启用了 WAL 日志模式、NORMAL同步级别与内存临时存储兼顾持久化安全与写入性能。url由${key.origin}/${key.path}拼成见#makeValueUrlheaders、vary、cacheControlDirectives等结构化字段以 JSON 字符串形式存放读取时JSON.parse还原。查询语句按(url, method, deleteAt)过滤并按deleteAt升序排列先命中的自然是较早过期的条目。列名body BUF NULL与#getValuesQuery中的字段顺序直接决定了get()返回的body是Buffer。sqliteCacheStore.close()返回 undefined。关闭底层 SQLite 数据库连接。应用退出或不再使用缓存时调用避免数据库文件句柄泄漏。sqliteCacheStore.size类型number。当前数据库中存储的响应条数。实现为对表执行SELECT COUNT(*)get size () { const { total } this.#countEntriesQuery.get() return total }sqliteCacheStore.get(key)key{CacheKey}返回{GetResult|undefined}命中的缓存响应其中body是 {Buffer}与内存版语义一致比较请求方法与vary表中列出的每个请求头。实现位于#findValuesqlite-cache-store.js按url method查出候选行跳过已过deleteAt的行除非canBeExpired再逐行做vary头匹配。注意与内存版的一个差异内存版把deleteAt是否在未来作为硬性匹配条件SQLite 版则依赖查询 SQL 中deleteAt参与过滤并结合now value.deleteAt跳过。sqliteCacheStore.set(key, value)key{CacheKey}value{CacheValue}body必须是 {Buffer}、{Buffer} 数组或null返回undefined直接写入数据库不经过流。流程为拼接body→ 若body字节数超过maxEntrySize则直接返回不存储 → 查找已存在条目存在则走UPDATE覆盖不存在则INSERT新行并调用#prune()清理。该方法是createWriteStream()的内部落库入口。#prune()的清理策略sqlite-cache-store.js顺序如下若maxCount有限且当前条数未超限直接返回先删除所有已过期条目deleteAt Date.now()仍超限则按cachedAt升序最早缓存的优先删除max(floor(maxCount * 0.1), 1)条。sqliteCacheStore.createWriteStream(key, value)key{CacheKey}value{CacheValue}返回{Writable|undefined}与内存版类似chunk 写入时累计大小超过maxEntrySize即销毁流流结束时final把累积的 Buffer 数组交给set()落库final (callback) { store.set(key, { ...value, body }) callback() }sqliteCacheStore.delete(key)key{CacheKey}返回undefined删除该key对应 URL 的全部缓存行DELETE FROM ... WHERE url ?。key不是对象时同样抛TypeError。注意与内存版的差异内存版按origin:path删除SQLite 版按拼接的origin/pathURL 删除效果等价。实现自定义 Cache StoreCache Store 的本质是一个实现了统一接口的对象任何满足该契约的对象都能传给缓存拦截器——这意味着你可以很方便地实现基于 Redis、远程服务或其他持久化手段的第三方 Store。接口要求由 assertCacheStore 强制校验方法签名说明get(key)(CacheKey) GetResult \| undefined \| PromiseGetResult \| undefined查找响应createWriteStream(key, value)(CacheKey, CacheValue) Writable \| undefined返回接收响应体的可写流不可存储时返回undefineddelete(key)(CacheKey) undefined \| Promiseundefined删除该 key 的全部响应get与delete返回 Promise 的能力非常关键它允许 Store 背后是异步资源如 Redis 网络请求。缓存拦截器会对这些 Promise 进行await包括在 revalidation 与 stale-while-revalidate 路径上见 cache.js 中对result.then的判断处理。一个最小可用的自定义 Store 骨架class MyStore { async get (key) { // 返回 GetResult | undefined } createWriteStream (key, value) { // 返回 Writable可引用实现自 node:stream 的 Writable } async delete (key) { // 清理 key.origin key.path 对应的全部条目 } }CacheKey被查找或存储的请求描述字段类型说明originstring请求源originmethodstring请求方法pathstring请求路径headersRecordstring, string|string[]请求头用于满足Vary匹配可选CacheKey 由拦截器内部通过makeCacheKey(opts, requestOrigin)生成见 util/cache.js其中path会把 query 序列化拼接进去。所有 Store 方法在入口处都会调用assertCacheKey校验origin、method、path必须是字符串headers必须是对象否则抛TypeError。CacheValue被缓存响应的元数据不含响应体字段类型说明statusCodenumberHTTP 状态码statusMessagestringHTTP 状态消息headersRecordstring, string|string[]响应头varyRecordstring, string|string[]|null响应Vary头列出的头名 → 原始请求中该头的值原请求无此头则为null可选etagstring响应实体标签可选cacheControlDirectivesObject解析后的响应Cache-Control指令可选cachedAtnumber缓存时间毫秒时间戳staleAtnumber变为 stale 的时间毫秒时间戳deleteAtnumber必须被驱逐的时间毫秒时间戳一旦超过该时间 Store 不得再返回该响应vary映射是响应选择的核心。假设响应头如下Vary: content-encoding, accept content-encoding: utf8 accept: application/json则记录下来的映射为{ content-encoding: utf8, accept: application/json }若原始请求没有携带accept头则该值为null{ content-encoding: utf8, accept: null }从 util/cache.js 的parseVaryHeader可以看到映射的生成规则头名统一转为小写Vary: *会直接退化为返回整个请求头此时任何请求都无法精确匹配缓存命中率极低无效的 token 会导致返回undefined表示该响应不可缓存。匹配侧的headerValueEquals内存与 SQLite 版共用同一实现对字符串、数组以及null缺失值做了严格的逐项比较。CacheValue同样有assertCacheValue校验statusCode、cachedAt、staleAt、deleteAt必须是 numberstatusMessage必须是 stringheaders、vary必须是对象etag必须是 string。GetResultget()的返回值包含CacheValue的全部字段外加响应体字段类型说明bodyReadable | Iterable | AsyncIterable | Buffer | string缓存的响应体可选两个内置 Store 的实现差异点MemoryCacheStore.get()返回的body是缓存的 Buffer 数组拼接结果保持原始 Buffer 引用而SqliteCacheStore.get()会从数据库行还原为全新的BufferBuffer.from(value.body.buffer, ...)见 sqlite-cache-store.js。测试与验证缓存 Store 的契约行为在仓库测试中有充分覆盖。例如 test/interceptors/cache.js 中大量用例直接构造MemoryCacheStore与SqliteCacheStore实例注入拦截器进行端到端验证涵盖命中、过期、Vary变体匹配、驱逐与maxSizeExceeded事件等场景test/interceptors/origin-isolation.js 则验证了 Store 在跨源请求隔离下的行为。若你实现了自定义 Store可参考这些测试来验证你的实现满足拦截器的调用契约。总结选择MemoryCacheStore进程内缓存、零依赖、无需管理数据库文件适合默认场景通过maxCount/maxSize/maxEntrySize三把尺子控制内存占用超限时 LRU 驱逐并触发maxSizeExceeded事件。选择SqliteCacheStore跨重启持久化、可共享到磁盘需要 Node.js 支持node:sqlite通过location指定数据库文件maxCount控制条数maxEntrySize上限为 2 GB。需要其他后端实现get/createWriteStream/delete三方法支持 Promise即可接入缓存拦截器CacheKey、CacheValue、GetResult是必须遵守的数据契约。详细的方法签名与语义请继续阅读 CacheStore.md、类型声明 cache-interceptor.d.ts以及两份内置实现 memory-cache-store.js 与 sqlite-cache-store.js。赞分享后端网络通信【免费下载链接】undiciAn HTTP/1.1 client, written from scratch for Node.js项目地址https://gitcode.com/gh_mirrors/un/undici点击查看免费下载相关推荐LMCache CPU RAM 后端深度指南用页锁定内存实现 KV Cache 热缓存与离线卸载LMCache CPU RAM 后端深度指南用页锁定内存实现 KV Cache 热缓存与离线卸载 CPU RAM 是 LMCache 将 KV Cache 卸人工智能大模型缓存抽象模型推理服务Gas Town Polecat 角色协议深度解析从 CLAUDE.md 模板看自主 Worker 的完整生命周期纪律Gas Town Polecat 角色协议深度解析从 CLAUDE.md 模板看自主 Worker 的完整生命周期纪律 导读 本文以 Gas Townmu人工智能AI AgentAgent 编排代码智能体CLIEMQX 授权缓存authz cache内存优化客户端断开即清理的设计与实现EMQX 授权缓存authz cache内存优化客户端断开即清理的设计与实现 本文围绕 EMQX 中 PR 15899 引入的优化——客户端断开连接时立即后端物联网消息队列通信上一篇GetQzonehistory三步轻松备份QQ空间完整回忆的终极指南下一篇如何快速掌握缠论自动化分析3步在通达信中实现精准技术分析的终极指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表