← 中文资源

简体中文版本

缓存预热

缓存预热在流量到来之前预填充关键条目——部署后、区域切换后、扩容后——让首批用户不必支付冷缓存延迟。

阅读英文原文

概念定义

冷缓存以源站延迟服务每个请求。预热把这笔成本搬到可控时刻——部署前、切换演练中——预取流量最先会请求的条目。

工作机制

按分析或访问日志的请求频次排序候选,再走与生产完全相同的读路径预取,保证条目形态、编码与 vary 变体匹配。时机在缓存失效之后、流量爬升之前;每个冷启动触发器都要重来一遍。

使用场景

部署钩子刷新 CDN 或应用缓存、新缓存集群上线、闲置后接管的主动被动切换、增加冷节点的自动扩容。把预热当作部署完成定义的一部分。

常见误区

“缓存自己很快就会热”忽略了窗口期首批用户吸收了全部 miss;预热全部数据不如按流量预热前 N——长尾根本没人请求。按字母序而非流量排序会错过真正的热集;用与生产不同的代码路径预热会填进真实请求命不中的条目。预热本身也是一次源站负载事件——要限速加抖动;度量看事件后首 N 分钟命中率与同时段源站 QPS。

代码示例

import asyncio

import httpx
from httpx_cache import CacheControlTransport

client = httpx.AsyncClient(
    transport=CacheControlTransport(
        cache=httpx_cache.FileCache(cache_dir='.http_cache'),
        cacheable_methods=('GET',),
        cacheable_status_codes=(200, 301, 404),
    )
)


async def fetch_product(product_id: int) -> dict | None:
    try:
        resp = await client.get(f'https://api.example.com/products/{product_id}')
        resp.raise_for_status()
        return resp.json()
    except httpx.HTTPStatusError:
        return None


async def warm_cache(ids: list[int]) -> int:
    results = await asyncio.gather(
        *(fetch_product(pid) for pid in ids),
        return_exceptions=True,
    )
    return sum(1 for r in results if not isinstance(r, Exception))


async def main() -> None:
    await warm_cache(list(range(10)))

    # 断网场景:文件缓存仍可回放此前成功的响应
    product = await fetch_product(3)
    print('served from cache offline:', product is not None)
    await client.aclose()


asyncio.run(main())
// Cache-busting URL builder for static assets.
export function withCacheBusting(url, version, param = "v") {
  if (typeof url !== "string" || url.length === 0) {
    throw new TypeError("url must be a non-empty string");
  }
  if (!/^[A-Za-z_][A-Za-z0-9_-]*$/.test(param)) {
    throw new TypeError("param must be a valid query parameter name");
  }
  const base = "https://cache-busting.invalid";
  const parsed = new URL(url, base);
  if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
    throw new TypeError("only http(s) URLs are supported");
  }
  parsed.searchParams.set(param, String(version));
  if (parsed.origin === base) {
    return parsed.pathname + parsed.search + parsed.hash;
  }
  return parsed.toString();
}

export function hashAssetContent(content) {
  let hash = 0x811c9dc5;
  const text = String(content);
  for (let index = 0; index < text.length; index += 1) {
    hash ^= text.charCodeAt(index);
    hash = Math.imul(hash, 0x01000193);
  }
  return (hash >>> 0).toString(36);
}