缓存使用的最佳实践,自定义缓存工具类

缓存穿透问题解决-缓存空值

访问数据库不存在的数据,会一直请求到数据库,被别有用心的人使用,可能会一直请求数据库,导致数据库宕机。解决方法有两

一:缓存空数据,二,使用布隆过滤器进行校验。

缓存空数据

在数据库查询到不存在的数据时,对该数据进行缓存为空(可以设置稍短的3~5分钟的TTL),之后相同的请求,就会在缓存中查到,而不去请求数据库。

代码案列

  /*** 查询商户信息* @param id* @return*/@Overridepublic Result queryById(Long id) {//查询缓存String string = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY+id);//hutool 工具类 符合条件“adc" 不符合条件“”,null, "/t/n"if (StrUtil.isNotBlank(string)){Shop shop = JSONUtil.toBean(string, Shop.class);return Result.ok(shop);}//若是 " " 上面已经判断了不是“” 不是null ,if(string != null){return Result.fail("商户不存在");}// 缓存不存在 查数据库Shop shop = getById(id);if (shop ==null) {//将空值写入缓存stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, "", CACHE_NULL_TTL, TimeUnit.MINUTES);return Result.fail("商户不存在");}//写入缓存stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+ id, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);return Result.ok(shop);}

缓存击穿问题- 互斥锁

热点key失效,构造缓存复杂,在构造缓存的期间大量请求,只允许一个请求到数据库构造缓存。

具体流程

假设现在线程1过来访问,他查询缓存没有命中,但是此时他获得到了锁的资源,那么线程1就会一个人去执行逻辑,假设现在线程2过来,线程2在执行过程中,并没有获得到锁,那么线程2就重试获取缓存资源和锁(递归),直到线程1把锁释放后,线程2获得到锁或者缓存资源,可能线程二执行到获取缓存就获得到缓存就之间返回了,也可能没查到缓存,执行到获得了锁,这时候要再次校验一下是否获得了缓存。没有获得缓存在取构建缓存。

 /*** 获取锁* @param key* @return*/private boolean tryLock(String key){Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 20, TimeUnit.SECONDS);return BooleanUtil.isTrue(flag);}/*** 释放锁* @param key*/private  void unlock(String key){stringRedisTemplate.delete(key);}
/*** 查询商户信息 缓存击穿互斥锁* @param id* @return*/public Shop queryWithMutex(Long id){String shopKey = CACHE_SHOP_KEY+ id;// 1. 从redis中查询店铺缓存String shopJson = stringRedisTemplate.opsForValue().get(shopKey);//2.判断是否命中缓存  isnotblank false: "" or "/t/n" or "null"if(StrUtil.isNotBlank(shopJson)){// 3.若命中则返回信息Shop shop = JSONUtil.toBean(shopJson, Shop.class);return shop;}//数据穿透判空   不是null 就是空串 ""if (shopJson != null){//返回错误信息
//            return  Result.fail("没有该商户信息(缓存)");return null;}//4.没有命中缓存,查数据库//todo :解决缓存击穿  不能直接查数据库。 利用互斥锁解决/*** 实现缓存重建* 1. 获取互斥锁* 2. 判断是否成功* 3. 失败就休眠重试* 4.成功 查数据库* 5 数据库存在该数据写入缓存* 6 不存在返回错误信息并写入缓存“”* 7 释放锁**///获取互斥锁 失败  休眠重试String lockKey = "lock:shop" + id;Shop shop=null;try {boolean isLock = tryLock(lockKey);//获取锁失败if (!isLock) {System.out.println("获取锁失败,重试");Thread.sleep(50);return queryWithMutex(id);//递归 重试}// 获取锁成功,再次检测缓存是否存在,存在就无需构建缓存,因为可能有的线程刚构建好缓存并释放锁,其他线程获取了锁//检测缓存是否存在  存在shopJson = stringRedisTemplate.opsForValue().get(shopKey);if (StrUtil.isNotBlank(shopJson)) {return JSONUtil.toBean(shopJson, Shop.class);}if (shopJson !=null){return null;}// 缓存不存在// 查数据库shop = super.getById(id);Thread.sleep(200);//模拟你测试环境 热点key失效模拟重建延迟if (shop == null){//没有该商户信息stringRedisTemplate.opsForValue().set(shopKey,"",CACHE_NULL_TTL,TimeUnit.SECONDS);return null;}//有该商户信息stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+ id, JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.MINUTES);} catch (InterruptedException e) {throw new RuntimeException(e);} finally {unlock(lockKey);}return shop;}

缓存击穿问题- 逻辑过期时间

需要添加逻辑过期时间字段,直接在shop类中添加不太友好改了源代码

可以新建一个类

/*** 逻辑过期类*/
@Data
public class RedisData {private LocalDateTime expireTime;private Object data;
}

数据预热

 /*** 添加逻辑过期时间* @param id* @param expireSeconds*/public void savaShop2Redis(Long id ,Long expireSeconds){// 查询店铺数据Shop shop = getById(id);//封装逻辑过期时间RedisData redisData = new RedisData();redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));redisData.setData(shop);//写入redisstringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,JSONUtil.toJsonStr(redisData));}

执行测试方法即可加入到redis。

正式代码

private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
public Shop queryWithLogicalExpire( Long id ) {String key = CACHE_SHOP_KEY + id;// 1.从redis查询商铺缓存String json = stringRedisTemplate.opsForValue().get(key);// 2.判断是否存在if (StrUtil.isBlank(json)) {// 3.存在,直接返回return null;}// 4.命中,需要先把json反序列化为对象RedisData redisData = JSONUtil.toBean(json, 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 lockKey = LOCK_SHOP_KEY + id;boolean isLock = tryLock(lockKey);// 6.2.判断是否获取锁成功if (isLock){CACHE_REBUILD_EXECUTOR.submit( ()->{try{//重建缓存this.saveShop2Redis(id,20L);}catch (Exception e){throw new RuntimeException(e);}finally {unlock(lockKey);}});}// 6.4.返回过期的商铺信息return shop;
}

缓存工具类

package com.hmdp.utils;import cn.hutool.core.lang.func.Func;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.hmdp.entity.Shop;
import javafx.beans.binding.ObjectExpression;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;import static com.hmdp.utils.RedisConstants.*;/*** Redis 工具类* * 方法1:将任意Java对象序列化为json并存储在string类型的key中,并且可以设置TTL过期时间* * 方法2:将任意Java对象序列化为json并存储在string类型的key中,并且可以设置逻辑过期时间,用于处理缓存击穿问题** * 方法3:根据指定的key查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透问题* * 方法4:根据指定的key查询缓存,并反序列化为指定类型,需要利用逻辑过期解决缓存击穿问题*/
@Component
public class CacheClient {private  final StringRedisTemplate stringRedisTemplate;public CacheClient(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}//range/*** 方法1:将任意Java对象序列化为json并存储在string类型的key中,并且可以设置TTL过期时间* @param key* @param value* @param time* @param unit*/public void set(String key , Object value, Long time, TimeUnit unit){stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time,unit);}/*** 方法2:将任意Java对象序列化为json并存储在string类型的key中,并且可以设置逻辑过期时间,用于处理缓存击穿问题* @param key  redis的key* @param value* @param time* @param unit*/public void setWithLogicalExpire(String key , Object value, Long time, TimeUnit unit){//RedisData 是自定义类RedisData redisData = new RedisData();redisData.setData(value);redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));}/***  方法3:根据指定的key查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透问题* @param* @param id* @return*/public <R,ID> R queryWithPassThrough(String keyPrefix, ID id, Class<R> type, Function<ID,R> dbFallback,Long time,TimeUnit unit){String key = keyPrefix+ id;// 1. 从redis中查询店铺缓存String json = stringRedisTemplate.opsForValue().get(key);//2.判断是否命中缓存  isnotblank false: "" or "/t/n" or "null"if(StrUtil.isNotBlank(json)){// 3.若命中则返回信息R r = JSONUtil.toBean(json, type);//            return Result.fail("没有该商户信息");return r;}//数据穿透判空   不是null 就是空串 ""if (json != null){return null;}//4.没有命中缓存,查数据库,因为不知道操作那个库,函数式编程,逻辑交给调用者完成
//       R r= getById(id); 交给调用者--》》函数式编程R r = dbFallback.apply(id);//5. 数据库为空,返回错误---》解决缓存穿透--》加入redis为空if (r == null){stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
//            return Result.fail("没有该商户信息");return null;}//6. 数据库不为空,返回查询的结果并加入缓存stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(r),time, unit);return r;}/***  方法4:根据指定的key查询缓存,并反序列化为指定类型,需要利用逻辑过期解决缓存击穿问题* @param id* @return*/public <R,ID> R queryWithLogicalExpire(String keyPrefix,ID id,Class<R> type,Function<ID,R>dbFallback,String lockPrefix,Long time,TimeUnit unit){String key = keyPrefix+ id;// 1. 从redis中查询店铺缓存String json = stringRedisTemplate.opsForValue().get(key);//2.判断数据是否存在(我们对于热点key设置永不过期)  isblankif(StrUtil.isBlank(json)){// 3.若未命中中则返回空return null;}//4.若命中缓存 判断是否过期RedisData redisData = JSONUtil.toBean(json, RedisData.class);JSONObject data = (JSONObject) redisData.getData();R r = JSONUtil.toBean(data, type);LocalDateTime expireTime = redisData.getExpireTime();//未过期 直接返回查询信息if (expireTime.isAfter(LocalDateTime.now())){return r;}//过期// 重建缓存// 获取锁String lockKey = lockPrefix + id;if (tryLock(lockKey)) {//再次校验缓存是否未过期(线程1刚写入缓存然后释放锁,线程2在线程1释放锁的同时,执行到获得锁)//  从redis中查询店铺缓存json = stringRedisTemplate.opsForValue().get(key);//2.判断数据是否存在(我们对于热点key设置永不过期)  isblankif(StrUtil.isBlank(json)){// 3.若未命中中则返回空return null;}//4.若命中缓存 判断是否过期redisData = JSONUtil.toBean(json, RedisData.class);data = (JSONObject) redisData.getData();r = JSONUtil.toBean(data, type);expireTime = redisData.getExpireTime();//未过期 直接返回查询信息if (expireTime.isAfter(LocalDateTime.now())){return r;}//二次校验过后还时过期的就新开线程重构缓存// 获得锁,开启新线程,重构缓存 ,老线程直接返回过期信息CACHE_REBUILD_EXECUTOR.submit( ()->{try{//重建缓存//先查数据库 封装逻辑过期时间 再写redisR r1 = dbFallback.apply(id);this.setWithLogicalExpire(key, r1, time, unit);}catch (Exception e){throw new RuntimeException(e);}finally {unlock(lockKey);}});}//未获得锁 直接返回无效信息return r;}/**缓存穿透互斥锁解** @param keyPrefix* @param id* @param type* @param dbFallback* @param time* @param unit* @return*/public <R,ID>  R queryMutex(String keyPrefix, ID id, Class<R> type, Function<ID,R>dbFallback,String lockPrefix, Long time, TimeUnit unit) {String key = keyPrefix + id;//1.从redis中查询店铺缓存String json = stringRedisTemplate.opsForValue().get(key);//2.判断数据是否存在缓存if (StrUtil.isNotBlank(json)) {//2.1存在缓存R r = JSONUtil.toBean(json, type);return r;}//  2.2 是否缓存“”//判断命中是否为空值  ""if (json != null) {return null;}// 2.3不存在缓存// 3 缓存重建// 3.1 获取互斥锁String lockKey = lockPrefix + id;R r = null;try {boolean isLock = tryLock(lockKey);// 成功获取锁 - 》查数据库缓存重建if (isLock) {//二次校验 缓存是否有值json = stringRedisTemplate.opsForValue().get(key);//判断缓存是否存在if (StrUtil.isNotBlank(json)) {//存在缓存r = JSONUtil.toBean(json, type);return r;}if (json != null) {//缓存为 ""return null;}// 缓存不存在--》 查询数据库//  查询数据库r = dbFallback.apply(id);if (r == null) {//缓存空值stringRedisTemplate.opsForValue().set(key, "", time, unit);}//缓存重建stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(r), time, unit);//返回数据return r;}// 3.2 获取锁失败 -》休眠重试//休眠Thread.sleep(50);// 递归重试return queryMutex(keyPrefix, id, type, dbFallback, lockPrefix, time, unit);}catch (InterruptedException e) {throw new RuntimeException(e);}finally {unlock(lockKey);}}//endrange/*** 线程池*/private  static  final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);/*** 获取所* @param key* @return*/private boolean tryLock(String key){Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);return BooleanUtil.isTrue(flag);}/*** 释放锁* @param key*/private  void unlock(String key){stringRedisTemplate.delete(key);}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/461291.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Cuebric:用AI重新定义3D创作的未来

一、简介 Cuebric 是一家成立于2022年夏天的好莱坞创新公司,致力于为电影、电视、游戏和时尚等行业提供先进的AI多模态SaaS平台。自2024年1月正式推出以来,Cuebric 已经在市场上获得了广泛的认可和积极的反馈。目前,该平台正处于1.0版本的beta测试阶段,已募集约50万美元的…

计算机的错误计算(一百四十)

摘要 探讨 MATLAB 中函数 的计算精度。 从计算机的错误计算&#xff08;一百三十九&#xff09;知&#xff0c;对于对数运算&#xff0c;当真数在 1 附近时&#xff0c;计算机的输出会出现较大误差。为此&#xff0c;IEEE 754-2019 中专门定义有函数 其目的就是当自变量在 …

《Python游戏编程入门》注-第4章2

《Python游戏编程入门》的“4.2.2 键盘事件”中介绍了通过键盘事件来监听键盘按键的方法。 1 键盘事件 玩家点击键盘中某个按键实际上包含了两个动作&#xff1a;点击按键和释放按键&#xff0c;也就是按键按下和松开。按键按下的对应的事件是KEYDOWN&#xff0c;按键松开对应…

《高频电子线路》 —— 高频谐振功放(2)

动态特性与负载特性 动态特性 静态特性是指&#xff0c;不考虑负载阻抗的时候获得的&#xff0c;即转移特性曲线和输出特性曲线。 考虑负载时&#xff0c;电流变化的时候&#xff0c;负载上的电压就会变化&#xff0c;管子上面的Vce也会变化。 考虑负载的反作用后&#xff0c…

SpringBoot 下的Excel文件损坏与内容乱码问题

序言 随着打包部署的方式的改变&#xff0c;原本正常运行的代码可能带来一些新的问题&#xff0c;比如我们现在使用SpringBoot 的方式生成Jar包直接运行&#xff0c;就会对我们再在Resource下的Excel文件产生影响&#xff0c;导入与预期不符的情况发生cuiyaonan2000163.com 比…

「Mac畅玩鸿蒙与硬件12」鸿蒙UI组件篇2 - Image组件的使用

在鸿蒙应用开发中,Image 组件用于加载和显示图片资源,并提供多种属性来控制图片的显示效果和适配方式。本篇将带你学习如何在鸿蒙应用中加载本地和远程图片、设置图片样式以及实现简单的图片轮播功能。 关键词 Image 组件图片加载本地资源远程图片图片轮播一、Image 组件基础…

上海亚商投顾:沪指缩量调整 华为概念股午后爆发

上海亚商投顾前言&#xff1a;无惧大盘涨跌&#xff0c;解密龙虎榜资金&#xff0c;跟踪一线游资和机构资金动向&#xff0c;识别短期热点和强势个股。 一.市场情绪 市场全天震荡调整&#xff0c;沪指、深成指午后跌超1%&#xff0c;创业板指一度跌逾2%&#xff0c;尾盘跌幅有…

vim命令及shell命令

目录 vim命令 vim三种工作模式 光标的跳转 复制粘贴 剪切删除 撤销回滚替换 翻页 其他 shell编程命令 判断用户的参数 可用的整数比较运算符 常见的字符串比较运算符 if条件测试语句 for条件循环语句 vim命令 vim三种工作模式 Vim编辑器中设置了三种模式: 命令…

Z 检验和 T 检验之间的区别

目录 一、说明 二、什么是假设检验&#xff1f; 三、假设检验基础 3.1 假设检验的基本概念 3.2 、执行假设验证的步骤 3.3 临界值、P 值 3.4 方向假设 3.5 非方向假设检验s 四、什么是 Z 检验统计量&#xff1f; 五、Z 检验示例 5.1 单样本 Z 检验 5.2 双样本 Z 检…

Qt限制QGraphicsScene QGraphicsItem内部的移动范围

用过QGraphicsView的都知道&#xff0c;原点一般设定在view和item的中心&#xff0c;所以帮助文档和这个网友说的不一定跟我们对的上&#xff1a; 关于Qt限制QGraphicsScene内部Item的移动范围_qgraphicsitem限制移动范围-CSDN博客 首先&#xff0c;设定view的scenerect&…

【Redis】浅析Redis大Key

目录 1、什么是Redis大Key 2、大 Key 是怎么产生的 3、大 Key 导致的问题 4、如何快速找到 Redis 大 Key 5、大 Key 优化策略 6、总结 我们在使用 Redis 的过程中&#xff0c;如果未能及时发现并处理 Big keys&#xff08;下文称为“大Key”&#xff09;&#xff0c;可能…

Jedis操作和springboot整合redis

Jedis-springboot整合redis Jedis 引入jedis依赖 注意事项 测试相关数据类型 Key String List set hash zset 案例 spring boot整合redis 引入相关依赖 在application.properties中配置redis 配置 创建redis配置类 创建测试类 Jedis 引入jedis依赖 <depen…

GenAI 生态系统现状:不止大语言模型和向量数据库

自 20 个月前 ChatGPT 革命性的推出以来&#xff0c;生成式人工智能&#xff08;GenAI&#xff09;领域经历了显著的发展和创新。最初&#xff0c;大语言模型&#xff08;LLMs&#xff09;和向量数据库吸引了最多的关注。然而&#xff0c;GenAI 生态系统远不止这两个部分&#…

HTML、JavaScript和CSS实现注册页面设计

目录 一、实现要求 二、实现页面图 1、注册页面 2.用户ID、用户名、口令验证成功后显示页面 三、用户ID、用户名、口令、确定口令验证逻辑js代码 1、验证用户ID 2、验证用户名 3、验证口令密码 四、总结 五、代码仓库 一、实现要求 综合使用HTML、JavaScript和CSS进…

Vue前端开发:事件绑定方式

事件定义 在Vue中&#xff0c;当一个元素通过使用v-on或语法糖指令绑定某个事件后&#xff0c;则完成了事件被定义的过程&#xff0c;在这定义的过程中&#xff0c;指令的后面是定义事件的名称&#xff0c;等号的后面是事件被触发后执行的函数&#xff0c;当然&#xff0c;也可…

mac-ubuntu虚拟机(扩容-共享-vmtools)

一、磁盘扩容 使用GParted工具对Linux磁盘空间进行扩展 https://blog.csdn.net/Time_Waxk/article/details/105675468 经过上面的方式后还不够&#xff0c;需要再进行下面的操作 lvextend 用于扩展逻辑卷的大小&#xff0c;-l 选项允许指定大小。resize2fs 用于调整文件系统的…

【AAOS】【源码分析】CarSystemUI

目录 目录 概述 CarSystemUI组件 源代码 源码位置 主要模块 编译选项 CarSystemUI与SystemUI 编译方式 Car Emulator默认服务 CarSystemUI 启动流程 缩略词 概述 AAOS中的SystemUI虽然相较手机要简单不少,但却是车载开发中的一个重要组件,它负责管理和控制车机…

C++知识点复习

对于这些问题的回答&#xff0c;可以按照思维导图的结构来组织答案&#xff0c;然后再进行回答。 C11用过哪些特性&#xff1f; &#xff08;首先&#xff0c;要回答好这个问题&#xff0c;需要注意问题的层次&#xff0c;不要一上来就说新增了某某关键字和语法&#xff0c;在…

.net framework 3.5sp1开启错误进度条不动如何解决

浏览器地址栏输入www.dnz9.com远程解决netframework问题 在Windows操作系统上安装或启用.NET Framework 3.5 SP1时&#xff0c;如果遇到进度条不动的问题&#xff0c;可能由多种原因引起。以下是一些可能的解决方案&#xff1a; 1. 使用Windows功能对话框 1.打开“控制面板”。…

openpnp - 在openpnp中单独测试相机

文章目录 openpnp - 在openpnp中单独测试相机概述笔记END openpnp - 在openpnp中单独测试相机 概述 底部相机的位置不合适, 重新做了零件&#xff0c;准备先确定一下相机和吸嘴的距离是多少才合适。 如果在设备上直接实验&#xff0c;那么拆装调整相机挺麻烦的。 准备直接在电…