bigywater.tech
Django · 96

缓存穿透、击穿与雪崩实验

缓存穿透、击穿、雪崩这三个词几乎人人听过,但它们各自的量级是多少?常见的防线真的有效吗?这篇用 LocMem + 线程 + 一个睡 50 ms 的「数据库」把三种情形各做一遍,数出数据库被调用了多少次。

100 → 1
同一个不存在的 key 请求 100 次:不缓存空结果 → 缓存空结果的数据库调用次数
50 → 1
热点 key 过期时 50 个并发请求:无保护 → cache.add 互斥锁的数据库调用次数
189 → 122
雪崩:相同 TTL vs TTL 抖动,250 ms 窗口内的数据库调用峰值

穿透

实测 · 本机执行输出
  不缓存空结果             对同一个不存在的 key 请求 100 次 → 数据库调用 100 次
  缓存空结果(短 TTL)       对同一个不存在的 key 请求 100 次 → 数据库调用 1 次
  缓存空结果,但每次请求的 key 都不同(随机 key 攻击)→ 数据库调用 100 次(空值缓存挡不住,需要布隆过滤器或参数校验,本文没有测)

「请求的 key 数据库里也没有」,所以缓存永远填不上。缓存空值(短 TTL)能挡住重复请求同一个不存在的 key;但随机 key 每个都是第一次,只能靠参数校验、布隆过滤器等前置手段。

击穿

实测 · 本机执行输出
  无保护                热点 key 已过期,50 个并发请求 → 数据库调用 50 次;最慢一次 55 ms;总耗时 56 ms
  互斥锁(cache.add)     热点 key 已过期,50 个并发请求 → 数据库调用  1 次;最慢一次 62 ms;总耗时 63 ms

互斥锁的写法:cache.add(lock, 1, 5) 成功者去回源,失败者轮询缓存。注意给锁设过期时间,防止拿锁的线程崩溃后锁永不释放;并发极高时轮询本身也有开销(本文没有测)。

雪崩

实测 · 本机执行输出
  相同 TTL       4 秒内数据库调用共 838 次;每 250 ms 窗口的最高调用数 189(窗口序列前 16 个:[0, 0, 0, 0, 0, 11, 189, 147, 30, 0, 0, 0, 128, 163, 148, 22])
  TTL 抖动 ±1s   4 秒内数据库调用共 1415 次;每 250 ms 窗口的最高调用数 122(窗口序列前 16 个:[0, 0, 76, 98, 73, 94, 122, 108, 117, 110, 108, 84, 98, 112, 103, 112])

相同 TTL 的 key 同时批量写入(比如启动时预热),会同时过期。抖动能把脉冲摊平(峰值下降),但不减少总量;真实系统里还要配合限流、降级(本文没有做)。

分步演示:三种缓存问题

穿透 / 击穿 / 雪崩
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 / Python 3.14.7 / Django 6.1.1(默认用 SQLite,其它数据库会在文中注明),用 django.test.Client 或真实的本机服务器进程实测;计时用 time.perf_counter,数字每次运行会有小幅抖动。LocMemCache + 线程;「数据库」是睡 50 ms 的函数;雪崩实验 40 线程、300 个 key、4 秒
    • 没有验证:Redis / Memcached 上的同类现象、多进程多机器下的互斥锁(需要分布式锁)、布隆过滤器、请求合并(singleflight)、缓存预热策略
    京ICP备2026067502号-1京公网安备11010502063487号