← 中文资源

简体中文版本

惊群效应 — 缓存过期时的缓存击穿

通过单飞(请求合并)、带抖动的 TTL 与 stale-while-revalidate,修复缓存过期时的惊群效应,避免压垮源站。

阅读英文原文

症状

错误“惊群效应 — 缓存过期时的缓存击穿”指向一个明确的失效模式。下面依次给出症状、定位该原因的检查命令、根本原因、修复方案,以及证明修复有效的验证步骤。

可观察到的症状包括:

  • 每当热门缓存项过期,源站即出现峰值
  • 延迟在规律时间间隔出现突发飙升
  • 所有实例同时重算同一个值

诊断

先在隔离环境中复现故障,再按顺序执行以下检查,逐步缩小范围。

步骤 1

redis-cli INFO stats | grep expired_keys; # correlate with origin latency

过期量突发且与源站延迟峰值对齐,即提示惊群。

步骤 2

grep -c "SELECT.*popular" app.log

大量相同的并发查询即确认缺少合并。

常见诱因(按出现频率排序):

  • 热门键过期时,所有请求同时重算
  • 所有实例使用相同 TTL,导致过期时刻一致
  • 没有请求合并,N 个并发未命中触发 N 次源站调用

根因

当热点条目过期时,所有并发请求都看到未命中,各自独立重算。若无合并或错开的过期,源站会收到远超稳态负载的同步突发。

修复

用单飞合并未命中,使每个键仅一个请求重算;为 TTL 加抖动以分散过期;并在后台刷新时继续提供过期内容。

注意:提供过期数据需契合用例;应记录每个键的容忍窗口。

import { setTimeout as sleep } from 'node:timers/promises';

const inflight = new Map(); // single-flight per key

async function getWithCoalescing(key, compute, ttlMs = 60000) {
  const cached = await cache.get(key);
  if (cached) return cached;
  if (inflight.has(key)) return inflight.get(key);

  const p = (async () => {
    const value = await compute();
    const jitter = Math.floor(Math.random() * 0.1 * ttlMs); // spread expiries
    await cache.set(key, value, ttlMs + jitter);
    return value;
  })().finally(() => inflight.delete(key));

  inflight.set(key, p);
  return p;
}

验证

在负载下触发过期,确认仅一次源站重算,其余短暂等待;验证源站 QPS 保持在预算内。

修复后补充一条回归测试,复现原始症状,防止问题再次出现。

这类故障通常是环境问题而非纯逻辑错误,因此在缺陷记录中应写明完整命令、环境变量与工作目录。请用最小的脚本复现,待最小用例清楚后再扩大范围。为这类故障准备一份简短的排查清单,能显著减少下次排障时间。