mcpField
C
cache-strategy

cache-strategy

github.com/ariadoss开发1259 次安装今天更新
代码仓库

关于 cache-strategy

指导开发者在应用中采用永久缓存优先策略:读取时先查缓存,未命中再查数据库,数据变更后使缓存失效。内容涵盖候选数据识别、缓存键约定和可选预热。

主要功能

  1. 01检查现有缓存基础设施
  2. 02识别适合缓存的数据库读取
  3. 03实现缓存优先读取
  4. 04在数据写入或删除后失效缓存
  5. 05制定缓存键命名约定
  6. 06提供可选的启动预热方案

适用场景

  1. 01减少频繁读取且少变数据的数据库查询
  2. 02为用户资料或商品详情增加缓存
  3. 03确保数据变更后清除相关条目和列表
  4. 04为关键数据准备启动时缓存预热

安装

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

$npx skills add https://github.com/ariadoss/superskills --skill cache-strategy

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

详细介绍

/cache-strategy

实现永久的缓存优先策略:先读取缓存,未命中时回退到 DB,随后写入缓存;仅在数据发生变更时使缓存失效。不设置过期计时器——只要底层数据没有变化,缓存就始终有效。

理念

目标是每项数据最多读取 DB 一次(直到数据发生变化):

READ:   cache hit? → return cached value
        cache miss? → query DB → write to cache → return value

WRITE:  update DB → invalidate (or update) cache entry → done

这是 Play Framework 的模式:没有 TTL、没有轮询,也没有后台刷新。缓存始终处于预热状态且始终正确,因为只有在源数据发生变化时才会精准地使缓存失效。

为什么选择永久缓存而非 TTL?

  • TTL 缓存以读取到过期数据为代价,换取简单性——每次过期都会确保触发一次 DB 读取,即使数据完全没有变化
  • 通过失效机制实现的永久缓存,既不会产生不必要的 DB 读取,也不会读取到过期数据
  • 只有在无法得知数据何时变化时(例如第三方 API)才适合使用 TTL

步骤 1:检查缓存基础设施

# Check for Redis
grep -rn "redis\|Redis\|REDIS_URL" --include="*.json" --include="*.env*" --include="*.yml" --include="*.yaml" --include="*.rb" --include="*.py" --include="*.js" --include="*.ts" . 2>/dev/null | grep -v node_modules | head -10

# Check for Memcached
grep -rn "memcached\|Memcached\|MEMCACHE" --include="*.json" --include="*.env*" --include="*.yml" . 2>/dev/null | grep -v node_modules | head -5

# Check for in-process cache (node-cache, lru-cache, etc.)
grep -rn "lru-cache\|node-cache\|memory-cache\|caffeine\|guava.*cache" --include="*.json" --include="*.js" --include="*.ts" --include="*.java" . 2>/dev/null | grep -v node_modules | head -5

报告有哪些可用设施。如果不存在缓存层,则推荐默认使用 Redis,并提出可以添加客户端库。

步骤 2:找出适合缓存的 DB 读取

扫描 DB 查询模式,并根据缓存适用性进行分类:

高价值缓存候选项(读取频繁、变更较少):

  • 用户资料、设置、偏好
  • 产品/商品详情
  • 配置表
  • 参考数据(存储在 DB 中的分类、标签、枚举)
  • 计算成本较高的聚合数据(计数、总和)

不适合缓存的候选项(跳过这些):

  • 实时数据(实时库存数量、聊天消息)
  • 每个会话的数据
  • 每次写入都会变化的数据(例如每个请求都会递增的计数器)
# Find the most-called query patterns
grep -rn "\.find\b\|\.findById\|\.findOne\|\.where\|SELECT" \
  --include="*.rb" --include="*.py" --include="*.js" --include="*.ts" \
  . 2>/dev/null | grep -v node_modules | grep -v test | grep -v spec | head -30

对于每个候选项,记录:

  • 缓存键模式(例如 user:{id}、product:{slug})
  • 写入/更新位置(以确定应在哪里添加失效逻辑)

步骤 3:实现缓存优先读取

对于每个适合缓存的读取,应用以下模式:

Redis(Node.js / TypeScript)
async function getUser(id: string) {
  const cacheKey = `user:${id}`;
  const cached = await redis.get(cacheKey);
  if (cached) return JSON.parse(cached);

  const user = await db.users.findById(id);
  await redis.set(cacheKey, JSON.stringify(user)); // no TTL — permanent until invalidated
  return user;
}
Redis(Ruby / Rails)
def find_user(id)
  Rails.cache.fetch("user:#{id}") do
    User.find(id)  # only called on cache miss
  end
end
Redis(Python)
def get_user(user_id: int):
    cache_key = f"user:{user_id}"
    cached = redis_client.get(cache_key)
    if cached:
        return json.loads(cached)
    user = db.session.get(User, user_id)
    redis_client.set(cache_key, json.dumps(user.to_dict()))  # no expiry
    return user
Play Framework(Scala)
def getUser(id: Long): Future[User] = {
  cache.getOrElseUpdate(s"user:$id") {
    userRepository.findById(id)
  }
}

步骤 4:实现写入时的缓存失效

对于每处会修改已缓存数据的位置,在 DB 写入成功后立即添加失效逻辑:

Node.js / TypeScript
async function updateUser(id: string, data: Partial<User>) {
  const user = await db.users.update(id, data);
  await redis.del(`user:${id}`); // invalidate on change
  return user;
}
Ruby / Rails
def update_user(id, attrs)
  user = User.find(id).tap { |u| u.update!(attrs) }
  Rails.cache.delete("user:#{id}")
  user
end

**重要提示:**删除时也要使缓存失效:

async function deleteUser(id: string) {
  await db.users.delete(id);
  await redis.del(`user:${id}`);
}

对于列表/集合中的任意项发生变化的情况,都要使集合键失效:

await redis.del(`user:${id}`);       // the specific item
await redis.del(`users:list`);       // any cached list that included this item

步骤 5:缓存键约定

在整个代码库中使用一致的键命名方案:

<entity>:<identifier>          → user:123, product:slug-name
<entity>:list:<filter>         → products:list:category-5
<entity>:<id>:<relation>       → user:123:orders
aggregate:<entity>:<filter>    → aggregate:orders:user-123-total

在缓存模块顶部的注释中记录此约定:

// Cache key conventions:
// user:{id}                — single user record
// users:list               — paginated user list (invalidate on any user change)
// user:{id}:orders         — orders belonging to a user

步骤 6:缓存预热(可选)

对于绝不能在冷启动时未命中的关键数据,在应用启动时添加预热步骤:

async function warmCache() {
  const activeUsers = await db.users.findActiveUsers();
  await Promise.all(
    activeUsers.map(u => redis.set(`user:${u.id}`, JSON.stringify(u)))
  );
}

步骤 7:总结

报告已实现的内容:

## Cache Strategy Applied

### Cache layer: Redis (permanent, no TTL)

### Reads cached (X total)
- user:{id} — UserService.getUser()
- product:{slug} — ProductService.findBySlug()
- ...

### Invalidation points added (Y total)
- user:{id} — UserService.updateUser(), UserService.deleteUser()
- product:{slug} — ProductService.updateProduct()
- ...

### Estimated DB load reduction
Before: ~X DB queries/min
After:  ~Y DB queries/min (first-read only, then cache)

注意:对于来自第三方 API 或其他无法拦截写入的数据,可将较长的 TTL(数小时,而非数分钟)作为后备方案;但应将这些数据单独标记,以便在有失效 Webhook 可用时重新评估。

评论

支持 Markdown