Redis缓存击穿、缓存、缓存雪崩?全TM是伪命题!
你们猜,目前的技术面试中, Redis 方向最高频的面试题是哪个,到底是 Redis 的持久化方式、常用数据类型,还是适用场景?
其实都不是,最高频的面试题竟然是缓存击穿、缓存穿透和缓存雪崩!
我一直觉得本身这三个问题就是伪命题,只要没有20年的脑残经验,工程师根本写不出来这样的代码。
下面听我进行一一拆解。
缓存击穿
缓存击穿的定义是,用户高并发地对某个已经失效的 Redis key 进行请求,从而导致 MySQL 的压力剧增而系统宕机的情况。
如下图所示:
其对应的解决方案包括三种:
(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 的压力剧增而系统宕机的情况。
如下图所示:
其对应的解决方案包括两种:
(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 的压力剧增而系统宕机的情况。
如下图所示:
其对应的解决方案包括三种:
(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 中的,谁还设置一个过期时间啊?
呵呵,为了这个场景还专门造了个词叫“缓存雪崩”,还莫名其妙地成了高频面试题,真的神奇。
- 点赞
- 收藏
- 关注作者
评论(0)