• Redis的缓存问题(四)将redis常用操作封装成工具类


    Redis的缓存问题(四)将redis常用操作封装成工具类

    Redis工具类功能设计

    Redis工具类中代码分析

    queryWithPassThrough()    缓存穿透分析

    (1)泛型 

    (2)Function 函数接口

    Redis工具类完整代码实现

    Redis缓存问题小结 


    Redis的缓存问题(四)将redis常用操作封装成工具类

    Redis工具类功能设计

    1. 将任意的Java对象序列化为json并储存在string类型的key中,并且可以设置TTL过期时间。

    2. 将任意的Java对象序列化为json并储存在string类型的key中,并且可以设置逻辑过期时间,用于缓存击穿。

    3. 根据指定的key查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透。

    4. 根据指定的key查询缓存,并反序列化为指定类型,利用逻辑过期解决缓存击穿。

    Redis工具类中代码分析

    本文查看黑马程序员视频,有一说一,这一节黑马的视频质量是真的高!链接如下:

    黑马程序员Redis入门到实战教程,全面透析redis底层原理+redis分布式锁+企业解决方案+redis实战_哔哩哔哩_bilibili

    完整的工具类代码在文末,这里主要是对工具类中的一些细节做一下分析!

    我们先来看一下 缓存穿透 的代码

    queryWithPassThrough()    缓存穿透分析

    1. // 缓存穿透
    2. public R queryWithPassThrough(
    3. String keyPrefix, ID id, Class type,
    4. Function dbFallback, Long time, TimeUnit unit){
    5. String key = keyPrefix + id;
    6. // 1.从redis查询商铺缓存
    7. String json = stringRedisTemplate.opsForValue().get(key);
    8. // 2.判断是否存在
    9. if (StrUtil.isNotBlank(json)) {
    10. // 3.存在,直接返回
    11. return JSONUtil.toBean(json, type);
    12. }
    13. // 判断命中的是否是空值
    14. if (json != null) {
    15. // 返回一个错误信息
    16. return null;
    17. }
    18. // 4.不存在,根据id查询数据库
    19. R r = dbFallback.apply(id);
    20. // 5.不存在,返回错误
    21. if (r == null) {
    22. // 将空值写入redis
    23. stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
    24. // 返回错误信息
    25. return null;
    26. }
    27. // 6.存在,写入redis
    28. this.set(key, r, time, unit);
    29. return r;
    30. }

    (1)泛型 

    里面使用了泛型,我们可以先看一下方法返回值泛型的写法,如下图:

    所以在我们在 R里面定义泛型,再在Class type 和 ID id 指明泛型R,ID的具体类型(泛型的推断)

    调用queryWithPassThrough() 

    (2)Function 函数接口

    关于这个内容可以参考一下这里:Java 8 Function 函数接口 | 未读代码 (wdbyte.com)

    因为我们实现的是Redis工具类,代码要有一定的复用性,原逻辑是“获取店铺ID”,但是下次可能就会用在“获取店铺类型”。

    1. // Shop shop = getById(id);
    2. R r = dbFallback.apply(id);

    Function dbFallback:表示参数是ID,返回值是R类型

    在 Java 8 中,Function 接口是一个函数接口,它位于包 java.util.function 下。 Function 接口中定义了一个 R apply(T t) 方法,它可以接受一个泛型 T 对象,返回一个泛型 R 对象,即参数类型和返回类型可以不同。

    所以说,在这里我们用 dbFallback 调用 apply() 方法,相当于是执行外部传进来的方法(下图)

    这里的 this::getById 等同于 id2 -> getById(id2)

    Java里面this后面跟着两个“冒号”的意思是:

    英文:double colon,双冒号(::)运算符在Java 8中被用作方法引用(method reference),方法引用是与lambda表达式相关的一个重要特性。它提供了一种不执行方法的方法。将方法作为参数传入stream中,使stream中每个元素都能进入方法中运行

    格式:类名::方法名

    user -> user.getAge()    等价于   User::getAge

    new HashMap<>()   等价于   HsahMap::new

    Redis工具类完整代码实现

    我们在工具类中还是会使用到RedisData这个类。

    RedisData 类

    1. @Data
    2. public class RedisData {
    3. // LocalDateTime : 同时含有年月日时分秒的日期对象
    4. // 并且LocalDateTime是线程安全的!
    5. private LocalDateTime expireTime;
    6. private Object data;
    7. }

    里面的属性expireTime(过期时间)使用了Java8新定义的时间类 LocalDateTime ,是线程安全的。 

    CacheClient 工具类

    1. package com.hmdp.utils;
    2. import cn.hutool.core.util.BooleanUtil;
    3. import cn.hutool.core.util.StrUtil;
    4. import cn.hutool.json.JSONObject;
    5. import cn.hutool.json.JSONUtil;
    6. import lombok.extern.slf4j.Slf4j;
    7. import org.springframework.data.redis.core.StringRedisTemplate;
    8. import org.springframework.stereotype.Component;
    9. import java.time.LocalDateTime;
    10. import java.util.concurrent.ExecutorService;
    11. import java.util.concurrent.Executors;
    12. import java.util.concurrent.TimeUnit;
    13. import java.util.function.Function;
    14. import static com.hmdp.utils.RedisConstants.CACHE_NULL_TTL;
    15. import static com.hmdp.utils.RedisConstants.LOCK_SHOP_KEY;
    16. /**
    17. * 1. 将任意的Java对象序列化为json并储存在string类型的key中,并且可以设置TTL过期时间。
    18. * 2. 将任意的Java对象序列化为json并储存在string类型的key中,并且可以设置逻辑过期时间,用于缓存击穿。
    19. * 3. 根据指定的key查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透。
    20. * 4. 根据指定的key查询缓存,并反序列化为指定类型,利用逻辑过期解决缓存击穿。
    21. */
    22. @Slf4j
    23. @Component
    24. public class CacheClient {
    25. private final StringRedisTemplate stringRedisTemplate;
    26. // stringRedisTemplate 构造函数注入 !
    27. public CacheClient(StringRedisTemplate stringRedisTemplate) {
    28. this.stringRedisTemplate = stringRedisTemplate;
    29. }
    30. // 存入redis的ket-value,并设计过期时间
    31. public void set(String key, Object value, Long time, TimeUnit unit) {
    32. // stringRedisTemplate要求是string类型,value直接拿下来是一个object
    33. // 使用 JSONUtil将 object 序列化为 string
    34. stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);
    35. }
    36. // 逻辑过期
    37. public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit) {
    38. // 设置逻辑过期
    39. RedisData redisData = new RedisData();
    40. redisData.setData(value);
    41. // LocalDateTime.now() 获取当前时间
    42. // plusSeconds 添加秒数
    43. // 使用TimeUnit包的 toSeconds将时间转换为秒数
    44. redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
    45. // 写入Redis
    46. stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
    47. }
    48. // 缓存穿透
    49. public R queryWithPassThrough(
    50. String keyPrefix, ID id, Class type, Function dbFallback, Long time, TimeUnit unit){
    51. // Java 8 定义了 Function 接口,apply()可以接受一个泛型 T 对象,返回一个泛型 R 对象
    52. // keyPrefix Key的前缀
    53. String key = keyPrefix + id;
    54. // 1.从redis查询商铺缓存
    55. String json = stringRedisTemplate.opsForValue().get(key);
    56. // 2.判断是否存在
    57. if (StrUtil.isNotBlank(json)) {
    58. // 3.存在,直接返回
    59. return JSONUtil.toBean(json, type);
    60. }
    61. // 判断命中的是否是空值
    62. if (json != null) {
    63. // 返回一个错误信息
    64. return null;
    65. }
    66. // 4.不存在,根据id查询数据库
    67. // Shop shop = getById(id); 但是需要使用到缓存穿透的场景有很多,可能是查shop,可能是user
    68. // 所以执行的orm是不一样的!
    69. // Function dbFallback : ID是参数、R是返回值。
    70. R r = dbFallback.apply(id);
    71. // 5.不存在,返回错误
    72. if (r == null) {
    73. // 将空值写入redis
    74. stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
    75. // 返回错误信息
    76. return null;
    77. }
    78. // 6.存在,写入redis
    79. this.set(key, r, time, unit);
    80. return r;
    81. }
    82. // 定义线程池
    83. private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
    84. // 逻辑过期
    85. public R queryWithLogicalExpire(
    86. String keyPrefix, ID id, Class type, Function dbFallback, Long time, TimeUnit unit) {
    87. String key = keyPrefix + id;
    88. // 1.从redis查询商铺缓存
    89. String json = stringRedisTemplate.opsForValue().get(key);
    90. // 2.判断是否存在
    91. if (StrUtil.isBlank(json)) {
    92. // 3.存在,直接返回
    93. return null;
    94. }
    95. // 4.命中,需要先把json反序列化为对象
    96. RedisData redisData = JSONUtil.toBean(json, RedisData.class);
    97. R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);
    98. LocalDateTime expireTime = redisData.getExpireTime();
    99. // 5.判断是否过期
    100. if(expireTime.isAfter(LocalDateTime.now())) {
    101. // 5.1.未过期,直接返回店铺信息
    102. return r;
    103. }
    104. // 5.2.已过期,需要缓存重建
    105. // 6.缓存重建
    106. // 6.1.获取互斥锁
    107. String lockKey = LOCK_SHOP_KEY + id;
    108. boolean isLock = tryLock(lockKey);
    109. // 6.2.判断是否获取锁成功
    110. if (isLock){
    111. // 6.3.成功,开启独立线程,实现缓存重建
    112. CACHE_REBUILD_EXECUTOR.submit(() -> {
    113. try {
    114. // 查询数据库
    115. R newR = dbFallback.apply(id);
    116. // 重建缓存
    117. this.setWithLogicalExpire(key, newR, time, unit);
    118. } catch (Exception e) {
    119. throw new RuntimeException(e);
    120. }finally {
    121. // 释放锁
    122. unlock(lockKey);
    123. }
    124. });
    125. }
    126. // 6.4.返回过期的商铺信息
    127. return r;
    128. }
    129. public R queryWithMutex(
    130. String keyPrefix, ID id, Class type, Function dbFallback, Long time, TimeUnit unit) {
    131. String key = keyPrefix + id;
    132. // 1.从redis查询商铺缓存
    133. String shopJson = stringRedisTemplate.opsForValue().get(key);
    134. // 2.判断是否存在
    135. if (StrUtil.isNotBlank(shopJson)) {
    136. // 3.存在,直接返回
    137. return JSONUtil.toBean(shopJson, type);
    138. }
    139. // 判断命中的是否是空值
    140. if (shopJson != null) {
    141. // 返回一个错误信息
    142. return null;
    143. }
    144. // 4.实现缓存重建
    145. // 4.1.获取互斥锁
    146. String lockKey = LOCK_SHOP_KEY + id;
    147. R r = null;
    148. try {
    149. boolean isLock = tryLock(lockKey);
    150. // 4.2.判断是否获取成功
    151. if (!isLock) {
    152. // 4.3.获取锁失败,休眠并重试
    153. Thread.sleep(50);
    154. return queryWithMutex(keyPrefix, id, type, dbFallback, time, unit);
    155. }
    156. // 4.4.获取锁成功,根据id查询数据库
    157. r = dbFallback.apply(id);
    158. // 5.不存在,返回错误
    159. if (r == null) {
    160. // 将空值写入redis
    161. stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
    162. // 返回错误信息
    163. return null;
    164. }
    165. // 6.存在,写入redis
    166. this.set(key, r, time, unit);
    167. } catch (InterruptedException e) {
    168. throw new RuntimeException(e);
    169. }finally {
    170. // 7.释放锁
    171. unlock(lockKey);
    172. }
    173. // 8.返回
    174. return r;
    175. }
    176. private boolean tryLock(String key) {
    177. Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
    178. return BooleanUtil.isTrue(flag);
    179. }
    180. private void unlock(String key) {
    181. stringRedisTemplate.delete(key);
    182. }
    183. }

    ShopServiceImpl 类

    我们没有使用到Redis工具类的时候,所编写的Service层的代码看起来是十分复杂的!

    现在将主要功能使用CacheClient工具类封装之后,只要寥寥数十行即可!

    1. package com.hmdp.service.impl;
    2. import cn.hutool.json.JSONUtil;
    3. import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
    4. import com.hmdp.dto.Result;
    5. import com.hmdp.entity.Shop;
    6. import com.hmdp.mapper.ShopMapper;
    7. import com.hmdp.service.IShopService;
    8. import com.hmdp.utils.CacheClient;
    9. import com.hmdp.utils.RedisData;
    10. import org.springframework.data.redis.core.StringRedisTemplate;
    11. import org.springframework.stereotype.Service;
    12. import org.springframework.transaction.annotation.Transactional;
    13. import javax.annotation.Resource;
    14. import java.time.LocalDateTime;
    15. import java.util.concurrent.TimeUnit;
    16. import static com.hmdp.utils.RedisConstants.*;
    17. @Service
    18. public class ShopServiceImpl extends ServiceImpl implements IShopService {
    19. @Resource
    20. private StringRedisTemplate stringRedisTemplate;
    21. @Resource
    22. private CacheClient cacheClient;
    23. @Override
    24. public Result queryById(Long id) {
    25. // 缓存穿透
    26. // Shop shop = cacheClient.queryWithPassThrough(CACHE_SHOP_KEY,id,Shop.class,this::getById,CACHE_SHOP_TTL,TimeUnit.MINUTES);
    27. // 1.互斥锁解决缓存击穿问题
    28. // 注意!!!
    29. // 如果使用了逻辑过期由于使用了 RedisData,所以存入的redis的类型发生了改变。
    30. // Shop shop = cacheClient.queryWithMutex(CACHE_SHOP_KEY, id, Shop.class, this::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES);
    31. // 2.逻辑过期解决缓存击穿问题
    32. Shop shop = cacheClient.queryWithLogicalExpire(CACHE_SHOP_KEY, id, Shop.class, this::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES);
    33. if (shop == null) {
    34. return Result.fail("店铺不存在");
    35. }
    36. System.out.println(shop);
    37. return Result.ok(shop);
    38. }
    39. /**
    40. * 重建缓存,先缓存预热一下,否则queryWithLogicalExpire() 的expire为null
    41. * @param id
    42. * @param expireSeconds
    43. */
    44. public void saveShopRedis(Long id, Long expireSeconds) {
    45. // 1.查询店铺数据
    46. Shop shop = getById(id);
    47. // 2.封装逻辑过期时间
    48. RedisData redisData = new RedisData();
    49. redisData.setData(shop);
    50. redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds)); // 过期时间
    51. // 3.写入redis
    52. stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
    53. }
    54. @Override
    55. @Transactional
    56. public Result update(Shop shop) {
    57. System.out.println("up");
    58. Long id = shop.getId();
    59. if (id == null) {
    60. return Result.fail("店铺id不能为空!");
    61. }
    62. // 1.更新数据库
    63. updateById(shop);
    64. // 2.删除缓存
    65. stringRedisTemplate.delete(CACHE_SHOP_KEY + id);
    66. return Result.ok();
    67. }
    68. }

    Redis缓存问题小结 

    • 如何添加缓存

    Redis的缓存问题(一)添加redis缓存与扩展_面向鸿蒙编程的博客-CSDN博客_redis添加缓存

    • 缓存更新策略

    Redis的缓存问题(二)缓存更新策略与实践_面向鸿蒙编程的博客-CSDN博客_redis修改缓存数据

    • 缓存穿透问题
    • 缓存雪崩问题
    • 缓存击穿问题

    Redis的缓存问题(三)缓存穿透、缓存雪崩、缓存击穿_面向鸿蒙编程的博客-CSDN博客

    • redis工具类的编写

    Redis的缓存问题(四)将redis常用操作封装成工具类_面向鸿蒙编程的博客-CSDN博客

  • 相关阅读:
    Linux常用操作汇总:内容有点杂,但很实用
    KubeSphere 网关的设计与实现(解读)
    Vue 源码解读(7)—— Hook Event
    Web Components详解-Shadow DOM样式控制
    zynq mpsoc裸机多中断运行
    【力扣的101夜】轮转数组
    鸿蒙应用开发初尝试《创建项目》,之前那篇hello world作废
    Docker Postgres 安装部署指南1.0
    js中super的使用
    【GoWeb项目-个人Blog】初始化数据库和日志
  • 原文地址:https://blog.csdn.net/weixin_43715214/article/details/126514517