关于 caching-strategies
面向工程师的应用缓存设计参考技能,涵盖缓存模式、数据新鲜度、失效处理与雪崩预防。内容强调按数据需求选择策略,避免陈旧数据和无限增长的缓存。
主要功能
- 01Cache-aside 与 write-through 模式
- 02按新鲜度需求设计 TTL
- 03Redis 失效与 Lua 模式
- 04HTTP 缓存与 CDN 指引
- 05缓存雪崩预防策略
适用场景
- 01为应用缓存选择模式
- 02为变化数据设定 TTL
- 03规划多层缓存失效
- 04防止缓存未命中引发并发过载
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ curiositech/ some_claude_skills --skill caching-strategies安装后在 Claude Code 中输入/caching-strategies确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
.claude-plugin/plugin.jsonreferences/http-caching.mdredis-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、Memcached | 1-5ms | 大(GB) | 是——所有实例共享 |
| L3:边缘/CDN | Cloudflare、Fastly、CloudFront | 10-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 缓存策略