缓存穿透/击穿/雪崩 + 解决方案

一句话:缓存穿透、击穿、雪崩是Redis缓存的三种常见问题,分别对应不同的场景和解决方案。

1. 缓存穿透

1.1 什么是缓存穿透?


graph TD

    A[客户端请求] --> B{缓存是否存在?}

    B -->|存在| C[返回数据]

    B -->|不存在| D{数据库是否存在?}

    D -->|存在| E[写入缓存并返回]

    D -->|不存在| F[返回空值]

    F --> G[恶意攻击?]

    style F fill:#ffebee

缓存穿透:查询一个一定不存在的数据,由于缓存不命中,每次都要去数据库查询,但数据库也没有这条数据,所以也不会写入缓存。这导致每次请求都打到数据库。

常见场景

  1. 恶意攻击:故意查询不存在的数据

  2. 业务Bug:查询错误的数据ID

  3. 数据未初始化:新数据还没写入数据库

1.2 解决方案

方案1:缓存空值

 
import redis
 
import json
 
r = redis.Redis()
 
def get_user(user_id):
 
    # 1. 查询缓存
 
    cache_key = f"user:{user_id}"
 
    cached = r.get(cache_key)
 
    if cached is not None:
 
        # 缓存命中
 
        if cached == "NULL":
 
            return None  # 缓存的空值
 
        return json.loads(cached)
 
    # 2. 查询数据库
 
    user = db.query_user(user_id)
 
    if user is None:
 
        # 数据不存在,缓存空值(短时间)
 
        r.setex(cache_key, 60, "NULL")  # 缓存60秒
 
        return None
 
    # 3. 写入缓存
 
    r.setex(cache_key, 3600, json.dumps(user))
 
    return user
 

优点:简单直接

缺点:占用内存,可能短期数据不一致

方案2:布隆过滤器

 
from pybloom_live import BloomFilter
 
# 创建布隆过滤器
 
bf = BloomFilter(capacity=1000000, error_rate=0.001)
 
# 初始化时添加所有有效的user_id
 
for user_id in db.get_all_user_ids():
 
    bf.add(user_id)
 
def get_user_with_bloom(user_id):
 
    # 1. 先检查布隆过滤器
 
    if user_id not in bf:
 
        return None  # 一定不存在
 
    # 2. 查询缓存
 
    cache_key = f"user:{user_id}"
 
    cached = r.get(cache_key)
 
    if cached:
 
        return json.loads(cached)
 
    # 3. 查询数据库
 
    user = db.query_user(user_id)
 
    if user:
 
        r.setex(cache_key, 3600, json.dumps(user))
 
    return user
 

优点:内存占用小,效率高

缺点:有误判率,删除困难

2. 缓存击穿

2.1 什么是缓存击穿?


graph TD

    A[热点key过期] --> B[大量并发请求]

    B --> C[同时查询数据库]

    C --> D[数据库压力剧增]

    D --> E[数据库崩溃]

    style D fill:#ffebee

缓存击穿:某个热点key在某个时间点过期,恰好在这个时间点有大量并发请求访问这个key,这些请求都打到数据库。

常见场景

  1. 热点数据过期:如秒杀商品、热门新闻

  2. 突发流量:如明星微博、热点事件

2.2 解决方案

方案1:互斥锁

 
import redis
 
import json
 
import threading
 
r = redis.Redis()
 
lock = threading.Lock()
 
def get_hot_data(key):
 
    # 1. 查询缓存
 
    cached = r.get(key)
 
    if cached:
 
        return json.loads(cached)
 
    # 2. 获取锁
 
    if lock.acquire(blocking=False):
 
        try:
 
            # 再次检查缓存(双重检查)
 
            cached = r.get(key)
 
            if cached:
 
                return json.loads(cached)
 
            # 3. 查询数据库
 
            data = db.query_hot_data(key)
 
            # 4. 写入缓存
 
            r.setex(key, 3600, json.dumps(data))
 
            return data
 
        finally:
 
            lock.release()
 
    else:
 
        # 未获取到锁,等待后重试
 
        import time
 
        time.sleep(0.1)
 
        return get_hot_data(key)
 

优点:保证数据一致性

缺点:性能下降,可能死锁

方案2:逻辑过期

 
import redis
 
import json
 
r = redis.Redis()
 
def get_hot_data_with_logical_expire(key):
 
    # 1. 查询缓存
 
    cached = r.get(key)
 
    if not cached:
 
        return None
 
    data = json.loads(cached)
 
    # 2. 检查逻辑过期时间
 
    if data.get("expire_time", 0) < time.time():
 
        # 已过期,异步更新
 
        if r.set(f"lock:{key}", "1", nx=True, ex=10):
 
            # 获取锁,异步更新
 
            threading.Thread(
 
                target=update_hot_data,
 
                args=(key,)
 
            ).start()
 
    # 3. 返回数据(可能是旧数据)
 
    return data.get("value")
 
def update_hot_data(key):
 
    try:
 
        # 查询数据库
 
        data = db.query_hot_data(key)
 
        # 写入缓存(带逻辑过期时间)
 
        cache_data = {
 
            "value": data,
 
            "expire_time": time.time() + 3600
 
        }
 
        r.setex(key, 7200, json.dumps(cache_data))
 
    finally:
 
        r.delete(f"lock:{key}")
 

优点:用户体验好,不阻塞

缺点:可能返回旧数据

3. 缓存雪崩

3.1 什么是缓存雪崩?


graph TD

    A[大量key同时过期] --> B[请求全部打到数据库]

    B --> C[数据库压力剧增]

    C --> D[数据库崩溃]

    D --> E[应用雪崩]

    style D fill:#ffebee

缓存雪崩:大量key在同一时间过期,或者Redis服务宕机,导致大量请求打到数据库。

常见场景

  1. 批量设置相同过期时间

  2. Redis服务宕机

  3. 流量突增

3.2 解决方案

方案1:随机过期时间

 
import random
 
def set_with_random_expire(key, value, base_expire=3600):
 
    # 随机过期时间(±10%)
 
    expire = base_expire + random.randint(-360, 360)
 
    r.setex(key, expire, json.dumps(value))
 

优点:避免同时过期

缺点:仍有可能集中过期

方案2:多级缓存

 
import redis
 
import memcache
 
# 本地缓存
 
local_cache = {}
 
# 分布式缓存
 
r = redis.Redis()
 
# 数据库
 
db = Database()
 
def get_data(key):
 
    # 1. 查询本地缓存
 
    if key in local_cache:
 
        return local_cache[key]
 
    # 2. 查询Redis
 
    cached = r.get(key)
 
    if cached:
 
        data = json.loads(cached)
 
        local_cache[key] = data
 
        return data
 
    # 3. 查询数据库
 
    data = db.query(key)
 
    # 4. 写入缓存
 
    r.setex(key, 3600, json.dumps(data))
 
    local_cache[key] = data
 
    return data
 

优点:减少Redis压力

缺点:实现复杂,数据一致性问题

方案3:熔断降级

 
import redis
 
import json
 
from circuitbreaker import circuit
 
r = redis.Redis()
 
@circuit(failure_threshold=5, recovery_timeout=60)
 
def get_data_with_circuit(key):
 
    # 1. 查询缓存
 
    cached = r.get(key)
 
    if cached:
 
        return json.loads(cached)
 
    # 2. 查询数据库
 
    data = db.query(key)
 
    # 3. 写入缓存
 
    r.setex(key, 3600, json.dumps(data))
 
    return data
 
def get_data_safe(key):
 
    try:
 
        return get_data_with_circuit(key)
 
    except Exception:
 
        # 熔断,返回降级数据
 
        return get_fallback_data(key)
 

优点:保护数据库

缺点:用户体验下降

4. 对比总结

问题场景原因解决方案
缓存穿透查询不存在的数据缓存和数据库都没有缓存空值、布隆过滤器
缓存击穿热点key过期大量并发请求互斥锁、逻辑过期
缓存雪崩大量key同时过期缓存集体失效随机过期、多级缓存、熔断降级

核心要点

 
# 解决方案:随机过期、多级缓存、熔断降级
 

速记卡(面试闪卡)

Q1:一句话讲清「缓存穿透/击穿/雪崩 + 解决方案」到底是什么?

A:缓存三大坑——穿透(查不存在)、击穿(热点过期)、雪崩(大量齐过期),分别用空值/布隆、互斥锁/逻辑过期、随机过期/多级缓存应对。

Q2:缓存穿透:查根本不存在的数据 —— 怎么理解?

A:查一个根本不存在的数据,缓存和库都没有,每次请求都打到库——常被恶意攻击利用。解法:缓存空值(短 TTL)拦一波,或布隆过滤器在入口就判”一定没有”。好比门卫先看名单再放人(cache penetration, bloom filter)。

Q3:缓存击穿:热点 key 刚好过期 —— 怎么理解?

A:某个热点 key 刚好过期,瞬间海量并发全挤到库上把库打崩。解法:互斥锁让只有一个去查库、其余等着;或逻辑过期——返回旧值同时后台异步刷新。好比抢限量款,一次只放一个人进(cache breakdown, mutex lock)。

Q4:缓存雪崩:大量 key 齐过期 —— 怎么理解?

A:大量 key 同一时刻过期(或 Redis 宕机),请求集体涌向数据库直接雪崩。解法:过期时间加随机抖动错峰、多级缓存分摊、熔断降级保护库。好比全员同时下班地铁挤爆——错峰就稳(cache avalanche, random TTL)。

Q5:三者对比一张表 —— 怎么理解?

A:穿透=数据不存在,治标用空值/布隆;击穿=单点热点过期,用锁/逻辑过期;雪崩=集体失效,用随机过期/多级/熔断。记住”场景 → 原因 → 解法”三栏即可(comparison summary)。

Q6:核心速记主线有哪些?

  • 穿透:查不存在数据,缓存库皆无 → 缓存空值 / 布隆过滤器

  • 击穿:热点 key 过期瞬间高并发 → 互斥锁 / 逻辑过期

  • 雪崩:大量 key 齐过期或宕机 → 随机过期 / 多级缓存 / 熔断

  • 口诀:穿透防问、击穿防挤、雪崩防塌

口诀

A:缓存三坑要分清,

穿透问空布隆屏;

击穿热点锁护行,

雪崩错峰多级撑。

相关链接