Claude Code Redis 缓存实战:TTL、失效策略与防雪崩设计
用 Claude Code 设计 Redis 缓存的实战指南,覆盖 key 设计、TTL、失效、Node.js 代码、测试与审查清单。
Redis 是一种把数据放在内存中的高速键值存储,适合缓存数据库聚合、外部 API 结果、排行榜、短期会话和限流计数。它的危险也很直接:如果只让 Claude Code “加一个 Redis 缓存”,而没有说明什么数据能变旧、多久必须刷新、更新后删哪些 key,最终可能得到更快但不可信的页面。
这篇文章给出一个可以直接交给 Claude Code 的 Redis 缓存工作流:缓存策略、key 设计、TTL、失效、stampede 防护、Node.js 实现、测试和 review 清单。更完整的多层缓存背景可以参考 Claude Code 真实应用缓存策略,如果 Redis 同时用于队列,也建议读 Claude Code 队列与后台任务。
flowchart LR
Request["HTTP request"] --> Cache["Redis cache"]
Cache -->|hit| Response["Fast response"]
Cache -->|miss| Lock["Short lock"]
Lock --> Loader["DB or external API"]
Loader --> Cache
Loader --> Response
Admin["Update event"] --> Invalidate["Delete known keys"]
Invalidate --> Cache
先写缓存策略
在实现之前,把下面这种策略放进 CLAUDE.md 或 Claude Code 任务正文。代码能告诉模型数据从哪里来,但不能自动判断业务上允许旧多久。
| 数据 | key 示例 | TTL 建议 | 失效时机 | 风险 |
|---|---|---|---|---|
| 公开商品列表 | claudecodelab:v1:products:list:zh | 5 分钟 | 商品更新成功后删除列表 key | 价格变化不能只等 TTL |
| 文章详情 | claudecodelab:v1:posts:item:{slug} | 10 分钟 | 发布、编辑、下线后删除 | 不缓存草稿和预览 |
| 后台 PV 汇总 | claudecodelab:v1:analytics:daily:{date} | 30 秒 | 通常只靠 TTL | 不用于财务真值 |
| 外部 API 结果 | claudecodelab:v1:exchange-rate:usd-jpy | 1 到 15 分钟 | 手动刷新或 TTL | 检查服务条款 |
| 登录用户信息 | 默认不缓存 | 0 秒 | 无 | 避免个人数据进入共享缓存 |
Claude Code 的任务要写清楚:公开且可再生成的数据可以进 Redis;权限、账单、认证状态和个人资料默认不要进 Redis。
Claude Code 任务模板
请为这个 Node.js 应用增加 Redis cache-aside 层。
要求:
1. 使用官方 node-redis 包,也就是 redis
2. key 格式为 claudecodelab:v1:{domain}:{resource}:{id}
3. TTL 从缓存策略表选择,并加入 10% 以内的 jitter
4. 更新流程必须先完成 DB write,成功后再删除已知相关 key
5. 生产代码禁止使用 KEYS;需要批量处理时用已知 key、SCAN 或相关 key Set
6. 对热门 key 的同时 miss 增加短锁,防止 cache stampede
7. 用 node:test 覆盖 key 生成、TTL 范围、getOrSet、并发 miss
返回:
- 修改文件
- 执行过的测试
- 明确排除在缓存外的数据和原因
这段模板也可以放进 Claude Code code review checklist,让 review 不只看“是否变快”,还看“是否能正确失效”。
可运行的 Node.js 实现
Redis 官方 Node.js 客户端是 redis 包,连接方式见 node-redis guide。
mkdir redis-cache-demo
cd redis-cache-demo
npm init -y
npm install redis
docker run --name redis-cache-demo -p 6379:6379 -d redis:7-alpine
先集中管理 key 和 TTL。
// cache-policy.js
const CACHE_PREFIX = "claudecodelab";
const CACHE_VERSION = "v1";
const CACHE_POLICY = {
productList: { ttl: 300, jitter: 30 },
productItem: { ttl: 600, jitter: 60 },
dailyStats: { ttl: 30, jitter: 5 },
};
function normalizePart(value) {
const part = String(value).trim().toLowerCase();
if (part.length === 0) {
throw new Error("cache key part must not be empty");
}
return encodeURIComponent(part);
}
function cacheKey(parts) {
if (!Array.isArray(parts) || parts.length === 0) {
throw new Error("cacheKey requires a non-empty parts array");
}
return [CACHE_PREFIX, CACHE_VERSION, ...parts.map(normalizePart)].join(":");
}
function ttlWithJitter(baseSeconds, maxJitterSeconds = 30) {
if (!Number.isInteger(baseSeconds) || baseSeconds <= 0) {
throw new Error("base TTL must be a positive integer");
}
const jitter = Math.max(0, Math.floor(maxJitterSeconds));
return baseSeconds + Math.floor(Math.random() * (jitter + 1));
}
module.exports = { CACHE_POLICY, cacheKey, ttlWithJitter };
连接使用单例,避免每个请求都重新 connect()。
// redis-client.js
const { createClient } = require("redis");
const redis = createClient({
url: process.env.REDIS_URL || "redis://localhost:6379",
});
redis.on("error", (error) => {
console.error("Redis Client Error", error);
});
let connecting;
async function getRedis() {
if (redis.isOpen) return redis;
if (!connecting) {
connecting = redis.connect();
}
await connecting;
return redis;
}
async function closeRedis() {
if (redis.isOpen) {
await redis.quit();
}
connecting = undefined;
}
module.exports = { getRedis, closeRedis };
核心 helper 使用 cache-aside:先读 Redis,miss 时才调用 loader,并用短锁减少热门 key 同时过期造成的 DB 冲击。
// redis-cache.js
const { randomUUID } = require("node:crypto");
const UNLOCK_SCRIPT = `
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
end
return 0
`;
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
class RedisJsonCache {
constructor(redis, options = {}) {
this.redis = redis;
this.defaultTtl = options.defaultTtl || 300;
this.lockMs = options.lockMs || 5000;
this.waitMs = options.waitMs || 50;
this.waitRetries = options.waitRetries || 10;
}
async get(key) {
const raw = await this.redis.get(key);
if (raw === null) return null;
try {
return JSON.parse(raw);
} catch {
await this.redis.del(key);
return null;
}
}
async set(key, value, ttlSeconds = this.defaultTtl) {
if (!Number.isInteger(ttlSeconds) || ttlSeconds <= 0) {
throw new Error("ttlSeconds must be a positive integer");
}
await this.redis.set(key, JSON.stringify(value), { EX: ttlSeconds });
}
async invalidate(keys) {
const list = Array.isArray(keys) ? keys : [keys];
if (list.length === 0) return 0;
return this.redis.del(list);
}
async getOrSet(key, ttlSeconds, loader) {
const cached = await this.get(key);
if (cached !== null) {
return { value: cached, cacheStatus: "hit" };
}
const lockKey = `${key}:lock`;
const token = randomUUID();
const acquired = await this.redis.set(lockKey, token, { NX: true, PX: this.lockMs });
if (acquired === "OK") {
try {
const fresh = await loader();
await this.set(key, fresh, ttlSeconds);
return { value: fresh, cacheStatus: "miss" };
} finally {
await this.redis.eval(UNLOCK_SCRIPT, { keys: [lockKey], arguments: [token] });
}
}
for (let attempt = 0; attempt < this.waitRetries; attempt += 1) {
await sleep(this.waitMs);
const afterWait = await this.get(key);
if (afterWait !== null) {
return { value: afterWait, cacheStatus: "hit-after-wait" };
}
}
const fallback = await loader();
await this.set(key, fallback, Math.max(5, Math.floor(ttlSeconds / 3)));
return { value: fallback, cacheStatus: "miss-after-timeout" };
}
}
module.exports = { RedisJsonCache };
应用侧可以这样调用。真实项目里把 loadProductsFromDb() 换成 Prisma、Supabase 或 API 客户端即可;相关主题可看 Prisma ORM with Claude Code 和 Supabase integration with Claude Code。
// demo-products.js
const { CACHE_POLICY, cacheKey, ttlWithJitter } = require("./cache-policy");
const { getRedis, closeRedis } = require("./redis-client");
const { RedisJsonCache } = require("./redis-cache");
const db = {
products: [
{ id: "p1", locale: "zh", name: "CLAUDE.md 模板", price: 9, published: true },
{ id: "p2", locale: "zh", name: "Claude Code 培训", price: 199, published: true },
],
};
async function loadProductsFromDb(locale) {
await new Promise((resolve) => setTimeout(resolve, 80));
return db.products.filter((product) => product.locale === locale && product.published);
}
async function listPublishedProducts(cache, locale) {
const key = cacheKey(["products", "list", locale]);
const ttl = ttlWithJitter(CACHE_POLICY.productList.ttl, CACHE_POLICY.productList.jitter);
return cache.getOrSet(key, ttl, () => loadProductsFromDb(locale));
}
async function main() {
const redis = await getRedis();
const cache = new RedisJsonCache(redis);
const first = await listPublishedProducts(cache, "zh");
const second = await listPublishedProducts(cache, "zh");
console.log({ firstStatus: first.cacheStatus, secondStatus: second.cacheStatus, products: second.value });
await cache.invalidate([cacheKey(["products", "list", "zh"])]);
await closeRedis();
}
main().catch(async (error) => {
console.error(error);
await closeRedis();
process.exitCode = 1;
});
node demo-products.js
测试重点
缓存 bug 很容易被“响应很快”掩盖,所以要测试 key、TTL、命中逻辑和并发 miss。
// redis-cache.test.js
const test = require("node:test");
const assert = require("node:assert/strict");
const { cacheKey, ttlWithJitter } = require("./cache-policy");
const { RedisJsonCache } = require("./redis-cache");
class FakeRedis {
constructor() {
this.store = new Map();
}
async get(key) {
const entry = this.store.get(key);
if (!entry) return null;
if (entry.expiresAt && entry.expiresAt <= Date.now()) {
this.store.delete(key);
return null;
}
return entry.value;
}
async set(key, value, options = {}) {
if (options.NX && (await this.get(key)) !== null) return null;
const ttlMs = options.PX || (options.EX ? options.EX * 1000 : 0);
this.store.set(key, { value, expiresAt: ttlMs ? Date.now() + ttlMs : 0 });
return "OK";
}
async del(keys) {
const list = Array.isArray(keys) ? keys : [keys];
let deleted = 0;
for (const key of list) {
if (this.store.delete(key)) deleted += 1;
}
return deleted;
}
async eval(_script, options) {
const [key] = options.keys;
const [token] = options.arguments;
if ((await this.get(key)) === token) return this.del(key);
return 0;
}
}
test("cacheKey encodes dynamic parts", () => {
assert.equal(cacheKey(["Products", "List", "zh/CN"]), "claudecodelab:v1:products:list:zh%2Fcn");
});
test("ttlWithJitter stays inside the expected range", () => {
for (let i = 0; i < 50; i += 1) {
const ttl = ttlWithJitter(300, 30);
assert.ok(ttl >= 300);
assert.ok(ttl <= 330);
}
});
test("getOrSet caches the first loader result", async () => {
const cache = new RedisJsonCache(new FakeRedis());
let loads = 0;
const first = await cache.getOrSet("products:list", 60, async () => {
loads += 1;
return [{ id: "p1" }];
});
const second = await cache.getOrSet("products:list", 60, async () => {
loads += 1;
return [{ id: "p2" }];
});
assert.equal(first.cacheStatus, "miss");
assert.equal(second.cacheStatus, "hit");
assert.equal(loads, 1);
assert.deepEqual(second.value, [{ id: "p1" }]);
});
test("getOrSet waits instead of running duplicate loaders", async () => {
const cache = new RedisJsonCache(new FakeRedis(), { waitMs: 5, waitRetries: 20 });
let loads = 0;
const loader = async () => {
loads += 1;
await new Promise((resolve) => setTimeout(resolve, 20));
return { total: 42 };
};
const results = await Promise.all([
cache.getOrSet("analytics:daily", 30, loader),
cache.getOrSet("analytics:daily", 30, loader),
]);
assert.equal(loads, 1);
assert.deepEqual(results[0].value, { total: 42 });
assert.deepEqual(results[1].value, { total: 42 });
});
node --test redis-cache.test.js
实际使用场景
第一个场景是公开目录和文章列表。所有读者看到同一结果,更新后删除列表 key 和详情 key 即可。
第二个场景是后台统计。PV、转化率、收入预览可以短暂缓存,但权限判断、发票、支付状态不能用这个缓存代替真实 DB。
第三个场景是外部 API。汇率、天气、SaaS 套餐、公开 GitHub 元数据可以短时间缓存,降低限流压力。
第四个场景是限流。Redis 的 INCR 加 EXPIRE 很适合短窗口计数,但认证数据需要额外的安全设计。
常见坑
最常见的问题是 key 不完整。products:list 没有区分语言、货币、租户、筛选条件和发布状态。凡是会改变结果的条件,都要进入 key。
第二个问题是先删缓存、后写 DB。正确顺序是 DB 写入成功、删除已知 Redis key、必要时 purge CDN。
第三个问题是在生产代码使用 KEYS user:*。keyspace 大时它会阻塞 Redis。优先使用已知 key、相关 key Set 或基于 SCAN 的维护命令。
还要注意 null 值。上面的实现把 null 当成 miss;如果要缓存“未找到”,请保存 { found: false } 这样的对象。
Review 清单
- 个人信息、权限、账单数据是否被排除或有明确理由
- key 是否包含 locale、tenant、role、query、version 等条件
- TTL 是否能用业务鲜度解释
- 更新是否在 DB 成功后删除相关 key
- 生产代码是否避免
KEYS - 是否有 lock、jitter 或 stale fallback 防止 stampede
- 测试是否覆盖 key、TTL、命中和并发 miss
- 日志或指标是否能看到 hit rate 和 fallback
官方资料与下一步
建议在 Claude Code 任务中直接附上 Redis 官方文档、node-redis guide、Redis patterns 和 Redis optimization。
如果想把缓存策略、CLAUDE.md、prompt 和 review 清单做成团队流程,可以从 ClaudeCodeLab 产品与模板开始。需要把 Redis、CDN、DB 更新和监控映射到真实项目时,可以使用 Claude Code 培训与咨询。
Masa 在小型 Node.js demo 中实际测试后,第一次列表请求调用 loader,第二次变成 Redis hit,并发 miss 测试也把 loader 控制在一次。真正的收益不只是 Redis 变快,而是让 Claude Code 在明确的 key、TTL、失效和 review 规则下工作。
相关文章
用 Claude Code 实现 API Rate Limiting:429、Redis 与 Cloudflare 实战
用 Claude Code 实现安全的 API 限流:429、Redis、Cloudflare、防滥用和常见坑。
Claude Code 实战缓存策略:HTTP、CDN、Service Worker 与 Redis
用 Claude Code 为真实应用设计 HTTP、CDN、Service Worker、Redis 缓存与失效流程。
Claude Code 邮件自动化:从线索获取到变现的 Node.js 实战
用Claude Code搭建邮件自动化:线索磁铁、同意、退订、重试、分析和变现闭环。
免费 PDF: Claude Code 速查表
输入邮箱即可获取一页 PDF,整理常用命令、审查习惯和安全工作流。
我们会妥善保护你的信息,不发送垃圾邮件。
让 Claude Code 真正进入可验证的工作流
先用免费 PDF 固定基础,再用 Gumroad 教材复用工作流;如果涉及团队导入、权限或收入路径,可以直接咨询。
关于作者
Masa
专注 Claude Code 实务流程、团队导入和内容转化的工程师。