美文网首页
Redis-缓存工具类

Redis-缓存工具类

作者: 石头耳东 | 来源:发表于2022-03-25 20:38 被阅读0次

阅读本文需要了解Java泛型以及lambda表达式的基础使用,会微量包含这些内容,但这些又是代码的一些关键。

零、本文纲要

  • 一、Redis缓存相关工具类
  • 二、缓存穿透相关方法
    0、缓存穿透相关概念
  • 三、缓存击穿相关方法
    0、缓存击穿相关概念
  • 四、缓存雪崩(补充)
    0、缓存雪崩相关概念

Redis指令在线使用:Try Redis

tips:Ctrl + F快速定位所需内容阅读吧。

一、Redis缓存相关工具类

1、基础依赖

  • ① redis相关依赖
    spring-boot-starter-data-redis:redis基础依赖;
    commons-pool2:redis连接池;
  • ② web相关依赖
    spring-boot-starter-web:SpringBoot的Web依赖;
  • ③ 数据库连接依赖
    mysql-connector-java:mysql连接依赖;
  • ④ mybatis-plus相关依赖
    mybatis-plus-boot-starter:MyBatis Plus的相关依赖;
  • ⑤ lombok相关依赖
    lombok:方便编写实体类;
  • ⑥ hutool相关依赖
    hutool-all:各种工具类的依赖;
  • ⑦ test相关依赖
    spring-boot-starter-test:测试相关依赖;
    <dependencies>
        <!--spring_data_redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!--redis_pool-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
        <!--spring_boot-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--mysql_connector-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
            <version>5.1.47</version>
        </dependency>
        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <!--spring_boot_test-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!--mybatis_plus-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.3</version>
        </dependency>
        <!--hutool-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.7.17</version>
        </dependency>
    </dependencies>

2、编写缓存工具类

  • ① @Slf4j注解
    方便打日志,省去private final Logger logger = LoggerFactory.getLogger(当前类名.class);
  • ② @Component
    将工具类注册到Spring容器中,方便使用;
  • ③ StringRedisTemplate类
    Redis的模板类,其keyvalue的形式均为String类型
@Slf4j
@Component
public class CacheClient {

    private final StringRedisTemplate stringRedisTemplate;

    public CacheClient(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    ... ...

}

二、缓存穿透相关方法

0、缓存穿透相关概念

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

常见的两种解决方案:Ⅰ、缓存空对象;Ⅱ、布隆过滤器。

注意:以下案例以缓存空对象为例。

1、保存任意Java类型对象到缓存,并设置过期时间

  • ① JSONUtil#toJsonStr方法
    toJsonStr方法可以将Java对象类型转换为String类型;
  • ② stringRedisTemplate#opsForValue#set方法
    set(K key, V value, final long timeout, final TimeUnit unit)方法对应redis指令中的set key value ex time
    官方提供的尝试redis指令的网址:Try Redis
    /**
     * 设置任意Java对象的缓存过期时间
     *
     * @param key 缓存key
     * @param value 缓存Java对象
     * @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、预防缓存穿透的方法

缓存穿透缓存空对象方案.png
  • ① keyPrefix参数
    String key = keyPrefix + id;用来保证某一资源key的唯一性;
  • ② ID id
    由于id的数据类型可能是Long、Integer、String,所以此处使用泛型;
  • ③ R
    结果类型,查询缓存的数据类型不确定,也使用泛型;
  • ④ JSONUtil#toBean方法
    toBean(String jsonString, Class<T> beanClass)方法可以将JSON字符串转为实体类对象;
    由于参数中需要实体类对象的类型,所以我们传入Class<R> type
  • ⑤ Function<ID, R> dbFallback参数
    从数据库中查询所需对象,这个函数会因结果对象不同而不同,所以我们也通过函数参数化传入函数参数;
  • ⑥ StrUtil#isNotBlank方法
    StrUtil.isNotBlank(null) // false
    StrUtil.isNotBlank("") // false
    StrUtil.isNotBlank(" \t\n") // false
    StrUtil.isNotBlank("abc") // true
    /**
     * 预防缓存穿透的方法
     *
     * @param keyPrefix key前缀
     * @param id 查询id
     * @param type 结果类型
     * @param dbFallback 查询函数
     * @param time 过期时间
     * @param unit 过期时间单位
     * @param <R> 结果类型泛型
     * @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. 判断缓存中是否存在数据
        if (StrUtil.isNotBlank(json)){
            //3. 存在,直接返回
            R r = JSONUtil.toBean(json, type);
            return r;
        }
        //判断json是等于空值
        if (json != null){ //即json等于""的情形
            //结果不存在
            return null;
        }
        //4. 从数据库中查询
        R r = dbFallback.apply(id);
        //4.1 在数据库中也不存在
        if (r == null){
            //将空值写入Redis中
            stringRedisTemplate.opsForValue().set(key, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
            //返回错误信息
            return null;
        }
        //4.2 在数据库中存在,写入redis,返回信息
        this.set(key, r, time, unit);
        return r;
    }

3、service类

  • ① ShopMapper接口
    其中,BaseMapper是DAO层的CRUD封装;
BaseMapper.png
public interface ShopMapper extends BaseMapper<Shop> {

}
  • ② IShopService接口
    其中,IService是业务逻辑层的CRUD封装,多了批量增、删、改的操作封装;
IService.png
public interface IShopService extends IService<Shop> {

    Result queryShopById(Long id);

}
  • ③ ShopServiceImpl实现类
    其中,ServiceImpl 针对业务逻辑层的实现;
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {

    @Resource
    private CacheClient cacheClient;

    /**
     * 根据id查询商铺信息
     *
     * @param id 商铺id
     * @return 结果
     */
    @Override
    public Result queryShopById(Long id) {
        //1. 通过缓存工具类调用预防缓存穿透的查询方法
        Shop shop = cacheClient.queryWithPassThrough(
                RedisConstants.CACHE_SHOP_KEY,
                id,
                Shop.class,
                this::getById,
                RedisConstants.CACHE_SHOP_TTL,
                TimeUnit.MINUTES);
        //2. 判断查询结果是否为null
        if (shop == null){
            return Result.fail("店铺不存在!");
        }
        //3. 返回结果
        return Result.ok(shop);
    }

}

三、缓存击穿相关方法

0、缓存击穿相关概念

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

常见的两种解决方案:Ⅰ、互斥锁;Ⅱ、逻辑过期。

注意:以下方案以逻辑过期为例。

1、设置任意Java对象的逻辑过期时间

  • ① RedisData类
    用于存储时间数据expireTime,以及对象数据data
@Data
public class RedisData {
    private LocalDateTime expireTime;
    private Object data;
}
  • ② LocalDateTime.now().plusSeconds方法
    给当前时间加上设置的过期时间;
  • ③ unit.toSeconds方法
    TimeUnit#toSeconds方法,将单位换算成秒;
    /**
     * 设置任意Java对象的逻辑过期时间
     *
     * @param key 缓存key
     * @param value 缓存的Java对象
     * @param time 过期时间
     * @param unit 过期时间单位
     */
    public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit){
        //设置存储数据
        RedisData redisData = new RedisData();
        redisData.setData(value);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time))); //将对应单位转换成秒
        // 写入Redis
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
    }

注意:设置逻辑过期,并没有真正的给缓存设置过期时间。
2、预防缓存击穿的方法

缓存击穿逻辑过期方案.png
  • ① 准备线程池
    //线程池
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
  • ② 互斥锁准备
    BooleanUtil#isTrue方法处理Boolean结果拆箱为null的问题,返回boolean类型;
    stringRedisTemplate.opsForValue().setIfAbsent方法对应SETNX指令
    /**
     * 预防缓存击穿查询方法(互斥锁方案) 获取互斥锁
     *
     * @param key id
     * @return 结果
     */
    public boolean tryLock(String key){
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", RedisConstants.LOCK_CACHE_TTL, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }

    /**
     * 预防缓存击穿查询方法(互斥锁方案) 释放互斥锁
     *
     * @param key id
     */
    public void unlock(String key){
        stringRedisTemplate.delete(key);
    }
  • ③ queryWithLogicalExpire方法主体

  • Ⅰ keyPrefix参数
    String key = keyPrefix + id;用来保证某一资源key的唯一性;

  • Ⅱ ID id
    由于id的数据类型可能是Long、Integer、String,所以此处使用泛型;

  • Ⅲ R
    结果类型,查询缓存的数据类型不确定,也使用泛型;

  • Ⅳ JSONUtil#toBean方法
    toBean(String jsonString, Class<T> beanClass)方法可以将JSON字符串转为实体类对象;
    由于参数中需要实体类对象的类型,所以我们传入Class<R> type
    toBean(JSONObject json, Class<T> beanClass)方法可以将JSONObject对象转换成我们指定的对象类型;

  • Ⅴ Function<ID, R> dbFallback参数
    从数据库中查询所需对象,这个函数会因结果对象不同而不同,所以我们也通过函数参数化传入函数参数;

  • Ⅵ LocalDateTime#isAfter方法
    该方法用于判断当前时间(此处我们使用逻辑过期时间为此时间)是否晚于比较时间(此处我们获取当前时间为被对比的时间)
    即逻辑过期时间晚于当前时间,则不算过期;

注意:步骤3中判断为空即返回不存在,是因为业务中保存店铺信息时就会将店铺信息保存到Redis中。

        if (StrUtil.isBlank(json)){
            //3. 不存在,直接返回
            return null;
        }

完整方法如下:

    /**
     * 预防缓存击穿的方法(逻辑过期方案)
     *
     * @param keyPrefix key前缀
     * @param id 查询id
     * @param type 结果类型
     * @param dbFallback 查询函数
     * @param time 过期时间
     * @param unit 过期时间单位
     * @param <R> 返回结果类型泛型
     * @param <ID> 查询id类型泛型
     * @return 结果
     */
    public <R, ID> R queryWithLogicalExpire(
            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. 判断缓存中是否存在数据
        if (StrUtil.isBlank(json)){
            //3. 不存在,直接返回
            return null;
        }
        //4. 命中,需要先把json反序列化为对象
        RedisData redisData = JSONUtil.toBean(json, RedisData.class);
        JSONObject jsonObject = (JSONObject) redisData.getData();
        R r = JSONUtil.toBean(jsonObject, type);
        LocalDateTime expireTime = redisData.getExpireTime();
        //5. 判断是否过期
        if (expireTime.isAfter(LocalDateTime.now())){
            //5.1 未过期,直接返回缓存信息
            return r;
        }
        //5.2 已过期,需要缓存重建
        //6. 缓存重建
        String lockKey = keyPrefix + id;
        //6.1 获取互斥锁
        boolean isLock = tryLock(lockKey);
        //6.2 判断是否获取锁成功
        if (isLock){
            //6.3 成功,开启独立线程,实现缓存重建
            CACHE_REBUILD_EXECUTOR.submit(() -> {
                try {
                    // 查询数据库
                    R r1 = dbFallback.apply(id);
                    // 重建缓存
                    this.setWithLogicalExpire(key, r1, time, unit);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    // 释放锁
                    unlock(lockKey);
                }
            });
        }
        //6.4 返回过期的商铺信息
        return r;
    }

可以看到,我们将重建缓存数据的任务交由线程池中的线程来完成了,单独看如下:

        //6. 缓存重建
        String lockKey = keyPrefix + id;
        //6.1 获取互斥锁
        boolean isLock = tryLock(lockKey);
        //6.2 判断是否获取锁成功
        if (isLock){
            //6.3 成功,开启独立线程,实现缓存重建
            CACHE_REBUILD_EXECUTOR.submit(() -> {
                try {
                    // 查询数据库
                    R r1 = dbFallback.apply(id);
                    // 重建缓存
                    this.setWithLogicalExpire(key, r1, time, unit);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    // 释放锁
                    unlock(lockKey);
                }
            });
        }

3、service类

注意:此处service接口、mapper接口省略了,具体可以看上面的缓存穿透方案。

    /**
     * 根据id查询商铺信息
     *
     * @param id 商铺id
     * @return 结果
     */
    @Override
    public Result queryShopById(Long id) {
        //5. 通过缓存工具类调用预防缓存击穿的方法(逻辑过期方案)
        Shop shop = cacheClient.queryWithLogicalExpire(
                RedisConstants.CACHE_SHOP_KEY,
                id,
                Shop.class,
                this::getById,
                RedisConstants.CACHE_SHOP_TTL,
                TimeUnit.MINUTES);

        if (shop == null){
            return Result.fail("店铺不存在!");
        }
        //6. 返回结果
        return Result.ok(shop);
    }

四、缓存雪崩(补充)

0、缓存雪崩相关概念

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

常见的解决方案:
Ⅰ、 给不同的Key的TTL添加随机值;
Ⅱ、 利用Redis集群提高服务的可用性;
Ⅲ、 给缓存业务添加降级限流策略;
Ⅳ、 给业务添加多级缓存。

五、结尾

以上即为Redis缓存实践的部分内容,感谢阅读。

相关文章

  • Redis-缓存工具类

    阅读本文需要了解Java泛型以及lambda表达式的基础使用,会微量包含这些内容,但这些又是代码的一些关键。 零、...

  • Android中的缓存以及缓存清理

    缓存工具类 缓存目录的处理 清理缓存 获取缓存的大小 1. 缓存工具类 GitHub地址: https://gi...

  • 使用过Redis,我竟然还不知道Rdb

    使用过Redis,那就先说说使用过那些场景吧 字符串缓存 //举例$redis->set();$redis->ge...

  • thinkphp 缓存管理类

    一个基于thinkphp,redis的缓存工具管理类 对应的数据表 缓存设置类 数据表模型 缓存工具类 使用案例

  • 常用工具类

    BaseApp ToastUtil(Toast 工具类) MyAppGlideModule(自动缓存图片的工具类,...

  • 常用的工具类

    BaseApp ToastUtil(Toast 工具类) MyAppGlideModule(自动缓存图片的工具类,...

  • 缓存工具类

    安卓开发一般都需要进行数据缓存,常用操作老司机已为你封装完毕,经常有小伙伴问怎么判断缓存是否可用,那我告诉你,你可...

  • Java 缓存优化2 - 实现简单的内存缓存

    不使用淘汰策略的map的缓存实现方式 工具类 测试类 运行结果 使用FIFO淘汰策略的map的缓存实现方式 工具类...

  • redis-mysql缓存不一致,双写

    redis-缓存不一致,双写 但是在更新缓存方面,对于更新完数据库,是更新缓存呢,还是删除缓存。又或者是先删除缓存...

  • springboot redis整合笔记

    引入 application.yml配置 redis配置 定义序列化方式 缓存工具接口 缓存工具实现类

网友评论

      本文标题:Redis-缓存工具类

      本文链接:https://www.haomeiwen.com/subject/hrrdjrtx.html