一个超热点的商品缓存在过期那一瞬间,成千上万的请求同时涌向数据库去重建它,把 DB 瞬间打垮,我对着缓存击穿这个热点 key 过期引发的惊群效应排查了大半天的复盘

一个让我对缓存不是万能挡箭牌彻底清醒的架构坑,诡异在平时缓存把 DB 保护得很好负载很低,可某个特定瞬间 DB 会突然遭受排山倒海的并发冲击而崩溃——而这瞬间恰是某个最热门的缓存刚好过期那一刻。系统用 Redis 缓存商品详情设 1 小时过期,DB 监控却毫无规律间歇性出现瞬时高负载连接打满。代码是标准的读缓存没有就查 DB 回填。隐患在并发:一个爆款商品每秒上万请求,缓存过期那一瞬间这上万并发几乎同时发现缓存 null、同时涌去查 DB 重建缓存,DB 瞬间承受上万倍并发被打垮。深究才看清这叫缓存击穿(惊群):热点 key 过期瞬间大量并发同时穿透重建打垮 DB,根源是缓存过期瞬间的并发真空。要和另两个区分:缓存雪崩是大量 key 同时过期、缓存穿透是查根本不存在的数据缓存帮不上忙。这篇从故障现场、击穿/穿透/雪崩真相、正解(击穿用互斥锁只让一个请求重建其余等或逻辑过期后台刷新、穿透用缓存空值+布隆过滤器、雪崩用过期时间加随机抖动+多级缓存+预热、对 DB 加限流熔断兜底)、用缓存其他坑(一致性更 DB 后删缓存、脏数据、大热 key、序列化、没 TTL、把缓存当数据库)、缓存三大问题对照表、缓存是用代价换性能的权衡、用缓存决策图与铁律,到附上一个集防击穿穿透雪崩于一体的安全缓存读取封装。核心领悟:建立清晰的问题分类透过相似表象精确辨别成因(击穿单key雪崩多key穿透查不存在)对症下药;用空间换时间用冗余换性能的优化本质是引入数据多副本必然带来一致性难题要为全套代价做设计;任何防护机制自己也会失效,健壮系统要为每道防线的失效准备下一道防线(缓存击穿→互斥锁→DB限流),不迷信单一防护、层层设防;把安全用法沉淀成统一封装让正确成为默认而非靠个人纪律。

一个超热点的商品缓存在过期那一瞬间,成千上万的请求同时涌向数据库去重建它,把 DB 瞬间打垮,我对着缓存击穿这个热点 key 过期引发的惊群效应排查了大半天的复盘

这是一个让我对"缓存不是万能挡箭牌"彻底清醒的架构坑。它的诡异之处在于:平时缓存把数据库保护得好好的,DB 负载很低;可就在某个特定的瞬间,DB 会突然遭受一波排山倒海的并发冲击而崩溃——而这个瞬间,恰恰是某个最热门的缓存刚好过期的那一刻。

事情起于一次诡异的 DB 告警。我的系统用 Redis 缓存商品详情,给缓存设了过期时间(比如 1 小时),平时 DB 几乎没什么压力。可监控显示,DB 会毫无规律地、间歇性地出现一波瞬时高负载、甚至连接被打满。我把缓存读取的代码拎出来,逻辑很标准:

// 读商品详情: 缓存读不到就查DB并回填(有问题的版本)
public Product getProduct(Long id) {
    String key = "product:" + id;
    Product p = redis.get(key);          // 1. 先读缓存
    if (p == null) {                     // 2. 缓存没有(未命中或已过期)
        p = db.queryProduct(id);         // 3. 查数据库
        redis.set(key, p, 1, HOURS);     // 4. 回填缓存, 设1小时过期
    }
    return p;
}

这段"读缓存→没有就查 DB 回填"的标准写法,在大多数情况下没问题。可它有一个致命的并发隐患:设想一个超级热点的商品(比如正在大促的爆款),每秒有上万次请求访问它。当它的缓存过期的那一瞬间,这上万个并发请求几乎同时执行到第 2 步,同时发现缓存没了,于是同时涌向第 3 步——成千上万个请求在同一瞬间一起去查数据库、一起去重建这个缓存!数据库在这一瞬间遭受了平时上万倍的并发冲击,瞬间被打垮。我盯着监控上那一根根与"热点缓存过期"时刻吻合的 DB 负载尖刺,意识到问题的本质:缓存过期的那一刻,它对数据库的"保护"出现了一个短暂但致命的"真空期",而海量并发恰好涌入了这个真空。

第一件事:看清真相——热点 key 过期瞬间,海量并发同时穿透重建,这叫"缓存击穿"

我去深入理解了"缓存击穿(cache breakdown / thundering herd 惊群)"以及它和"缓存穿透""缓存雪崩"的区别,才彻底看清这场 DB 崩溃的成因——一个热点 key 在过期的瞬间,大量并发请求同时发现它失效、同时去查数据库重建缓存,这一波瞬时并发直击 DB、把它打垮,这就是"缓存击穿";它的根源是"缓存过期瞬间的并发真空"

缓存击穿 / 穿透 / 雪崩 的真相

# 1. 缓存击穿(本文, cache breakdown / 惊群 thundering herd):
#    - 场景: 某个【热点key】(高并发访问)在【过期的瞬间】
#    - 大量并发请求同时发现它没了 → 同时去查DB、同时重建缓存
#    - → DB 在这一瞬间被【上万倍并发】直击, 被打垮
#    - 关键词: 【单个热点key】、【过期瞬间】、【并发重建】

# 2. 缓存雪崩(cache avalanche):
#    - 场景: 【大量key】在【同一时刻】集体过期(比如都设了相同过期时间)
#    - → 大批请求同时穿透到DB → DB被海量请求压垮
#    - 区别: 击穿是"单个热点key", 雪崩是"大量key同时失效"

# 3. 缓存穿透(cache penetration):
#    - 场景: 查询一个【根本不存在】的数据(缓存、DB里都没有)
#    - → 缓存永远不命中(因为没数据可缓存) → 每次都打到DB
#    - 若被恶意大量请求不存在的key → DB被压垮
#    - 区别: 穿透是"查不存在的数据", 缓存帮不上忙

# 4. 本文"击穿"的发生链(以爆款商品为例):
#    a) 商品缓存正常, 上万QPS的请求都命中缓存, DB很轻松
#    b) 到了过期时间, 缓存失效的【那一瞬间】
#    c) 这一瞬间涌入的成千上万个并发请求, 都发现缓存==null
#    d) 它们【全部】执行"查DB + 回填缓存"
#    e) → DB瞬间承受上万并发查询 → 连接打满/被压垮
#    f) (而且它们重复查了同样的数据、重复回填, 极大浪费)

# 5. 共同本质: 缓存是DB的"保护层", 但这个保护层会在某些时刻"破洞"
#    (热点key过期=击穿、大量key同时过期=雪崩、查不存在数据=穿透);
#    一旦破洞, 平时被缓存挡住的流量就会直击DB, 可能压垮它。

# 核心: 缓存击穿=热点key过期瞬间海量并发同时查DB重建打垮DB; 还有雪崩(大量key同时过期)、
#   穿透(查不存在的数据); 本质都是"缓存保护层破洞, 流量直击DB"。

真相大白,我恍然大悟。原来这叫"缓存击穿(惊群效应)":一个热点 key 在过期的瞬间,大量并发请求同时发现它失效、同时去查数据库重建缓存,这一波瞬时并发直击 DB、把它打垮;根源是"缓存过期瞬间的并发真空"。它和另外两个"缓存灾难"要区分开:缓存雪崩是"大量 key 在同一时刻集体过期"(比如都设了相同过期时间),大批请求同时穿透压垮 DB;缓存穿透是"查询根本不存在的数据"(缓存、DB 都没有),缓存永远不命中、每次都打 DB(被恶意大量请求不存在的 key 时更危险)。我这次的"击穿"发生链是:爆款商品缓存正常时上万 QPS 都命中缓存、DB 很轻松;到过期那一瞬间,涌入的成千上万并发请求都发现缓存为 null,全部去"查 DB + 回填",DB 瞬间承受上万并发查询被压垮(而且它们重复查同样的数据、重复回填,极大浪费)。共同本质是:缓存是 DB 的"保护层",但这个保护层会在某些时刻"破洞"(热点 key 过期=击穿、大量 key 同时过期=雪崩、查不存在数据=穿透),一旦破洞,平时被挡住的流量就直击 DB。

第二件事:正解——用互斥锁/单飞让只有一个请求重建,或热点永不过期

搞懂了原理,正解就清晰了:缓存击穿的核心是"让过期瞬间只有一个请求去重建、其他请求等结果",用互斥锁(单飞)或逻辑过期;并配合处理穿透(空值/布隆)和雪崩(过期加抖动)

// ====== 正解一(击穿核心): 互斥锁, 只让一个请求重建 ======
public Product getProduct(Long id) {
    String key = "product:" + id;
    Product p = redis.get(key);
    if (p != null) return p;          // 命中直接返回

    // ★ 没命中: 用分布式锁, 只让"第一个"请求去重建, 其他请求等
    String lockKey = "lock:" + key;
    if (redis.setIfAbsent(lockKey, "1", 10, SECONDS)) {  // 抢到锁
        try {
            p = redis.get(key);       // 双重检查(可能别人已重建好)
            if (p == null) {
                p = db.queryProduct(id);
                redis.set(key, p, 1, HOURS);   // 只有这一个请求查DB、回填
            }
        } finally {
            redis.del(lockKey);       // 释放锁
        }
    } else {
        Thread.sleep(50);             // 没抢到锁: 稍等, 然后重试读缓存(别人在建)
        return getProduct(id);
    }
    return p;
}
// → 过期瞬间, 上万请求里只有1个真的去查DB重建, 其余都等那1个建好后读缓存
//   → DB只承受1次查询, 而不是上万次! 击穿被堵住。

// ====== 正解二(击穿): 逻辑过期, 不真过期, 后台异步刷新 ======
// 缓存value里存一个"逻辑过期时间", key本身永不过期(或很长);
// 读到逻辑过期的, 返回旧值的同时, 异步起一个线程去刷新缓存。
// → 永远有值可返回(不阻塞)、只有一个后台任务刷新。适合热点。

// ====== 正解三(穿透): 缓存空值 + 布隆过滤器 ======
// 缓存空值: 查DB也没有时, 把"空"也缓存一小段时间(如5分钟),
//   下次查这个不存在的key直接返回空, 不再打DB。
if (p == null && dbHasNoData) redis.set(key, EMPTY, 5, MINUTES);
// 布隆过滤器: 用它快速判断"这个key一定不存在", 不存在直接拦截, 不查DB。

// ====== 正解四(雪崩): 过期时间加随机抖动, 别让大量key同时过期 ======
int ttl = 3600 + new Random().nextInt(600);   // ★ 基础1小时 + 0~10分钟随机
redis.set(key, p, ttl, SECONDS);
// → 让key的过期时间分散开, 避免大量key在同一秒集体失效(防雪崩)
// 配合: 多级缓存、缓存预热、对DB加熔断限流兜底。

// 核心: 击穿用互斥锁(只让1个请求重建其余等)或逻辑过期+后台刷新; 穿透用缓存空值+布隆过滤器;
//   雪崩用过期时间加随机抖动+多级缓存+预热; 并对DB加限流熔断作最后兜底。

修复的核心,是"让过期瞬间只有一个请求去重建,并分别处理穿透和雪崩"正解一(击穿核心):互斥锁(单飞)——没命中时用分布式锁(setIfAbsent),只让抢到锁的那"第一个"请求去查 DB 重建、其他请求稍等后重试读缓存;过期瞬间上万请求里只有 1 个真去查 DB,DB 只承受 1 次查询而非上万次正解二(击穿):逻辑过期——value 里存逻辑过期时间、key 本身永不过期,读到逻辑过期的返回旧值同时异步刷新,永远有值可返回、只有一个后台任务刷新正解三(穿透):缓存空值 + 布隆过滤器——查不到也把"空"缓存一小段时间,布隆过滤器快速拦截一定不存在的 key正解四(雪崩):过期时间加随机抖动——3600 + random(600) 让 key 过期时间分散开,避免大量 key 同一秒集体失效;配合多级缓存、缓存预热、对 DB 加熔断限流归根结底:击穿用互斥锁或逻辑过期+后台刷新;穿透用缓存空值+布隆;雪崩用过期加抖动+多级缓存+预热;并对 DB 加限流熔断作最后兜底。

第三件事:用缓存的其他常见坑

排查后我把用缓存相关的其他常见坑也系统梳理了一遍。

用缓存的其他常见坑

# 1. 击穿(本文): 热点key过期瞬间并发重建。→ 互斥锁/逻辑过期。

# 2. 雪崩: 大量key同时过期。→ 过期加随机抖动。

# 3. 穿透: 查不存在的数据。→ 缓存空值/布隆过滤器。

# 4. 缓存与DB一致性: 更新数据时, 缓存和DB怎么保持一致?
#    → 常用"先更新DB, 再删除缓存"(Cache Aside); 仍有边界case, 要权衡。

# 5. 缓存了脏数据/老数据: 更新DB后忘了删缓存 → 一直读到老数据。

# 6. 大key/热key: 单个value巨大、或单key访问极热, 拖慢/压垮某个redis节点。
#    → 拆分大key、热key做本地缓存/多副本。

# 7. 缓存数据结构选错/序列化开销: value序列化反序列化慢、占内存。

# 8. 没有过期时间: 缓存只增不减, 内存爆; 或永远是老数据。→ 合理设TTL。

# 9. 把缓存当数据库: 强依赖缓存(缓存挂了系统就挂)。→ 缓存应可降级到DB。

# 共同根源: 缓存是用"一定的不一致/复杂性"换"性能"的权衡; 它引入了
#   "数据两副本(缓存+DB)、过期、并发、一致性"等一系列新问题, 用不好反而成为故障源。

# 核心: 缓存是性能利器但有代价——要处理击穿/穿透/雪崩、一致性、大热key、过期、可降级;
#   别把缓存当万能挡箭牌或当数据库, 要为它的各种失效模式做好设计。

排查让我把用缓存的其他坑也梳理清了。一、击穿(本文)。二、雪崩(大量 key 同时过期,过期加抖动)。三、穿透(查不存在的数据,空值/布隆)。四、缓存与 DB 一致性(常用先更 DB 再删缓存 Cache Aside)。五、缓存脏数据(更新 DB 忘删缓存)。六、大 key/热 key(拖垮单节点,拆分/本地缓存)。七、序列化开销八、没有过期时间(内存爆/老数据)。九、把缓存当数据库(强依赖缓存,应可降级到 DB)。它们的共同根源是:缓存是用"一定的不一致/复杂性"换"性能"的权衡;它引入了"数据两副本、过期、并发、一致性"等一系列新问题,用不好反而成为故障源核心是:缓存是性能利器但有代价——要处理击穿/穿透/雪崩、一致性、大热 key、过期、可降级;别把缓存当万能挡箭牌或当数据库下面这张图,是这次缓存击穿的成因与解法:

第四件事:缓存三大问题对照表

这次踩坑后,我把缓存击穿、穿透、雪崩这三个最容易混淆的问题整理成一张表。

问题 触发场景 特点 解法
击穿 单个热点key过期瞬间 单key, 并发重建 互斥锁/逻辑过期/热点永不过期
雪崩 大量key同时过期 多key, 集体失效 过期加随机抖动/多级缓存/预热
穿透 查根本不存在的数据 缓存帮不上忙 缓存空值/布隆过滤器/参数校验

这张表把三个常被混淆的缓存问题钉清了。核心区别是:击穿是"单个热点 key 在过期瞬间被并发重建"(关键是单 key + 并发)、雪崩是"大量 key 同时失效"(关键是大量 key + 集体)、穿透是"查不存在的数据"(关键是缓存根本帮不上忙);三者的触发场景和解法各不相同它给我的最大启发是:这三个问题虽然现象类似(都是"流量直击 DB"),但成因截然不同;能精确地区分它们,是对症下药的前提——你得先搞清"到底是哪种破洞"(单热点过期?大量同时过期?查不存在的?),才能用对应的解法去补这其实反映了一个普遍的排查/解决问题的方法论:当几个问题"表象相似"时,绝不能笼统地一概而论、用一个解法套所有;而要透过相似的表象,去精确辨别它们各自的成因;因为成因不同,解法就不同——把击穿当雪崩治(给所有 key 加抖动),解决不了热点 key 的并发重建问题这让我形成一个习惯:面对一类问题,要去建立"清晰的分类"——搞清楚它有哪几种不同的子类型、各自的特征和成因是什么;有了这个分类框架,遇到具体问题时就能快速"对号入座"、精准施治,而不是凭模糊的印象乱试建立清晰的问题分类、透过相似表象精确辨别成因——是这个缓存坑教给我的高效解决问题的方法。

第五件事:缓存的本质是一种"权衡"

这次也让我重新认识了缓存的本质——它从来不是"免费的午餐"。

缓存带来的好处 缓存带来的代价
大幅提升读性能 引入数据一致性问题(两副本)
降低数据库压力 过期瞬间可能击穿/雪崩
提高系统吞吐 增加架构复杂度(多一层)
降低响应延迟 额外的内存/运维成本
缓存本身可能成为新的故障点/单点

这张表道出了缓存的"两面性"。核心是:缓存用"引入一致性问题、过期失效风险、架构复杂度、额外成本、新的故障点"这些代价,换取了"性能、吞吐、低延迟";它不是一个"只有好处"的银弹,而是一个需要认真权衡、并妥善管理其代价的工程选择它给我的深刻启发是:几乎所有"用空间换时间、用冗余换性能"的优化手段(缓存、索引、副本、预计算、CDN),本质上都是引入了"同一份数据的多个副本/表示";而"多副本"必然带来一个永恒的难题——一致性(如何让这些副本保持同步、不出现矛盾);以及一系列衍生问题(副本失效、副本与源的差异、副本本身的开销和风险)这让我对"加缓存"这类决策更加审慎:引入缓存(或任何"副本式"优化)之前,不能只看到它"让读变快了"的好处,而要同时想清楚它带来的全套代价:数据怎么保持一致?它失效/出问题时系统怎么办?它值不值得引入这层复杂度?;很多系统的复杂性和故障,都源于"为了性能加了缓存,却没有妥善管理缓存带来的那些代价"清醒地认识"缓存是用代价换性能的权衡"、并为它的全套代价做好设计——是这个击穿坑,在技术之上,教给我的关于"如何做优化决策"的成熟思考。

第六件事:用缓存时,我现在的判断习惯

现在每当我给一个数据加缓存,我都会按这张图先想清楚:

这张图的精髓,是"加缓存前,把击穿/雪崩/穿透/一致性/兜底逐一想清楚"超热点 key 防击穿(互斥锁/逻辑过期);大量 key 一起过期防雪崩(加随机抖动);会被查不存在的 key 防穿透(空值/布隆/校验);数据会更新就想清一致性(更 DB 后删缓存),并对 DB 加限流熔断兜底这套习惯,让我用缓存时,从"读不到就查 DB 回填一把梭"变成了"先想清这个缓存的各种失效模式怎么防"——核心始终是:缓存会破洞(击穿/雪崩/穿透),加缓存就要为它的失效模式做好设计,并给 DB 留兜底。

我立下的几条规矩

这场"缓存击穿打垮 DB"的事故,换来了我做架构时,刻进骨子里的几条铁律:

  1. 热点 key 过期瞬间会被并发击穿。读不到就查 DB 回填的写法藏着这个雷。
  2. 击穿用互斥锁,只让一个请求重建。或逻辑过期/热点永不过期。
  3. 雪崩用过期时间加随机抖动。别让大量 key 同一秒集体失效。
  4. 穿透用缓存空值 + 布隆过滤器。拦住查不存在数据的请求。
  5. 缓存与 DB 一致性要想清。更新 DB 后删缓存,别留脏数据。
  6. 给 DB 加限流熔断兜底。缓存破洞时保护 DB 最后一道防线。
  7. 缓存是权衡不是银弹。用代价换性能,要管好它的全套代价。

附:一个集防击穿/穿透/雪崩于一体的缓存读取封装

这次踩坑后,我把"防击穿 + 防穿透 + 防雪崩"的逻辑封装成了一个统一的缓存读取方法,所有"读缓存→回源"都走它,从一开始就内建这三道防护:

public class SafeCache {
    private final RedisClient redis;
    private static final String EMPTY = "__NULL__";   // 空值标记(防穿透)

    /**
     * 安全的"读缓存, 没有则回源"——内建防击穿(互斥锁)、防穿透(空值)、防雪崩(抖动TTL)
     * @param loader 回源函数(查DB)
     */
    public  T get(String key, long baseTtlSec, Supplier loader, Class type) {
        String cached = redis.get(key);
        if (EMPTY.equals(cached)) return null;          // 防穿透: 命中空值标记, 直接返回null
        if (cached != null) return deserialize(cached, type);  // 正常命中

        // 未命中: 防击穿——用互斥锁, 只让一个线程回源重建
        String lock = "lock:" + key;
        if (redis.setIfAbsent(lock, "1", 10, SECONDS)) {
            try {
                String again = redis.get(key);           // 双重检查
                if (again != null) return EMPTY.equals(again) ? null : deserialize(again, type);
                T data = loader.get();                    // 只有这一个线程查DB
                if (data == null) {
                    redis.set(key, EMPTY, 60, SECONDS);   // 防穿透: 缓存空值(短TTL)
                    return null;
                }
                // 防雪崩: 基础TTL + 随机抖动, 避免大量key同时过期
                long ttl = baseTtlSec + ThreadLocalRandom.current().nextLong(baseTtlSec / 5);
                redis.set(key, serialize(data), ttl, SECONDS);
                return data;
            } finally {
                redis.del(lock);
            }
        } else {
            sleep(50);                                    // 没抢到锁, 稍等重试(别人在重建)
            return get(key, baseTtlSec, loader, type);
        }
    }
}

// 用起来极简, 三道防护自动生效:
// Product p = safeCache.get("product:" + id, 3600, () -> db.queryProduct(id), Product.class);

// 核心: 把防击穿(互斥锁)+防穿透(缓存空值)+防雪崩(抖动TTL)封装进一个统一的缓存读取方法;
//   所有回源都走它, 让"安全的缓存使用"成为默认, 而不是每处自己拼、容易漏。

这个安全缓存封装,是我这次踩坑后最有价值的工程沉淀。它把缓存的三大经典灾难的防护——防击穿(互斥锁让单线程重建)、防穿透(缓存空值标记)、防雪崩(TTL 加随机抖动)——全部内建进了一个统一的 get 方法;此后所有"读缓存、没有则回源查 DB"的地方都调用它,一行代码就同时拥有这三道防护,而不必每个地方自己手写、东漏一个西漏一个。它和我最初那个"裸读缓存、没有就查 DB 回填"的版本最大的不同,不在代码多寡,而在于一种设计立场的转变:最初版本只考虑了"缓存正常工作"的happy path,对各种失效模式毫无防备;而这个封装,从设计之初就把"缓存会被击穿、会被穿透、会雪崩"这些失效模式,当成必须默认防住的常态这正是我想分享的核心思想:对于"缓存使用"这种"每处都要写、又处处有同样的坑"的模式,最可靠的做法,是把"正确且安全的用法"沉淀成一个统一的封装,让"安全"成为调用它就自动拥有的默认能力,而不是依赖"每个开发者每次都记得手动加上三道防护"因为"防护"如果是"需要每次自觉去加"的,就一定会在某个角落被遗漏(就像我最初连一道都没加);只有把它做成"用这个封装就默认有",整个系统的缓存使用才真正安全可靠;这也是"用架构/封装来保证正确,而非靠个人纪律"这一原则,在缓存场景的又一次体现把缓存的安全用法沉淀成统一封装、让"防住击穿穿透雪崩"成为默认——这,是我用一次缓存击穿的事故,换来的、关于"如何系统性地用好缓存"的实用架构智慧。

写在最后

回头看,这场由"缓存击穿"引发的、DB 被瞬时并发打垮的事故,真正教给我的,远不止"加个互斥锁"这一个技巧。它让我对"防护手段本身也需要被防护",以及"系统的边界与极端时刻",有了一次深刻的体会。我栽跟头,根源是我把"缓存"当成了一个"一旦加上,数据库就高枕无忧"的、完美无缺的"保护罩"。我享受着平时缓存带来的高性能、低 DB 压力,却从没想过这个保护罩本身会不会失效、会在什么时候失效、失效的那一刻会发生什么。我只设计了"缓存正常工作时"的美好路径,却完全忽略了"缓存失效的那一瞬间"这个虽然短暂、却致命的边界时刻——而真实的灾难,恰恰就发生在这个被我忽略的边界上。这让我领悟到一个深刻的认知:任何一个"防护/优化机制"(缓存、限流、降级、连接池),它自己也有失效、被绕过、或在边界条件下出问题的时候;一个健壮的系统设计,不仅要让这些机制在"正常情况下"发挥作用,更要认真考虑"当这个机制本身失效时,会发生什么?有没有下一道防线接住?";"缓存击穿"的本质,正是"缓存这道防线在过期瞬间失效了,而后面没有第二道防线(如互斥锁、DB 限流)接住涌向 DB 的洪流"这其实就是"纵深防御"思想在另一个场景的体现:不要把系统的安危,寄托在任何单一的防护机制永不失效上;而要假设"每一道防线都可能在某个时刻、某个边界条件下失效",并为这种失效准备好下一道防线(缓存可能击穿 → 用互斥锁兜住并发重建 → 再用 DB 限流熔断兜住极端流量);真正的鲁棒,来自层层设防、来自对"万一这道防线破了怎么办"的反复追问不迷信任何单一防护、为每道防线的失效准备下一道防线——这,是我用一次缓存击穿的事故,换来的、关于架构、关于系统鲁棒性的、最朴素也最深刻的领悟。如果这篇复盘,能让你下次给热点数据加缓存时,多想一句"它过期的那一瞬间,会发生什么?",那我对着那一根根 DB 负载尖刺排查的这大半天,就值了。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

我训练的欺诈检测模型准确率高达 99%,我正得意,一看才发现它把所有交易都判成了正常、一笔欺诈都没抓到,我对着类别极度不平衡时 accuracy 完全失真这个坑排查了大半天的复盘

2026-6-2 11:32:16

技术教程

我写了个 LINQ 查询,以为它只执行了一次,结果发现里面那个耗时的转换被重复执行了好几遍,性能差得离谱,我对着 LINQ 延迟执行每次枚举都重新跑一遍这个坑排查大半天的复盘

2026-6-2 11:44:10

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索