Redis数据库

Redis 缓存策略与常见陷阱

2024-10-15
数据库

为什么需要缓存?

缓存是提升系统性能的重要手段。通过缓存热点数据,可以减少数据库压力,降低响应时间。Redis 作为高性能的内存数据库,是最常用的缓存方案之一。

常见缓存策略

1. 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. 写入缓存
  await redis.set(`user:${id}`, JSON.stringify(user), 'EX', 3600);
  
  return user;
}

2. Read/Write Through

应用只与缓存交互,缓存负责与数据库同步:

class CacheThrough {
  async get(key) {
    const value = await redis.get(key);
    if (!value) {
      const dbValue = await db.get(key);
      await redis.set(key, dbValue);
      return dbValue;
    }
    return value;
  }
  
  async set(key, value) {
    await redis.set(key, value);
    await db.set(key, value);
  }
}

3. Write Behind(异步写入)

先更新缓存,异步更新数据库:

async function updateUser(id, data) {
  // 1. 立即更新缓存
  await redis.set(`user:${id}`, JSON.stringify(data));
  
  // 2. 异步更新数据库
  setTimeout(async () => {
    await db.update('users', { id }, data);
  }, 0);
}

缓存三大问题

1. 缓存穿透

**问题**:查询一个不存在的数据,缓存未命中,每次都要查数据库。

**解决方案**:

async function getUser(id) {
  const cached = await redis.get(`user:${id}`);
  
  // 缓存空值,防止穿透
  if (cached === 'NULL') {
    return null;
  }
  
  if (cached) {
    return JSON.parse(cached);
  }
  
  const user = await db.query('SELECT * FROM users WHERE id = ?', [id]);
  
  if (!user) {
    // 缓存空值,设置较短的过期时间
    await redis.set(`user:${id}`, 'NULL', 'EX', 60);
    return null;
  }
  
  await redis.set(`user:${id}`, JSON.stringify(user), 'EX', 3600);
  return user;
}

2. 缓存雪崩

**问题**:大量缓存同时失效,导致数据库压力骤增。

**解决方案**:

// 1. 过期时间加随机值
const ttl = 3600 + Math.floor(Math.random() * 300);
await redis.set(key, value, 'EX', ttl);

// 2. 使用永不过期 + 主动更新
async function getWithRefresh(key, fetchFn) {
  const cached = await redis.get(key);
  
  if (cached) {
    // 检查是否需要刷新
    const refreshTime = await redis.get(`${key}:refresh`);
    if (!refreshTime || Date.now() > parseInt(refreshTime)) {
      // 异步刷新
      refreshCache(key, fetchFn);
    }
    return JSON.parse(cached);
  }
  
  return refreshCache(key, fetchFn);
}

async function refreshCache(key, fetchFn) {
  const value = await fetchFn();
  await redis.set(key, JSON.stringify(value));
  await redis.set(`${key}:refresh`, Date.now() + 3600 * 1000);
  return value;
}

3. 缓存击穿

**问题**:热点 key 失效瞬间,大量请求打到数据库。

**解决方案**:

// 使用互斥锁
async function getUserWithLock(id) {
  const cached = await redis.get(`user:${id}`);
  if (cached) {
    return JSON.parse(cached);
  }
  
  // 尝试获取锁
  const lockKey = `lock:user:${id}`;
  const locked = await redis.set(lockKey, '1', 'EX', 10, 'NX');
  
  if (locked) {
    try {
      // 再次检查缓存(可能已被其他线程更新)
      const doubleCheck = await redis.get(`user:${id}`);
      if (doubleCheck) {
        return JSON.parse(doubleCheck);
      }
      
      // 查数据库
      const user = await db.query('SELECT * FROM users WHERE id = ?', [id]);
      await redis.set(`user:${id}`, JSON.stringify(user), 'EX', 3600);
      return user;
    } finally {
      await redis.del(lockKey);
    }
  } else {
    // 等待其他线程更新缓存
    await new Promise(resolve => setTimeout(resolve, 100));
    return getUserWithLock(id);
  }
}

缓存更新策略

1. 主动更新

数据变更时立即更新缓存:

async function updateUser(id, data) {
  await db.update('users', { id }, data);
  await redis.del(`user:${id}`);
}

2. 延迟双删

async function updateUser(id, data) {
  // 1. 先删除缓存
  await redis.del(`user:${id}`);
  
  // 2. 更新数据库
  await db.update('users', { id }, data);
  
  // 3. 延迟再次删除缓存
  setTimeout(async () => {
    await redis.del(`user:${id}`);
  }, 500);
}

监控与告警

// 监控缓存命中率
const hits = await redis.info('keyspace_hits');
const misses = await redis.info('keyspace_misses');
const hitRate = hits / (hits + misses);

if (hitRate < 0.8) {
  // 发送告警
  alert('缓存命中率过低');
}

总结

Redis 缓存的关键要点:

  • **选择合适的策略**:根据业务场景选择 Cache Aside、Read/Write Through 或 Write Behind
  • **预防三大问题**:缓存穿透、雪崩、击穿
  • **合理设置过期时间**:避免同时失效
  • **监控与告警**:及时发现问题
  • 记住,缓存不是万能的,需要根据实际业务场景合理使用。