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 规则下工作。
免费 PDF: Claude Code 速查表
输入邮箱即可获取一页 PDF,整理常用命令、审查习惯和安全工作流。
我们会妥善保护你的信息,不发送垃圾邮件。
把 Claude Code 变成真正能带来结果的工作流
先领取中文说明的免费 PDF,再进入英文商品页选择合适的教材。如果你需要团队落地、流程设计或内容变现支持,也可以直接咨询。
关于作者
Masa
专注 Claude Code 实务流程、团队导入和内容转化的工程师。
相关文章
Claude Code Permission Receipt Pattern:记录权限、证据和回滚方式
Claude Code 权限 receipt:记录允许动作、需要批准的边界、验证命令、回滚说明,以及 Gumroad 和咨询 CTA 检查。
Claude Code/Codex 安全 Agent Harness 实战:权限、验证与回滚
用权限策略、执行计划、验证脚本和回滚日志,为 Claude Code 与 Codex 搭建更安全的 AI Agent 工作流。
Claude Code 子代理实战指南:安全委派并行文章与代码工作
用 Claude Code 子代理安全拆分文章和代码工作:委派规则、提示词模板、失败模式与检查清单。