Skip to content

Redis 缓存穿透、击穿、雪崩

缓存穿透

什么是缓存穿透?

请求的数据既不在缓存中,也不在数据库中,导致每次请求都会穿透缓存直接到达数据库。

正常请求: Client → Redis(命中) → 返回数据
穿透请求: Client → Redis(未命中) → MySQL(不存在) → 返回空 → 下次继续穿透

产生原因

  • 恶意攻击:使用不存在的 ID 大量请求
  • 业务 Bug:请求了不存在的数据

解决方案

1. 缓存空值

java
/**
 * 缓存空值方案
 * @author yjhu
 */
public String getById(Long id) {
    String key = "user:" + id;
    String value = redis.get(key);
    
    if (value != null) {
        // 命中缓存(包括空值)
        return "null".equals(value) ? null : value;
    }
    
    // 查询数据库
    User user = userMapper.selectById(id);
    if (user == null) {
        // 缓存空值,设置较短过期时间
        redis.setex(key, 60, "null");
        return null;
    }
    
    redis.setex(key, 3600, JSON.toJSONString(user));
    return JSON.toJSONString(user);
}

2. 布隆过滤器

在缓存之前增加一层布隆过滤器,快速判断数据是否存在。

java
/**
 * 布隆过滤器方案
 * @author yjhu
 */
public String getById(Long id) {
    // 先判断布隆过滤器
    if (!bloomFilter.mightContain(id)) {
        return null;  // 一定不存在
    }
    
    String key = "user:" + id;
    String value = redis.get(key);
    if (value != null) {
        return value;
    }
    
    User user = userMapper.selectById(id);
    if (user != null) {
        redis.setex(key, 3600, JSON.toJSONString(user));
    }
    return user != null ? JSON.toJSONString(user) : null;
}

缓存击穿

什么是缓存击穿?

热点数据的缓存过期瞬间,大量并发请求直接打到数据库。

时间线:
----[缓存有效]----[缓存过期]----[重建缓存]----

                  大量请求同时到达 MySQL

解决方案

1. 互斥锁

只允许一个线程查询数据库并重建缓存,其他线程等待。

java
/**
 * 互斥锁方案
 * @author yjhu
 */
public String getById(Long id) {
    String key = "user:" + id;
    String value = redis.get(key);
    
    if (value != null) {
        return value;
    }
    
    String lockKey = "lock:" + key;
    try {
        // 尝试获取锁
        boolean locked = redis.setnx(lockKey, "1", 10, TimeUnit.SECONDS);
        if (locked) {
            // 获取锁成功,查询数据库
            User user = userMapper.selectById(id);
            redis.setex(key, 3600, JSON.toJSONString(user));
            return JSON.toJSONString(user);
        } else {
            // 获取锁失败,休眠后重试
            Thread.sleep(50);
            return getById(id);
        }
    } finally {
        redis.del(lockKey);
    }
}

2. 逻辑过期

不设置 TTL,而是在值中存储逻辑过期时间,由后台异步更新。

java
/**
 * 逻辑过期方案
 * @author yjhu
 */
@Data
public class CacheData {
    private Object data;
    private LocalDateTime expireTime;
}

public String getById(Long id) {
    String key = "user:" + id;
    String json = redis.get(key);
    
    if (json == null) {
        return null;
    }
    
    CacheData cacheData = JSON.parseObject(json, CacheData.class);
    
    // 缓存未过期
    if (cacheData.getExpireTime().isAfter(LocalDateTime.now())) {
        return JSON.toJSONString(cacheData.getData());
    }
    
    // 缓存已过期,异步重建
    String lockKey = "lock:" + key;
    if (redis.setnx(lockKey, "1", 10, TimeUnit.SECONDS)) {
        // 开启异步线程重建缓存
        executor.submit(() -> {
            try {
                User user = userMapper.selectById(id);
                CacheData newData = new CacheData();
                newData.setData(user);
                newData.setExpireTime(LocalDateTime.now().plusHours(1));
                redis.set(key, JSON.toJSONString(newData));
            } finally {
                redis.del(lockKey);
            }
        });
    }
    
    // 返回旧数据
    return JSON.toJSONString(cacheData.getData());
}

缓存雪崩

什么是缓存雪崩?

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

解决方案

1. 过期时间随机化

java
// 在基础过期时间上增加随机值
int baseExpire = 3600;
int randomExpire = new Random().nextInt(300);  // 0-5 分钟随机
redis.setex(key, baseExpire + randomExpire, value);

2. Redis 高可用

  • 主从复制 + 哨兵模式
  • Redis Cluster 集群模式

3. 多级缓存

Client → 本地缓存(Caffeine) → Redis → MySQL
java
/**
 * 多级缓存方案
 * @author yjhu
 */
@Autowired
private Cache<Long, User> localCache;  // Caffeine

public User getById(Long id) {
    // L1: 本地缓存
    User user = localCache.getIfPresent(id);
    if (user != null) {
        return user;
    }
    
    // L2: Redis
    String key = "user:" + id;
    String json = redis.get(key);
    if (json != null) {
        user = JSON.parseObject(json, User.class);
        localCache.put(id, user);
        return user;
    }
    
    // L3: MySQL
    user = userMapper.selectById(id);
    if (user != null) {
        redis.setex(key, 3600, JSON.toJSONString(user));
        localCache.put(id, user);
    }
    return user;
}

三种问题对比

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