Redis--缓存工具封装

article/2025/6/16 6:28:31

经过前面的学习,发现缓存中的问题,无论是缓存穿透,缓存雪崩,还是缓存击穿,这些问题的解决方案业务代码逻辑都很复杂,我们也不应该每次都来重写这些逻辑,我们可以将其封装成工具。而在封装的时候,也会有不少的问题需要去解决。

案例学习:缓存工具封装

基于StringRedisTemplate封装一个缓存工具类,满足下列需求:

  • 方法一:将任意Java对象序列化成JSON并存储在String类型的key中,并且可以设置TTL过期时间

  • 方法二:将任意Java对象序列化成JSON并存储在String类型的key中,并且可以设置逻辑过期时间,用于处理缓存击穿问题

  • 方法三:根据指定的key查询缓存,并且反序列化成指定类型,利用缓存空值的方式解决缓存穿透问题

  • 方法四:根据指定的key查询缓存,并且反序列化成指定类型,需要利用逻辑过期解决缓存击穿问题

代码展示:

首先需要先声明是一个组件@Component,方便spring管理,再添加一个@slf4j注解方便日志输出,管理等,再注入StringRedisTemplate的Bean对象,使用构造器方式注入

 @Slf4j@Componentpublic class CacheClient {//  注入redisTemplate,用构造器模式注入private final StringRedisTemplate  stringRedisTemplate;​public CacheClient(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}

方法一:将任意Java对象序列化成JSON并存储在String类型的key中,并且可以设置TTL过期时间

 //方法一:将任意Java对象序列化成JSON并存储在String类型的key中,并且可以设置TTL过期时间//编译set方法,需要注意的是 key是String类型,value是Object类型,所以需要转换一下,将 value 转为json字符串public void set(String key, Object value, Long time, TimeUnit unit){stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value),time,unit);}

方法二:将任意Java对象序列化成JSON并存储在String类型的key中,并且可以设置逻辑过期时间,用于处理缓存击穿问题

 //方法二:- 将任意Java对象序列化成JSON并存储在String类型的key中,并且可以设置逻辑过期时间,用于处理缓存击穿问题public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit){//逻辑过期的核心思想,时将过期时间作为字段写入到数据中, 读取的时候,先判断是否过期,如果过期,则返回null,否则返回数据//设置逻辑过期RedisData redisData = new RedisData();redisData.setData(value);redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));}

方法三:根据指定的key查询缓存,并且反序列化成指定类型,利用缓存空值的方式解决缓存穿透问题

 //方法三:根据指定的key查询缓存,并且反序列化成指定类型,利用缓存控制的方式解决缓存穿透问题//设置返回值为泛型,因为在编译工具类,返回值无法确定,需要调用者告知。//  参数:keyPrefix key前缀,id 商铺id,type 返回值类型//id类型也不确定,需要使用泛型public  <R,ID> R queryWithPassThrough(String KeyPrefix, ID id, Class<R> type, Function<ID,R> dbFallback,Long time,TimeUnit unit) {//1.从redis中查询商铺缓存String json = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);// 2.判断是否存在if (StrUtil.isNotBlank(json)) {//  3.存在,做反序列化,返回return  JSONUtil.toBean(json, type);}//判断命中的是否是空值if (json!= null) {return null;}//  4.不存在,根据id查询数据库 我们工具类不清楚实体类以及数据库方法,所以我们需要让调用者将这段逻辑提供给我们,使用函数式接口,//有参数,有返回值 ,使用函数式接口//Function<ID,R> dbFallback,ID为参数,R为返回值R r = dbFallback.apply(id);//  5.不存在,返回错误if (r == null) {//  6.不存在,写入redisstringRedisTemplate.opsForValue().set(KeyPrefix + id, "", CACHE_NULL_TTL, TimeUnit.MINUTES);//  5.不存在,返回错误return null;}//存在,写入redis//时间不能够写死,需要根据业务来定this.set(KeyPrefix + id, r, time, unit);return r;}

在这里进行测试,进入ShopServiceImpl,调用工具类,尝试替代解决缓存穿透方案的业务代码

首先注入工具类Bean对象

 @Resourceprivate CacheClient  cacheClient;

进行方法三调用

 Shop shop = cacheClient.queryWithPassThrough(CACHE_SHOP_KEY, id, Shop.class, this::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES);

开始测试:

访问测试接口,观察数据库查询次数

image-20250529213718631

数据库仅查询一次。

测试成功。工具类搭建成功

方法四:根据指定的key查询缓存,并且反序列化成指定类型,需要利用逻辑过期解决缓存击穿问题

 
 private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);//方法四:根据指定的key查询缓存,并且反序列化成指定类型,利用逻辑过期解决缓存击穿问题public <R ,ID> R queryWithLogicalExpire(String keyPrefix,ID id,Class<R> type,Function<ID,R> dbFallback,Long time,TimeUnit unit){//1.从redis中查询商铺缓存String json = stringRedisTemplate.opsForValue().get(keyPrefix+id);// 2.判断是否存在if(StrUtil.isBlank(json)) {​//  3.不存在,直接返回空return null;}​//4.命中,需要把json反序列化为对象RedisData redisData = JSONUtil.toBean(json, RedisData.class);//  4.存在,判断缓存是否过期//Data实际上是jsonObject对象R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);LocalDateTime expireTime = redisData.getExpireTime();//  5.判断是否过期if(expireTime.isAfter(LocalDateTime.now())) {//  5.1.未过期,直接返回店铺信息return r;}//  6.重建缓存// 6.1.获取互斥锁String lock = LOCK_SHOP_KEY + id;boolean isLock = tryLock(lock);// 6.2.判断是否获取锁成功if(isLock) {//再次检测redis缓存是否过期redisData = JSONUtil.toBean(stringRedisTemplate.opsForValue().get(keyPrefix + id), RedisData.class);if (redisData.getExpireTime().isAfter(LocalDateTime.now())) {//  6.1.未过期,直接返回。return JSONUtil.toBean((JSONObject) redisData.getData(), type);}//  6.3.成功,开启独立线程,实现缓存重建CACHE_REBUILD_EXECUTOR.submit(() -> {try {//  重建缓存R r1 = dbFallback.apply(id);//写入RedissetWithLogicalExpire(keyPrefix+id,r1,time,unit);} catch (Exception e) {throw new RuntimeException(e);} finally {//  释放锁unlock(lock);}});}return r;}private boolean tryLock(String key){//设置有效期时间取决于业务执行时间,一般比业务时间长一些即可。Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);//建议不要直接返回flag,防止返回空指针,因为Boolean是boolean的包装类,需要进行拆箱操作,可能导致空指针 网络问题或者键不存在但Redis未响应,可能会返回null,因此需要实用工具类判断。改成BooleanUtil.isTrue(flag)。return  BooleanUtil.isTrue(flag);}//  释放锁private void unlock(String key){stringRedisTemplate.delete(key);}

测试:

调用工具类:

 Shop shop = cacheClient.queryWithLogicalExpire(CACHE_SHOP_KEY, id, Shop.class, this::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES);

注意事项:进行测试之前需要提前将数据存入Redis中,并且保证已经过期。

在测试单元中进行存储:

 @SpringBootTestclass HmDianPingApplicationTests {@Resourceprivate ShopServiceImpl shopService;@Resourceprivate CacheClient cacheClient;@Testvoid testSaveShop() throws InterruptedException {shopService.getById(1L);cacheClient.setWithLogicalExpire(CACHE_SHOP_KEY,1L,10L, TimeUnit.SECONDS);}

测试结果:

新建100个线程并发执行。

image-20250529192207335

检查成果:

image-20250529192354366

测试成功,在前一半线程中,数据为旧数据,后一半线程数据为新数据。

查看数据库查询次数,只查询一次,说明并无线程并发问题。

image-20250529213718631

代码汇总:

 @Slf4j@Componentpublic class CacheClient {//  注入redisTemplate,用构造器模式注入private final StringRedisTemplate stringRedisTemplate;​public CacheClient(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}​//方法一:将任意Java对象序列化成JSON并存储在String类型的key中,并且可以设置TTL过期时间//编译set方法,需要注意的是 key是String类型,value是Object类型,所以需要转换一下,将 value 转为json字符串public void set(String key, Object value, Long time, TimeUnit unit) {stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);}​//方法二:- 将任意Java对象序列化成JSON并存储在String类型的key中,并且可以设置逻辑过期时间,用于处理缓存击穿问题public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit) {//逻辑过期的核心思想,时将过期时间作为字段写入到数据中, 读取的时候,先判断是否过期,如果过期,则返回null,否则返回数据//设置逻辑过期RedisData redisData = new RedisData();redisData.setData(value);redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));}​//方法三:根据指定的key查询缓存,并且反序列化成指定类型,利用缓存控制的方式解决缓存穿透问题//设置返回值为泛型,因为在编译工具类,返回值无法确定,需要调用者告知。//  参数:keyPrefix key前缀,id 商铺id,type 返回值类型//id类型也不确定,需要使用泛型public <R, ID> R queryWithPassThrough(String KeyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {//1.从redis中查询商铺缓存String json = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);// 2.判断是否存在if (StrUtil.isNotBlank(json)) {//  3.存在,做反序列化,返回return JSONUtil.toBean(json, type);}//判断命中的是否是空值if (json != null) {return null;}//  4.不存在,根据id查询数据库 我们工具类不清楚实体类以及数据库方法,所以我们需要让调用者将这段逻辑提供给我们,使用函数式接口,//有参数,有返回值 ,使用函数式接口//Function<ID,R> dbFallback,ID为参数,R为返回值R r = dbFallback.apply(id);//  5.不存在,返回错误if (r == null) {//  6.不存在,写入redisstringRedisTemplate.opsForValue().set(KeyPrefix + id, "", CACHE_NULL_TTL, TimeUnit.MINUTES);//  5.不存在,返回错误return null;}//存在,写入redis//时间不能够写死,需要根据业务来定this.set(KeyPrefix + id, r, time, unit);return r;}​​private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);//方法四:根据指定的key查询缓存,并且反序列化成指定类型,利用逻辑过期解决缓存击穿问题public <R ,ID> R queryWithLogicalExpire(String keyPrefix,ID id,Class<R> type,Function<ID,R> dbFallback,Long time,TimeUnit unit){//1.从redis中查询商铺缓存String json = stringRedisTemplate.opsForValue().get(keyPrefix+id);// 2.判断是否存在if(StrUtil.isBlank(json)) {​//  3.不存在,直接返回空return null;}​//4.命中,需要把json反序列化为对象RedisData redisData = JSONUtil.toBean(json, RedisData.class);//  4.存在,判断缓存是否过期//Data实际上是jsonObject对象R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);LocalDateTime expireTime = redisData.getExpireTime();//  5.判断是否过期if(expireTime.isAfter(LocalDateTime.now())) {//  5.1.未过期,直接返回店铺信息return r;}//  6.重建缓存// 6.1.获取互斥锁String lock = LOCK_SHOP_KEY + id;boolean isLock = tryLock(lock);// 6.2.判断是否获取锁成功if(isLock) {//再次检测redis缓存是否过期redisData = JSONUtil.toBean(stringRedisTemplate.opsForValue().get(keyPrefix + id), RedisData.class);if (redisData.getExpireTime().isAfter(LocalDateTime.now())) {//  6.1.未过期,直接返回。return JSONUtil.toBean((JSONObject) redisData.getData(), type);}//  6.3.成功,开启独立线程,实现缓存重建CACHE_REBUILD_EXECUTOR.submit(() -> {try {//  重建缓存R r1 = dbFallback.apply(id);//写入RedissetWithLogicalExpire(keyPrefix+id,r1,time,unit);} catch (Exception e) {throw new RuntimeException(e);} finally {//  释放锁unlock(lock);}});}return r;}private boolean tryLock(String key){//设置有效期时间取决于业务执行时间,一般比业务时间长一些即可。Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);//建议不要直接返回flag,防止返回空指针,因为Boolean是boolean的包装类,需要进行拆箱操作,可能导致空指针 网络问题或者键不存在但Redis未响应,可能会返回null,因此需要实用工具类判断。改成BooleanUtil.isTrue(flag)。return  BooleanUtil.isTrue(flag);}//  释放锁private void unlock(String key){stringRedisTemplate.delete(key);}​​}
工具类小结:

难点:

  • 在查询函数中返回值的类型是不确定的,还有ID类型也无法确定,要善于利用泛型,在数据类型不确定的情况下去指定对应的类型,由调用者告知工具类真实类型,从而做出泛型的推断

  • 在封装查询逻辑时,牵扯到数据库查询,而调用者的数据库类型及查询方式以及实体类都不得而知,都需要让调用者告知如何查询,而查数据库是一段函数,因此调用者需要传入一段函数,这就是用到了函数式编程,因为是根据ID查询后返回,有参数以及返回值,正对应了Java中的Function<参数,返回值 >,要调用者传递进来。

希望对大家有所帮助


http://www.hkcw.cn/article/wnIhkDPRUP.shtml

相关文章

ZC-OFDM雷达通信一体化减小PAPR——选择性映射法(SLM)

文章目录 前言一、SLM 技术1、简介2、原理 二、MATLAB 仿真1、核心代码2、仿真结果 三、资源自取 前言 在 OFDM 雷达通信一体化系统中&#xff0c;信号的传输由多个子载波协同完成&#xff0c;多个载波信号相互叠加形成最终的发射信号。此叠加过程可能导致信号峰值显著高于其均…

ESP32-idf学习(四)esp32C3驱动lcd

一、前言 屏幕是人机交互的重要媒介&#xff0c;而且现在我们产品升级的趋势越来越高大尚&#xff0c;不少产品都会用lcd来做界面&#xff0c;而esp32c3在一些项目上是可以替代主mcu&#xff0c;所以驱动lcd也是必须学会的啦 我新买的这块st7789&#xff0c;突然发现是带触摸…

Remote Sensing投稿记录(投稿邮箱写错、申请大修延期...)风雨波折投稿路

历时近一个半月&#xff0c;我中啦&#xff01; RS是中科院二区&#xff0c;2023-2024影响因子4.2&#xff0c;五年影响因子4.9。 投稿前特意查了下预警&#xff0c;发现近五年都不在预警名单中&#xff0c;甚至最新中科院SCI分区&#xff08;2025年3月&#xff09;在各小类上…

ZC-OFDM雷达通信一体化减小PAPR——部分传输序列法(PTS)

文章目录 前言一、PTS 技术1、简介2、原理 二、MATLAB 仿真1、核心代码2、仿真结果 三、资源自取 前言 在 OFDM 雷达通信一体化系统中&#xff0c;信号的传输由多个子载波协同完成&#xff0c;多个载波信号相互叠加形成最终的发射信号。此叠加过程可能导致信号峰值显著高于其均…

第6章 放大电路的反馈

本章基本要求 会判&#xff1a;判断电路中有无反馈及反馈的性质 会算&#xff1a;估算深度负反馈条件下的放大倍数 会引&#xff1a;根据需求引入合适的反馈 会判振消振&#xff1a;判断电路是否能稳定工作&#xff0c;会消除自激振荡。 6.1 反馈的概念及判断 一、反馈的…

知识管理五强对比:Baklib高效突围

Baklib核心技术优势 Baklib的底层技术架构以知识中台为核心&#xff0c;深度融合自然语言处理&#xff08;NLP&#xff09;与分布式存储技术&#xff0c;实现多源异构数据的统一纳管。其智能分类引擎通过语义理解自动关联碎片化文档&#xff0c;结合动态标签体系与多维度权限控…

电机驱动器辐射骚扰整改

定位低压DC部分的骚扰源&#xff08;排除法&#xff09;&#xff1a; 为确定是电源哪部分出现问题&#xff0c;可以采取如下步骤进行验证&#xff1a; a.将12V转5V的芯片去掉&#xff0c;仅剩12V器件工作&#xff0c;然后测试&#xff1b; b.将5V转3.3V和隔离5V的芯片去掉&am…

CTFHub-RCE 命令注入-过滤空格

观察源代码 代码里面可以发现过滤了空格 判断是Windows还是Linux 源代码中有 ping -c 4 说明是Linux 查看有哪些文件 127.0.0.1|ls 打开flag文件 我们尝试将空格转义打开这个文件 利用 ${IFS} 127.0.0.1|cat${IFS}flag_195671031713417.php 可是发现 文本内容显示不出来&…

2022年 中国商务年鉴(excel电子表格版)

2022年 中国商务年鉴&#xff08;excel电子表格版&#xff09;.ziphttps://download.csdn.net/download/2401_84585615/89772883 https://download.csdn.net/download/2401_84585615/89772883 《中国商务年鉴2022》是由商务部国际贸易经济合作研究院主办的年度统计资料&#xf…

家长速查!3岁男童误吞“水精灵”危及生命

给孩子挑选放心的玩具是不少家长群讨论的热点。“小玩具”关乎“大安全”,如何帮助孩子远离“毒”“危”玩具?怎样合理选购、安全使用,让玩具成为孩子的益友?“六一”国际儿童节前夕,记者就此进行了走访。“毒”“危”玩具有何隐患?“本月我们又接诊了一名3岁男童误吞‘水…

划龙舟有多拼 鼓点一响全员开挂 岭南文化盛宴

广东龙舟不仅是一种仪式,更是一种文化符号。每一声鼓点都充满了热血与奋进,每一次冲刺都体现了拼搏与荣光。“下水!起桨!”有着20多年“龙舟龄”的东莞万江街道龙舟划手黄柱良,为了近日在东江江面举行的龙舟趁景活动,和伙伴们准备了1个多星期。活动当天上午,黄柱良和其他…

大巴黎如何拿到2025年欧冠的 战术转型与团队足球

2025年6月1日凌晨,2024-2025赛季欧冠决赛在慕尼黑安联球场举行,巴黎圣日耳曼以5-0大胜国际米兰,队史首次夺得欧冠奖杯。这场胜利不仅终结了巴黎多年来的“欧冠魔咒”,也标志着球队在姆巴佩离队后的战术转型取得巨大成功。比赛期间,大巴黎主帅恩里克延续了本赛季后半段的43…

thinkpad T-440p 2025.05.31

thinkpad T-440p 2025.05.31 老了退休了&#xff0c;说起来真的可恶现在笔记本的设计师&#xff0c;只有固态硬盘了

堆与堆排序及 Top-K 问题解析:从原理到实践

一、堆的本质与核心特性 堆是一种基于完全二叉树的数据结构&#xff0c;其核心特性为父节点与子节点的数值关系&#xff0c;分为大堆和小堆两类&#xff1a; 大堆&#xff1a;每个父节点的值均大于或等于其子节点的值&#xff0c;堆顶元素为最大值。如: 小堆&#xff1a;每个…

【题解-洛谷】P8094 [USACO22JAN] Cow Frisbee S

题目&#xff1a;P8094 [USACO22JAN] Cow Frisbee S 题目描述 Farmer John 的 N ( N ≤ 3 10 5 ) N\ (N\le 3\times 10^5) N (N≤3105) 头奶牛的高度为 1 , 2 , … , N 1, 2, \ldots, N 1,2,…,N。一天&#xff0c;奶牛以某个顺序排成一行玩飞盘&#xff1b;令 h 1 … h …

如何利用差分隐私技术在医疗领域守护患者隐私

在数字化医疗快速发展的当下&#xff0c;医疗数据已然成为一座蕴藏无限价值的宝库。一份完整的电子病历&#xff0c;不仅记录着患者的疾病诊断、治疗记录&#xff0c;还可能包含基因数据、生活习惯等敏感信息&#xff1b;而基因检测报告中携带的遗传密码&#xff0c;更是与个人…

Kanass入门教程- 事项管理

kanass是一款国产开源免费、简洁易用的项目管理工具&#xff0c;包含项目管理、项目集管理、事项管理、版本管理、迭代管理、计划管理等相关模块。工具功能完善&#xff0c;用户界面友好&#xff0c;操作流畅。本文主要介绍事项管理使用指南。 1、添加事项 事项有多种类型 分…

主人回应狗王“长毛”爆火 小狗成网红引来百万关注

近日,河北承德一只下司犬“长毛”的视频在外网爆火。视频中,“长毛”凭借威严的姿态让闹事的狗狗臣服。因此小狗被外国网友取名“查理国王”“狗王”等称号,连小狗的肖像都被印在T恤上作为周边售卖。火爆全网的狗王“长毛”。网络截图网友们纷纷表达了自己的惊叹与崇拜:“阿…

描述性统计的可视化分析

初步研究数据的分布时&#xff0c;最直观的方法就是可视化分析了。 1. 直方图 直方图&#xff08;histogram&#xff09;出现得很早&#xff0c;而且应用广泛。 直方图是以一种图形方法来概括给定数值X的分布情况的图示。 如果X是离散的变量&#xff0c;比如股票类型&#xf…

梅花鹿横穿马路被车撞倒后跑进丛林 后视镜遭殃引发热议

5月31日清晨,大连市民在滨海路晨跑时目睹了一起意外。一只梅花鹿试图穿过马路时被一辆小车撞翻在地,但随后它站起身来,迅速跑进了路边的树林。这辆小车的左侧后视镜被撞断。网友拍摄的视频显示,这只梅花鹿从绿化带突然跑向机动车道,一辆白色汽车避让不及撞了上去。此事引起…