关于 caching
Caching 是一项开发 Skill,指导选择缓存模式并规划数据新鲜度与失效机制。内容涵盖 TTL、写穿、事件驱动和旁路缓存,并说明陈旧数据的风险。
主要功能
- 01要求为每个缓存制定失效方案
- 02说明 TTL 和写穿模式
- 03介绍事件驱动与旁路缓存策略
- 04列出适合及不适合缓存的数据
- 05指出数据陈旧和内存限制风险
适用场景
- 01为重复的数据查询添加缓存
- 02提升高成本计算的性能
- 03规划数据库或 API 结果的新鲜度
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ yanko-belov/ code-craft --skill caching安装后在 Claude Code 中输入/caching确认已加载。
详细介绍
缓存
概述
积极缓存,但始终要有失效策略。
缓存能大幅提升性能,但过期数据会导致错误。每个缓存都需要有保持数据新鲜度的方案。
适用场景
- 反复获取相同数据
- 计算开销大
- 数据库查询缓慢
- 外部 API 有速率限制
- 有人要求“只要加个缓存”
铁律
NEVER add a cache without defining its invalidation strategy.
没有例外:
- 不能因为“它很少变化”
- 不能因为“TTL 就够了”
- 不能因为“以后再想办法”
- 不能因为“用户可以刷新”
检测:没有策略的缓存异味
如果缓存没有失效方案,请停止:
// ❌ VIOLATION: Cache without invalidation strategy
const cache = new Map();
async function getUser(id: string) {
if (cache.has(id)) {
return cache.get(id); // Could be stale forever!
}
const user = await db.users.findById(id);
cache.set(id, user); // When does this expire? When user updates?
return user;
}
问题:
- 用户更新姓名 → 缓存仍显示旧姓名
- 没有内存限制 → 内存泄漏
- 没有 TTL → 数据永远过期
- 分布式系统 → 存在多个过期副本
正确模式:带策略的缓存
// ✅ CORRECT: Cache with TTL and invalidation
interface CacheEntry<T> {
data: T;
expiresAt: number;
}
class UserCache {
private cache = new Map<string, CacheEntry<User>>();
private TTL_MS = 5 * 60 * 1000; // 5 minutes
async get(id: string): Promise<User> {
const cached = this.cache.get(id);
if (cached && cached.expiresAt > Date.now()) {
return cached.data;
}
const user = await db.users.findById(id);
this.set(id, user);
return user;
}
set(id: string, user: User): void {
this.cache.set(id, {
data: user,
expiresAt: Date.now() + this.TTL_MS
});
}
// Explicit invalidation on updates
invalidate(id: string): void {
this.cache.delete(id);
}
invalidateAll(): void {
this.cache.clear();
}
}
// Usage with invalidation on write
async function updateUser(id: string, data: UpdateUserDto) {
const user = await db.users.update(id, data);
userCache.invalidate(id); // Clear stale cache
return user;
}
缓存失效策略
1. 基于时间(TTL)
// Good for: data that can be slightly stale
const TTL = 60 * 1000; // 1 minute
cache.set(key, value, { ttl: TTL });
2. 写穿透
// Good for: data you control writes for
async function updateProduct(id, data) {
const product = await db.products.update(id, data);
await cache.set(`product:${id}`, product); // Update cache on write
return product;
}
3. 基于事件
// Good for: distributed systems
eventBus.on('user.updated', (userId) => {
cache.delete(`user:${userId}`);
});
eventBus.on('product.priceChanged', (productId) => {
cache.delete(`product:${productId}`);
});
4. 缓存旁路(惰性加载)
// Good for: read-heavy, tolerance for staleness
async function getProduct(id) {
let product = await cache.get(`product:${id}`);
if (!product) {
product = await db.products.findById(id);
await cache.set(`product:${id}`, product, { ttl: 300 });
}
return product;
}
缓存什么
| 适合缓存 | 不适合缓存 |
|---|---|
| 用户资料 | 会话令牌 |
| 产品目录 | 支付状态 |
| 配置 | 实时库存 |
| API 响应 | 特定用户的计算结果 |
| 计算得出的聚合数据 | 快速变化的数据 |
Redis 示例
import Redis from 'ioredis';
const redis = new Redis();
class ProductCache {
private prefix = 'product:';
private ttl = 300; // 5 minutes
async get(id: string): Promise<Product | null> {
const cached = await redis.get(this.prefix + id);
return cached ? JSON.parse(cached) : null;
}
async set(id: string, product: Product): Promise<void> {
await redis.setex(
this.prefix + id,
this.ttl,
JSON.stringify(product)
);
}
async invalidate(id: string): Promise<void> {
await redis.del(this.prefix + id);
}
async invalidatePattern(pattern: string): Promise<void> {
const keys = await redis.keys(this.prefix + pattern);
if (keys.length) await redis.del(...keys);
}
}
抵御压力的协议
1. “它很少变化”
压力:“这些数据几乎不会更新”
回应:“几乎不会”仍然意味着有时会更新。一旦更新,过期缓存就会导致错误。
**行动:**至少添加 TTL,并在写入时添加失效机制。
2. “TTL 就够了”
压力:“我们只要在 5 分钟后让缓存过期就行”
**回应:**5 分钟的过期数据可能无法接受。用户更新资料后,看到的还是旧数据。
**行动:**TTL + 写穿透失效。
3. “以后再想办法”
压力:“只要加缓存,如果过期数据成了问题,我们再处理”
**回应:**过期数据导致的错误很难调试。应提前设计失效机制。
**行动:**未定义失效策略之前,不要使用缓存。
危险信号——停止并重新考虑
- 缓存没有 TTL
- 数据更新时没有失效机制
- “用户可以刷新以查看新数据”
- 分布式系统中使用内存缓存
- 缓存没有内存限制
以上情况都意味着:定义失效策略。
速查
| 模式 | 适用场景 | 失效方式 |
|---|---|---|
| 仅 TTL | 可以接受数据过期 | 自动过期 |
| 写穿透 | 由你控制写入 | 写入时更新缓存 |
| 基于事件 | 分布式系统 | 发生变更时通过发布/订阅通知 |
| 缓存旁路 | 读多写少 | TTL + 手动失效 |
常见借口(均不成立)
| 借口 | 事实 |
|---|---|
| “很少变化” | 很少 ≠ 从不。要做好应对准备。 |
| “TTL 就够了” | TTL + 失效机制更好。 |
| “以后再想办法” | 过期数据导致的错误很难追踪。 |
| “用户可以刷新” | 这是错误,不是功能。 |
| “只是为了提升性能” | 过期数据会破坏功能。 |
底线
每个缓存都需要:TTL、大小限制和失效策略。
积极缓存以提升性能。但始终要清楚数据变化时如何使缓存失效。“它很少变化”不是策略。
评论
支持 Markdown