DEV Community

port smith
port smith

Posted on

Redis缓存三连击:击穿、雪崩、穿透全解析!

Redis缓存三连击:击穿、雪崩、穿透,一次讲透!

双十一大促零点,你手速拉满抢限量神券——结果页面卡死、刷新失败、“系统繁忙”弹窗反复刷屏。后台可能正被三股力量轮番暴击:有人用脚本狂刷根本不存在的用户ID(穿透);爆款商品详情页在缓存过期那一毫秒,被上万请求同时“凿穿”(击穿);而整个商品库的缓存Key又恰巧在整点集体失效(雪崩)。这三者不是玄学黑话,而是真实可复现、可定位、可防御的典型故障链。


一、缓存穿透:查不到,还拼命查

定义:查询压根不存在的数据(比如user:999999999),缓存无记录,请求直冲数据库,高频无效查询拖垮DB。

▶️ 极简复现:

EXISTS user:999999999  # 返回0,但每秒1万次?DB瞬间变筛子。
Enter fullscreen mode Exit fullscreen mode

✅ 防御双保险(Python示例):

import redis, hashlib
from bloom_filter import BloomFilter

r = redis.Redis()
bf = BloomFilter(capacity=1000000, error_rate=0.001)

def get_user(user_id):
    key = f"user:{user_id}"
    # 1. 布隆过滤器快速拦截绝对不存在的key
    if not bf.contains(key):
        return None

    # 2. 查缓存
    data = r.get(key)
    if data:
        return data

    # 3. 缓存未命中,查DB(此处省略具体实现)
    db_data = query_db_user(user_id)

    if db_data:
        r.setex(key, 3600, db_data)  # 正常数据缓存1小时
    else:
        r.setex(key, 300, "NULL")  # 空值缓存5分钟,防反复穿透

    return db_data
Enter fullscreen mode Exit fullscreen mode

核心就两点:存在性前置校验 + 空值短期缓存,把无效流量挡在数据库门外。


二、缓存击穿:热点Key过期的“生死1毫秒”

定义:某个超高频Key(如首页推荐位hot_item)恰好过期,瞬间大量并发请求穿透缓存,争抢重建,数据库CPU飙升。

▶️ 复现场景:

GET hot_item 返回 nil 的刹那,1000个线程同时执行SET——谁先写?谁来查库?没协调就是灾难。

✅ 加锁重建(Python + Redis SETNX):

def get_hot_item():
    key = "hot_item"
    data = r.get(key)
    if data:
        return data

    lock_key = f"lock:{key}"
    # 尝试加分布式锁(带自动过期,防死锁)
    if r.set(lock_key, "1", nx=True, ex=5):
        try:
            # 二次检查:防止重复重建
            data = r.get(key)
            if not data:
                data = build_hot_item_from_db()
                r.setex(key, 3600, data)
            return data
        finally:
            r.delete(lock_key)  # 必须释放锁
    else:
        # 未抢到锁,短暂等待后重试(或返回旧缓存/降级数据)
        time.sleep(0.01)
        return get_hot_item()
Enter fullscreen mode Exit fullscreen mode

关键点:锁粒度最小化、自动过期、二次检查、异常必释放


三、缓存雪崩:大批Key集体“下班”

定义:大量Key设置相同过期时间(如凌晨2点统一TTL=3600),到期后集中失效,流量洪峰同步涌向数据库。

▶️ 探测风险(仅限开发环境!):

KEYS item:*  # ⚠️ 生产禁用!更安全方式:SCAN + TTL采样
Enter fullscreen mode Exit fullscreen mode

✅ 根治方案(Python):

import random

def set_item_with_jitter(key, value, base_ttl=3600):
    jitter = random.randint(-int(base_ttl*0.1), int(base_ttl*0.1))
    final_ttl = max(600, base_ttl + jitter)  # 不低于10分钟
    r.setex(key, final_ttl, value)

# 核心数据兜底:逻辑永不过期 + 异步刷新
def set_essential_item(key, value):
    r.set(key, value)
    # 后台任务每30分钟主动更新一次
Enter fullscreen mode Exit fullscreen mode

口诀记牢:不设固定过期时间,只设“基础TTL+随机抖动”;核心数据宁可冗余更新,也不集体断供。


四、真实踩坑与监控预警实战

我们曾在线上活动遭遇穿透事故:未对注册手机号做空值缓存,黑产脚本遍历user:138****0001138****9999,DB连接数10秒飙至2000+,服务全面超时。

如何提前发现?两个低成本监控项:

  • Redis慢日志:执行 SLOWLOG GET 5,若频繁出现GET命令+大量nil响应,大概率有穿透苗头;
  • 客户端错误率:用Prometheus监控redis_connection_errors_total,配置告警规则: rate(redis_connection_errors_total[5m]) > 0.1 → 立即排查是否雪崩前兆。

监控不是锦上添花,而是故障前的最后一道哨兵。


结语:稳如磐石的缓存铁三角

穿透、击穿、雪崩,本质是缓存与数据一致性之间的三道裂缝。守住它们,只需三招:

🔹 存在性校验——布隆过滤器+空值缓存,让“不存在”止步于缓存层;

🔹 热点保护——原子锁+双重检查,把重建动作串行化;

🔹 过期分散——随机TTL+异步刷新,避免缓存集体休眠。

缓存不是银弹,而是需要精心设计的防护体系。你在项目中用过哪种防护组合?欢迎在评论区晒出你的redis.conf关键配置或自研工具片段——Dev.to的极客们,正等着抄作业呢!

Top comments (0)