mcpField
C
caching-strategies

caching-strategies

github.com/curiositech开发257141 次安装2 天前更新
代码仓库

关于 caching-strategies

面向工程师的应用缓存设计参考技能,涵盖缓存模式、数据新鲜度、失效处理与雪崩预防。内容强调按数据需求选择策略,避免陈旧数据和无限增长的缓存。

主要功能

  1. 01Cache-aside 与 write-through 模式
  2. 02按新鲜度需求设计 TTL
  3. 03Redis 失效与 Lua 模式
  4. 04HTTP 缓存与 CDN 指引
  5. 05缓存雪崩预防策略

适用场景

  1. 01为应用缓存选择模式
  2. 02为变化数据设定 TTL
  3. 03规划多层缓存失效
  4. 04防止缓存未命中引发并发过载

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/curiositech/some_claude_skills --skill caching-strategies

安装后在 Claude Code 中输入/caching-strategies确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

.claude-plugin/
plugin.json
references/
http-caching.md
redis-patterns.md

详细介绍

缓存策略

缓存是最常被误用的性能优化手段。问题不在于“缓存太少”,而在于“没有失效策略就使用缓存,然后在六个月后于生产环境中发现问题——用户抱怨数据过时,而你却无法解释原因”。

适用场景

✅ 适用于:

  • 为用例选择合适的缓存模式(旁路缓存、直写缓存、回写缓存)
  • 根据不同的数据新鲜度要求设计 TTL 值
  • 实现 Redis 缓存模式:有序集合、发布/订阅失效、Lua 脚本
  • 配置 Cache-Control 标头、ETag 和 CDN 行为
  • 通过锁定、概率性提前过期或后台刷新来防止缓存雪崩
  • 针对冷启动场景制定缓存预热策略
  • 设计多级缓存(内存 L1、Redis L2、CDN L3)

❌ 不适用于:

  • 数据库内部的查询计划缓存(由数据库处理)
  • Python functools.lru_cache / JavaScript memoize 工具(纯函数记忆化)
  • CPU 分支预测或硬件缓存调优
  • 会话存储(使用专用的会话技能)

选择哪种缓存模式?

flowchart TD
    Q1{Who writes to cache?} --> WA[Application writes]
    Q1 --> WC[Cache writes automatically]
    WA --> Q2{When does the cache get populated?}
    Q2 -->|On read miss| CA[Cache-Aside\n'Lazy loading']
    Q2 -->|On every write| WT[Write-Through\n'Eager write']
    WC --> Q3{Sync or async write-back?}
    Q3 -->|Sync — write completes when cache updates| WT
    Q3 -->|Async — write returns fast, flush later| WB[Write-Behind\n'Write-back']
    CA --> N1{Is stale data OK\nfor a short period?}
    N1 -->|Yes| CA_USE[Use cache-aside\nwith TTL expiry]
    N1 -->|No| INVAL[Add explicit invalidation\nor use write-through]
    WT --> NOTE2[Good for read-heavy data\nthat changes infrequently]
    WB --> NOTE3[Good for write-heavy workloads\nRisk: data loss on crash]

多级缓存架构

flowchart LR
    USER[User Request] --> CDN{CDN / Edge Cache\nL3 — 100ms+ saved}
    CDN -->|Cache hit| RESP[Response]
    CDN -->|Cache miss| LB[Load Balancer]
    LB --> APP[App Server]
    APP --> L1{In-Process Cache\nL1 — ~0ms}
    L1 -->|Hit| APP
    L1 -->|Miss| REDIS{Redis\nL2 — 1-5ms}
    REDIS -->|Hit| APP
    REDIS -->|Miss| DB[(Database\n10-100ms)]
    DB --> REDIS
    REDIS --> APP
    APP --> L1
    APP --> CDN
    APP --> RESP
级别技术延迟容量是否共享?
L1:进程内Node.js Map、Python dict、LRU-cache~0ms小(MB)否——每个实例独有
L2:分布式Redis、Memcached1-5ms大(GB)是——所有实例共享
L3:边缘/CDNCloudflare、Fastly、CloudFront10-100ms极大是——全球共享

规则:数据首先在一个位置发生变更。失效操作向外传播:DB → Redis → CDN。失效时绝不要跳过任何级别。


旁路缓存模式(最常见)

由应用显式管理缓存。读取时:检查缓存,如果未命中则从 DB 获取数据、填充缓存并返回。写入时:更新 DB,删除缓存条目。

class UserCache {
  private redis: Redis;
  private readonly TTL_SECONDS = 300; // 5 minutes

  async getUser(userId: string): Promise<User> {
    const key = `user:${userId}`;

    // 1. Check cache
    const cached = await this.redis.get(key);
    if (cached) return JSON.parse(cached);

    // 2. Cache miss — fetch from source
    const user = await db.users.findById(userId);
    if (!user) throw new NotFoundError('User', userId);

    // 3. Populate cache
    await this.redis.setex(key, this.TTL_SECONDS, JSON.stringify(user));

    return user;
  }

  async updateUser(userId: string, data: Partial<User>): Promise<User> {
    const user = await db.users.update(userId, data);

    // 4. Invalidate — delete, don't update
    // Updating in cache risks race conditions; let the next read repopulate
    await this.redis.del(`user:${userId}`);

    return user;
  }
}

何时删除而非覆盖失效项:几乎总是应该删除。写入后覆盖缓存会产生竞争条件:在你写入 DB 与写入缓存之间,其他请求可能已经获取了旧值。删除缓存会促使下一个读取者获取最新数据。


直写缓存模式

每次写入都会同步写入缓存和 DB。缓存始终有数据。适合只写入一次、之后会读取多次的数据。

async function createProduct(data: CreateProductInput): Promise<Product> {
  // Write to DB first (source of truth)
  const product = await db.products.create(data);

  // Immediately populate cache — no future cache miss for this product
  const key = `product:${product.id}`;
  await redis.setex(key, 3600, JSON.stringify(product));

  // Also invalidate list caches that include this product
  await redis.del('products:list:*'); // pattern delete via SCAN, see redis-patterns.md

  return product;
}

权衡:写入延迟更高(每次操作需要写入两次)。创建后再也不会读取的条目会浪费缓存空间。最适合读写比高的数据。


TTL 设计

TTL 不是缓存失效策略,而是允许数据过时的时限。应根据数据变化频率和可接受的过时程度来设计 TTL:

数据类型TTL原因
用户会话令牌与会话过期时间一致安全要求
用户资料(姓名、头像)5-15 分钟很少更改;时间足够短,响应及时
产品目录1-4 小时偶尔更改;可接受一定延迟
库存数量30 秒经常更改;时间应短,但不能为零
汇率60 秒受监管要求约束;不能过时太久
静态配置 / 功能标志60 秒 + 发布/订阅失效更改时需要主动推送失效通知
计算出的聚合数据(日统计)直至下一次计算重新计算时显式失效

TTL 抖动:许多键具有相同 TTL 时,它们会同时过期,从而引发惊群效应。添加随机抖动:

const jitter = Math.floor(Math.random() * 60); // 0-60 seconds
await redis.setex(key, baseTtl + jitter, value);

防止缓存雪崩

缓存雪崩(也称为 dog-pile、惊群效应)是指许多请求同时未命中已过期的缓存键,并争相计算或获取对应值。

策略 1:概率性提前过期(XFetch)

根据键距离过期还有多近,以相应概率在过期前重新获取:

async function getWithEarlyExpiry<T>(
  key: string,
  fetcher: () => Promise<T>,
  ttlSeconds: number,
  beta = 1.0
): Promise<T> {
  const entry = await redis.get(key + ':meta');
  if (entry) {
    const { value, expiresAt, fetchDurationMs } = JSON.parse(entry);
    const now = Date.now();
    const ttlRemaining = expiresAt - now;
    // Fetch early if within probabilistic window
    const shouldRefetch = ttlRemaining < beta * fetchDurationMs * Math.log(Math.random());
    if (!shouldRefetch) return value;
  }

  // Fetch and cache
  const start = Date.now();
  const value = await fetcher();
  const fetchDurationMs = Date.now() - start;
  const expiresAt = Date.now() + ttlSeconds * 1000;

  await redis.setex(key + ':meta', ttlSeconds, JSON.stringify({ value, expiresAt, fetchDurationMs }));
  return value;
}
策略 2:未命中时使用互斥锁

只有一个工作线程重新计算值;其他线程等待锁或返回过期数据:

async function getWithLock<T>(
  key: string,
  fetcher: () => Promise<T>,
  ttl: number
): Promise<T> {
  const cached = await redis.get(key);
  if (cached) return JSON.parse(cached);

  const lockKey = `lock:${key}`;
  const lockAcquired = await redis.set(lockKey, '1', 'NX', 'PX', 5000); // 5s TTL

  if (!lockAcquired) {
    // Another worker is computing — poll briefly then return stale or throw
    await sleep(100);
    const retried = await redis.get(key);
    if (retried) return JSON.parse(retried);
    throw new Error('Cache unavailable');
  }

  try {
    const value = await fetcher();
    await redis.setex(key, ttl, JSON.stringify(value));
    return value;
  } finally {
    await redis.del(lockKey);
  }
}

请参阅 references/redis-patterns.md,了解此锁的 Lua 原子版本(可防止错误的客户端释放锁)。


反模式

反模式:永久缓存所有内容

新手:“缓存能提升速度。将 TTL 设为 0(永不过期)或一年,以最大限度提高缓存命中率。”

专家:无界缓存只不过是换了种形式的内存泄漏。它们还会导致数据必然过期——用户看到的是数月前的价格、权限和内容。生产事故中,若问题是“为什么这个用户看到的是旧的套餐限额”,几乎总是永久缓存导致的。

// Wrong — no expiry means the cache grows forever
await redis.set(`user:${id}`, JSON.stringify(user)); // no TTL

// Right — every cache entry has a maximum lifetime
await redis.setex(`user:${id}`, 300, JSON.stringify(user)); // 5 minutes

Python 等效写法:

# Wrong
redis.set(f"user:{id}", json.dumps(user))

# Right
redis.setex(f"user:{id}", 300, json.dumps(user))

检测方法:redis.set(key, value) 未设置 EX/PX/EXAT 选项。Redis TTL key 对缓存键返回 -1。内存随时间增长,且没有趋于平稳的迹象。

时间线:这种做法一直都是错误的,但 Redis 默认不设置过期时间,因此很容易无意中这样做。Redis 7.0(2022 年)引入了键驱逐策略作为默认设置,降低了问题的严重性——但仍然会产生过期数据。


反模式:没有失效策略

新手:“我会设置较短的 TTL,这样过期数据的问题就能自行解决。”

专家:仅靠 TTL 失效意味着每次数据变更都会产生一个等于 TTL 的传播延迟。对于某些数据(用户角色、权限、促销结束后的价格),这种延迟无法接受。更糟的是,这会形成一项从未记录的隐式约定,团队之后可能为了性能而延长 TTL,却没意识到这会大幅延长数据过期窗口。

// Problem: user loses admin role, but can still access admin routes for 5 minutes
await redis.setex(`user:permissions:${id}`, 300, JSON.stringify(permissions));

// Right: invalidate explicitly on change
async function revokeAdminRole(userId: string) {
  await db.userRoles.delete(userId, 'admin');
  await redis.del(`user:permissions:${userId}`); // immediate invalidation
  // Also publish to notify other app instances to clear L1 caches
  await redis.publish('permissions:invalidated', userId);
}

LLM 常见错误:LLM 在生成代码时经常省略失效逻辑,因为在简单的缓存旁路示例中看不到它。每篇教程都会展示“写入时设置”,却很少展示“更新时删除”。

检测方法:缓存设置操作没有对应的写入路径删除操作。对用户可控数据(角色、权限、设置),TTL 是唯一的驱逐机制。代码库的更新处理程序中没有 DEL、UNLINK 或 pub/sub 事件。


参考资料

  • references/redis-patterns.md — 请参阅 Redis 专用模式:用于排行榜的有序集合、Lua 原子操作、通过 pub/sub 进行缓存失效、基于 SCAN 的键删除、管道批处理
  • references/http-caching.md — 请参阅浏览器缓存:Cache-Control 指令、ETag、Vary 标头、CDN 配置、Service Worker 缓存策略

评论

支持 Markdown