引言:HTTP缓存的重要性

HTTP缓存是Web性能优化的核心技术之一,它通过在客户端(浏览器)和中间代理服务器上存储资源副本,显著减少网络请求、降低服务器负载并提升用户体验。根据Google的研究,合理的缓存策略可以将页面加载时间减少50%以上。

缓存的核心价值

  • 减少网络延迟:避免重复下载相同资源
  • 降低服务器压力:减少服务器处理相同请求的次数
  • 节省带宽成本:减少数据传输量
  • 提升用户体验:更快的页面响应速度

HTTP缓存基础机制

缓存决策流程

当浏览器发起HTTP请求时,会按照以下流程判断是否使用缓存:

客户端请求 → 检查本地缓存 → 是否过期? → 未过期 → 直接使用缓存
                                      ↓
                                    已过期 → 发送请求到服务器 → 服务器返回304? → 是 → 使用缓存
                                      ↓
                                    否 → 下载新资源并更新缓存

缓存分类

  1. 强缓存:浏览器直接使用缓存,不与服务器通信
  2. 协商缓存:浏览器发送请求,服务器返回304状态码表示缓存有效

强缓存策略

强缓存通过Cache-ControlExpires头部控制,浏览器在缓存有效期内直接使用本地副本。

Cache-Control头部

Cache-Control是HTTP/1.1标准,提供更精细的控制:

Cache-Control: max-age=3600, public, immutable

常用指令详解

  • max-age=seconds:缓存最大有效期(秒)
  • public:允许任何缓存(包括CDN)
  • private:仅允许用户浏览器缓存
  • no-cache注意:这不表示不缓存,而是必须重新验证
  • no-store:完全不缓存
  • immutable:资源在缓存期间不会改变(适用于版本化资源)

Expires头部

HTTP/1.0遗留头部,使用绝对时间:

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

优先级Cache-Control > Expires

实战示例:静态资源缓存

对于版本化的静态资源(如app.v123.js),可以设置长期缓存:

HTTP/1.1 200 OK
Cache-Control: public, max-age=31536000, immutable
Content-Type: application/javascript

// 资源内容...

这样浏览器会在一年内直接使用缓存,无需任何验证。

协商缓存策略

当强缓存过期后,浏览器会与服务器进行协商,确认资源是否更新。

基于时间的协商:Last-Modified / If-Modified-Since

流程

  1. 服务器返回资源时携带Last-Modified头部
  2. 浏览器下次请求时携带If-Modified-Since头部
  3. 服务器比较时间,未修改则返回304
# 首次响应
HTTP/1.1 200 OK
Last-Modified: Wed, 21 Oct 2025 07:28:00 GMT
Content-Type: text/html

# 后续请求
GET /index.html HTTP/1.1
If-Modified-Since: Wed, 21 Oct 2025 07:28:00 GMT

# 服务器响应(未修改)
HTTP/1.1 304 Not Modified

局限性

  • 时间精度只有秒级
  • 文件内容修改但时间戳未变不会触发更新
  • 服务器时区问题可能导致误判

基于内容的协商:ETag / If-None-Match

流程

  1. 服务器生成资源的唯一标识(ETag)
  2. 浏览器下次请求时携带If-None-Match头部
  3. 服务器比较ETag,匹配则返回304
# 首次响应
HTTP/1.1 200 OK
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
Content-Type: text/html

# 后续请求
GET /index.html HTTP/1.1
If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"

# 服务器响应(未修改)
HTTP/1.1 304 Not Modified

ETag生成策略

  • 强ETag:完全字节匹配
  • 弱ETag:语义等价(W/“33a64df551425fcc55e4d42a148795d9f25f89d4”)

缓存策略最佳实践

1. 静态资源(版本化)

策略:长期缓存 + 版本号/哈希值

// Webpack配置示例
module.exports = {
  output: {
    filename: '[name].[contenthash:8].js',
    chunkFilename: '[name].[contenthash:8].chunk.js'
  }
}

HTTP响应头

Cache-Control: public, max-age=31536000, immutable

原理:文件名变化强制浏览器重新下载,相同文件名可缓存一年。

2. 静态资源(非版本化)

策略:较短缓存 + 验证

Cache-Control: public, max-age=86400
ETag: "abc123"

3. HTML文档

策略:不缓存或极短缓存

Cache-Control: no-cache, no-store, must-revalidate
Pragma: no-cache
Expires: 0

原因:HTML是入口文件,需要确保用户获取最新版本。

4. API响应

策略:根据业务场景选择

// 动态数据 - 不缓存
Cache-Control: no-cache, private

// 用户特定数据
Cache-Control: private, max-age=60

// 公共数据(如城市列表)
Cache-Control: public, max-age=3600

服务器端实现示例

Node.js + Express 实现

const express = require('express');
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');

const app = express();

// 1. 静态资源(版本化)- 长期缓存
app.use('/static', express.static('public', {
  maxAge: '1y',
  etag: true,
  lastModified: true,
  setHeaders: (res, path) => {
    if (path.endsWith('.js') || path.endsWith('.css')) {
      res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
    }
  }
}));

// 2. HTML - 不缓存
app.get('/', (req, res) => {
  res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
  res.setHeader('Pragma', 'no-cache');
  res.setHeader('Expires', '0');
  res.sendFile(path.join(__dirname, 'index.html'));
});

// 3. API端点 - 条件缓存
app.get('/api/user/:id', (req, res) => {
  const userId = req.params.id;
  const userData = getUserData(userId); // 假设从数据库获取
  
  // 生成ETag
  const etag = crypto
    .createHash('md5')
    .update(JSON.stringify(userData))
    .digest('hex');
  
  // 检查客户端ETag
  if (req.headers['if-none-match'] === etag) {
    return res.status(304).end(); // 未修改
  }
  
  res.set('ETag', etag);
  res.set('Cache-Control', 'private, max-age=300'); // 5分钟
  res.json(userData);
});

// 4. 处理POST请求 - 禁止缓存
app.post('/api/submit', (req, res) => {
  res.set('Cache-Control', 'no-store');
  // 处理业务逻辑...
  res.json({ success: true });
});

app.listen(3000);

Nginx 配置示例

# 静态资源(版本化)- 长期缓存
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
    root /var/www/html;
    expires 1y;
    add_header Cache-Control "public, immutable";
    
    # 开启ETag
    etag on;
}

# HTML文件 - 不缓存
location ~* \.html$ {
    root /var/www/html;
    expires -1;
    add_header Cache-Control "no-cache, no-store, must-revalidate";
    add_header Pragma "no-cache";
    add_header Expires "0";
}

# API代理
location /api/ {
    proxy_pass http://backend;
    proxy_cache_valid 200 5m;  # 缓存5分钟
    proxy_cache_key "$scheme$request_method$host$request_uri";
    
    # 根据请求类型设置不同缓存
    if ($request_method = POST) {
        proxy_cache_valid any 0;  # POST不缓存
    }
}

Apache .htaccess 配置

# 静态资源长期缓存
<FilesMatch "\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$">
    Header set Cache-Control "public, max-age=31536000, immutable"
</FilesMatch>

# HTML文件不缓存
<FilesMatch "\.html$">
    Header set Cache-Control "no-cache, no-store, must-revalidate"
    Header set Pragma "no-cache"
    Header set Expires "0"
</FilesMatch>

# API端点
<If "%{REQUEST_URI} =~ m#^/api/#">
    Header set Cache-Control "private, max-age=300"
</If>

常见缓存难题及解决方案

难题1:缓存雪崩

问题:大量缓存同时过期,导致请求瞬间打到服务器。

解决方案

// 1. 错开过期时间
function getRandomTTL(baseTTL = 3600, variance = 600) {
  return baseTTL + Math.floor(Math.random() * variance);
}

// 2. 热点数据永不过期 + 后台更新
const热点数据 = new Map();

async function getHotData(key) {
  if (热点数据.has(key)) {
    return 热点数据.get(key);
  }
  
  // 异步更新,不阻塞读取
  updateHotData(key).catch(console.error);
  
  // 返回旧数据或默认值
  return 热点数据.get(key) || null;
}

// 3. 多级缓存
async function getWithMultiCache(key) {
  // L1: 本地内存缓存(秒级)
  const local = localCache.get(key);
  if (local) return local;
  
  // L2: Redis缓存(分钟级)
  const redisData = await redis.get(key);
  if (redisData) {
    localCache.set(key, redisData, 5); // 本地缓存5秒
    return redisData;
  }
  
  // L3: 数据库
  const dbData = await db.query(key);
  await redis.setex(key, 300, dbData); // Redis缓存5分钟
  localCache.set(key, dbData, 5);
  return dbData;
}

难题2:缓存穿透

问题:查询不存在的数据,导致缓存无效,请求直接打到数据库。

解决方案

// 1. 布隆过滤器(Bloom Filter)
const BloomFilter = require('bloom-filter');

const filter = BloomFilter.create(100000, 0.01); // 10万条目,1%误判率

function queryData(id) {
  // 先检查布隆过滤器
  if (!filter.contains(id)) {
    // 确定不存在,返回空
    return null;
  }
  
  // 可能存在,继续查缓存和数据库
  return getFromCacheOrDB(id);
}

// 2. 缓存空值
async function queryWithNullCache(id) {
  const cacheKey = `data:${id}`;
  const cached = await redis.get(cacheKey);
  
  if (cached !== null) {
    return cached === 'NULL' ? null : JSON.parse(cached);
  }
  
  const data = await db.query('SELECT * FROM table WHERE id = ?', [id]);
  
  if (!data) {
    // 缓存空值,设置较短TTL
    await redis.setex(cacheKey, 60, 'NULL');
    return null;
  }
  
  await redis.setex(cacheKey, 300, JSON.stringify(data));
  return data;
}

难题3:缓存击穿

问题:热点key过期瞬间,大量请求同时查询数据库。

解决方案

// 1. 互斥锁(Mutex)
async function getHotKeyWithLock(key) {
  const lockKey = `lock:${key}`;
  const cacheKey = `data:${key}`;
  
  // 尝试获取缓存
  const cached = await redis.get(cacheKey);
  if (cached) return JSON.parse(cached);
  
  // 尝试获取分布式锁
  const lock = await redis.set(lockKey, '1', 'NX', 'EX', 10);
  
  if (lock) {
    try {
      // 只有拿到锁的线程查询数据库
      const data = await db.query(key);
      await redis.setex(cacheKey, 300, JSON.stringify(data));
      return data;
    } finally {
      await redis.del(lockKey);
    }
  } else {
    // 未拿到锁,等待后重试
    await sleep(50);
    return getHotKeyWithLock(key);
  }
}

// 2. 逻辑过期
async function getWithLogicalExpiration(key) {
  const cacheKey = `data:${key}`;
  const result = await redis.get(cacheKey);
  
  if (!result) {
    return await updateCache(key);
  }
  
  const { data, expireTime } = JSON.parse(result);
  
  if (Date.now() > expireTime) {
    // 异步更新,返回旧数据
    updateCache(key).catch(console.error);
  }
  
  return data;
}

async function updateCache(key) {
  const data = await db.query(key);
  const expireTime = Date.now() + 300000; // 5分钟后过期
  await redis.setex(key, 600, JSON.stringify({ data, expireTime }));
  return data;
}

难题4:缓存与数据库一致性

问题:更新数据库后,缓存未更新导致数据不一致。

解决方案

// 1. Cache Aside Pattern(旁路缓存)
// 读取:先缓存 → 后数据库
// 更新:先数据库 → 后缓存

async function updateData(id, newData) {
  // 方案A:先更新数据库,再删除缓存(推荐)
  await db.update(id, newData);
  await redis.del(`data:${id}`);
  
  // 方案B:延迟双删(解决主从延迟)
  await db.update(id, newData);
  await redis.del(`data:${id}`);
  setTimeout(async () => {
    await redis.del(`data:${id}`);
  }, 500);
}

// 2. Write Through Pattern(写穿)
async function writeThrough(id, data) {
  // 同时更新缓存和数据库
  await Promise.all([
    db.update(id, data),
    redis.setex(`data:${id}`, 300, JSON.stringify(data))
  ]);
}

// 3. 基于Binlog的最终一致性
// 使用Canal等工具监听数据库变更,自动更新缓存
const canal = require('canal-node');

canal.on('UPDATE', (event) => {
  const { table, data } = event;
  const key = `data:${data.id}`;
  redis.del(key); // 或重新查询更新
});

难题5:移动端缓存控制

问题:移动端网络环境复杂,缓存策略难以统一。

解决方案

// 1. 按网络类型调整缓存
function getCacheStrategy(networkType) {
  switch (networkType) {
    case '4G':
      return 'max-age=300'; // 5分钟
    case '3G':
      return 'max-age=600'; // 10分钟
    case '2G':
      return 'max-age=1800'; // 30分钟
    case 'wifi':
      return 'max-age=60'; // 1分钟
    default:
      return 'no-cache';
  }
}

// 2. 按设备存储调整
async function getDeviceAwareCache(key, deviceInfo) {
  const { storage, platform } = deviceInfo;
  
  if (storage < 100) {
    // 存储空间不足,不缓存或短缓存
    return await fetchFromServer(key);
  }
  
  if (platform === 'iOS') {
    // iOS可能有更严格的缓存限制
    return await getWithShortTTL(key);
  }
  
  return await getWithStandardCache(key);
}

高级缓存技术

1. Service Worker缓存

// service-worker.js
const CACHE_NAME = 'app-v1';
const urlsToCache = [
  '/',
  '/styles/main.css',
  '/scripts/app.js',
  '/images/logo.png'
];

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;
        });
      })
  );
});

2. CDN缓存策略

# Cloudflare Workers 示例
addEventListener('fetch', event => {
  event.respondWith(handleRequest(event.request));
});

async function handleRequest(request) {
  const url = new URL(request.url);
  
  // 根据路径设置不同缓存
  if (url.pathname.startsWith('/api/')) {
    // API响应 - 短缓存
    const response = await fetch(request);
    response.headers.set('Cache-Control', 'private, max-age=60');
    return response;
  } else if (url.pathname.match(/\.(js|css|png)$/)) {
    // 静态资源 - 长缓存
    const response = await fetch(request);
    response.headers.set('Cache-Control', 'public, max-age=31536000, immutable');
    return response;
  }
  
  return fetch(request);
}

缓存监控与调试

1. 浏览器开发者工具

  • Network面板:查看请求的Size列(来自缓存/来自服务器)
  • Application面板:查看Cache Storage、Local Storage等
  • Coverage面板:分析未使用的缓存资源

2. 服务器日志分析

// Node.js中间件记录缓存命中率
function cacheMetricsMiddleware(req, res, next) {
  const originalSetHeader = res.setHeader;
  
  res.setHeader = function(name, value) {
    if (name.toLowerCase() === 'x-cache-status') {
      // 记录缓存状态
      console.log(`[${new Date().toISOString()}] ${req.url} - ${value}`);
    }
    return originalSetHeader.call(this, name, value);
  };
  
  next();
}

// 使用
app.use(cacheMetricsMiddleware);

3. 缓存命中率监控

// Redis监控脚本
async function monitorCacheHitRate() {
  const stats = {
    hits: 0,
    misses: 0,
    get total() { return this.hits + this.misses; },
    get rate() { return this.total > 0 ? (this.hits / this.total * 100).toFixed(2) + '%' : '0%'; }
  };
  
  // 模拟监控
  setInterval(() => {
    console.log(`缓存命中率: ${stats.rate} (${stats.hits}/${stats.total})`);
  }, 60000);
  
  return stats;
}

总结与建议

核心原则

  1. 分层缓存:浏览器 → CDN → 应用服务器 → 数据库
  2. 差异化策略:根据资源类型、用户、场景制定不同策略
  3. 监控驱动:持续监控缓存命中率,优化策略
  4. 一致性优先:在性能与一致性之间找到平衡点

推荐工具

  • Redis:分布式缓存
  • Memcached:内存缓存
  • Varnish:HTTP缓存加速器
  • Canal:数据库Binlog监听
  • Webpack:资源版本化

检查清单

  • [ ] 静态资源是否使用版本化文件名?
  • [ ] HTML文档是否设置no-cache
  • [ ] API响应是否根据业务场景设置合理TTL?
  • [ ] 是否实现缓存穿透/击穿/雪崩防护?
  • [ ] 是否有缓存命中率监控?
  • [ ] 是否测试过不同网络环境下的缓存行为?

通过合理运用HTTP缓存策略,可以显著提升网站性能,解决常见的缓存难题。记住,没有银弹,最佳策略需要根据具体业务场景持续调整和优化。