引言:HTTP缓存的重要性

HTTP缓存是Web性能优化的核心技术之一,它通过在客户端(浏览器)和服务器端存储资源副本,减少网络传输和服务器负载,从而显著提升网站加载速度和用户体验。根据Google的研究,页面加载时间每增加1秒,用户跳出率会上升32%。合理的缓存策略不仅能提升性能,还能降低带宽成本和服务器压力。

本文将深入探讨HTTP缓存的各个层面,从浏览器缓存机制到服务端优化策略,并详细解析缓存失效与一致性难题的解决方案。我们将通过实际案例和代码示例,帮助开发者全面掌握缓存技术。

一、浏览器缓存机制详解

1.1 缓存分类与存储位置

浏览器缓存主要分为强缓存协商缓存两类,它们在不同的缓存阶段发挥作用:

  • 强缓存:直接从浏览器本地缓存读取资源,不发送HTTP请求到服务器
  • 协商缓存:浏览器发送请求到服务器,由服务器判断缓存是否有效

缓存存储位置按优先级排序:

  1. Service Worker:PWA技术中的独立线程,可完全控制网络请求
  2. Memory Cache:内存缓存,读取最快但容量小,随页面关闭消失
  3. Disk Cache:磁盘缓存,容量大持久化存储
  4. Push Cache:HTTP/2的服务器推送缓存,仅在会话中存在

1.2 强缓存:Expires与Cache-Control

Expires是HTTP/1.0的产物,表示资源的过期时间(GMT格式):

Expires: Thu, 31 Dec 2023 23:59:59 GMT

Cache-Control是HTTP/1.1的产物,采用相对时间,优先级更高:

Cache-Control: max-age=3600, public, must-revalidate

常用指令说明:

  • max-age=seconds:资源最大新鲜时间(秒)
  • public:可被任何缓存(CDN、浏览器)缓存
  • private:仅浏览器缓存,CDN不缓存
  • no-cache:跳过强缓存,进入协商缓存
  • no-store:不缓存,每次都从服务器获取
  • must-revalidate:缓存过期后必须向服务器验证

代码示例:Node.js设置强缓存

const http = require('http');
const fs = require('fs');

http.createServer((req, res) => {
  // 设置强缓存1小时
  res.setHeader('Cache-Control', 'max-age=3600');
  res.setHeader('Content-Type', 'text/html');
  res.end('<h1>缓存测试页面</h1>');
}).listen(3000);

1.3 协商缓存:Last-Modified与ETag

当强缓存过期或使用no-cache时,进入协商缓存阶段:

Last-Modified / If-Modified-Since

  • 服务器返回Last-Modified(资源最后修改时间)
  • 浏览器下次请求时携带If-Modified-Since,服务器比较时间

ETag / If-None-Match

  • 服务器返回ETag(资源唯一标识,通常是哈希值)
  • 浏览器下次请求时携带If-None-Match,服务器比较标识

代码示例:Node.js实现协商缓存

const http = require('http');
const crypto = require('crypto');
const fs = require('fs');

http.createServer((req, res) => {
  const content = fs.readFileSync('./index.html');
  const etag = crypto.createHash('md5').update(content).digest('hex');
  
  // 检查If-None-Match请求头
  if (req.headers['if-none-match'] === etag) {
    res.writeHead(304); // Not Modified
    res.end();
    return;
  }
  
  res.setHeader('ETag', etag);
  res.setHeader('Cache-Control', 'no-cache'); // 强制协商缓存
  res.end(content);
}).listen(3000);

1.4 缓存优先级与决策流程

浏览器缓存决策流程如下:

  1. 检查Service Worker缓存
  2. 检查Memory Cache
  3. 检查Disk Cache
  4. 检查是否强缓存有效(Cache-Control/Expires)
  5. 强缓存无效则发送请求,进入协商缓存
  6. 协商缓存无效返回200,有效返回304

流程图示意

请求资源 → Service Worker → Memory Cache → Disk Cache → 强缓存检查 → 协商缓存检查 → 网络请求

二、服务端缓存策略与实现

2.1 CDN缓存策略

CDN(内容分发网络)通过边缘节点缓存资源,减少回源请求:

配置示例(Nginx)

# 静态资源缓存1年
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
    expires 1y;
    add_header Cache-Control "public, immutable";
    add_header X-Cache-Status $upstream_cache_status;
}

# HTML文件缓存策略
location ~* \.html$ {
    expires 5m;  # 5分钟
    add_header Cache-Control "public, must-revalidate";
}

# API接口不缓存
location /api/ {
    expires -1;
    add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate";
}

CDN缓存键规则

  • 默认:完整URL(包含查询参数)
  • 自定义:可忽略特定参数,如utm_跟踪参数

2.2 反向代理缓存(Nginx Proxy Cache)

Nginx可以作为反向代理缓存后端动态内容:

# 缓存路径配置
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=my_cache:10m max_size=10g 
                 inactive=60m use_temp_path=off;

# 缓存规则
server {
    listen 80;
    server_name example.com;
    
    location /api/ {
        proxy_pass http://backend;
        proxy_cache my_cache;
        proxy_cache_key "$scheme$request_method$host$request_uri";
        proxy_cache_valid 200 302 10m;  # 200/302缓存10分钟
        proxy_cache_valid 404 1m;       # 404缓存1分钟
        proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
        
        # 缓存状态头(调试用)
        add_header X-Cache-Status $upstream_cache_status;
        
        # 后端响应头控制
        proxy_ignore_headers Cache-Control;
        proxy_cache_valid any 5m;
    }
}

2.3 应用层缓存(Redis/Memcached)

对于动态内容,应用层缓存是关键:

Node.js + Redis示例

const redis = require('redis');
const client = redis.createClient();

// 缓存中间件
async function cacheMiddleware(req, res, next) {
  const key = `cache:${req.url}`;
  
  try {
    // 尝试从缓存获取
    const cached = await client.get(key);
    if (cached) {
      console.log('Cache HIT:', key);
      return res.json(JSON.parse(cached));
    }
    
    // 重写res.json以缓存结果
    const originalJson = res.json.bind(res);
    res.json = function(data) {
      // 缓存5分钟
      client.setex(key, 300, JSON.stringify(data));
      return originalJson(data);
    };
    
    next();
  } catch (err) {
    next();
  }
}

// 路由使用
app.get('/api/user/:id', cacheMiddleware, async (req, res) => {
  // 数据库查询等耗时操作
  const user = await db.query('SELECT * FROM users WHERE id = ?', [req.params.id]);
  res.json(user);
});

2.4 数据库查询缓存

MySQL查询缓存(注意:MySQL 8.0已移除):

-- 查看查询缓存状态
SHOW VARIABLES LIKE 'query_cache%';

-- 在my.cnf配置
[mysqld]
query_cache_type = 1
query_cache_size = 64M
query_cache_limit = 2M

-- 显式缓存查询(SQL_NO_CACHE)
SELECT SQL_CACHE * FROM products WHERE category_id = 5;
SELECT SQL_NO_CACHE * FROM products WHERE category_id = 5;

对于现代数据库,更推荐使用应用层缓存数据库内置缓存(如MySQL的InnoDB Buffer Pool)。

三、缓存失效与一致性难题

3.1 缓存失效策略

主动失效 vs 被动失效

  • 主动失效:数据更新时立即清除相关缓存
  • 被动失效:依赖缓存过期时间,期间可能读取旧数据

缓存更新模式

  1. Cache-Aside(旁路缓存):最常用模式

    • 读取:先读缓存,命中返回;未命中读数据库并写入缓存
    • 写入:先更新数据库,再删除缓存
  2. Write-Through(写穿透):数据同时写入缓存和数据库

  3. Write-Behind(写回):先写缓存,异步批量写入数据库

代码示例:Cache-Aside模式实现

class CacheAside {
  constructor(redisClient, dbClient) {
    this.redis = redisClient;
    this.db = dbClient;
  }
  
  async get(key) {
    // 1. 读缓存
    const cached = await this.redis.get(key);
    if (cached) return JSON.parse(cached);
    
    // 2. 读数据库
    const data = await this.db.query(`SELECT * FROM data WHERE id = ?`, [key]);
    
    // 3. 写缓存
    if (data) {
      await this.redis.setex(key, 300, JSON.stringify(data));
    }
    
    return data;
  }
  
  async set(key, value) {
    // 1. 更新数据库
    await this.db.query(`UPDATE data SET value = ? WHERE id = ?`, [value, key]);
    
    // 2. 删除缓存(推荐)或更新缓存
    await this.redis.del(key);
    
    // 注意:这里存在"先删缓存后更新数据库"期间的并发问题
    // 解决方案:延迟双删、分布式锁等
  }
}

3.2 缓存一致性挑战

经典问题:缓存与数据库不一致

场景:用户更新用户名,数据库更新成功,但缓存删除失败,导致后续读取旧数据。

解决方案

方案1:延迟双删

async function updateUser(id, newName) {
  // 1. 删除缓存
  await redis.del(`user:${id}`);
  
  // 2. 更新数据库
  await db.query('UPDATE users SET name = ? WHERE id = ?', [newName, id]);
  
  // 3. 延迟再次删除(防止并发写入导致的脏数据)
  setTimeout(async () => {
    await redis.del(`user:${id}`);
  }, 500);
}

方案2:基于Binlog的缓存同步(Canal)

// Canal客户端监听MySQL Binlog变化
public class CacheUpdater {
    public void handleUpdate(RowData rowData) {
        String table = rowData.getTableName();
        List<String> changedKeys = extractKeys(rowData);
        
        // 根据变更表删除对应缓存
        if ("users".equals(table)) {
            for (String key : changedKeys) {
                redis.del("user:" + key);
            }
        }
    }
}

方案3:版本号/时间戳控制

// 在缓存中存储版本号
async function getWithVersion(key) {
  const data = await redis.get(key);
  if (!data) return null;
  
  const { value, version } = JSON.parse(data);
  const currentVersion = await redis.get(`version:${key}`);
  
  if (version !== currentVersion) {
    await redis.del(key);
    return null;
  }
  
  return value;
}

// 更新时递增版本号
async function updateWithVersion(key, value) {
  await db.query('UPDATE data SET value = ? WHERE id = ?', [value, key]);
  await redis.incr(`version:${key}`);
}

3.3 缓存穿透、缓存击穿、缓存雪崩

缓存穿透:查询不存在的数据,导致请求直接打到数据库

  • 解决方案
    • 布隆过滤器(Bloom Filter)拦截无效请求
    • 缓存空值(设置较短过期时间)
// 布隆过滤器示例(使用redisbloom模块)
const BloomFilter = require('bloom-filter');

// 初始化布隆过滤器
const filter = BloomFilter.create(1000, 0.01); // 1000容量,1%误判率

// 插入有效ID
validIds.forEach(id => filter.insert(id.toString()));

// 查询时先检查
function queryUser(id) {
  if (!filter.contains(id.toString())) {
    return null; // 肯定不存在
  }
  
  // 再走正常缓存流程
  return cacheAside.get(`user:${id}`);
}

缓存击穿:热点key过期瞬间大量请求涌入数据库

  • 解决方案
    • 互斥锁(Mutex)保证单线程重建缓存
    • 热点数据永不过期 + 异步更新
// 互斥锁实现缓存重建
async function getHotKey(key) {
  const cached = await redis.get(key);
  if (cached) return JSON.parse(cached);
  
  // 获取分布式锁
  const lockKey = `lock:${key}`;
  const lockValue = Date.now().toString();
  const acquired = await redis.set(lockKey, lockValue, 'NX', 'EX', 10);
  
  if (acquired) {
    try {
      // 双重检查,防止锁过期后重复计算
      const cached2 = await redis.get(key);
      if (cached2) return JSON.parse(cached2);
      
      // 重建缓存
      const data = await expensiveOperation();
      await redis.setex(key, 300, JSON.stringify(data));
      return data;
    } finally {
      // 释放锁(使用Lua脚本保证原子性)
      const script = `
        if redis.call("get", KEYS[1]) == ARGV[1] then
          return redis.call("del", KEYS[1])
        else
          return 0
        end
      `;
      await redis.eval(script, 1, lockKey, lockValue);
    }
  } else {
    // 未获取锁,等待后重试
    await sleep(50);
    return getHotKey(key);
  }
}

缓存雪崩:大量缓存同时过期,导致数据库压力激增

  • 解决方案
    • 设置随机过期时间(基础时间 + 随机值)
    • 热点数据永不过期 + 后台异步更新
    • 多级缓存架构(本地缓存 + 分布式缓存)
// 随机过期时间设置
function setRandomExpire(key, baseTTL, randomRange) {
  const ttl = baseTTL + Math.floor(Math.random() * randomRange);
  return redis.setex(key, ttl, value);
}

// 多级缓存示例
class MultiLevelCache {
  constructor() {
    this.localCache = new Map();
    this.redis = redis.createClient();
  }
  
  async get(key) {
    // 1. 本地缓存
    const local = this.localCache.get(key);
    if (local && local.expiry > Date.now()) {
      return local.value;
    }
    
    // 2. Redis缓存
    const remote = await this.redis.get(key);
    if (remote) {
      // 回填本地缓存
      this.localCache.set(key, {
        value: JSON.parse(remote),
        expiry: Date.now() + 60000 // 1分钟
      });
      return JSON.parse(remote);
    }
    
    // 3. 数据库
    const data = await this.queryDB(key);
    await this.redis.setex(key, 300, JSON.stringify(data));
    return data;
  }
}

3.4 缓存预热与监控

缓存预热:在系统启动或低峰期提前加载热点数据

// 缓存预热脚本
async function cacheWarmup() {
  const hotKeys = [
    'home:products',
    'home:banner',
    'config:app',
    // 从数据库或配置中心获取热点key列表
  ];
  
  for (const key of hotKeys) {
    try {
      const data = await generateData(key);
      await redis.setex(key, 3600, JSON.stringify(data));
      console.log(`Warmed up: ${key}`);
    } catch (err) {
      console.error(`Failed to warmup ${key}:`, err);
    }
  }
}

// 定时预热(每天凌晨执行)
const cron = require('node-cron');
cron.schedule('0 2 * * *', cacheWarmup); // 每天凌晨2点

缓存监控指标

  • 缓存命中率(Hit Rate):目标 > 95%
  • 缓存大小与内存使用
  • 缓存失效频率
  • 缓存响应时间
// 监控示例
async function monitorCache() {
  const info = await redis.info('stats');
  const hitRate = parseInt(info.match(/keyspace_hits:(\d+)/)[1]) / 
                  (parseInt(info.match(/keyspace_hits:(\d+)/)[1]) + 
                   parseInt(info.match(/keyspace_misses:(\d+)/)[1]));
  
  if (hitRate < 0.95) {
    await alertLowHitRate(hitRate);
  }
}

四、现代前端缓存策略

4.1 Webpack资源打包与缓存

长期缓存策略

  • 使用[contenthash]:文件内容变化时hash才变化
  • 提取第三方库到vendor chunk
  • 使用immutable指令
// webpack.config.js
module.exports = {
  output: {
    filename: '[name].[contenthash:8].js',
    chunkFilename: '[name].[contenthash:8].chunk.js',
  },
  optimization: {
    splitChunks: {
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all',
          priority: 10,
        },
      },
    },
  },
  plugins: [
    // 生成带hash的文件名
    new HtmlWebpackPlugin({
      template: './src/index.html',
      // 自动注入带hash的资源
    }),
  ],
};

Service Worker缓存策略

// service-worker.js
const CACHE_NAME = 'app-v1';
const urlsToCache = [
  '/',
  '/index.html',
  '/styles/main.css',
  '/scripts/bundle.js',
];

// 安装时缓存
self.addEventListener('install', event => {
  event.waitUntil(
    caches.open(CACHE_NAME).then(cache => cache.addAll(urlsToCache))
  );
});

// 拦截请求并返回缓存
self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request).then(response => {
      // 缓存命中
      if (response) return response;
      
      // 缓存未命中,网络请求并缓存
      return fetch(event.request).then(response => {
        // 只缓存成功的响应
        if (!response || response.status !== 200 || response.type !== 'basic') {
          return response;
        }
        
        const responseToCache = response.clone();
        caches.open(CACHE_NAME).then(cache => {
          cache.put(event.request, responseToCache);
        });
        
        return response;
      });
    })
  );
});

// 更新时清理旧缓存
self.addEventListener('activate', event => {
  event.waitUntil(
    caches.keys().then(cacheNames => {
      return Promise.all(
        cacheNames.map(cacheName => {
          if (cacheName !== CACHE_NAME) {
            return caches.delete(cacheName);
          }
        })
      );
    })
  );
});

4.2 HTTP/2 Server Push与缓存

HTTP/2 Server Push可以主动推送资源,但需谨慎使用:

// Node.js + HTTP/2 Push
const http2 = require('http2');
const fs = require('fs');

const server = http2.createSecureServer({
  key: fs.readFileSync('server.key'),
  cert: fs.readFileSync('server.crt'),
});

server.on('stream', (stream, headers) => {
  // 主推CSS和JS
  stream.pushStream({ ':path': '/styles/main.css' }, (pushStream) => {
    pushStream.respond({ ':status': 200 });
    pushStream.end('body{color:red}');
  });
  
  stream.pushStream({ ':path': '/scripts/bundle.js' }, (pushStream) => {
    pushStream.respond({ ':status': 200 });
    pushStream.end('console.log("pushed")');
  });
  
  stream.respond({ ':status': 200 });
  stream.end('<html>...</html>');
});

注意:Server Push的资源会被浏览器缓存,但推送的资源如果已在缓存中,会造成带宽浪费。Chrome 106+已默认禁用Server Push。

4.3 ETag在前端构建中的应用

ETag可用于实现灰度发布A/B测试

// 根据用户分组返回不同ETag
function handleRequest(req, res) {
  const userId = getUserId(req);
  const group = getABTestGroup(userId); // 'A' or 'B'
  
  const content = group === 'A' ? versionA : versionB;
  const etag = `"${group}-${hash(content)}"`;
  
  if (req.headers['if-none-match'] === etag) {
    return res.status(304).end();
  }
  
  res.setHeader('ETag', etag);
  res.send(content);
}

五、高级缓存优化技巧

5.1 缓存键设计最佳实践

设计原则

  1. 包含请求方法GET:/api/users/123
  2. 包含用户标识(私有缓存):user:123:profile
  3. 版本控制v2:product:456
  4. 规范化:忽略无关参数
// 缓存键生成器
class CacheKeyGenerator {
  static generate(req, options = {}) {
    const parts = [];
    
    // 版本
    if (options.version) {
      parts.push(`v${options.version}`);
    }
    
    // 用户ID(私有缓存)
    if (options.userId) {
      parts.push(`user:${options.userId}`);
    }
    
    // 请求方法和路径
    parts.push(`${req.method}:${req.path}`);
    
    // 查询参数(规范化)
    if (req.query && Object.keys(req.query).length > 0) {
      const filtered = { ...req.query };
      // 移除跟踪参数
      delete filtered.utm_source;
      delete filtered.utm_medium;
      
      if (Object.keys(filtered).length > 0) {
        const sorted = Object.keys(filtered).sort().map(k => `${k}=${filtered[k]}`).join('&');
        parts.push(sorted);
      }
    }
    
    return parts.join(':');
  }
}

// 使用示例
const key = CacheKeyGenerator.generate(req, { version: 2, userId: 123 });
// 结果: "v2:user:123:GET:/api/products:category=electronics&sort=price"

5.2 缓存压缩与序列化优化

压缩存储

const zlib = require('zlib');
const { promisify } = require('util');
const gzip = promisify(zlib.gzip);

async function setCompressed(key, data) {
  const json = JSON.stringify(data);
  const compressed = await gzip(json);
  // 存储二进制数据
  await redis.setBuffer(key, compressed);
}

async function getCompressed(key) {
  const compressed = await redis.getBuffer(key);
  if (!compressed) return null;
  
  const decompressed = await promisify(zlib.gunzip)(compressed);
  return JSON.parse(decompressed.toString());
}

序列化优化

  • 使用MessagePack代替JSON(更快更小)
  • 避免存储大对象(>100KB)
  • 只存储必要字段
const msgpack = require('msgpack-lite');

// 使用MessagePack序列化
async function setWithMsgpack(key, data) {
  const encoded = msgpack.encode(data);
  await redis.setBuffer(key, encoded);
}

async function getWithMsgpack(key) {
  const buffer = await redis.getBuffer(key);
  return msgpack.decode(buffer);
}

5.3 缓存分层与多级架构

典型多级缓存架构

客户端 → 浏览器缓存 → Service Worker → 本地缓存(Redis/Memcached) → CDN → 数据库

实现示例

class HierarchicalCache {
  constructor() {
    this.levels = [
      new LocalCache(),      // Level 0: 进程内缓存(如Node.js内存)
      new RedisCache(),      // Level 1: 分布式缓存
      new DatabaseCache(),   // Level 2: 数据库查询缓存
    ];
  }
  
  async get(key) {
    let value = null;
    
    // 从高级别缓存开始查找
    for (let i = 0; i < this.levels.length; i++) {
      value = await this.levels[i].get(key);
      if (value !== null) {
        // 回填低级别缓存
        for (let j = i - 1; j >= 0; j--) {
          await this.levels[j].set(key, value, this.levels[i].ttl);
        }
        return value;
      }
    }
    
    return null;
  }
  
  async set(key, value, ttl) {
    // 写入所有级别
    await Promise.all(this.levels.map(level => level.set(key, value, ttl)));
  }
}

5.4 缓存安全与防护

缓存污染攻击防护

// 限制缓存键长度和数量
const MAX_KEY_LENGTH = 255;
const MAX_CACHE_SIZE = 10000;

function validateCacheKey(key) {
  if (key.length > MAX_KEY_LENGTH) {
    throw new Error('Cache key too long');
  }
  
  // 检查是否包含恶意字符
  if (/[<>]/.test(key)) {
    throw new Error('Invalid characters in cache key');
  }
  
  return true;
}

// 限制缓存值大小
function validateCacheValue(value) {
  const size = Buffer.byteLength(JSON.stringify(value));
  if (size > 1024 * 1024) { // 1MB
    throw new Error('Cache value too large');
  }
  return true;
}

敏感数据缓存防护

// 不缓存包含敏感信息的响应
function shouldCacheResponse(req, res) {
  // 检查响应头
  if (res.getHeader('Cache-Control')?.includes('no-store')) {
    return false;
  }
  
  // 检查响应体是否包含敏感信息
  const body = res.getBody();
  if (body && containsSensitiveData(body)) {
    return false;
  }
  
  // 检查请求路径
  const sensitivePaths = ['/api/user/profile', '/api/payment'];
  if (sensitivePaths.some(path => req.path.startsWith(path))) {
    return false;
  }
  
  return true;
}

六、实战案例分析

6.1 电商网站缓存策略

场景:商品详情页,包含基本信息、库存、价格、评论等。

分层缓存设计

// 商品详情服务
class ProductService {
  async getProduct(productId, userId) {
    const cacheKey = `product:${productId}:v2`; // 版本控制
    
    // 1. 浏览器缓存(通过HTTP头)
    // 2. CDN缓存(5分钟,公共)
    // 3. Redis缓存(10分钟)
    const cached = await redis.get(cacheKey);
    if (cached) {
      return JSON.parse(cached);
    }
    
    // 4. 数据库查询(分步查询)
    const [basicInfo, inventory, price] = await Promise.all([
      db.query('SELECT * FROM products WHERE id = ?', [productId]),
      db.query('SELECT stock FROM inventory WHERE product_id = ?', [productId]),
      db.query('SELECT price FROM prices WHERE product_id = ?', [productId]),
    ]);
    
    const product = { ...basicInfo, inventory: inventory[0], price: price[0] };
    
    // 5. 写入缓存
    await redis.setex(cacheKey, 600, JSON.stringify(product));
    
    return product;
  }
  
  async updateProductPrice(productId, newPrice) {
    // 更新数据库
    await db.query('UPDATE prices SET price = ? WHERE product_id = ?', [newPrice, productId]);
    
    // 删除缓存(主动失效)
    await redis.del(`product:${productId}:v2`);
    
    // 发送MQ消息,异步清理CDN缓存
    await mq.send('cache:invalidate', { key: `product:${productId}` });
  }
}

HTTP缓存头配置

# 商品详情页HTML
Cache-Control: public, max-age=300, must-revalidate

# 商品JSON API
Cache-Control: private, max-age=60, must-revalidate
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"

# 静态资源(图片、CSS、JS)
Cache-Control: public, max-age=31536000, immutable

6.2 社交媒体Feed流缓存

挑战:个性化内容、实时性强、数据量大。

解决方案

class FeedService {
  async getFeed(userId, page = 1) {
    const cacheKey = `feed:${userId}:${page}`;
    
    // 1. 尝试缓存
    const cached = await redis.get(cacheKey);
    if (cached) {
      return JSON.parse(cached);
    }
    
    // 2. 计算Feed(复杂查询)
    const feed = await this.calculateFeed(userId, page);
    
    // 3. 短期缓存(1分钟)
    await redis.setex(cacheKey, 60, JSON.stringify(feed));
    
    return feed;
  }
  
  async calculateFeed(userId, page) {
    // 关注的人
    const following = await db.query(
      'SELECT following_id FROM relationships WHERE follower_id = ?', 
      [userId]
    );
    
    if (following.length === 0) return [];
    
    const followingIds = following.map(f => f.following_id);
    
    // 获取帖子(带权重排序)
    const posts = await db.query(`
      SELECT p.*, u.username, u.avatar,
             (p.likes * 0.5 + p.comments * 0.3 + p.shares * 0.2) as score
      FROM posts p
      JOIN users u ON p.user_id = u.id
      WHERE p.user_id IN (?) AND p.status = 'published'
      ORDER BY score DESC, p.created_at DESC
      LIMIT ? OFFSET ?
    `, [followingIds, 10, (page - 1) * 10]);
    
    return posts;
  }
  
  // 发布新帖子时清理相关缓存
  async onNewPost(userId) {
    // 清理该用户所有粉丝的Feed缓存
    const followers = await db.query(
      'SELECT follower_id FROM relationships WHERE following_id = ?',
      [userId]
    );
    
    const pipeline = redis.pipeline();
    followers.forEach(f => {
      // 清理第一页
      pipeline.del(`feed:${f.follower_id}:1`);
    });
    
    await pipeline.exec();
  }
}

6.3 API网关缓存

场景:聚合多个微服务的API,减少下游调用。

// API网关缓存中间件
class GatewayCache {
  constructor() {
    this.redis = redis.createClient();
    this.rateLimiter = new RateLimiterRedis({
      storeClient: this.redis,
      keyPrefix: 'gw_limit',
      points: 100, // 100 requests
      duration: 1,
    });
  }
  
  async handleRequest(req, res, next) {
    // 1. 限流检查
    try {
      await this.rateLimiter.consume(req.ip);
    } catch (rejRes) {
      return res.status(429).json({ error: 'Too Many Requests' });
    }
    
    // 2. 生成缓存键
    const cacheKey = this.generateKey(req);
    
    // 3. 检查缓存
    const cached = await this.redis.get(cacheKey);
    if (cached) {
      const data = JSON.parse(cached);
      res.setHeader('X-Cache', 'HIT');
      res.setHeader('X-Cache-Key', cacheKey);
      return res.json(data);
    }
    
    // 4. 代理请求到后端
    res.setHeader('X-Cache', 'MISS');
    
    // 重写res.json以缓存结果
    const originalJson = res.json.bind(res);
    res.json = function(data) {
      // 根据响应决定缓存时间
      const ttl = this.getCacheTTL(req, data);
      if (ttl > 0) {
        this.redis.setex(cacheKey, ttl, JSON.stringify(data));
      }
      return originalJson(data);
    }.bind(this);
    
    next();
  }
  
  generateKey(req) {
    // 包含用户ID(私有缓存)
    const userId = req.user?.id || 'anonymous';
    // 规范化查询参数
    const query = Object.keys(req.query).sort().join('&');
    return `api:${userId}:${req.method}:${req.path}:${query}`;
  }
  
  getCacheTTL(req, data) {
    // 根据数据特征决定缓存时间
    if (req.path.startsWith('/api/products')) {
      return 300; // 5分钟
    }
    if (req.path.startsWith('/api/user/profile')) {
      return 60; // 1分钟
    }
    if (data.error) {
      return 10; // 错误响应缓存短
    }
    return 0; // 不缓存
  }
}

七、缓存测试与调试

7.1 缓存命中率监控

// 缓存监控类
class CacheMonitor {
  constructor() {
    this.metrics = {
      hits: 0,
      misses: 0,
      total: 0,
      startTime: Date.now(),
    };
  }
  
  recordHit() {
    this.metrics.hits++;
    this.metrics.total++;
  }
  
  recordMiss() {
    this.metrics.misses++;
    this.metrics.total++;
  }
  
  getStats() {
    const duration = (Date.now() - this.metrics.startTime) / 1000;
    return {
      hitRate: this.metrics.hits / this.metrics.total,
      hits: this.metrics.hits,
      misses: this.metrics.misses,
      total: this.metrics.total,
      requestsPerSecond: this.metrics.total / duration,
    };
  }
  
  // 每分钟打印统计
  startReporting() {
    setInterval(() => {
      const stats = this.getStats();
      console.log(`Cache Stats: Hit Rate ${(stats.hitRate * 100).toFixed(2)}%`);
      
      // 重置计数器
      this.metrics = { hits: 0, misses: 0, total: 0, startTime: Date.now() };
    }, 60000);
  }
}

// 使用
const monitor = new CacheMonitor();
monitor.startReporting();

// 在缓存访问处埋点
async function getCachedData(key) {
  const data = await redis.get(key);
  if (data) {
    monitor.recordHit();
    return JSON.parse(data);
  }
  monitor.recordMiss();
  // ...从数据库获取
}

7.2 缓存调试工具

使用Chrome DevTools

  • Network面板:查看Size列,(from disk cache)表示磁盘缓存
  • Application面板:查看Cache Storage、Local Storage等
  • 命令行:chrome://net-internals/#httpCache

使用curl测试缓存

# 第一次请求(MISS)
curl -I -H "Cache-Control: no-cache" https://example.com/resource

# 第二次请求(HIT)
curl -I https://example.com/resource

# 携带If-None-Match
curl -I -H "If-None-Match: \"abc123\"" https://example.com/resource

# 查看缓存详情
curl -v -H "Cache-Control: no-cache" https://example.com/resource 2>&1 | grep -i cache

Nginx缓存状态监控

# 在响应头添加缓存状态
add_header X-Cache-Status $upstream_cache_status;

# 日志格式
log_format cache '$remote_addr - $upstream_cache_status - $request_uri';
access_log /var/log/nginx/cache.log cache;

7.3 缓存问题排查清单

常见问题与解决方案

问题现象 可能原因 排查步骤
缓存不生效 Cache-Control头被覆盖 检查中间件顺序,确认没有重复设置
缓存命中率低 缓存键设计不合理 检查是否包含用户标识、版本号
数据不一致 删除缓存失败 检查Redis连接,添加重试机制
缓存过大 存储了完整响应 只存储必要字段,启用压缩
缓存穿透 查询大量不存在的数据 实现布隆过滤器或缓存空值

八、总结与最佳实践

8.1 缓存策略决策树

资源类型?
├── 静态资源(JS/CSS/图片)
│   └── Cache-Control: public, max-age=31536000, immutable
│
├── 动态HTML
│   └── Cache-Control: public/private, max-age=300, must-revalidate
│
├── API数据
│   ├── 个性化数据 → private, max-age=60
│   ├── 公共数据 → public, max-age=300
│   └── 实时数据 → no-store
│
└── 用户特定内容
    └── ETag + private, max-age=0, must-revalidate

8.2 黄金法则

  1. 缓存一切可缓存的:从静态资源到API响应
  2. 分层缓存:浏览器 → CDN → 应用 → 数据库
  3. 主动失效:数据更新时立即清理缓存
  4. 监控驱动:持续监控命中率并优化
  5. 安全第一:防止缓存穿透、击穿、雪崩
  6. 版本控制:使用hash或版本号避免缓存污染

8.3 性能指标参考

  • 缓存命中率:> 95%(静态资源应接近100%)
  • TTFB(首字节时间):缓存命中应 < 50ms
  • CDN回源率:> 80%(理想情况)
  • 缓存响应时间:P99 < 10ms

8.4 未来趋势

  • HTTP/3:QUIC协议对缓存的影响
  • 边缘计算:在CDN边缘节点执行缓存逻辑
  • 智能缓存:基于机器学习预测缓存热点
  • Web Bundles:资源打包对缓存的影响

通过本文的详细解析,你应该已经掌握了HTTP缓存的完整知识体系。记住,缓存是性能优化的利器,但需要根据业务场景合理设计,持续监控和调优,才能发挥最大价值。