当前位置: 首页 > news >正文

redis缓存常见问题

redis缓存常见问题

一、缓存三剑客(穿透、雪崩、击穿)

1.redis穿透

(1)什么是redis缓存穿透

缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求就都打到数据库里面了。

穿透穿透,顾名思义,就是请求穿过了redis又穿过了数据库(Mysql)就像下面的图片一样,请求就是子弹,redis就是防弹衣,Mysql就是身体,当我们请求redis缓存没有命中时,就会打到数据库上。这就是穿透。

在这里插入图片描述

(注:下面这个图片是我在哔哩哔哩观看徐庶老师的课所截的图,感觉非常的形象,视频地址如下:Redis夺命连环40问,1天掌握别人半个月刷的redis数据库面试内容,直接让你上岸,成功拿下45K!_哔哩哔哩_bilibili)

(2)解决方法
(2.1)缓存空对象

当数据库中没有查询到数据时,可以将这个请求存储到Redis中,并设置一个较短的过期时间(如5分钟)。这样,在发送这样的请求就打到redis上,不会打到数据库上了(在这设置的5分钟里)。

其实就是将请求过来在redis缓存和数据库里都没有的数据存储到redis中,防止这个请求在打到数据库。

如下代码的CACHE_SHOP_TTL是一个常量,设置的过期时间。

public Shop queryWithPassThrough(Long id) {String key = CACHE_SHOP_KEY + id;// 1. 从Redis查询商铺缓存String shopJson = stringRedisTemplate.opsForValue().get(key);// 2. 缓存命中(包含空值)直接返回if (shopJson != null) {// 反序列化空字符串为nullreturn shopJson.equals("") ? null : JSONUtil.toBean(shopJson, Shop.class);}// 3. 缓存未命中,查询数据库Shop shop = getById(id);// 4. 数据库中不存在,缓存空值if (shop == null) {// 缓存空字符串(或特定标识),并设置较短过期时间stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);return null;}// 5. 数据库中存在,写入Redis缓存(正常TTL)stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);// 6. 返回结果return shop;
}
(2.2)布隆过滤

在缓存和数据库之间加入一个布隆过滤器,它可以预存储一些可能存在的键。如果查询的键不在布隆过滤器中,直接返回不存在,避免查询数据库。布隆过滤器通过哈希函数实现,误判率可以通过调整其大小和哈希函数的数量来控制。(有黑名单与白名单两种)(其实都是判断数据库里,有没有,没有就存入布隆过滤器,这只是我个人的理解)

在这里插入图片描述

(注:该图片来自黑马的哔哩哔哩视频,视频地址如下:实战篇-商户查询缓存-06.缓存穿透的解决思路_哔哩哔哩_bilibili)

2.缓存雪崩

(1)什么是缓存雪崩

缓存雪崩是指同一时段大量的缓存的key同时失效或redis服务宕机,导致大量请求到达数据库,带来巨大压力。

在这里插入图片描述

(注:下面这个图片是我在哔哩哔哩观看徐庶老师的课所截的图,感觉非常的形象,视频地址如下:Redis夺命连环40问,1天掌握别人半个月刷的redis数据库面试内容,直接让你上岸,成功拿下45K!_哔哩哔哩_bilibili)

(2)解决办法
(2.1)给不同的key添加随机时间(防止大量key同时过期问题)

实现方式
在设置缓存时,为每个 key 的过期时间增加一个随机偏移量。基础过期时间保证数据不会长时间不更新,随机范围则确保各个 key 不会集中失效。

给不同的 key 添加随机时间

public class RandomExpireTimeCacheService {@Autowiredprivate RedisTemplate<String, Object> redisTemplate;// 基础过期时间(秒)private static final long BASE_EXPIRE_TIME = 60 * 30;  // 30分钟// 随机范围(秒)private static final long RANDOM_RANGE = 60 * 15;  // 15分钟private final Random random = new Random();/*** 设置带有随机过期时间的缓存* @param key 缓存键* @param value 缓存值*/public void setWithRandomExpireTime(String key, Object value) {// 生成随机过期时间:基础时间 + 随机时间long expireTime = BASE_EXPIRE_TIME + random.nextInt((int) RANDOM_RANGE);redisTemplate.opsForValue().set(key, value, expireTime, TimeUnit.SECONDS);}
}
(2.2)利用redis集群(如哨兵模式,有效防止redis宕机带来的问题)

核心思想:通过部署 Redis 集群提高可用性,避免因单点故障导致的缓存服务整体不可用,从而引发雪崩。

哨兵模式工作原理

  • 哨兵节点 (Sentinel) 监控主从节点状态
  • 当主节点故障时,自动进行主从切换
  • 客户端通过哨兵获取 Redis 服务地址

利用 Redis 集群(哨兵模式)配置

@Configuration
public class RedisSentinelConfig {@Beanpublic RedisSentinelConfiguration sentinelConfiguration() {RedisSentinelConfiguration sentinelConfig = new RedisSentinelConfiguration().master("mymaster")  // 主节点名称.sentinels(Arrays.asList(new RedisNode("sentinel1.host", 26379),new RedisNode("sentinel2.host", 26379),new RedisNode("sentinel3.host", 26379)));return sentinelConfig;}@Beanpublic JedisConnectionFactory jedisConnectionFactory() {return new JedisConnectionFactory(sentinelConfiguration());}@Beanpublic RedisTemplate<String, Object> redisTemplate() {RedisTemplate<String, Object> template = new RedisTemplate<>();template.setConnectionFactory(jedisConnectionFactory());return template;}
}
(2.3)限流降级

核心思想:当系统负载过高时,通过限制流量和降级非核心服务,确保核心功能的可用性,防止整个系统被拖垮。

  • 限流:控制请求的访问速率,防止过多请求进入系统
  • 降级:当检测到系统异常时,自动返回预设的默认值或错误信息
// 使用Sentinel注解定义受保护的资源
@SentinelResource(value = "protectedResource", blockHandler = "handleBlock")
public String process(String param) {// 正常业务逻辑
}// 限流降级处理方法
public String handleBlock(String param, BlockException ex) {// 资源被限流或降级时的处理逻辑return "系统繁忙,请稍后再试";
}
(2.4)添加多级缓存

核心思想:通过组合本地缓存和分布式缓存,减少对 Redis 的访问频率,提高系统响应速度,同时增强系统容错能力。


@Service
public class MultiLevelCacheService {// Caffeine本地缓存private final Cache<String, Object> localCache;// Redis分布式缓存@Autowiredprivate RedisTemplate<String, Object> redisTemplate;// 缓存默认过期时间(分钟)private static final long DEFAULT_EXPIRE_TIME = 30;public MultiLevelCacheService() {// 初始化本地缓存this.localCache = Caffeine.newBuilder().maximumSize(1000)  // 最大缓存条目数.expireAfterWrite(DEFAULT_EXPIRE_TIME, TimeUnit.MINUTES)  // 写入后过期时间.build();}/*** 从多级缓存中获取数据* @param key 缓存键* @param dataLoader 数据加载器,当缓存未命中时用于加载数据* @param expireTime 缓存过期时间(分钟)* @return 缓存值*/public <T> T get(String key, Supplier<T> dataLoader, long expireTime) {// 1. 先从本地缓存获取T value = (T) localCache.getIfPresent(key);if (value != null) {return value;}// 2. 本地缓存未命中,从Redis获取value = (T) redisTemplate.opsForValue().get(key);if (value != null) {// 将数据写入本地缓存localCache.put(key, value);return value;}// 3. Redis未命中,从数据源加载数据value = dataLoader.get();if (value != null) {// 将数据写入RedisredisTemplate.opsForValue().set(key, value, expireTime, TimeUnit.MINUTES);// 将数据写入本地缓存localCache.put(key, value);}return value;}/*** 从多级缓存中获取数据(使用默认过期时间)* @param key 缓存键* @param dataLoader 数据加载器,当缓存未命中时用于加载数据* @return 缓存值*/public <T> T get(String key, Supplier<T> dataLoader) {return get(key, dataLoader, DEFAULT_EXPIRE_TIME);}/*** 更新缓存* @param key 缓存键* @param value 缓存值* @param expireTime 过期时间(分钟)*/public void update(String key, Object value, long expireTime) {// 更新Redis缓存redisTemplate.opsForValue().set(key, value, expireTime, TimeUnit.MINUTES);// 更新本地缓存localCache.put(key, value);}/*** 删除缓存* @param key 缓存键*/public void delete(String key) {// 删除Redis缓存redisTemplate.delete(key);// 删除本地缓存localCache.invalidate(key);}
}

3、缓存击穿

(1)什么是缓存击穿

缓存击穿问题也叫热点 Key 问题,就是一个被高并发访问并且缓存重建业务较复杂的 key 突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。

在这里插入图片描述

(注:下面这个图片是我在哔哩哔哩观看徐庶老师的课所截的图,感觉非常的形象,视频地址如下:Redis夺命连环40问,1天掌握别人半个月刷的redis数据库面试内容,直接让你上岸,成功拿下45K!_哔哩哔哩_bilibili)

(2)解决方案
(2.1)互斥锁

使用互斥锁确保只有一个线程可以查询数据库并更新缓存。其他线程等待锁释放后直接从缓存中获取数据。可以使用Redis的SETNX命令实现分布式锁。

注意:可能发送死锁问题,需要设置有效期。(当一个线程获取锁成功之后,程序出问题了,没有释放锁,就可能发生死锁)

在这里插入图片描述

(注:该图片来自黑马的哔哩哔哩视频,视频地址如下:实战篇-商户查询缓存-06.缓存穿透的解决思路_哔哩哔哩_bilibili)

相关代码(通过redis的SETNX命令实现)

//写一个自定义锁//获取锁private boolean tryLock(String key){Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);return BooleanUtil.isTrue(flag);}//释放锁private void unLock(String key){stringRedisTemplate.delete(key);}

使用锁的代码块(主要是第4部分,实现缓存重建)

//互斥锁解决缓存击穿代码块private Shop queryWithMutex(Long id) {String key = RedisConstants.CACHE_SHOP_KEY + id;
//        1.从redis查询缓存String shopJson = stringRedisTemplate.opsForValue().get(key);
//        2.判断缓存是否存在if (StrUtil.isNotBlank(shopJson)) {//        3.存在,返回Shop shop = JSONUtil.toBean(shopJson, Shop.class);return shop;}//判断命中是否是空值if (shopJson != null) {//返回错误信息return null;}
//        4.实现缓存重建String lock = RedisConstants.LOCK_SHOP_KEY + id;Shop shop = null;try {
//        4.1  获取互斥锁boolean isLock = tryLock(lock);
//        4.2判断获取锁是否成功if (!isLock){//            4.3不成功,休眠一段时间重试Thread.sleep(50);}
//        4.成功,查询数据库shop = getById(id);if (shop == null) {//将空值存入redisstringRedisTemplate.opsForValue().set(key, "", RedisConstants.CACHE_NULL_TTL);return null;}
//        5.写入redisstringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop),  RedisConstants.CACHE_SHOP_TTL);} catch (InterruptedException e) {throw new RuntimeException(e);} finally {// 6.释放互斥锁unLock(lock);}
//        7.返回return shop;}
(2.2)逻辑过期

逻辑过期并非真正在缓存层面设置过期时间,而是在缓存数据结构中增加一个代表过期时间的字段 。当应用程序读取缓存时,通过判断该字段与当前时间的关系,来确定数据是否 “过期”。如果数据被判定为 “过期”,应用程序会在后台异步地对数据进行更新,而在更新完成前,仍然返回旧数据给请求方。

在这里插入图片描述

(注:该图片来自黑马的哔哩哔哩视频,视频地址如下:实战篇-商户查询缓存-06.缓存穿透的解决思路_哔哩哔哩_bilibili)

定义一个 RedisData 类,用于将实际缓存数据和逻辑过期时间封装在一起,方便后续操作和判断。

// 用于封装缓存数据及其逻辑过期时间
public class RedisData {private LocalDateTime expireTime; // 逻辑过期时间private Object data; // 实际缓存的数据
}

重建缓存代码(设置过期时间)

  //逻辑过期解决缓存击穿代码块private void saveShop2Redis(Long id, Long expireSeconds) {//1查询店铺数据Shop shop = getById(id);//2封装逻辑过期时间RedisData redisData = new RedisData();redisData.setData(shop);redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));//3写入redisstringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));}

通过逻辑过期实现缓存击穿代码块

 //线程池private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);//逻辑过期解决缓存击穿代码块private Shop queryWithLogicalExpire(Long id) {String key = RedisConstants.CACHE_SHOP_KEY + id;
//        1.从redis查询缓存String shopJson = stringRedisTemplate.opsForValue().get(key);
//        2.判断缓存是否存在if (StrUtil.isBlank(shopJson)) {//        3.不存在,返回return null;}
//        4.命中,把JSON反序列化为对象RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);LocalDateTime expireTime = redisData.getExpireTime();
//        5.判断是否过期if (expireTime.isAfter(LocalDateTime.now())) {//        5.1未过期,直接返回信息return shop;}
//        5.2  已过期,需要重建缓存
//        6.缓存重建
//        6.1获取互斥锁String lock = RedisConstants.LOCK_SHOP_KEY + id;boolean isLock = tryLock(lock);
//        6.2判断获取锁是否成功if (!isLock){
//        6.3  成功,开启独立线程,实现缓存重建CACHE_REBUILD_EXECUTOR.submit(() -> {try {//        6.3.1缓存重建this.saveShop2Redis(id, 20L);} catch (Exception e) {throw new RuntimeException(e);} finally {//        6.3.2释放锁unLock(lock);}});}
//        7.失败,返回过期信息return shop;}

相关文章:

  • 12.7 LangChain实战:1.2秒响应!用LCEL构建高效RAG系统,准确率提升41%
  • 力扣 88.合并两个有序数组
  • vscode配置lua
  • PowerShell脚本编程基础指南
  • 《认知觉醒》第二章——驯服你的“脑内大象”:理智、本能与情绪的共生之道
  • 【Harmony OS】数据存储
  • Modbus转Ethernet IP网关助力罗克韦尔PLC数据交互
  • 项目目标和期望未被清晰传达,如何改进?
  • 【计算机网络】第七章 运输层
  • 动态规划-数位DP
  • 【学习笔记】深度学习-过拟合解决方案
  • 基于Halcon深度学习之分类
  • 【bpmn.js 使用总结】最简单实现Palette
  • 在Mathematica中实现Newton-Raphson迭代
  • 从零打造AI面试系统全栈开发
  • 生成JavaDoc文档
  • [Java 基础]运算符,将盒子套起来
  • Qiskit:量子计算模拟器
  • 01-python爬虫-第一个爬虫程序
  • VueUse:组合式API实用函数全集
  • 男的做直播哪个网站/主流网站关键词排名
  • 企业邮箱是哪个/360排名优化工具
  • JAVA网站开发ssm/企业邮箱如何申请注册
  • 有哪些网站开发技术/全网线报 实时更新
  • 建网站的域名是什么意思/如何引流推广产品
  • 建筑设计公司名称/厦门零基础学seo