Redis缓存击穿、缓存、缓存雪崩?全TM是伪命题!

举报
托尼学长 发表于 2024/11/09 20:56:28 2024/11/09
【摘要】 你们猜,目前的技术面试中, Redis 方向最高频的面试题是哪个,到底是 Redis 的持久化方式、常用数据类型,还是适用场景?其实都不是,最高频的面试题竟然是缓存击穿、缓存穿透和缓存雪崩!我一直觉得本身这三个问题就是伪命题,只要没有20年的脑残经验,工程师根本写不出来这样的代码。下面听我进行一一拆解。缓存击穿缓存击穿的定义是,用户高并发地对某个已经失效的 Redis key 进行请求,从而...

你们猜,目前的技术面试中, Redis 方向最高频的面试题是哪个,到底是 Redis 的持久化方式、常用数据类型,还是适用场景?

其实都不是,最高频的面试题竟然是缓存击穿、缓存穿透和缓存雪崩!

我一直觉得本身这三个问题就是伪命题,只要没有20年的脑残经验,工程师根本写不出来这样的代码。

下面听我进行一一拆解。

缓存击穿

缓存击穿的定义是,用户高并发地对某个已经失效的 Redis key 进行请求,从而导致 MySQL 的压力剧增而系统宕机的情况。

如下图所示:

1.png

其对应的解决方案包括三种:

(1)将 Redis key 设置为永不过期;

(2)从 MySQL 中读取数据时,将 Redis 中没有的 key 重新加载进来;

(3)通过分布式锁限制访问;

其中,通过分布式锁的方式解决缓存穿透问题,实现代码如下:

import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
 
import java.util.concurrent.TimeUnit;
 
public class CacheService {
 
    private RedissonClient redissonClient;
    private static final String KEY_PREFIX = "lock:";
 
    public CacheService() {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");
        redissonClient = Redisson.create(config);
    }
 
    public String getDataWithLock(String key) {
        RLock lock = redissonClient.getLock(KEY_PREFIX + Key);
        try {
            // 尝试获取锁,最多等待100秒,锁定之后10秒自动解锁
            boolean isLocked = lock.tryLock(100, 10, TimeUnit.SECONDS);
            if (isLocked) {
                // 加锁成功,执行数据库查询
                String data = queryDataFromDatabase(key);
                // 更新缓存
                storeDataToCache(key, data);
                return data;
            } else {
                // 加锁失败,表示其他线程正在更新缓存,等待100毫秒后重试
                Thread.sleep(100);
                return getDataWithLock(key); // 递归调用自身
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException(e);
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
 
    private String queryDataFromDatabase(String key) {
        // 模拟从数据库查询数据
        return "data_from_db";
    }
 
    private void storeDataToCache(String key, String data) {
        // 模拟将数据存储到缓存中
    }
}

为什么我说缓存击穿是个伪命题?

因为工程师在日常 coding 代码的时候,谁 TM 会从 MySQL 中读取数据后,明明知道 Redis 中不存在该数据却不重新进行加载呢,要知道这种实现方式早就形成了肌肉记忆了。

呵呵,为了这个场景还专门造了个词叫“缓存击穿”,还莫名其妙地成了高频面试题,真的神奇。


缓存穿透

缓存穿透的定义是,用户高并发地对某些在 Redis 和 MySQL 都不存在的 key 进行请求,从而导致 MySQL 的压力剧增而系统宕机的情况。

如下图所示:

2.png

其对应的解决方案包括两种:

(1)将不存在的 key 存放进Redis中

当出现在 Redis 和 MySQL 中都查不到该数据的情况时,我们就把该数据的 key 保存在 Redis 中,把 value 设置为 null,并为其设置较短的过期时间。

后面再有请求查询该数据的时候,就被 Redis 挡住直接返回 null,而无需二次查询 MySQL 了。

该解决方案的优点是实现简单,无需过多的代码改动,缺点则是无法解决大量不存在的随机 key 进行访问的场景。

(2)使用布隆过滤器(Bloom Filter)

布隆过滤器是一种空间效率很高的概率型数据结构,用于判断一个元素是否存在于一个集合中,具有较高的空间效率和查询速度,但可能会产生误判‌。

如果布隆过滤器判定某个 key 不存在布隆过滤器中,那么就一定不存在,反之判定某个 key 存在,那么极大概率是存在的(存在一定的误判率)。

我们可以通过 Redis 实现一个布隆过滤器,将 MySQL 中所有记录的 key 存储在布隆过滤器中,在请求访问 MySQL 前先通过布隆过滤器判定其是否存在,判定为不存在的请求直接丢弃,判定存在的请求再访问 MySQL 进行查询,从而避免了对 MySQL 的查询压力。

其中,通过布隆过滤器的方式解决缓存击穿问题,实现代码如下:

import org.redisson.Redisson;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
 
public class RedissonBloomFilterExample {
 
    public static void main(String[] args) {
        // 1. 配置RedissonClient
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");
        RedissonClient redisson = Redisson.create(config);
 
        // 2. 创建布隆过滤器
        RBloomFilter<String> bloomFilter = redisson.getBloomFilter("myBloomFilter");
        // 初始化布隆过滤器,假设预计放入10000个元素,期望错误率为0.01
        bloomFilter.tryInit(10000, 0.01);
 
        // 3. 使用布隆过滤器检查元素是否存在
        String key = "myKey";
        if (bloomFilter.contains(key)) {
            // 如果布隆过滤器认为key存在,则尝试从缓存获取数据
            String value = redisson.getBucket(key, String.class).get();
            if (value != null) {
                // 缓存命中,直接返回
                System.out.println("Cache hit: " + value);
            } else {
                // 缓存未命中,可以设置默认值或执行后续逻辑
                System.out.println("Cache miss");
                // 将查询到的null结果或默认值放入布隆过滤器,防止下次查询
                redisson.getBucket(key, String.class).set("defaultValue");
                bloomFilter.add(key);
            }
        } else {
            // 如果布隆过滤器认为key不存在,执行缓存穿透时的后备操作
            System.out.println("Key does not exist: " + key);
            // 将查询到的结果放入布隆过滤器,防止下次查询
            redisson.getBucket(key, String.class).set("defaultValue");
            bloomFilter.add(key);
        }
 
        // 4. 关闭RedissonClient
        redisson.shutdown();
    }
}

为什么我说缓存穿透是个伪命题?

网上大多数的说法,缓存穿透产生的原因是,黑客恶意攻击请求不存在于 Redis 和 MySQL 中的数据,使得这些请求绕过 Redis 全部打在 MySQL 上,使其压力剧增从而导致系统崩溃。

话说术业有专攻,如果真的出现了黑客恶意攻击的情况,那也应该在防火墙层面去进行解决啊,这些又关业务系统和 Redis 什么事呢?如果系统中没用到 Redis,那这种恶意攻击该解决不还是得解决吗?

呵呵,为了这个场景还专门造了个词叫“缓存穿透”,还莫名其妙地成了高频面试题,真的神奇。


缓存雪崩

缓存雪崩的定义是,用户高并发地对系统进行请求期间,Redis 中的 key 在某一个时刻大规模失效,从而导致 MySQL 的压力剧增而系统宕机的情况。

如下图所示:

3.png

其对应的解决方案包括三种:

(1)将 Redis key 设置为永不过期;

(2)通过限流降级来降低 MySQL 压力;

(3)修改 Redis key 过期时间的策略,避免其在同一时刻出现大量过期的情况,比如:在每个 key 的失效时间上设定一个随机值;

其中,在每个 key 的失效时间上设定一个随机值来解决缓存雪崩问题,实现代码如下:

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import java.util.Random;
import java.util.concurrent.TimeUnit;
 
public class RedisCacheWithRandomTTL {
 
    private RedissonClient redissonClient;
    private static final int MAX_TTL = 300; // 最大过期时间设定为300秒
    private static final Random random = new Random();
 
    public RedisCacheWithRandomTTL() {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");
        redissonClient = Redisson.create(config);
    }
 
    public void setValueWithRandomTTL(String key, String value) {
        int ttl = random.nextInt(MAX_TTL) + 1; // 生成1到MAX_TTL之间的随机数
        redissonClient.getBucket(key, String.class).set(value, ttl, TimeUnit.SECONDS);
    }
 
    public static void main(String[] args) {
        RedisCacheWithRandomTTL cache = new RedisCacheWithRandomTTL();
        cache.setValueWithRandomTTL("myKey", "myValue");
    }
}

为什么我说缓存雪崩是个伪命题?

因为工程师习惯于把 Redis key 的过期时间设置为固定值,这种情况很难出现在同一时刻 key 大量过期的情况。

如果真的要硬找场景的话,那也就是在应用服务器启动的时候进行缓存预热,在同一时刻大量的 key 被刷到 Redis 中,并设置上固定的过期时间。

可正常情况下,应用服务器启动预热的 Redis key,都是常驻在 Redis 中的,谁还设置一个过期时间啊?

呵呵,为了这个场景还专门造了个词叫“缓存雪崩”,还莫名其妙地成了高频面试题,真的神奇。

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。