Redis 实战:从入门到线上故障

Redis 用了三年,踩过不少坑。记录一下经验。

基础用法

缓存

最常见的场景:

async function getUser(id) {
  const cacheKey = `user:${id}`;

  // 先查缓存
  const cached = await redis.get(cacheKey);
  if (cached) {
    return JSON.parse(cached);
  }

  // 查数据库
  const user = await db.users.find(id);

  // 写缓存,过期 1 小时
  await redis.setex(cacheKey, 3600, JSON.stringify(user));

  return user;
}

分布式锁

async function withLock(key, ttl, fn) {
  const lockKey = `lock:${key}`;
  const token = crypto.randomUUID();

  // SET NX EX 原子操作
  const acquired = await redis.set(lockKey, token, 'NX', 'EX', ttl);
  if (!acquired) {
    throw new Error('获取锁失败');
  }

  try {
    return await fn();
  } finally {
    // Lua 脚本保证原子性
    await redis.eval(`
      if redis.call("get", KEYS[1]) == ARGV[1] then
        return redis.call("del", KEYS[1])
      end
      return 0
    `, [lockKey, token]);
  }
}

限流

async function rateLimit(key, limit, window) {
  const now = Date.now();
  const windowStart = now - window * 1000;

  // 使用 ZSET 实现滑动窗口
  await redis
    .multi()
    .zremrangebyscore(key, 0, windowStart)
    .zadd(key, now, `${now}-${Math.random()}`)
    .zcard(key)
    .expire(key, window)
    .exec();

  const count = await redis.zcard(key);
  return count <= limit;
}

数据结构选型

结构场景复杂度
String缓存、计数器、锁O(1)
Hash对象缓存O(1)
List队列、最新列表O(N)
Set去重、标签O(1)
ZSet排行榜、延迟队列O(log N)
Stream消息队列O(1)

Redis 数据结构

线上故障案例

案例 1:缓存穿透

现象:数据库 CPU 飙升,Redis 命中率只有 10%。

原因:大量请求查询不存在的 key,缓存里没有,全打到数据库。

解决

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

  if (cached === 'null') {
    // 缓存了空值,直接返回
    return null;
  }

  if (cached) {
    return JSON.parse(cached);
  }

  const user = await db.users.find(id);

  if (user) {
    await redis.setex(cacheKey, 3600, JSON.stringify(user));
  } else {
    // 空值也缓存,过期时间短一点
    await redis.setex(cacheKey, 60, 'null');
  }

  return user;
}

案例 2:缓存雪崩

现象:凌晨 3 点,服务突然大量超时。

原因:大量缓存同时过期,瞬间压力打到数据库。

解决:过期时间加随机值:

const baseExpire = 3600;
const randomExpire = Math.random() * 600; // 0-10 分钟随机
await redis.setex(key, baseExpire + randomExpire, value);

案例 3:大 Key

现象:Redis 偶尔卡顿,info 看到 used_memory 持续增长。

原因:有个 List 存了 100 万条数据,每次 lrange 操作都很慢。

解决

  1. 拆分大 Key
  2. 使用 hash 结构分片
// 之前:一个大 List
await redis.lpush('messages', ...messages);

// 之后:按日期分片
await redis.lpush(`messages:${date}`, ...messages);

案例 4:热 Key

现象:某个接口响应慢,Redis 单节点 CPU 高。

原因:一个热点 Key 被高频访问。

解决

// 本地缓存 + Redis 多级缓存
const localCache = new Map();

async function getHotData(key) {
  // 先查本地
  if (localCache.has(key)) {
    return localCache.get(key);
  }

  // 再查 Redis
  const data = await redis.get(key);
  if (data) {
    localCache.set(key, data);
    setTimeout(() => localCache.delete(key), 5000); // 本地缓存 5 秒
  }

  return data;
}

监控面板

性能优化

Pipeline 批量操作

// 不好:多次网络往返
for (const item of items) {
  await redis.set(`item:${item.id}`, JSON.stringify(item));
}

// 好:一次网络往返
const pipeline = redis.pipeline();
for (const item of items) {
  pipeline.set(`item:${item.id}`, JSON.stringify(item));
}
await pipeline.exec();

Lua 脚本

复杂逻辑用 Lua 保证原子性:

-- 扣库存
local stock = redis.call('get', KEYS[1])
if tonumber(stock) >= tonumber(ARGV[1]) then
  redis.call('decrby', KEYS[1], ARGV[1])
  return 1
end
return 0

监控指标

指标告警阈值
内存使用率> 80%
命令延迟> 10ms
连接数> 80% maxclients
键空间命中率< 80%
持久化延迟> 1s

总结

Redis 很强,但用好用坏差距很大。关键点:

  1. 理解数据结构,选对的
  2. 防止缓存穿透/雪崩/击穿
  3. 避免大 Key、热 Key
  4. 做好监控告警

出问题通常不是 Redis 本身的锅,是用法有问题。建议熟读官方文档,理解原理。