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

Redis--缓存工具封装

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

案例学习:缓存工具封装

基于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.xdnf.cn/news/748549.html

相关文章:

  • 【PhysUnits】15.6 引入P1后的左移运算(shl.rs)
  • 佳能 Canon G3030 Series 打印机信息
  • 【C语言练习】075. 使用C语言访问硬件资源
  • [LitCTF 2024]浏览器也能套娃?
  • [学习] RTKlib 实用工具介绍
  • JDK17 与JDK8 共同存在一个电脑上
  • 静态综合实验
  • 软件性能之CPU
  • 机器学习算法——KNN
  • vue3的watch用法
  • 树莓派PWM控制LED灯
  • 使用arthas热替换在线运行的java class文件
  • 描述性统计的可视化分析
  • Java弱引用与软引用的核心区别
  • ubuntu20.04.5-arm64版安装robotjs
  • 牛客周赛94
  • 使用Java实现简单的计算机案例
  • uv:一个现代化的 Python 依赖管理工具
  • AMBER软件介绍
  • JS和TS的区别
  • 姜老师MBTI课程:ISTP和ISFP
  • Vue事件处理
  • 【razor】采集模块设置了窗体句柄但并不能直接渲染
  • 《C 盘清理技巧分享》
  • 经济法-7-上市公司首次发行、配股增发条件
  • 【数据治理】要点整理-信息技术数据质量评价指标-GB/T36344-2018
  • 【数据集】30 m空间/1 h时间分辨率地表温度LST数据集
  • 投稿Cover Letter怎么写
  • C语言 — 自定义类型(结构体,联合体,枚举)
  • stm32默认复位刚开始由hsi作为主时钟源而后来才换成的pll