Redis 缓存三大问题与解决方案

Redis 缓存三大问题与解决方案

缓存是提升系统性能的关键技术,但引入缓存也带来了缓存穿透、缓存击穿、缓存雪崩三大问题。本文深入剖析这三大问题的成因、危害,以及布隆过滤器、互斥锁、永不过期等解决方案。

一、缓存架构概述

1.1 缓存模式

Write-Behind
写请求
写缓存
异步写数据库
Write-Through
写请求
写缓存
写数据库
Cache-Aside (旁路缓存)


读请求
缓存命中?
返回缓存
查数据库
写入缓存
写请求
写数据库
删除缓存

1.2 三大问题概述

缓存雪崩
大量 key 同时过期
或 Redis 宕机
数据库压力过大
缓存击穿
热点 key 过期
大量并发请求
同时查询数据库
缓存穿透
查询不存在的数据
缓存没有,数据库也没有
每次请求都打到数据库


二、缓存穿透

2.1 问题成因

影响
数据库压力增大
响应时间变长
甚至数据库宕机
问题场景
恶意请求 / 爬虫
查询不存在的 ID
缓存无数据
数据库也无数据
返回空
未写入缓存
再次查询

2.2 解决方案:布隆过滤器

特点
空间效率高
查询效率 O(k)
不支持删除
有误判率(可接受)
布隆过滤器原理


添加元素
计算 k 个哈希函数
将 k 个位置设为 1
查询元素
计算 k 个哈希函数
检查 k 个位置
都为 1?
可能存在
一定不存在

2.3 布隆过滤器实现

java 复制代码
// Guava 实现布隆过滤器
public class BloomFilterDemo {
    
    private static BloomFilter<Long> bloomFilter = 
        BloomFilter.create(
            Funnels.longFunnel(),
            1000000,           // 预期插入数量
            0.01               // 误判率
        );
    
    // 初始化时加载所有存在的 ID
    public void init() {
        List<Long> allIds = userMapper.selectAllIds();
        for (Long id : allIds) {
            bloomFilter.put(id);
        }
    }
    
    // 查询时先检查布隆过滤器
    public User getUser(Long id) {
        // 布隆过滤器判断
        if (!bloomFilter.mightContain(id)) {
            return null;  // 一定不存在,直接返回
        }
        
        // 可能存在,查缓存
        User user = cache.get(id);
        if (user != null) {
            return user;
        }
        
        // 查数据库
        user = userMapper.selectById(id);
        if (user != null) {
            cache.put(id, user);
        }
        
        return user;
    }
}

2.4 Redisson 实现布隆过滤器

java 复制代码
// Redisson 布隆过滤器
@Service
public class UserService {
    
    @Autowired
    private RedissonClient redissonClient;
    
    private RBloomFilter<Long> bloomFilter;
    
    @PostConstruct
    public void init() {
        // 创建布隆过滤器
        bloomFilter = redissonClient.getBloomFilter("user:bloom");
        bloomFilter.tryInit(1000000, 0.01);  // 100万数据,1%误判率
    }
    
    public User getUser(Long id) {
        if (!bloomFilter.contains(id)) {
            return null;
        }
        
        // ... 正常查询逻辑
        return userMapper.selectById(id);
    }
    
    // 用户创建时添加到布隆过滤器
    public void createUser(User user) {
        userMapper.insert(user);
        bloomFilter.add(user.getId());
    }
}

2.5 其他穿透解决方案

解决方案
布隆过滤器
拦截不存在的数据
缓存空值
将空结果也缓存
设置短过期时间
参数校验
过滤非法参数
ID 必须为正整数

java 复制代码
// 方案2:缓存空值
public User getUser(Long id) {
    // 先查缓存
    User user = cache.get("user:" + id);
    
    if (user != null) {
        if (user == NULL_USER) {
            return null;  // 空值缓存
        }
        return user;
    }
    
    // 查数据库
    user = userMapper.selectById(id);
    
    if (user == null) {
        // 缓存空值,过期时间短一些
        cache.put("user:" + id, NULL_USER, Duration.ofMinutes(5));
        return null;
    }
    
    cache.put("user:" + id, user);
    return user;
}

三、缓存击穿

3.1 问题成因

问题场景
热点 key 存在
缓存过期瞬间
大量并发请求
同时查询数据库
数据库压力剧增

3.2 解决方案:互斥锁

互斥锁流程


请求查询 key
获取互斥锁?
查数据库
等待后重试
再次查询缓存
写入缓存
释放锁
返回结果

java 复制代码
// 互斥锁实现
public User getUserWithLock(Long id) {
    String cacheKey = "user:" + id;
    String lockKey = "lock:user:" + id;
    
    // 先查缓存
    User user = cache.get(cacheKey);
    if (user != null) {
        return user;
    }
    
    // 获取互斥锁
    String token = UUID.randomUUID().toString();
    boolean locked = redisTemplate.opsForValue()
        .setIfAbsent(lockKey, token, Duration.ofSeconds(30));
    
    if (!locked) {
        // 获取锁失败,短暂等待后重试
        try {
            Thread.sleep(50);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        return getUserWithLock(id);  // 递归重试
    }
    
    try {
        // 获取锁成功,再查一次缓存
        user = cache.get(cacheKey);
        if (user != null) {
            return user;
        }
        
        // 查数据库
        user = userMapper.selectById(id);
        
        // 写入缓存
        if (user != null) {
            cache.put(cacheKey, user, Duration.ofHours(1));
        }
        
        return user;
    } finally {
        // 释放锁(使用 Lua 脚本保证原子性)
        releaseLock(lockKey, token);
    }
}

3.3 解决方案:永不过期

实现逻辑


查询时检查是否逻辑过期
逻辑过期?
开启异步线程重建缓存
返回缓存数据
返回旧数据
永不过期方案
缓存 value 中存储过期时间
逻辑过期字段
后台线程定期刷新

java 复制代码
// 永不过期方案
public class UserCache {
    
    @Data
    public static class CacheUser {
        private User user;
        private long expireTime;  // 逻辑过期时间
    }
    
    public User getUser(Long id) {
        String cacheKey = "user:" + id;
        CacheUser cacheUser = cache.get(cacheKey);
        
        if (cacheUser == null) {
            // 缓存不存在,加载并写入
            User user = loadFromDb(id);
            CacheUser newCacheUser = new CacheUser();
            newCacheUser.setUser(user);
            newCacheUser.setExpireTime(System.currentTimeMillis() + 30 * 60 * 1000);
            cache.put(cacheKey, newCacheUser);
            return user;
        }
        
        // 逻辑过期检查
        if (System.currentTimeMillis() > cacheUser.getExpireTime()) {
            // 异步刷新缓存
            threadPool.execute(() -> {
                User user = loadFromDb(id);
                CacheUser newCacheUser = new CacheUser();
                newCacheUser.setUser(user);
                newCacheUser.setExpireTime(System.currentTimeMillis() + 30 * 60 * 1000);
                cache.put(cacheKey, newCacheUser);
            });
        }
        
        return cacheUser.getUser();
    }
}

3.4 Redisson 分布式锁

java 复制代码
// 使用 Redisson 实现
public User getUser(Long id) {
    String cacheKey = "user:" + id;
    RLock lock = redissonClient.getLock("lock:user:" + id);
    
    try {
        lock.lock(10, TimeUnit.SECONDS);
        
        // 查缓存
        User user = cache.get(cacheKey);
        if (user != null) {
            return user;
        }
        
        // 查数据库
        user = userMapper.selectById(id);
        if (user != null) {
            cache.put(cacheKey, user);
        }
        
        return user;
    } finally {
        if (lock.isHeldByCurrentThread()) {
            lock.unlock();
        }
    }
}

四、缓存雪崩

4.1 问题成因

Redis宕机
Redis 故障
所有请求打到数据库
数据库崩溃
大面积雪崩
大量 key 同时过期
大量请求同时涌入
数据库压力过大
服务不可用

4.2 解决方案:过期时间随机化

解决方案
过期时间随机化
基础时间 + 随机偏移
避免同时过期
热点数据永不过期
逻辑过期
后台异步刷新

java 复制代码
// 过期时间随机化
public void setCache(String key, Object value, long baseExpireSeconds) {
    // 基础时间 + 随机偏移
    long expireSeconds = baseExpireSeconds + 
        (long) (Math.random() * baseExpireSeconds);
    
    cache.put(key, value, Duration.ofSeconds(expireSeconds));
}

// 示例
setCache("user:1", user, 3600);  // 3600 + 0~3600 = 3600~7200 秒

4.3 解决方案:多级缓存

特点
本地缓存: 最快,无网络开销
Redis: 分布式共享
MySQL: 最终数据源
多级缓存架构
本地缓存 (Guava/Caffeine)
分布式缓存 (Redis)
数据库 (MySQL)

java 复制代码
// 多级缓存实现
@Service
public class MultiLevelCache {
    
    // 一级:本地缓存
    private Cache<Long, User> localCache = Caffeine.newBuilder()
        .maximumSize(1000)
        .expireAfterWrite(30, TimeUnit.SECONDS)
        .build();
    
    // 二级:分布式缓存
    @Autowired
    private RedisTemplate<String, User> redisTemplate;
    
    public User getUser(Long id) {
        // 先查本地缓存
        User user = localCache.getIfPresent(id);
        if (user != null) {
            return user;
        }
        
        // 查分布式缓存
        String key = "user:" + id;
        user = redisTemplate.opsForValue().get(key);
        
        if (user != null) {
            // 写入本地缓存
            localCache.put(id, user);
            return user;
        }
        
        // 查数据库
        user = userMapper.selectById(id);
        
        if (user != null) {
            // 写入两级缓存
            localCache.put(id, user);
            redisTemplate.opsForValue().set(key, user, Duration.ofHours(1));
        }
        
        return user;
    }
}

4.4 解决方案:限流降级

降级策略
服务降级
返回默认值
返回友好提示
使用兜底数据
限流策略
计数器限流
滑动窗口
令牌桶
漏桶算法

java 复制代码
// 限流注解
@Aspect
@Component
public class RateLimitAspect {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    @Around("@annotation(RateLimit)")
    public Object rateLimit(ProceedingJoinPoint point, RateLimit rateLimit) 
            throws Throwable {
        
        String key = "rate:" + rateLimit.value() + ":" + getUserId();
        long count = redisTemplate.opsForValue().increment(key);
        
        if (count == 1) {
            redisTemplate.expire(key, Duration.ofSeconds(rateLimit.window()));
        }
        
        if (count > rateLimit.maxRequests()) {
            throw new BusinessException("请求过于频繁,请稍后重试");
        }
        
        return point.proceed();
    }
}

// 使用
@RateLimit(value = "user:get", maxRequests = 100, window = 60)
public User getUser(Long id) {
    return userService.getUser(id);
}

4.5 解决方案:Redis 高可用

高可用架构
Redis Sentinel
自动故障转移
一主多从
Redis Cluster
数据分片
多主多从


五、综合解决方案

5.1 完整方案设计

各层职责
过滤非法请求
拦截不存在数据
热点数据本地缓存
重建缓存互斥
保护数据库
防护层次
第一层:参数校验
第二层:布隆过滤器
第三层:多级缓存
第四层:互斥锁
第五层:限流降级

5.2 代码实现

java 复制代码
@Service
public class UserService {
    
    // 布隆过滤器
    @Autowired
    private RBloomFilter<Long> bloomFilter;
    
    // 本地缓存
    private Cache<Long, User> localCache = Caffeine.newBuilder()
        .maximumSize(1000)
        .expireAfterWrite(30, TimeUnit.SECONDS)
        .build();
    
    // 分布式缓存
    @Autowired
    private RedissonClient redissonClient;
    
    @Autowired
    private UserMapper userMapper;
    
    public User getUser(Long id) {
        // 1. 参数校验
        if (id == null || id <= 0) {
            return null;
        }
        
        // 2. 布隆过滤器检查
        if (!bloomFilter.contains(id)) {
            return null;
        }
        
        // 3. 本地缓存
        User user = localCache.getIfPresent(id);
        if (user != null) {
            return user;
        }
        
        // 4. 获取分布式锁
        String lockKey = "lock:user:" + id;
        RLock lock = redissonClient.getLock(lockKey);
        
        try {
            lock.lock(10, TimeUnit.SECONDS);
            
            // 5. 双重检查本地缓存
            user = localCache.getIfPresent(id);
            if (user != null) {
                return user;
            }
            
            // 6. 查分布式缓存
            String cacheKey = "user:" + id;
            user = redisTemplate.opsForValue().get(cacheKey);
            
            if (user != null) {
                localCache.put(id, user);
                return user;
            }
            
            // 7. 查数据库
            user = userMapper.selectById(id);
            
            if (user != null) {
                // 8. 写入缓存
                localCache.put(id, user);
                redisTemplate.opsForValue().set(cacheKey, user, 
                    Duration.ofHours(1) + Duration.ofSeconds(random.nextInt(3600)));
            }
            
            return user;
            
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

六、面试高频问题

6.1 什么是缓存穿透?如何解决?

复制代码
问题:
- 查询不存在的数据
- 缓存没有,数据库也没有
- 每次请求都打到数据库

解决方案:
1. 布隆过滤器
   - 使用多个哈希函数映射到位数组
   - 判断不存在一定不存在,判断存在可能误判

2. 缓存空值
   - 将空结果也缓存
   - 设置较短的过期时间

3. 参数校验
   - 过滤非法参数

6.2 什么是缓存击穿?如何解决?

复制代码
问题:
- 热点 key 过期瞬间
- 大量并发请求同时查询数据库
- 数据库压力剧增

解决方案:
1. 互斥锁
   - 保证只有一个线程查数据库
   - 其他线程等待后从缓存获取

2. 永不过期
   - value 中存储过期时间
   - 逻辑过期,后台异步刷新

3. 预热
   - 热点数据提前加载

6.3 什么是缓存雪崩?如何解决?

复制代码
问题:
- 大量 key 同时过期
- 或 Redis 宕机
- 数据库压力过大

解决方案:
1. 过期时间随机化
   - 基础时间 + 随机偏移

2. 多级缓存
   - 本地缓存 + Redis + 数据库

3. 限流降级
   - 限制并发访问
   - 服务降级返回默认值

4. Redis 高可用
   - Sentinel / Cluster

6.4 布隆过滤器的误判率如何计算?

复制代码
误判率公式:
P = (1 - e^(-kn/m))^k

其中:
- k: 哈希函数数量
- n: 插入元素数量
- m: 位数组长度

最优哈希函数数量:
k = (m/n) * ln(2)

示例:
- n = 1000000
- m = 10000000 (约 10M bits = 1.25MB)
- k = (10000000/1000000) * 0.693 ≈ 7

七、总结

7.1 三大问题对比

问题 成因 解决方案
穿透 查询不存在的数据 布隆过滤器、缓存空值
击穿 热点 key 过期瞬间 互斥锁、永不过期
雪崩 大量 key 同时过期 随机过期、多级缓存、限流

7.2 最佳实践

复制代码
缓存最佳实践:

1. 穿透防护
   ✅ 使用布隆过滤器
   ✅ 缓存空值(短过期)

2. 击穿防护
   ✅ 使用互斥锁
   ✅ 永不过期 + 逻辑过期

3. 雪崩防护
   ✅ 过期时间随机化
   ✅ 多级缓存架构
   ✅ 限流降级
   ✅ Redis 高可用
相关推荐
二哈赛车手1 小时前
新人笔记---Spring AI的Advisor以及其底层机制讲解(涉及源码),包含一些遇见的Spring AI的Advisor缺陷问题的解决方案
java·人工智能·spring boot·笔记·spring
pq2172 小时前
Spring FactoryBean源码解析
java·spring boot·spring
pq2172 小时前
spring如何扫描解析bean(注册bean的多种方式)
spring
IT空门:门主4 小时前
spring ai alibaba -流式+invoke的人工介入的实现
java·后端·spring
人道领域4 小时前
【黑马点评日记】RedisGEO实战:黑马点评附近商铺功能
java·数据库·redis·adb
javachen__5 小时前
Spring MVC 动态支持 JSON/XML 的技巧
spring·springmvc
敲敲千反田6 小时前
Spring 相关
java·后端·spring
薪火铺子6 小时前
Redis 分布式锁与 Redisson 原理深度解析
java·redis·分布式·后端
摇滚侠7 小时前
基于 Redis 实现验证码登录
javascript·redis·bootstrap