DukeDuke
主页
文档转换
关于我们
主页
文档转换
关于我们
  • Redis

    • Redis简介
    • Redis(单机)安装
    • Redis配置
    • Redis数据结构
    • RDB、AOF 和混合持久化机制
    • Redis内存管理
    • Redis缓存一致性
    • Redis缓存穿透
    • Redis缓存击穿
    • Redis缓存雪崩
    • Redis Lua脚本
    • Redis主从复制
    • Redis哨兵模式
    • Redis集群
    • Redis数据分片
    • Redis CPU使用率过高
    • Redis面试题
  • MySQL

    • MySQL简介
    • MySQL安装
    • MySQL配置
    • MYSQL日常维护
    • MYSQL优化-慢查询
    • MYSQL优化-索引
    • MYSQL数据库设计规范

Redis 缓存雪崩

缓存雪崩是指缓存中大量的 key 同时过期,导致大量的请求直接打到数据库上,从而压垮数据库。这种情况通常发生在系统启动时或者缓存预热时,大量的 key 同时过期,导致系统性能急剧下降。

1. 缓存雪崩的原因

  1. 大量 key 同时过期:在系统启动时或者缓存预热时,大量的 key 同时过期,导致大量的请求直接打到数据库上。

  2. Redis 服务宕机:如果 Redis 服务宕机,所有的请求都会直接打到数据库上。

  3. 缓存预热不当:在系统启动时,如果没有合理的缓存预热策略,可能会导致大量的 key 同时过期。

2. 缓存雪崩的解决方案

2.1 设置不同的过期时间

为了避免大量的 key 同时过期,可以给不同的 key 设置不同的过期时间,比如在基础过期时间上加上一个随机值。

@Service
public class UserService {
    @Autowired
    private StringRedisTemplate redisTemplate;

    public void cacheUser(User user) {
        String key = "user:" + user.getId();
        // 基础过期时间:1小时
        int baseExpire = 3600;
        // 随机过期时间:0-300秒
        int randomExpire = new Random().nextInt(300);
        // 最终过期时间:1小时 + 随机时间
        int finalExpire = baseExpire + randomExpire;

        redisTemplate.opsForValue().set(key, JSON.toJSONString(user), finalExpire, TimeUnit.SECONDS);
    }
}

2.2 使用互斥锁

当缓存未命中时,使用互斥锁确保只有一个请求去查询数据库,其他请求等待。

@Service
public class UserService {
    @Autowired
    private StringRedisTemplate redisTemplate;

    public User getUserById(Long id) {
        String key = "user:" + id;
        // 1. 先从缓存中查询
        String userJson = redisTemplate.opsForValue().get(key);
        if (userJson != null) {
            return JSON.parseObject(userJson, User.class);
        }

        // 2. 缓存未命中,尝试获取锁
        String lockKey = "lock:" + key;
        Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
        if (Boolean.TRUE.equals(locked)) {
            try {
                // 3. 获取锁成功,查询数据库
                User user = userMapper.selectById(id);
                if (user != null) {
                    // 4. 将结果写入缓存
                    redisTemplate.opsForValue().set(key, JSON.toJSONString(user), 3600, TimeUnit.SECONDS);
                }
                return user;
            } finally {
                // 5. 释放锁
                redisTemplate.delete(lockKey);
            }
        } else {
            // 6. 获取锁失败,等待一段时间后重试
            try {
                Thread.sleep(100);
                return getUserById(id);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                return null;
            }
        }
    }
}

2.3 使用缓存降级

当缓存服务不可用时,可以降级为本地缓存或者直接返回默认值。

@Service
public class UserService {
    @Autowired
    private StringRedisTemplate redisTemplate;

    // 本地缓存
    private final Map<Long, User> localCache = new ConcurrentHashMap<>();

    public User getUserById(Long id) {
        // 1. 先从本地缓存中查询
        User user = localCache.get(id);
        if (user != null) {
            return user;
        }

        try {
            // 2. 本地缓存未命中,查询Redis
            String key = "user:" + id;
            String userJson = redisTemplate.opsForValue().get(key);
            if (userJson != null) {
                user = JSON.parseObject(userJson, User.class);
                // 更新本地缓存
                localCache.put(id, user);
                return user;
            }

            // 3. Redis未命中,查询数据库
            user = userMapper.selectById(id);
            if (user != null) {
                // 更新Redis和本地缓存
                redisTemplate.opsForValue().set(key, JSON.toJSONString(user), 3600, TimeUnit.SECONDS);
                localCache.put(id, user);
            }
            return user;
        } catch (Exception e) {
            // 4. Redis服务异常,降级为本地缓存
            return localCache.getOrDefault(id, new User());
        }
    }
}

2.4 使用缓存预热

在系统启动时,提前将热点数据加载到缓存中。

@Service
public class CacheWarmUpService {
    @Autowired
    private StringRedisTemplate redisTemplate;
    @Autowired
    private UserMapper userMapper;

    @PostConstruct
    public void warmUp() {
        // 1. 获取热点数据
        List<User> hotUsers = userMapper.getHotUsers();

        // 2. 将热点数据写入缓存
        for (User user : hotUsers) {
            String key = "user:" + user.getId();
            // 设置不同的过期时间
            int baseExpire = 3600;
            int randomExpire = new Random().nextInt(300);
            int finalExpire = baseExpire + randomExpire;

            redisTemplate.opsForValue().set(key, JSON.toJSONString(user), finalExpire, TimeUnit.SECONDS);
        }
    }
}

3. 最佳实践

  1. 合理设置过期时间:避免大量的 key 同时过期,可以给不同的 key 设置不同的过期时间。

  2. 使用互斥锁:当缓存未命中时,使用互斥锁确保只有一个请求去查询数据库。

  3. 使用缓存降级:当缓存服务不可用时,可以降级为本地缓存或者直接返回默认值。

  4. 使用缓存预热:在系统启动时,提前将热点数据加载到缓存中。

  5. 监控缓存状态:实时监控缓存的状态,及时发现和处理缓存雪崩问题。

  6. 使用多级缓存:可以使用多级缓存来减少缓存雪崩的影响,比如本地缓存 + Redis 缓存。

  7. 使用限流措施:当检测到缓存雪崩时,可以使用限流措施来保护数据库。

4. 注意事项

  1. 互斥锁方案要注意死锁问题
  2. 本地缓存要注意内存占用
  3. 缓存预热要注意预热时机
  4. 降级策略要考虑数据一致性
  5. 限流措施要考虑业务影响
最近更新:: 2026/4/17 13:21
Contributors: Duke
Prev
Redis缓存击穿
Next
Redis Lua脚本