Redis 缓存用得好,系统性能翻倍;用不好,缓存雪崩、穿透、击穿三连暴击。本文总结了三种经典缓存策略和它们各自的适用场景与坑点。

Cache-Aside 策略流程
1
应用请求数据,先查 Redis 缓存
GET user:1001
2
缓存命中 → 直接返回(跳过数据库)
HIT → return
3
缓存未命中 → 查数据库
MISS → SELECT * FROM users
4
将结果写入缓存(设置 TTL)
SET user:1001 ... EX 3600
5
返回数据给应用
return data

一、Cache-Aside(旁路缓存)

最常用的策略。应用自己管理缓存的读写逻辑。

async function getUser(id) {
  // 1. 先查缓存
  const cached = await redis.get(`user:${id}`);
  if (cached) return JSON.parse(cached);

  // 2. 查数据库
  const user = await db.query('SELECT * FROM users WHERE id = ?', [id]);

  // 3. 写入缓存(TTL 1小时)
  if (user) {
    await redis.set(`user:${id}`, JSON.stringify(user), 'EX', 3600);
  }

  return user;
}

二、三大经典问题

2.1 缓存穿透

查询一个根本不存在的数据,缓存和数据库都查不到,每次请求都打到数据库。

🚨 场景:攻击者用随机 ID 批量请求 GET /api/user/{random},所有请求穿透缓存直达数据库,直接打挂。

解决方案:缓存空值 + 布隆过滤器。

async function getUserSafe(id) {
  const cached = await redis.get(`user:${id}`);

  // 命中缓存(包括空值标记)
  if (cached !== null) {
    return cached === '__NULL__' ? null : JSON.parse(cached);
  }

  const user = await db.query('SELECT * FROM users WHERE id = ?', [id]);

  if (user) {
    await redis.set(`user:${id}`, JSON.stringify(user), 'EX', 3600);
  } else {
    // 缓存空值,短 TTL 防穿透
    await redis.set(`user:${id}`, '__NULL__', 'EX', 60);
  }

  return user;
}

2.2 缓存击穿

某个热点 key 过期的瞬间,大量并发请求同时打到数据库。

解决方案:互斥锁(分布式锁)。

async function getHotData(key) {
  let data = await redis.get(key);
  if (data) return JSON.parse(data);

  // 尝试获取锁
  const lockKey = `lock:${key}`;
  const locked = await redis.set(lockKey, 1, 'NX', 'EX', 10);

  if (locked) {
    // 拿到锁,查数据库并回填
    data = await queryDb(key);
    await redis.set(key, JSON.stringify(data), 'EX', 3600);
    await redis.del(lockKey);
    return data;
  } else {
    // 没拿到锁,等一下重试
    await sleep(100);
    return getHotData(key);
  }
}

2.3 缓存雪崩

大量 key 同时过期,或 Redis 宕机,导致所有请求同时打到数据库。

解决方案:TTL 加随机偏移 + 多级缓存 + 熔断降级。

// TTL 加随机偏移,避免同时过期
const baseTTL = 3600;
const jitter = Math.floor(Math.random() * 600);
await redis.set(key, value, 'EX', baseTTL + jitter);

三、策略选择指南

四、总结