Skip to content

缓存穿透 / 击穿 / 雪崩

TIP

使用 Redis 作为缓存时,需要了解并防范缓存穿透、击穿和雪崩这三大常见问题。

缓存穿透

请求缓存和数据库中都不存在的数据,导致请求直接打到数据库。

解决方式

java
// 1. 缓存空值
public User getUser(Long id) {
    User user = redis.get("user:" + id);
    if (user != null) {
        return user;
    }
    user = userDao.findById(id);
    if (user == null) {
        redis.set("user:" + id, "", 60); // 缓存空值,过期时间短
    } else {
        redis.set("user:" + id, user, 3600);
    }
    return user;
}

// 2. 布隆过滤器(Bloom Filter)
// 将所有可能存在的数据哈希到一个足够大的 bitmap 中
if (!bloomFilter.mightContain(id)) {
    return null; // 布隆过滤器说不在,一定不存在
}

缓存击穿

某一个热点 key 过期瞬间,大量并发请求直接打到数据库。

解决方式

java
// 1. 互斥锁(分布式锁)
public User getUser(Long id) {
    User user = redis.get("user:" + id);
    if (user == null) {
        String lockKey = "lock:user:" + id;
        String lockId = UUID.randomUUID().toString();
        if (redis.setnx(lockKey, lockId, 10)) { // 获取锁
            try {
                user = userDao.findById(id);
                redis.set("user:" + id, user, 3600);
            } finally {
                if (lockId.equals(redis.get(lockKey))) {
                    redis.del(lockKey); // 释放锁
                }
            }
        } else {
            Thread.sleep(100);
            return getUser(id); // 重试
        }
    }
    return user;
}

// 2. 逻辑过期(预先设置逻辑过期时间,后台异步更新)

缓存雪崩

大量缓存同时过期,或 Redis 宕机,导致请求全部打到数据库。

解决方式

java
// 1. 过期时间加随机值
int baseTtl = 3600;
int randomTtl = baseTtl + new Random().nextInt(600);
redis.set("user:" + id, user, randomTtl);

// 2. 多级缓存
// 本地缓存(Caffeine)→ Redis → 数据库

// 3. Redis 高可用
// 主从 + Sentinel / Cluster

// 4. 限流降级
// 使用 Sentinel 等服务进行流量控制

对比总结

问题原因解决方案
穿透数据不存在布隆过滤器 / 缓存空值
击穿热点 key 过期互斥锁 / 逻辑过期
雪崩大量 key 同时过期或宕机随机过期时间 / 多级缓存 / 高可用