您现在的位置是:网站首页 > 缓存策略与实现方案文章详情

缓存策略与实现方案

缓存策略的基本概念

缓存是提升应用性能的关键手段之一,通过将频繁访问的数据存储在快速访问的介质中,减少对慢速存储(如数据库)的依赖。在Express框架中,缓存策略的选择直接影响应用的响应速度和资源利用率。常见的缓存类型包括内存缓存、分布式缓存和浏览器缓存。

内存缓存将数据存储在应用进程的内存中,访问速度最快但容量有限。Redis等分布式缓存解决了单机内存缓存的容量和持久性问题。浏览器缓存则利用HTTP缓存头控制客户端资源的缓存行为。

// 简单的内存缓存实现示例
const cache = {};
function getFromCache(key) {
  return cache[key] || null;
}
function setToCache(key, value, ttl = 60) {
  cache[key] = value;
  setTimeout(() => delete cache[key], ttl * 1000);
}

Express中的内存缓存实现

在Express应用中实现内存缓存可以直接使用Node.js的内存对象。对于小型应用,这种方案简单有效。关键要考虑缓存淘汰策略,常见的有FIFO(先进先出)、LRU(最近最少使用)和TTL(生存时间)。

const express = require('express');
const app = express();
const cache = {};

app.get('/api/data', (req, res) => {
  const cacheKey = req.originalUrl;
  
  // 检查缓存
  if (cache[cacheKey] && cache[cacheKey].expiry > Date.now()) {
    return res.json(cache[cacheKey].data);
  }

  // 模拟数据库查询
  const data = { id: 1, content: '缓存数据' };
  
  // 设置缓存(TTL 60秒)
  cache[cacheKey] = {
    data,
    expiry: Date.now() + 60000
  };

  res.json(data);
});

更复杂的场景可以使用node-cachememory-cache这类专门的内存缓存库:

const NodeCache = require('node-cache');
const myCache = new NodeCache({ stdTTL: 60, checkperiod: 120 });

app.get('/api/users/:id', async (req, res) => {
  const userId = req.params.id;
  const cacheKey = `user_${userId}`;
  
  let user = myCache.get(cacheKey);
  if (user) {
    return res.json(user);
  }

  user = await UserModel.findById(userId);
  myCache.set(cacheKey, user);
  res.json(user);
});

Redis缓存集成

对于需要跨进程或跨服务器共享缓存的场景,Redis是最常用的解决方案。在Express中集成Redis可以通过ioredisredis客户端库实现。

const Redis = require('ioredis');
const redis = new Redis();

app.get('/api/products', async (req, res) => {
  const cacheKey = 'all_products';
  
  try {
    const cachedData = await redis.get(cacheKey);
    if (cachedData) {
      return res.json(JSON.parse(cachedData));
    }

    const products = await ProductModel.find();
    await redis.setex(cacheKey, 3600, JSON.stringify(products)); // 缓存1小时
    
    res.json(products);
  } catch (err) {
    console.error('Redis error:', err);
    // 降级处理:直接查询数据库
    const products = await ProductModel.find();
    res.json(products);
  }
});

对于更复杂的缓存策略,可以使用Redis的哈希类型存储结构化数据:

app.get('/api/user/:id/profile', async (req, res) => {
  const userId = req.params.id;
  const cacheKey = `user:${userId}`;
  
  const cachedProfile = await redis.hgetall(cacheKey);
  if (cachedProfile && Object.keys(cachedProfile).length > 0) {
    return res.json(cachedProfile);
  }

  const profile = await UserProfileModel.findOne({ userId });
  if (profile) {
    await redis.hmset(cacheKey, profile.toObject());
    await redis.expire(cacheKey, 1800); // 30分钟过期
  }
  
  res.json(profile || {});
});

HTTP缓存头设置

Express可以方便地设置HTTP缓存头,控制浏览器和CDN的缓存行为。常见的缓存头包括Cache-ControlETagLast-Modified

app.get('/static/js/app.js', (req, res) => {
  const filePath = path.join(__dirname, 'public/js/app.js');
  const fileStats = fs.statSync(filePath);
  
  // 设置强缓存(1年)
  res.set('Cache-Control', 'public, max-age=31536000');
  
  // 设置弱缓存验证头
  res.set('ETag', fileStats.mtimeMs.toString());
  res.set('Last-Modified', fileStats.mtime.toUTCString());
  
  res.sendFile(filePath);
});

app.get('/api/dynamic-data', (req, res) => {
  // 禁止缓存动态数据
  res.set('Cache-Control', 'no-store, no-cache, must-revalidate');
  res.set('Pragma', 'no-cache');
  res.set('Expires', '0');
  
  res.json({ data: '实时数据', timestamp: Date.now() });
});

对于条件请求(If-None-Match/If-Modified-Since)的处理:

app.get('/api/articles/:id', async (req, res) => {
  const article = await ArticleModel.findById(req.params.id);
  if (!article) return res.status(404).send();
  
  const etag = crypto.createHash('md5').update(JSON.stringify(article)).digest('hex');
  res.set('ETag', etag);
  
  // 检查客户端缓存是否有效
  if (req.headers['if-none-match'] === etag) {
    return res.status(304).end();
  }
  
  res.json(article);
});

缓存失效策略

合理的缓存失效机制是保证数据一致性的关键。常见的失效策略包括:

  1. 时间失效(TTL):设置固定过期时间
  2. 事件驱动失效:数据变更时主动清除相关缓存
  3. 写穿透:更新数据时同步更新缓存
  4. 延迟加载:仅在读取时填充缓存
// 事件驱动缓存失效示例
app.post('/api/articles', async (req, res) => {
  const newArticle = await ArticleModel.create(req.body);
  
  // 清除相关缓存
  await redis.del('all_articles');
  await redis.del(`article_${newArticle.id}`);
  
  // 发布缓存失效事件
  eventEmitter.emit('article:created', newArticle.id);
  
  res.status(201).json(newArticle);
});

// 写穿透示例
app.put('/api/users/:id', async (req, res) => {
  const userId = req.params.id;
  const updatedUser = await UserModel.findByIdAndUpdate(userId, req.body, { new: true });
  
  // 同步更新缓存
  await redis.setex(`user_${userId}`, 3600, JSON.stringify(updatedUser));
  
  res.json(updatedUser);
});

对于复杂的关联数据,可以使用通配符批量删除缓存:

// 删除用户所有相关缓存
async function invalidateUserCache(userId) {
  const keys = await redis.keys(`user:${userId}:*`);
  if (keys.length > 0) {
    await redis.del(keys);
  }
}

多级缓存架构

在高并发场景下,可以采用多级缓存架构:

  1. 浏览器缓存:通过HTTP头控制
  2. CDN缓存:缓存静态资源和API响应
  3. 应用内存缓存:快速响应热点数据
  4. 分布式缓存:共享数据,如Redis
  5. 数据库缓存:如MySQL查询缓存
// 多级缓存实现示例
async function getProductWithMultiCache(productId) {
  // 1. 检查内存缓存
  const memoryCacheKey = `product_${productId}`;
  const memoryCached = myCache.get(memoryCacheKey);
  if (memoryCached) return memoryCached;
  
  // 2. 检查Redis缓存
  const redisCacheKey = `product:${productId}`;
  const redisCached = await redis.get(redisCacheKey);
  if (redisCached) {
    const product = JSON.parse(redisCached);
    myCache.set(memoryCacheKey, product); // 回填内存缓存
    return product;
  }
  
  // 3. 查询数据库
  const product = await ProductModel.findById(productId);
  if (product) {
    await redis.setex(redisCacheKey, 3600, JSON.stringify(product));
    myCache.set(memoryCacheKey, product);
  }
  
  return product;
}

缓存击穿与雪崩防护

缓存失效可能导致严重性能问题:

  • 缓存击穿:热点key过期瞬间大量请求直达数据库
  • 缓存雪崩:大量key同时过期导致数据库压力激增

解决方案:

// 使用互斥锁防止缓存击穿
async function getProductSafe(productId) {
  const cacheKey = `product:${productId}`;
  const lockKey = `lock:${cacheKey}`;
  
  let data = await redis.get(cacheKey);
  if (data) return JSON.parse(data);
  
  // 获取分布式锁
  const locked = await redis.set(lockKey, '1', 'EX', 10, 'NX');
  if (!locked) {
    // 未获取到锁,短暂等待后重试
    await new Promise(resolve => setTimeout(resolve, 100));
    return getProductSafe(productId);
  }
  
  try {
    // 再次检查缓存(可能在等待期间已被其他进程填充)
    data = await redis.get(cacheKey);
    if (data) return JSON.parse(data);
    
    // 查询数据库
    const product = await ProductModel.findById(productId);
    if (product) {
      await redis.setex(cacheKey, 3600, JSON.stringify(product));
    }
    return product;
  } finally {
    // 释放锁
    await redis.del(lockKey);
  }
}

防止缓存雪崩可以通过随机过期时间:

// 设置缓存时添加随机抖动
function setCacheWithJitter(key, value, baseTTL = 3600) {
  const jitter = Math.floor(Math.random() * 600); // 0-10分钟随机抖动
  const actualTTL = baseTTL + jitter;
  return redis.setex(key, actualTTL, JSON.stringify(value));
}

缓存监控与调优

有效的缓存系统需要持续监控和调优:

  1. 命中率监控:记录缓存命中/未命中次数
  2. 内存使用监控:防止内存溢出
  3. 性能指标:缓存操作耗时
// 简单的缓存统计中间件
const cacheStats = {
  hits: 0,
  misses: 0,
  errors: 0
};

app.use((req, res, next) => {
  const start = Date.now();
  const originalJson = res.json;
  
  res.json = function(body) {
    const duration = Date.now() - start;
    if (req.cacheHit) {
      cacheStats.hits++;
    } else if (req.cacheMiss) {
      cacheStats.misses++;
    }
    
    // 记录慢查询
    if (duration > 500) {
      console.warn(`Slow response: ${req.method} ${req.url} - ${duration}ms`);
    }
    
    originalJson.call(this, body);
  };
  
  next();
});

// 暴露统计端点
app.get('/cache-stats', (req, res) => {
  res.json({
    ...cacheStats,
    hitRate: cacheStats.hits / (cacheStats.hits + cacheStats.misses) || 0
  });
});

对于Redis可以使用INFO命令获取详细指标:

app.get('/redis-stats', async (req, res) => {
  const info = await redis.info();
  const stats = {
    used_memory: info.match(/used_memory:\d+/)[0].split(':')[1],
    hits: info.match(/keyspace_hits:\d+/)[0].split(':')[1],
    misses: info.match(/keyspace_misses:\d+/)[0].split(':')[1]
  };
  res.json(stats);
});

我的名片

网名:~川~

岗位:console.log 调试员

坐标:重庆市-九龙坡区

邮箱:cc@qdcc.cn

沙漏人生

站点信息

  • 建站时间:2013/03/16
  • 本站运行
  • 文章数量
  • 总访问量
微信公众号
每次关注
都是向财富自由迈进的一步