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

缓存策略

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

缓存策略的基本概念

缓存策略是提升应用性能的关键手段之一,通过将频繁访问的数据存储在高速存储介质中,减少对慢速数据源(如数据库或远程API)的依赖。在Node.js中,缓存可以显著降低I/O操作的开销,特别是在处理高并发请求时。

常见的缓存类型包括:

  • 内存缓存(如Node.js的Map对象或专用库)
  • 分布式缓存(如Redis)
  • 浏览器缓存(通过HTTP头控制)
  • CDN缓存(针对静态资源)

内存缓存实现

Node.js中最简单的缓存实现是使用内存存储。以下是一个基于Map的基础缓存示例:

class SimpleCache {
  constructor() {
    this.cache = new Map();
    this.ttl = 60000; // 默认1分钟过期时间
  }

  set(key, value, ttl = this.ttl) {
    const expiresAt = Date.now() + ttl;
    this.cache.set(key, { value, expiresAt });
  }

  get(key) {
    const item = this.cache.get(key);
    if (!item) return null;
    
    if (Date.now() > item.expiresAt) {
      this.cache.delete(key);
      return null;
    }
    return item.value;
  }

  delete(key) {
    this.cache.delete(key);
  }

  clear() {
    this.cache.clear();
  }
}

使用示例:

const cache = new SimpleCache();
cache.set('user:123', { name: 'Alice', role: 'admin' });

// 5秒后获取
setTimeout(() => {
  console.log(cache.get('user:123')); // 返回缓存对象
}, 5000);

// 2分钟后获取
setTimeout(() => {
  console.log(cache.get('user:123')); // 返回null,已过期
}, 120000);

缓存失效策略

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

  1. 时间过期(TTL)

    • 设置固定过期时间
    • 适合变化不频繁的数据
  2. 写时失效

    • 数据变更时主动清除缓存
    • 适合需要强一致性的场景
  3. LRU(最近最少使用)

    • 当缓存达到上限时,淘汰最久未使用的条目
    • Node.js实现示例:
class LRUCache {
  constructor(capacity = 100) {
    this.capacity = capacity;
    this.cache = new Map();
  }

  get(key) {
    if (!this.cache.has(key)) return null;
    
    const value = this.cache.get(key);
    this.cache.delete(key);
    this.cache.set(key, value);
    return value;
  }

  set(key, value) {
    if (this.cache.has(key)) {
      this.cache.delete(key);
    } else if (this.cache.size >= this.capacity) {
      // 删除最久未使用的
      const oldestKey = this.cache.keys().next().value;
      this.cache.delete(oldestKey);
    }
    this.cache.set(key, value);
  }
}

多级缓存架构

对于高性能应用,可以采用多级缓存策略:

graph TD
    A[客户端] -->|请求| B[浏览器缓存]
    B -->|未命中| C[CDN缓存]
    C -->|未命中| D[应用内存缓存]
    D -->|未命中| E[Redis缓存]
    E -->|未命中| F[数据库]

Node.js中实现多级缓存的代码结构:

async function getWithMultiLevelCache(key) {
  // 1. 检查内存缓存
  let data = memoryCache.get(key);
  if (data) return data;

  // 2. 检查Redis缓存
  data = await redisClient.get(key);
  if (data) {
    memoryCache.set(key, data); // 回填内存缓存
    return data;
  }

  // 3. 查询数据库
  data = await db.query('SELECT * FROM data WHERE key = ?', [key]);
  
  // 写入各级缓存
  await redisClient.setex(key, 3600, data); // Redis缓存1小时
  memoryCache.set(key, data); // 内存缓存
  
  return data;
}

缓存击穿与雪崩防护

缓存击穿解决方案

当热点key过期时,大量请求直接打到数据库:

async function getProduct(id) {
  const cacheKey = `product:${id}`;
  let product = await redis.get(cacheKey);
  
  if (!product) {
    // 使用互斥锁防止并发重建
    const lockKey = `lock:${cacheKey}`;
    const lock = await redis.set(lockKey, '1', 'EX', 10, 'NX');
    
    if (lock) {
      try {
        product = await db.getProduct(id);
        await redis.setex(cacheKey, 3600, product);
      } finally {
        await redis.del(lockKey);
      }
    } else {
      // 等待其他进程重建缓存
      await new Promise(resolve => setTimeout(resolve, 100));
      return getProduct(id); // 重试
    }
  }
  
  return product;
}

缓存雪崩防护

大量key同时过期导致数据库压力激增:

  1. 随机化过期时间:
function setWithRandomTtl(key, value, baseTtl) {
  const ttl = baseTtl + Math.floor(Math.random() * 60 * 1000); // 添加随机0-60秒
  redis.setex(key, ttl, value);
}
  1. 永不过期+后台更新:
// 设置永不过期的key
redis.set('hot:data', data);

// 后台定时更新
setInterval(async () => {
  const newData = await fetchLatestData();
  redis.set('hot:data', newData);
}, 5 * 60 * 1000); // 每5分钟更新

HTTP缓存策略

Node.js中实现HTTP缓存头控制:

const express = require('express');
const app = express();

// 静态资源缓存
app.use('/static', express.static('public', {
  maxAge: '1y', // 浏览器缓存1年
  immutable: true // 内容不变
}));

// API响应缓存
app.get('/api/data', async (req, res) => {
  const data = await getData();
  
  // 设置缓存头
  res.set({
    'Cache-Control': 'public, max-age=300', // 5分钟
    'ETag': generateETag(data) // 基于内容生成ETag
  });
  
  res.json(data);
});

// 条件GET请求处理
app.get('/api/data', (req, res) => {
  const ifNoneMatch = req.get('If-None-Match');
  const currentETag = generateETag(data);
  
  if (ifNoneMatch === currentETag) {
    return res.status(304).end(); // 未修改
  }
  
  // ...返回新数据
});

缓存监控与调优

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

  1. 关键指标监控:

    • 缓存命中率
    • 平均响应时间
    • 内存使用情况
  2. Node.js实现简单监控:

class MonitoredCache extends SimpleCache {
  constructor() {
    super();
    this.stats = {
      hits: 0,
      misses: 0,
      size: 0
    };
  }

  get(key) {
    const value = super.get(key);
    if (value) {
      this.stats.hits++;
    } else {
      this.stats.misses++;
    }
    this.stats.size = this.cache.size;
    return value;
  }

  getHitRate() {
    const total = this.stats.hits + this.stats.misses;
    return total > 0 ? (this.stats.hits / total) : 0;
  }
}
  1. 使用Redis监控命令:
# 查看内存使用
redis-cli info memory

# 查看键空间统计
redis-cli info keyspace

# 监控命中率
redis-cli info stats | grep -E '(keyspace_hits|keyspace_misses)'

缓存模式实践

写穿透模式

async function writeThrough(key, value) {
  // 1. 先更新数据库
  await db.update(key, value);
  
  // 2. 更新缓存
  await cache.set(key, value);
}

写回模式

async function writeBack(key, value) {
  // 1. 只更新缓存
  await cache.set(key, value);
  
  // 2. 异步批量更新数据库
  if (!this.batchUpdate) {
    this.batchUpdate = setTimeout(async () => {
      const dirtyKeys = cache.getDirtyKeys();
      await db.bulkUpdate(dirtyKeys);
      this.batchUpdate = null;
    }, 5000); // 每5秒批量更新
  }
}

缓存预热

启动时加载热点数据:

async function warmUpCache() {
  const hotProducts = await db.query(`
    SELECT * FROM products 
    WHERE views > 1000 
    ORDER BY views DESC 
    LIMIT 100
  `);
  
  await Promise.all(
    hotProducts.map(p => 
      redis.setex(`product:${p.id}`, 86400, JSON.stringify(p))
    )
  );
}

// 服务启动时
app.listen(3000, () => {
  warmUpCache().then(() => {
    console.log('缓存预热完成');
  });
});

特殊场景处理

分页查询缓存

async function getProductsPage(page, size) {
  const cacheKey = `products:page:${page}:size:${size}`;
  const cached = await redis.get(cacheKey);
  
  if (cached) return JSON.parse(cached);
  
  const products = await db.query(
    'SELECT * FROM products LIMIT ? OFFSET ?',
    [size, (page - 1) * size]
  );
  
  // 设置较短过期时间,因为分页数据变化较快
  await redis.setex(cacheKey, 300, JSON.stringify(products));
  
  return products;
}

关联数据缓存

处理关联数据时,可以采用延迟加载策略:

async function getUserWithPosts(userId) {
  const userKey = `user:${userId}`;
  const postsKey = `user:${userId}:posts`;
  
  const [user, posts] = await Promise.all([
    redis.get(userKey),
    redis.get(postsKey)
  ]);
  
  if (!user) {
    const userData = await db.getUser(userId);
    await redis.setex(userKey, 3600, JSON.stringify(userData));
    user = userData;
  } else {
    user = JSON.parse(user);
  }
  
  if (!posts) {
    const postsData = await db.getUserPosts(userId);
    await redis.setex(postsKey, 1800, JSON.stringify(postsData));
    posts = postsData;
  } else {
    posts = JSON.parse(posts);
  }
  
  return { ...user, posts };
}

性能优化技巧

  1. 批量操作
// 批量获取缓存
async function batchGet(keys) {
  if (redis) {
    return redis.mget(keys);
  }
  
  // 内存缓存实现
  return keys.map(key => this.cache.get(key));
}
  1. 压缩缓存数据
const zlib = require('zlib');

async function setCompressed(key, value) {
  const compressed = await new Promise((resolve, reject) => {
    zlib.deflate(JSON.stringify(value), (err, buffer) => {
      err ? reject(err) : resolve(buffer);
    });
  });
  await redis.set(key, compressed);
}

async function getCompressed(key) {
  const compressed = await redis.getBuffer(key);
  return new Promise((resolve, reject) => {
    zlib.inflate(compressed, (err, buffer) => {
      if (err) return reject(err);
      resolve(JSON.parse(buffer.toString()));
    });
  });
}
  1. 缓存分区
// 按业务分片
const SHARD_COUNT = 16;
function getShard(key) {
  const hash = simpleHash(key);
  return `cache:shard:${hash % SHARD_COUNT}`;
}

function simpleHash(str) {
  let hash = 0;
  for (let i = 0; i < str.length; i++) {
    hash = ((hash << 5) - hash) + str.charCodeAt(i);
    hash |= 0; // 转换为32位整数
  }
  return Math.abs(hash);
}

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

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

上一篇:垃圾回收机制

下一篇:负载测试

前端川

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