Redis缓存雪崩、击穿、穿透

缓存雪崩、缓存击穿、缓存穿透
这三个问题都发生在缓存失效或不存在时,大量请求直接涌向后端数据库,导致数据库压力激增甚至崩溃。

一、缓存雪崩 (Cache Avalanche)

1. 问题描述

同一时间大量的缓存Key集体失效(例如,设置了相同的过期时间),导致所有对这些数据的请求同时无法命中缓存(Cache Miss),全部直接落到数据库上,引起数据库瞬时压力过大而崩溃。

2. 发生场景

  • 业务高峰期,一批缓存数据(如首页商品列表、热门文章等)设置了相同的 TTL(Time-To-Live),同时过期。
  • Redis 服务宕机重启,导致整个缓存层不可用。

3. 解决方案

解决方案的核心是:避免大量Key同时失效

  1. 设置随机过期时间
    这是最简单有效的预防措施。给缓存数据的过期时间加上一个随机值,打散它们的失效时间点,避免集体失效。

    import random
    # 设置缓存时,在基础过期时间上增加一个随机抖动(例如0-300秒)
    expire_time = 3600 + random.randint(0, 300) # 1小时 ± 5分钟
    redis_client.setex(cache_key, expire_time, data)
    
  2. 构建高可用的Redis集群
    通过 Redis Sentinel(哨兵)或 Redis Cluster(集群)模式,实现主从切换和多节点负载均衡,即使单个节点宕机,整个缓存层依然可用,防止“全盘皆崩”。

  3. 缓存永不过期 + 后台更新
    对极热点数据,可以设置为永不过期(-1),然后由后台任务定时任务定期异步地更新缓存。这样用户请求永远不会遇到缓存失效。


二、缓存击穿 (Cache Breakdown)

1. 问题描述

一个访问量极高的热点Key(如某顶流明星的新闻、秒杀商品)在失效的瞬间,持续的高并发请求会像子弹一样“击穿”缓存,全部直接请求数据库,导致数据库瞬间压力过大。

2. 解决方案

解决方案的核心是:防止单个热点Key失效时被大量并发访问

  1. 互斥锁 (Mutex Lock) - 最常用
    当缓存失效时,不是所有请求都去查数据库,而是让第一个请求去查数据库并重建缓存,其他请求等待,待缓存重建后再从缓存中获取数据。

    import redis
    import threading
    from datetime import datetime
    
    def get_data_with_lock(key):
        # 1. 先尝试从缓存获取
        data = redis_client.get(key)
        if data is not None:
            return data
    
        # 2. 缓存未命中,尝试获取分布式锁
        lock_key = f"lock:{key}"
        # 使用 setnx (SET if Not eXists) 命令争抢锁,并设置锁的过期时间(防止死锁)
        acquired_lock = redis_client.setnx(lock_key, datetime.now().strftime("%s"))
        if acquired_lock:
            redis_client.expire(lock_key, 10)  # 设置锁10秒后自动过期
            try:
                # 3. 成功获取锁,查询数据库
                data = get_data_from_db(key)
                # 4. 写入缓存
                redis_client.setex(key, 3600, data)
            finally:
                # 5. 释放锁
                redis_client.delete(lock_key)
            return data
        else:
            # 6. 未获取到锁,等待片刻后重试(或直接返回默认值)
            time.sleep(0.1)
            return get_data_with_lock(key)  # 重试
    
    
  2. 逻辑过期 (Logical Expiration)
    不给缓存设置 TTL,而是在缓存Value中存储一个逻辑过期时间。当请求发现逻辑时间已过期,则发起一个异步任务去更新缓存,当前请求仍返回旧数据。

    # Value 结构:{"data": real_data, "expire_ts": 1649873100}
    value = {
        "data": {"name": "Hot Product", "price": 99},
        "expire_ts": int(time.time()) + 3600  # 1小时后逻辑过期
    }
    redis_client.set(key, json.dumps(value)) # 不设置TTL
    
    # 读取时:
    cached_value = redis_client.get(key)
    if cached_value:
        obj = json.loads(cached_value)
        if obj['expire_ts'] > time.time():
            return obj['data']  # 未逻辑过期,直接返回
        else:
            # 已逻辑过期,触发异步更新
            async_update_cache(key)
            return obj['data']  # 仍然先返回旧数据
    

三、缓存穿透 (Cache Penetration)

1. 问题描述

请求查询一个数据库中根本不存在的数据(如 id=-1 的商品,或随机生成的、不存在的用户ID)。由于缓存中也不会有该数据,导致每次请求都会穿透缓存去查询数据库。如果有人恶意攻击,会发送大量此类请求,从而压垮数据库。

2. 解决方案

解决方案的核心是:在缓存层拦截掉对不存在数据的请求

  1. 缓存空对象 (Cache Null Object)
    即使从数据库没查到,也缓存一个空值(如 None, NULL)或特定的错误标记,并设置一个较短的过期时间(如 1-5 分钟)。后续相同的请求会命中这个空缓存,从而保护数据库。

    def get_data(key):
        data = redis_client.get(key)
        if data is not None:
            # 如果缓存的是空标记,直接返回None或错误
            if data == "NULL_OBJECT":
                return None
            return data
    
        # 查数据库
        data = db.query("SELECT * FROM table WHERE id = %s", key)
        if not data:
            # 数据库不存在,缓存空对象,有效期5分钟
            redis_client.setex(key, 300, "NULL_OBJECT")
            return None
        else:
            # 数据库存在,写入缓存
            redis_client.setex(key, 3600, data)
            return data
    

缺点:如果攻击者每次用不同的Key,此方法会缓存大量无用的空值,浪费内存。

  1. 布隆过滤器 (Bloom Filter) - 最优解
    在缓存之前,设置一个布隆过滤器。它是一个概率型数据结构,用于快速判断一个元素是否绝对不存在于某个集合中
  • 写入时:当向数据库插入新数据时,同时将该数据的Key写入布隆过滤器。

  • 查询时:收到请求后,先用布隆过滤器判断Key是否存在。

    • 如果不存在,则直接返回 None,根本不会查询缓存和数据库。
    • 如果存在,再继续后续的缓存查询流程。
    # 使用 Redis 4.0+ 自带的布隆过滤器模块 (redisbloom)
    # 或者使用 Python 的 redisbloomclient 库
    
    from redisbloom.client import Client
    
    rb = Client()
    key = "user:10000"
    
    # 1. 先检查布隆过滤器
    if not rb.bfExists("users_filter", key):
        print("Key definitely not exists, return directly.")
        return None
    
    # 2. 如果布隆过滤器说存在,再继续查缓存和数据库
    data = redis_client.get(key)
    if data:
        return data
    # ... (后续流程)
    

    优点:内存占用极小,效率极高。
    缺点:有极低的误判率(判断为存在,但实际可能不存在),但不会误判“不存在”。对于缓存穿透场景,宁可错杀一千(放过极少数不存在的请求去查库),也不能放过一个。

  1. 接口层增加校验
    对请求的参数做基础的合法性校验。例如,id 为负数的请求、非法的邮箱格式等,直接在入口层拦截并返回错误。
posted @ 2025-09-05 17:30  xclic  阅读(96)  评论(0)    收藏  举报