阿里云主机折上折
  • 微信号
您当前的位置:网站首页 > 缓存策略与实现方案

缓存策略与实现方案

作者:陈川 阅读数:32397人阅读 分类: Node.js

缓存是提升应用性能的重要手段,合理的缓存策略能显著减少服务器负载和响应时间。Express作为Node.js的流行框架,提供了灵活的缓存实现方式,包括内存缓存、分布式缓存和HTTP缓存等。

内存缓存实现

内存缓存是最简单的缓存形式,适合小型应用或开发环境。Node.js的memory-cache模块可以快速实现内存缓存:

const express = require('express');
const cache = require('memory-cache');
const app = express();

// 缓存中间件
const memCache = (duration) => {
  return (req, res, next) => {
    const key = '__express__' + req.originalUrl;
    const cachedContent = cache.get(key);
    
    if (cachedContent) {
      res.send(cachedContent);
      return;
    } else {
      res.sendResponse = res.send;
      res.send = (body) => {
        cache.put(key, body, duration * 1000);
        res.sendResponse(body);
      };
      next();
    }
  };
};

// 使用示例
app.get('/api/data', memCache(30), (req, res) => {
  // 模拟耗时操作
  setTimeout(() => {
    res.json({ data: Date.now() });
  }, 1000);
});

这种方案需要注意内存泄漏问题,当缓存数据量较大时需要设置合理的过期时间和内存限制。

Redis分布式缓存

对于生产环境,Redis是更可靠的选择。以下是Express集成Redis的完整示例:

const redis = require('redis');
const { promisify } = require('util');
const client = redis.createClient({
  host: 'redis-server',
  port: 6379
});

const getAsync = promisify(client.get).bind(client);
const setexAsync = promisify(client.setex).bind(client);

app.get('/api/users/:id', async (req, res) => {
  const cacheKey = `user_${req.params.id}`;
  
  try {
    // 尝试从缓存获取
    const cachedData = await getAsync(cacheKey);
    if (cachedData) {
      return res.json(JSON.parse(cachedData));
    }

    // 缓存未命中则查询数据库
    const user = await User.findById(req.params.id);
    
    // 设置缓存(30秒过期)
    await setexAsync(cacheKey, 30, JSON.stringify(user));
    
    res.json(user);
  } catch (err) {
    res.status(500).send(err.message);
  }
});

Redis方案需要考虑缓存雪崩问题,可以通过随机过期时间或互斥锁解决:

// 解决缓存雪崩的互斥锁实现
const acquireLock = async (lockKey, expireTime = 10) => {
  const result = await setnxAsync(lockKey, 'LOCK');
  if (result === 1) {
    await expireAsync(lockKey, expireTime);
    return true;
  }
  return false;
};

HTTP缓存策略

Express可以通过设置响应头实现HTTP缓存:

app.get('/static/image.jpg', (req, res) => {
  res.set({
    'Cache-Control': 'public, max-age=86400', // 1天
    'ETag': '123456789',
    'Last-Modified': new Date('2023-01-01').toUTCString()
  });
  res.sendFile('/path/to/image.jpg');
});

对于动态内容,可以使用条件请求:

app.get('/api/news', async (req, res) => {
  const news = await getLatestNews();
  const etag = crypto.createHash('md5').update(JSON.stringify(news)).digest('hex');
  
  if (req.headers['if-none-match'] === etag) {
    return res.status(304).end();
  }
  
  res.set({
    'ETag': etag,
    'Cache-Control': 'no-cache'
  }).json(news);
});

缓存更新策略

常见的缓存更新策略需要根据业务场景选择:

  1. Cache Aside模式:
// 读取
async function getProduct(id) {
  let product = await cache.get(id);
  if (!product) {
    product = await db.query(id);
    await cache.set(id, product);
  }
  return product;
}

// 更新
async function updateProduct(id, data) {
  await db.update(id, data);
  await cache.del(id); // 使缓存失效
}
  1. Write Through模式:
async function writeThroughUpdate(id, data) {
  await cache.set(id, data); // 先更新缓存
  await db.update(id, data); // 再更新数据库
}
  1. Write Behind模式:
const writeQueue = new Queue();

async function writeBehindUpdate(id, data) {
  await cache.set(id, data);
  writeQueue.add({ id, data }); // 异步批量写入数据库
}

缓存监控与调优

实现缓存监控中间件:

app.use((req, res, next) => {
  const start = Date.now();
  const originalSend = res.send;
  
  res.send = function(body) {
    const duration = Date.now() - start;
    const cacheStatus = res.get('X-Cache') || 'MISS';
    
    // 记录缓存命中率
    statsd.increment(`cache.${cacheStatus.toLowerCase()}`);
    statsd.timing('response_time', duration);
    
    originalSend.call(this, body);
  };
  
  next();
});

使用Redis监控命令:

# 查看缓存命中率
redis-cli info stats | grep keyspace_hits
redis-cli info stats | grep keyspace_misses

# 内存使用情况
redis-cli info memory

缓存问题解决方案

缓存穿透解决方案:

// 布隆过滤器实现
const { BloomFilter } = require('bloom-filters');
const filter = new BloomFilter(1000, 0.01);

// 添加合法ID
validIds.forEach(id => filter.add(id));

app.get('/api/items/:id', async (req, res) => {
  if (!filter.has(req.params.id)) {
    return res.status(404).send('Not Found');
  }
  // ...正常处理逻辑
});

缓存击穿解决方案:

// 使用互斥锁
const mutex = new Mutex();

app.get('/api/hot-item', async (req, res) => {
  const cacheKey = 'hot_item';
  let data = await cache.get(cacheKey);
  
  if (!data) {
    const release = await mutex.acquire();
    try {
      // 双重检查
      data = await cache.get(cacheKey);
      if (!data) {
        data = await fetchHotItem();
        await cache.set(cacheKey, data, 60);
      }
    } finally {
      release();
    }
  }
  
  res.json(data);
});

多级缓存架构

大型应用可以采用多级缓存架构:

// 三级缓存示例:内存 -> Redis -> CDN
app.get('/api/popular', async (req, res) => {
  // 1. 检查内存缓存
  const memoryKey = `popular_${req.query.page}`;
  const memoryCache = localCache.get(memoryKey);
  if (memoryCache) return res.json(memoryCache);

  // 2. 检查Redis缓存
  const redisKey = `popular:${req.query.page}`;
  const redisData = await redisClient.get(redisKey);
  if (redisData) {
    localCache.set(memoryKey, redisData, 10); // 回写到内存缓存
    return res.json(JSON.parse(redisData));
  }

  // 3. 回源查询
  const data = await fetchPopularData(req.query.page);
  
  // 设置各级缓存
  localCache.set(memoryKey, data, 10); // 内存缓存10秒
  await redisClient.setex(redisKey, 3600, JSON.stringify(data)); // Redis缓存1小时
  cdnCache.set(`/api/popular?page=${req.query.page}`, data); // CDN缓存
  
  res.json(data);
});

缓存版本控制

对于长期缓存需要版本控制:

// 基于内容hash的版本控制
app.get('/static/:file', (req, res) => {
  const filePath = path.join(__dirname, 'static', req.params.file);
  const fileContent = fs.readFileSync(filePath);
  const hash = crypto.createHash('sha1').update(fileContent).digest('hex').slice(0, 8);
  
  res.set({
    'Cache-Control': 'public, max-age=31536000', // 1年
    'ETag': hash
  });
  
  res.sendFile(filePath);
});

前端引用时使用带hash的文件名:

<script src="/static/app.a1b2c3d4.js"></script>
<link href="/static/style.e5f6g7h8.css" rel="stylesheet">

本站部分内容来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn

前端川

前端川,陈川的代码茶馆🍵,专治各种不服的Bug退散符💻,日常贩卖秃头警告级的开发心得🛠️,附赠一行代码笑十年的摸鱼宝典🐟,偶尔掉落咖啡杯里泡开的像素级浪漫☕。‌