From ee36db75c341fca6bc1b1773ad6361358904d5f5 Mon Sep 17 00:00:00 2001 From: wehub-skill-sync Date: Mon, 13 Jul 2026 21:36:47 +0800 Subject: [PATCH] chore: import zh skill caching --- README.wehub.md | 9 +++ SKILL.md | 163 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 172 insertions(+) create mode 100644 README.wehub.md create mode 100644 SKILL.md diff --git a/README.wehub.md b/README.wehub.md new file mode 100644 index 0000000..31babfa --- /dev/null +++ b/README.wehub.md @@ -0,0 +1,9 @@ +# WeHub 来源说明 + +- Skill 名称:`caching` +- 中文类目:缓存策略 +- 上游仓库:`zebbern__claude-code-guide` +- 上游路径:`skills/caching/SKILL.md` +- 上游链接:https://github.com/zebbern/claude-code-guide/blob/HEAD/skills/caching/SKILL.md +- 本仓库为 WeHub 中文 Skill 汉化包,基于 skill 市场筛选 Top200 清单整理 +- 原作者、版权和许可证信息以上游仓库为准 diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..0fcdfd2 --- /dev/null +++ b/SKILL.md @@ -0,0 +1,163 @@ +--- +name: caching +description: 缓存策略——失效策略、TTL 指导原则、缓存键、缓存层级,以及何时不应使用缓存。适用于实现或审查缓存逻辑时使用。 +--- + +## 适用场景 + +- 在为实现 API 或服务添加缓存层(内存缓存、Redis、CDN)时。 +- 在为缓存数据选择 TTL 值或失效策略时。 +- 在设计缓存键模式以避免冲突或脏数据 bug 时。 +- 在审查从缓存读取或写入缓存的代码时。 +- 在调试脏数据、缓存雪崩或不一致响应时。 +- 在配置 TanStack Query 的 `staleTime`/`gcTime` 以进行客户端缓存时。 + +## 失效策略 + +- [P0-MUST] 为每一个缓存定义失效策略。脏数据比没有缓存更糟糕。 +- [P0-MUST] 当底层数据发生变化时,必须使缓存失效——不要仅依赖 TTL 过期。 +- [P1-SHOULD] 优先选择事件驱动的失效策略(写入/更新/删除时触发),而非单纯依赖基于时间的过期。 +- [P1-SHOULD] 当数据模式发生变化时,使用缓存版本化(包含版本键)。 + +## TTL 指导原则 + +- [P1-SHOULD] 根据数据变化频率设置 TTL:静态配置(小时/天)、用户资料(分钟)、实时数据(秒级或不缓存)。 +- [P1-SHOULD] 使用陈旧时重新验证策略:立即返回陈旧数据,同时在后台刷新。 +- [P2-MAY] 在开发环境中使用较短的 TTL,在生产环境中使用较长的 TTL。 + +## 缓存键 + +- [P0-MUST] 缓存键必须包含所有影响查询结果的条件参数。 +- [P1-SHOULD] 使用统一的键格式:`<实体>::<变体>`(例如 `user:123:profile`、`products:list:page=2`)。 +- [P1-SHOULD] 按服务或模块对键进行命名空间划分,防止冲突。 +- [P2-MAY] 对较长或较复杂的键进行哈希处理,以保持存储效率。 + +## 缓存层级 + +- [P1-SHOULD] 根据使用场景选择合适的缓存层级: + +| 层级 | 最适合的场景 | TTL 范围 | +|-------|----------|-----------| +| 内存缓存(Map、LRU) | 热点数据、单实例应用 | 秒到分钟 | +| Redis / Memcached | 跨实例的共享缓存、会话 | 分钟到小时 | +| CDN / 边缘节点 | 静态资源、公开 API 响应 | 小时到天 | +| HTTP 缓存头 | 浏览器缓存、API 响应 | 因资源而异 | + +- [P1-SHOULD] 分层使用缓存:检查内存 → Redis → 源服务器。未命中时回填写入。 + +## 何时不应使用缓存 + +- [P0-MUST] 不得在共享缓存中缓存用户特定的敏感数据(身份令牌、支付信息)。 +- [P1-SHOULD] 不应缓存变化频繁且过时数据会导致错误行为的数据(库存数量、实时定价)。 +- [P1-SHOULD] 不应缓存错误响应——使用较短的 TTL,或在失败时跳过缓存。 +- [P2-MAY] 当计算开销小且数据量不大时,应避免使用缓存。 + +## 代码示例 + +### 带 TTL 的内存 LRU 缓存 + +```ts +const cache = new Map(); +const MAX_SIZE = 500; + +export function getOrSet(key: string, ttlMs: number, compute: () => T): T { + const entry = cache.get(key); + if (entry && entry.expires > Date.now()) return entry.value as T; + + const value = compute(); + if (cache.size >= MAX_SIZE) { + // 淘汰最旧的条目(最先插入的) + const oldest = cache.keys().next().value!; + cache.delete(oldest); + } + cache.set(key, { value, expires: Date.now() + ttlMs }); + return value; +} +``` + +### 使用 ioredis 的陈旧时重新验证模式 + +```ts +import Redis from "ioredis"; +const redis = new Redis(process.env.REDIS_URL); + +export async function swr( + key: string, + freshSec: number, + staleSec: number, + fetcher: () => Promise, +): Promise { + const raw = await redis.get(key); + if (raw) { + const { value, createdAt } = JSON.parse(raw) as { value: T; createdAt: number }; + const ageMs = Date.now() - createdAt; + if (ageMs < freshSec * 1000) return value; // 新鲜——直接返回 + if (ageMs < staleSec * 1000) { + // 陈旧——返回缓存值,后台刷新 + fetcher().then((v) => + redis.set(key, JSON.stringify({ value: v, createdAt: Date.now() }), "EX", staleSec), + ); + return value; + } + } + const value = await fetcher(); + await redis.set(key, JSON.stringify({ value, createdAt: Date.now() }), "EX", staleSec); + return value; +} +``` + +### Express/Hono 中的 HTTP 缓存头 + +```ts +// 不可变资源(已哈希文件名) +app.use("/assets", (_, res, next) => { + res.setHeader("Cache-Control", "public, max-age=31536000, immutable"); + next(); +}); + +// API 响应——短缓存 + 重新验证 +app.get("/api/products", (_, res) => { + res.setHeader("Cache-Control", "public, max-age=60, stale-while-revalidate=300"); + res.json(products); +}); +``` + +### TanStack Query 缓存配置 + +```tsx +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 5 * 60 * 1000, // 数据在 5 分钟内视为新鲜 + gcTime: 30 * 60 * 1000, // 30 分钟后进行垃圾回收 + retry: 2, + refetchOnWindowFocus: false, + }, + }, +}); + +// 在组件中使用 +const { data } = useQuery({ + queryKey: ["products", { page, category }], // 缓存键包含参数 + queryFn: () => fetchProducts({ page, category }), +}); +``` + +## 反模式 + +- **缓存后不闻不问**——缓存数据但没有失效策略。数据永久变脏。 + - 替代做法:为每个缓存键定义显式的失效策略(写入时事件驱动,或设定有边界的 TTL)。 + +- **统一 TTL**——无论数据变化频率如何,对所有数据使用相同的 TTL(例如 1 小时)。 + - 替代做法:使 TTL 匹配数据变化频率——价格用秒级,用户资料用分钟级,配置用小时级。 + +- **缺少关键参数**——缓存键缺少用户 ID、语言环境或查询参数,导致返回错误数据。 + - 替代做法:包含所有影响结果的条件参数:`products:list:page=2:locale=en`。 + +- **缓存错误响应**——将错误响应(500 错误、超时)以较长 TTL 存储。 + - 替代做法:失败时跳过缓存,或使用极短的 TTL(5-10 秒)以便快速重试。 + +- **缓存雪崩**——当某个热门键过期时,所有实例同时请求源服务器。 + - 替代做法:使用陈旧时重新验证、带有抖动的 TTL,或通过互斥锁让单个实例刷新。