为了研究Redis 三大守护神,我做了一个本地模拟!

0 阅读6分钟

Kill-redis-plan

本地模拟缓存击穿、缓存穿透、缓存雪崩,用于研究redis缓存

开源网站自行🔍

本地复现 Redis 缓存穿透、击穿、雪崩。

注意力在缓存层:Compose 只跑 Redis,存储用进程内 FakeDB,用 Go goroutine 打并发。

观测挂在内核上:每秒输出一个生产指标窗口,结束后汇总全程指标,由人根据关联变化判断缓存状态。

  • 穿透penetrate 查不存在的数据。缓存一直空,每次请求都打到存储。
  • 击穿breakdown 单个热点 key 过期,大量并发同时 miss,瞬间打到存储。
  • 雪崩avalanche 大批量 key 同一时刻过期,大面积 miss,存储被打满。

reports日志 观测格式

avalanche  concurrency=2000 planned=2400 qps=400 duration=6s keys=200 expiry=2s
backend    redis=127.0.0.1:6379 db_latency=100ms db_concurrency=20 refill_ttl=30s

time       done  request_total  request_p99  request_errors  redis_hit  redis_miss  redis_hit_rate  redis_miss_rate  redis_expired  redis_errors  origin_total  origin_unique_keys  origin_breadth  origin_hot_share  db_total  db_saturation  db_max_waiting  db_wait_p99  db_found  db_not_found
1s     400/2400            400      1.553ms               0        399           0          100.0%             0.0%              0             0             0                   0            0.0%              0.0%         0           0.0%               0           0s         0             0
2s     794/2400            399      1.082ms               0        395           5           98.8%             1.2%              6             0             5                   5          100.0%             20.0%         5           0.0%               0           0s         0             0
3s    1143/2400            401    506.879ms               0        165         235           41.2%            58.8%            194             0           235                 195           83.0%              0.9%       198          96.2%             100    455.679ms       183             0
4s    1600/2400            400    556.031ms               0        400           0          100.0%             0.0%              0             0             0                   0            0.0%              0.0%        37          19.4%               0    157.456ms        57             0
5s    1999/2400            399        992µs               0        400           0          100.0%             0.0%              0             0             0                   0            0.0%              0.0%         0           0.0%               0           0s         0             0
6s    2400/2400            400      1.109ms               0        400           0          100.0%             0.0%              0             0             0                   0            0.0%              0.0%         0           0.0%               0           0s         0             0

result  status=ok elapsed=6.001s done=2400/2400 failures=0
request total=2400 errors=0 input_rejected=0 p99=505.855ms overflow=0
redis   hit=2160 miss=240 hit_rate=90.00% errors=0 ops_p99=1.074ms overflow=0 expired=200 evicted=0
origin  total=240 unique_keys=200 duplicate_reads=40 valid_rate=100.00%
hot_key key=item:155 total=2 share=0.83% max_inflight=2
db      total=240 found=240 not_found=0 errors=0 max_waiting=100 wait_p99=455.679ms wait_overflow=0 query_p99=101.055ms query_overflow=0

具体还是可以到项目中去观察,最好来到本地跑一下,观测一下数值在面对 三大问题的变化。

数据流

一次请求:cache.Get(id) → Redis GET

hit 则返回,FakeDB 不动。

miss 则 store.Get(占槽、睡延迟、查 map)。

有值就 Redis SET 带 TTL 再返回;没值只返回空,不写 Redis。

回源

回源= Redis Miss -> 向 DB 发起查询 (不管 Found 还是 Not Found)

回源是个动作

  • Found:一次有效回源
  • Not Found:一次无效回源

回源率 = origin_total / redis_miss

回源集中度 = origin_hot_key_total / origin_total

穿透 penetrate

Redis 返回 nil -> 回源查询 DB -> DB 返回结果为空 / 影响行数为 0 -> 缓存穿透

捕获一次就是一次穿透

穿透次数 = origin_totalDB Not Found

观测:Server / 全链路监控层 中加入 Redis Miss + DB Not Found 的复合判定埋点.

  • 恶意攻击:攻击者构造大量不存在的 ID(如负数 ID、随机字符串)频繁请求接口

  • 业务 bug:前端传入了错误的参数,如删除了某条数据后仍然不断查询

  • 爬虫扫描:遍历式爬虫尝试访问不存在的资源

  • -t unique:默认。下标 i(从 0 起)打 id 1000000+i,每个 ID 只请求一次,模拟不断生成新 ID 的随机扫描。

  • -t repeat:从一组不存在 ID 中均匀抽取并打散后齐发,模拟失效链接、爬虫或攻击脚本同时反复访问一批不存在资源。默认 id_pool=100;当 -n 较小时自动缩小池子,确保会产生重复请求。

两个模式都会齐发。开启方案 2 后,repeat 的首波仍可能在空值写入前并发回源;只有首波结束后的后续请求,才会命中短 TTL 空值缓存。

Q&A 为什么没有用redis_miss?

防击穿组件(SingleFlight / 互斥锁)的拦截:

如果有 1000 个并发请求打过来,Redis 确实记录了 1000 次 Miss。 但如果应用层配置了 singleflight,只有 1 个请求真正去查了 DB,其余 999 个在内存里挂起等待复用。 此时:Redis Miss = 1000 != 真实回源数 = 1 如果拿 Redis Miss 当回源,你会误以为数据库正承受 1000 次冲击,但实际上数据库压力只有 1。

击穿 breakdown

击穿 = 针对同一 Key 的高并发回源(origin_hot_key_max_inflight)DB Found

origin_total = 106
origin_unique_keys = 1
origin_hot_key = item:1
origin_hot_key_total = 106
origin_hot_key_max_inflight = 105

origin_hot_key_total/origin_total (回源集中度) ≈ 100%
origin_hot_key_max_inflight >> 1

针对同一 key 的高并发回源: 106 次回源全部针对 item:1,其中最多有 105 次同时进行。

热点 Key 过期/失效 -> 大量并发请求同时 Redis Miss -> 瞬间并发回源 DB -> DB 成功返回有效数据 -> 缓存击穿

预热:item:1 写入 Redis(TTL 用 config 里的值),每 50ms 轮询 EXISTS,直到 key 消失; 时限为 ttl+3s,超时则失败退出。 然后 -n 个 goroutine 同时打 id 1。 回写完成前,其余请求也会 miss,叠到同一条回源上。

雪崩 avalanche

雪崩特征 = 大面积 Redis Miss多 Key 的有效回源DB 满载、排队与请求延迟上升

redis_expired = 200
redis_miss_rate = 56.3%

origin_total = 224
origin_unique_keys = 200
origin_breadth = 89.3%
origin_hot_share = 0.9%

db_found = 180
db_saturation = 91.4%
db_max_waiting = 100
db_wait_p99 = 453.375ms
request_p99 = 503.295ms
  • 大面积 Redis Miss:200 个 key 同时过期,Redis Miss 率在该窗口升至 56.3%。、
  • 多 Key 的有效回源:224 次回源覆盖 200 个不同 key,广度为 89.3%;
  • 排除击穿与穿透db_found=180 表示这些请求查到有效数据,不是缓存穿透。origin_hot_share=0.9% 表示回源没有集中在单一热点 key,因此不是击穿。
  • DB 满载、排队与请求延迟上升:DB 在该窗口 91.4% 的时间处于满载,最多 100 个请求等待并发槽,DB 等待 P99 为 453.375ms,最终将请求 P99 推高到 503.295ms。
  • 预热:
    • 先用同一个绝对过期时间预热 item:1..keys,随后立即启动固定 QPS 的持续流量:
    • 0..ttl:缓存正常命中。
    • key 集体过期后:多 key 同时 miss、回源,DB 开始满载和排队。
    • 回填完成后:Redis 命中恢复,DB 清空积压请求。

文章内容来自README,非AI编写以及润色!