防止缓存击穿、缓存穿透和缓存雪崩

2024-09-08 14:28

本文主要是介绍防止缓存击穿、缓存穿透和缓存雪崩,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

使用Redis缓存防止缓存击穿、缓存穿透和缓存雪崩

在高并发系统中,缓存击穿、缓存穿透和缓存雪崩是三种常见的缓存问题。本文将介绍如何使用Redis、分布式锁和布隆过滤器有效解决这些问题,并且会通过Java代码详细说明实现的思路和原因。


1. 背景

缓存穿透:指的是大量请求缓存中不存在且数据库中也不存在的数据,导致大量请求直接打到数据库上,形成数据库压力。

缓存击穿:指的是某个热点数据在高并发时失效,大量请求同时穿透缓存,导致数据库负载瞬间激增。

缓存雪崩:指的是缓存集中过期或宕机,导致短时间内大量请求打到数据库,压垮后端服务。

为了应对这三种情况,本文介绍了三种方法:

  1. 缓存穿透:使用空值缓存和布隆过滤器。
  2. 缓存击穿:使用分布式锁保证只有一个线程访问数据库并重建缓存。
  3. 缓存雪崩:合理设置缓存过期时间,防止大规模缓存同时失效。

2. 缓存穿透处理

我们首先来看queryWithPassThrough方法,它处理的是缓存穿透问题。

public <R, P> R queryWithPassThrough(String key, Class<R> clazz, Function<P, R> bdCallback, P params, Duration duration) {// 1. 从缓存中查询数据String dataJsonStr = stringRedisTemplate.opsForValue().get(key);// 2. 如果缓存中存在数据,直接返回if (StrUtil.isNotBlank(dataJsonStr)) {return JSONUtil.toBean(dataJsonStr, clazz);}// 3. 如果缓存存在的是空字符串,表示数据库中也没有该数据,直接返回null,防止缓存穿透if ("".equals(dataJsonStr)) {return null;}// 4. 缓存没有命中,调用数据库查询R data = bdCallback.apply(params);// 5. 如果数据库中没有数据,缓存空字符串以防止缓存穿透,并设置较短的过期时间if (data == null) {stringRedisTemplate.opsForValue().set(key, "", Duration.ofSeconds(20));return null;}// 6. 数据库查询有结果,将结果缓存并设置过期时间stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(data), duration);return data;
}
解析:
  1. 缓存查询:首先从Redis缓存中查询数据,如果有数据,直接返回。
  2. 缓存空值机制:如果缓存中存储的是空字符串(即以前数据库查询也没有该数据),防止缓存穿透(同一个不存在的请求反复击穿缓存,查询数据库)。
  3. 查询数据库:当缓存没有命中且没有空值时,查询数据库并缓存结果。如果数据库中没有数据,缓存空值并设置短期过期时间(例如20秒)。
为什么要缓存空值?

防止缓存穿透。如果没有缓存空值,对于不存在的数据的查询会反复击穿缓存,导致数据库压力过大。


3. 缓存击穿处理

缓存击穿问题通常发生在热点数据过期时,同时有大量请求到达数据库。为了解决这个问题,我们使用分布式锁来保证在缓存失效时,只有一个线程能够访问数据库并更新缓存。

private <R, P> R queryWithMutex(String key, Class<R> clazz, Duration expireSeconds, Function<P, R> bdCallback, P params) {// 1. 从缓存查询数据String dataJson = stringRedisTemplate.opsForValue().get(key);// 2. 如果缓存中有数据,直接返回if (StrUtil.isNotBlank(dataJson)) {return JSONUtil.toBean(dataJson, clazz);}// 3. 如果缓存中是空值,返回null,防止穿透if ("".equals(dataJson)) {return null;}// 4. 缓存没有命中,尝试加锁String lock = "lock:" + key;try {while (!tryLock(lock)) {// 如果获取不到锁,等待并重复检查缓存Thread.sleep(50L);dataJson = stringRedisTemplate.opsForValue().get(key);if (StrUtil.isNotBlank(dataJson)) {return JSONUtil.toBean(dataJson, clazz);}if ("".equals(dataJson)) {return null;}}// 5. 加锁后再次检查缓存,防止其他线程已经重建缓存dataJson = stringRedisTemplate.opsForValue().get(key);if (StrUtil.isNotBlank(dataJson)) {return JSONUtil.toBean(dataJson, clazz);}if ("".equals(dataJson)) {return null;}// 6. 如果缓存没有数据,查询数据库R apply = bdCallback.apply(params);if (apply == null) {// 如果数据库没有数据,缓存空值stringRedisTemplate.opsForValue().set(key, "", Duration.ofMinutes(2));return null;}// 7. 将数据库数据存入缓存stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(apply), expireSeconds);return apply;} catch (Exception e) {throw new RuntimeException(e);} finally {// 8. 释放锁releaseLock(lock);}
}
解析:
  1. 缓存查询:如果缓存中有数据,直接返回,避免击穿。
  2. 加锁机制:如果缓存没有数据,尝试获取锁。如果获取不到锁,说明其他线程正在重建缓存,此时线程等待并轮询缓存,直到锁释放或缓存更新。
  3. 缓存重建:拿到锁的线程查询数据库,并将结果存入缓存。
  4. 锁的释放:确保在缓存重建完成后释放锁,避免死锁。
为什么需要加锁?

防止缓存击穿时,多个线程同时查询数据库,导致数据库压力激增。通过分布式锁,保证只有一个线程可以查询数据库并更新缓存。


4. 逻辑过期处理

为了解决缓存雪崩问题,可以通过逻辑过期方式处理热点数据的过期。即使数据过期了,系统也能继续返回旧数据,同时后台线程异步更新缓存。

private <R, P> R queryWithLogicalExpire(String key, Class<R> clazz, Duration expireSeconds, Function<P, R> bdCallback, P params) {// 1. 从缓存查询数据String dataJson = stringRedisTemplate.opsForValue().get(key);if (StrUtil.isBlank(dataJson)) {return null;}// 2. 反序列化数据RedisData data = JSONUtil.toBean(dataJson, RedisData.class);R r = JSONUtil.toBean((JSONObject) data.getData(), clazz);// 3. 检查数据是否过期,如果没有过期,直接返回数据if (data.getExpireTime().isAfter(LocalDateTime.now())) {return r;}// 4. 如果数据已过期,尝试加锁,进行缓存重建String lock = "lock:" + key;if (tryLock(lock)) {try {dataJson = stringRedisTemplate.opsForValue().get(key);data = JSONUtil.toBean(dataJson, RedisData.class);if (data.getExpireTime().isAfter(LocalDateTime.now())) {return JSONUtil.toBean((JSONObject) data.getData(), clazz);}// 5. 异步重建缓存CACHE_REBUILD_EXECUTOR.submit(() -> {try {R newData = bdCallback.apply(params);RedisData redisData = new RedisData(newData, LocalDateTime.now().plusSeconds(expireSeconds.getSeconds()));stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));} catch (Exception e) {throw new RuntimeException(e);} finally {releaseLock(lock);}});} finally {releaseLock(lock);}}// 6. 返回过期数据,保证系统可用性return r;
}
解析:
  1. 缓存逻辑过期:数据存入缓存时,同时存储逻辑过期时间。即使数据过期了,系统可以继续返回旧数据,防止雪崩。
  2. 后台异步更新:通过线程池,异步执行缓存重建,防止阻塞用户请求。
  3. 双重检查:在获取锁后,再次检查缓存

,避免重复重建缓存。

为什么需要异步更新?

即使数据过期,也可以返回旧数据,保证服务可用性。同时在后台异步更新缓存,减少对前台服务的影响。


5. 分布式锁

为确保只有一个线程能重建缓存,我们使用了Redis的setIfAbsent方法实现分布式锁,并且为锁设置了过期时间,防止死锁。

private boolean tryLock(String key) {// 尝试获取锁,并设置锁的过期时间,防止死锁Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", Duration.ofSeconds(10L));return Boolean.TRUE.equals(flag);
}private void releaseLock(String key) {// 释放锁stringRedisTemplate.delete(key);
}
解析:
  • 获取锁:通过setIfAbsent,确保只有一个线程能获取锁,并设置锁过期时间,防止死锁。
  • 释放锁:任务完成后,释放锁,允许其他线程继续操作。
为什么要设置锁的过期时间?

防止由于异常情况(例如服务器宕机)导致锁无法释放,产生死锁问题。


6. 总结

通过使用Redis缓存、分布式锁和逻辑过期策略,我们可以有效解决缓存穿透缓存击穿缓存雪崩问题。通过这套方案,我们能够在保证系统高可用的同时,减少数据库压力,并提高服务的性能和稳定性。

关键点包括:

  • 缓存空值机制:防止缓存穿透。
  • 分布式锁:防止缓存击穿时多个线程同时访问数据库。

这套方案对于处理高并发场景下的缓存问题是非常有效的。希望这篇文章能帮助大家更好地理解和应对缓存问题。

这篇关于防止缓存击穿、缓存穿透和缓存雪崩的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1148362

相关文章

Java实现本地缓存的常用方案介绍

《Java实现本地缓存的常用方案介绍》本地缓存的代表技术主要有HashMap,GuavaCache,Caffeine和Encahche,这篇文章主要来和大家聊聊java利用这些技术分别实现本地缓存的方... 目录本地缓存实现方式HashMapConcurrentHashMapGuava CacheCaffe

如何更改pycharm缓存路径和虚拟内存分页文件位置(c盘爆红)

《如何更改pycharm缓存路径和虚拟内存分页文件位置(c盘爆红)》:本文主要介绍如何更改pycharm缓存路径和虚拟内存分页文件位置(c盘爆红)问题,具有很好的参考价值,希望对大家有所帮助,如有... 目录先在你打算存放的地方建四个文件夹更改这四个路径就可以修改默认虚拟内存分页js文件的位置接下来从高级-

PyCharm如何更改缓存位置

《PyCharm如何更改缓存位置》:本文主要介绍PyCharm如何更改缓存位置的实现方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录PyCharm更改缓存位置1.打开PyCharm的安装编程目录2.将config、sjsystem、plugins和log的路径

JSR-107缓存规范介绍

《JSR-107缓存规范介绍》JSR是JavaSpecificationRequests的缩写,意思是Java规范提案,下面给大家介绍JSR-107缓存规范的相关知识,感兴趣的朋友一起看看吧... 目录1.什么是jsR-1072.应用调用缓存图示3.JSR-107规范使用4.Spring 缓存机制缓存是每一

Spring 缓存在项目中的使用详解

《Spring缓存在项目中的使用详解》Spring缓存机制,Cache接口为缓存的组件规范定义,包扩缓存的各种操作(添加缓存、删除缓存、修改缓存等),本文给大家介绍Spring缓存在项目中的使用... 目录1.Spring 缓存机制介绍2.Spring 缓存用到的概念Ⅰ.两个接口Ⅱ.三个注解(方法层次)Ⅲ.

Spring Boot 整合 Redis 实现数据缓存案例详解

《SpringBoot整合Redis实现数据缓存案例详解》Springboot缓存,默认使用的是ConcurrentMap的方式来实现的,然而我们在项目中并不会这么使用,本文介绍SpringB... 目录1.添加 Maven 依赖2.配置Redis属性3.创建 redisCacheManager4.使用Sp

springboot项目redis缓存异常实战案例详解(提供解决方案)

《springboot项目redis缓存异常实战案例详解(提供解决方案)》redis基本上是高并发场景上会用到的一个高性能的key-value数据库,属于nosql类型,一般用作于缓存,一般是结合数据... 目录缓存异常实践案例缓存穿透问题缓存击穿问题(其中也解决了穿透问题)完整代码缓存异常实践案例Red

Java应用如何防止恶意文件上传

《Java应用如何防止恶意文件上传》恶意文件上传可能导致服务器被入侵,数据泄露甚至服务瘫痪,因此我们必须采取全面且有效的防范措施来保护Java应用的安全,下面我们就来看看具体的实现方法吧... 目录恶意文件上传的潜在风险常见的恶意文件上传手段防范恶意文件上传的关键策略严格验证文件类型检查文件内容控制文件存储

防止SpringBoot程序崩溃的几种方式汇总

《防止SpringBoot程序崩溃的几种方式汇总》本文总结了8种防止SpringBoot程序崩溃的方法,包括全局异常处理、try-catch、断路器、资源限制、监控、优雅停机、健康检查和数据库连接池配... 目录1. 全局异常处理2. 使用 try-catch 捕获异常3. 使用断路器4. 设置最大内存和线

Spring三级缓存解决循环依赖的解析过程

《Spring三级缓存解决循环依赖的解析过程》:本文主要介绍Spring三级缓存解决循环依赖的解析过程,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、循环依赖场景二、三级缓存定义三、解决流程(以ServiceA和ServiceB为例)四、关键机制详解五、设计约