📖 第10课:缓存策略

阶段二:RESTful进阶

缓存是API性能最有效的优化手段之一——合理的缓存策略能让API响应速度提升10倍以上,同时减少服务器负载。但缓存不当会导致数据不一致、内存泄漏和难以调试的bug。本课深入HTTP缓存、应用层缓存、CDN缓存和缓存失效策略。

🗄️ 缓存层级

API缓存四层架构

层级位置典型工具特点
浏览器缓存客户端HTTP Cache-Control零网络开销,用户级
CDN缓存边缘节点Cloudflare, Fastly就近响应,全球加速
反向代理缓存网关层Nginx, Varnish服务端第一道防线
应用缓存应用层Redis, Memcached精细控制,可编程

📋 HTTP缓存机制

强缓存 vs 协商缓存

强缓存(不发送请求)

# Cache-Control 指令
Cache-Control: max-age=3600            # 缓存1小时
Cache-Control: no-cache                # 必须验证(常见误解:不是不缓存!)
Cache-Control: no-store                # 完全不缓存(敏感数据)
Cache-Control: private                 # 仅浏览器缓存(CDN不缓存)
Cache-Control: public                  # 允许所有缓存
Cache-Control: must-revalidate         # 过期后必须验证
Cache-Control: immutable               # 过期前不会变化(资源指纹)

# Expires(HTTP/1.0,不推荐)
Expires: Wed, 15 Jan 2025 08:00:00 GMT

协商缓存(发送验证请求)

# ETag 验证
# 首次响应
ETag: "abc123"

# 后续请求
If-None-Match: "abc123"

# 如果未变化 → 304 Not Modified(无响应体)
# 如果已变化 → 200 + 新资源 + 新ETag

# Last-Modified 验证
# 首次响应
Last-Modified: Wed, 15 Jan 2025 08:00:00 GMT

# 后续请求
If-Modified-Since: Wed, 15 Jan 2025 08:00:00 GMT

不同API的缓存策略

API类型缓存策略示例头
公共只读数据强缓存+长TTLCache-Control: public, max-age=86400
用户个人数据协商缓存Cache-Control: private, no-cache
实时数据不缓存Cache-Control: no-store
敏感数据不缓存Cache-Control: no-store, private
静态资源强缓存+immutableCache-Control: public, max-age=31536000, immutable

🛠️ 实战:完整缓存策略API

// caching-api.js - 带完整缓存策略的API
const express = require('express');
const crypto = require('crypto');
const app = express();
app.use(express.json());

// ===== 应用层缓存(LRU) =====

class LRUCache {
  constructor(maxSize = 1000, defaultTTL = 60) {
    this.maxSize = maxSize;
    this.defaultTTL = defaultTTL;  // 默认TTL(秒)
    this.cache = new Map();
    this.stats = { hits: 0, misses: 0, sets: 0, evictions: 0 };
  }

  get(key) {
    const entry = this.cache.get(key);
    if (!entry) {
      this.stats.misses++;
      return null;
    }

    // 检查过期
    if (Date.now() > entry.expiresAt) {
      this.cache.delete(key);
      this.stats.misses++;
      return null;
    }

    // LRU: 移到末尾
    this.cache.delete(key);
    this.cache.set(key, entry);
    this.stats.hits++;
    return entry.data;
  }

  set(key, data, ttl) {
    const effectiveTTL = ttl !== undefined ? ttl : this.defaultTTL;

    // 容量检查
    if (this.cache.size >= this.maxSize && !this.cache.has(key)) {
      const firstKey = this.cache.keys().next().value;
      this.cache.delete(firstKey);
      this.stats.evictions++;
    }

    this.cache.set(key, {
      data,
      expiresAt: Date.now() + effectiveTTL * 1000,
      createdAt: Date.now(),
    });
    this.stats.sets++;
  }

  invalidate(pattern) {
    if (pattern === '*') {
      this.cache.clear();
      return;
    }
    for (const key of this.cache.keys()) {
      if (key.startsWith(pattern)) {
        this.cache.delete(key);
      }
    }
  }

  getStats() {
    const total = this.stats.hits + this.stats.misses;
    return {
      ...this.stats,
      size: this.cache.size,
      hitRate: total > 0 ? (this.stats.hits / total * 100).toFixed(2) + '%' : '0%',
    };
  }
}

const cache = new LRUCache(500, 60);

// ===== 数据层 =====

const products = [];
for (let i = 1; i <= 100; i++) {
  products.push({
    id: i,
    name: `产品${i}`,
    price: Math.round(Math.random() * 5000 * 100) / 100,
    stock: Math.floor(Math.random() * 200),
    updatedAt: new Date().toISOString(),
  });
}

// ===== ETag工具 =====

function generateETag(data) {
  const str = JSON.stringify(data);
  return '"' + crypto.createHash('md5').update(str).digest('hex').substring(0, 12) + '"';
}

// ===== 缓存中间件 =====

// HTTP缓存中间件(ETag + Cache-Control)
function httpCache(options = {}) {
  const { maxAge = 60, private = false, noCache = false, noStore = false } = options;

  return (req, res, next) => {
    if (noStore) {
      res.setHeader('Cache-Control', 'no-store, private');
      return next();
    }

    if (noCache) {
      res.setHeader('Cache-Control', 'no-cache, private');
    } else {
      const directives = [private ? 'private' : 'public', `max-age=${maxAge}`];
      res.setHeader('Cache-Control', directives.join(', '));
    }

    // 覆盖res.json以自动添加ETag
    const originalJson = res.json.bind(res);
    res.json = function(data) {
      const etag = generateETag(data);
      res.setHeader('ETag', etag);

      // 检查客户端的If-None-Match
      const clientETag = req.headers['if-none-match'];
      if (clientETag === etag) {
        return res.status(304).end();
      }

      return originalJson(data);
    };

    next();
  };
}

// 应用层缓存中间件
function appCache(ttl) {
  return (req, res, next) => {
    const cacheKey = `${req.method}:${req.originalUrl}`;
    const cached = cache.get(cacheKey);

    if (cached) {
      res.setHeader('X-Cache', 'HIT');
      res.setHeader('X-Cache-Age', Math.floor((Date.now() - cached.cachedAt) / 1000) + 's');
      return res.json(cached.data);
    }

    res.setHeader('X-Cache', 'MISS');

    // 覆盖res.json以缓存响应
    const originalJson = res.json.bind(res);
    res.json = function(data) {
      // 只缓存成功响应
      if (res.statusCode >= 200 && res.statusCode < 300) {
        cache.set(cacheKey, { data, cachedAt: Date.now() }, ttl);
      }
      return originalJson(data);
    };

    next();
  };
}

// 缓存失效中间件
function invalidateCache(...patterns) {
  return (req, res, next) => {
    const originalEnd = res.end.bind(res);
    res.end = function(...args) {
      if (res.statusCode >= 200 && res.statusCode < 300) {
        patterns.forEach(p => cache.invalidate(p));
      }
      return originalEnd(...args);
    };
    next();
  };
}

// ===== 路由 =====

// 产品列表 - 双层缓存(HTTP + 应用层)
app.get('/api/products', httpCache({ maxAge: 60, private: false }), appCache(30), (req, res) => {
  const { page = 1, limit = 20 } = req.query;
  const start = (page - 1) * limit;
  const data = products.slice(start, start + Number(limit));

  res.json({
    success: true,
    data,
    pagination: { page: Number(page), limit: Number(limit), total: products.length },
  });
});

// 产品详情 - ETag缓存
app.get('/api/products/:id', httpCache({ maxAge: 300 }), appCache(120), (req, res) => {
  const product = products.find(p => p.id === parseInt(req.params.id));
  if (!product) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND' } });
  res.json({ success: true, data: product });
});

// 创建产品 - 失效列表缓存
app.post('/api/products', invalidateCache('GET:/api/products'), (req, res) => {
  const id = products.length + 1;
  const product = { id, ...req.body, updatedAt: new Date().toISOString() };
  products.push(product);
  res.status(201).json({ success: true, data: product });
});

// 更新产品 - 失效详情+列表缓存
app.patch('/api/products/:id',
  invalidateCache('GET:/api/products', `GET:/api/products/${req?.params?.id}`),
  (req, res) => {
    const product = products.find(p => p.id === parseInt(req.params.id));
    if (!product) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND' } });
    Object.assign(product, req.body, { updatedAt: new Date().toISOString() });
    res.json({ success: true, data: product });
  }
);

// 用户数据 - 不缓存(私有数据)
app.get('/api/users/me', httpCache({ noStore: true }), (req, res) => {
  res.json({ success: true, data: { id: 1, name: '当前用户', email: 'me@example.com' } });
});

// 缓存管理端点
app.get('/api/admin/cache/stats', (req, res) => {
  res.json({ success: true, data: cache.getStats() });
});

app.delete('/api/admin/cache', (req, res) => {
  cache.invalidate('*');
  res.json({ success: true, data: { message: '缓存已清除' } });
});

app.listen(3000, () => console.log('🗄️ 缓存策略API运行在 http://localhost:3000'));

测试缓存策略

# 1. 首次请求 - MISS
curl -s http://localhost:3000/api/products -D - | head -20
HTTP/1.1 200 OK
Cache-Control: public, max-age=60
ETag: "a1b2c3d4e5f6"
X-Cache: MISS
Content-Type: application/json
# 2. 应用层缓存命中 - HIT
curl -s http://localhost:3000/api/products -D - | grep X-Cache

# 3. HTTP ETag验证 - 304
curl -s http://localhost:3000/api/products \
  -H 'If-None-Match: "a1b2c3d4e5f6"' -D - | head -5
HTTP/1.1 304 Not Modified
ETag: "a1b2c3d4e5f6"
Cache-Control: public, max-age=60
# 4. 缓存统计
curl -s http://localhost:3000/api/admin/cache/stats | jq .

# 5. 清除缓存
curl -s -X DELETE http://localhost:3000/api/admin/cache | jq .

# 6. 私有数据 - 不缓存
curl -s http://localhost:3000/api/users/me -D - | grep Cache-Control

🔄 缓存失效策略

四种失效策略

策略说明适用场景
TTL过期设置过期时间,到时自动失效时间敏感度低的数据
主动失效数据变更时主动清除相关缓存强一致性要求
版本号/时间戳数据变更时更新版本号分布式缓存同步
事件通知通过消息队列广播失效事件多实例部署
// 主动缓存失效的完整模式

// 1. 写操作 → 失效读缓存
// 创建产品后,列表缓存失效
app.post('/api/products', (req, res) => {
  const product = createProduct(req.body);
  // 失效所有产品列表缓存(无论分页参数如何)
  cache.invalidate('GET:/api/products');
  // 也失效相关分类的缓存
  cache.invalidate(`GET:/api/categories/${product.category}/products`);
  res.status(201).json({ success: true, data: product });
});

// 2. 更新操作 → 失效详情+列表缓存
app.patch('/api/products/:id', (req, res) => {
  const product = updateProduct(req.params.id, req.body);
  // 失效详情缓存
  cache.invalidate(`GET:/api/products/${req.params.id}`);
  // 失效列表缓存
  cache.invalidate('GET:/api/products');
  res.json({ success: true, data: product });
});

// 3. 删除操作 → 失效所有相关缓存
app.delete('/api/products/:id', (req, res) => {
  deleteProduct(req.params.id);
  cache.invalidate('GET:/api/products');     // 列表
  cache.invalidate(`GET:/api/products/${req.params.id}`); // 详情
  cache.invalidate('GET:/api/stats');         // 统计
  res.status(204).end();
});

📝 本课小结

  1. 四层缓存:浏览器→CDN→反向代理→应用层,逐层加速
  2. 强缓存(max-age)不发送请求,协商缓存(ETag)发送验证请求
  3. 公共数据用强缓存,私有数据用协商缓存,敏感数据不缓存
  4. LRU + TTL应用层缓存兼顾性能和时效性
  5. 写操作必须主动失效相关缓存,避免数据不一致
  6. Cache-Control比Expires更强大,始终优先使用
  7. 缓存命中率是核心指标,定期监控和调优

💪 练习

练习1:实现多级缓存

组合LRU内存缓存(热数据,1分钟TTL)和Redis缓存(温数据,10分钟TTL),实现二级缓存。查询顺序:内存→Redis→数据库。

练习2:缓存击穿防护

当缓存过期时,大量请求同时到达数据库(缓存击穿)。实现singleflight模式:同一个key只允许一个请求查询数据库,其他请求等待结果。

🏆 本课成就:缓存架构师