Redis-缓存工具类

2022-03-25  本文已影响0人  石头耳东

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

零、本文纲要

Redis指令在线使用:Try Redis

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

一、Redis缓存相关工具类

1、基础依赖

    <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
@Component
public class CacheClient {

    private final StringRedisTemplate stringRedisTemplate;

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

    ... ...

}

二、缓存穿透相关方法

0、缓存穿透相关概念

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

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

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

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

    /**
     * 设置任意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
    /**
     * 预防缓存穿透的方法
     *
     * @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类

BaseMapper.png
public interface ShopMapper extends BaseMapper<Shop> {

}
IService.png
public interface IShopService extends IService<Shop> {

    Result queryShopById(Long id);

}
@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对象的逻辑过期时间

@Data
public class RedisData {
    private LocalDateTime expireTime;
    private Object data;
}
    /**
     * 设置任意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);
    /**
     * 预防缓存击穿查询方法(互斥锁方案) 获取互斥锁
     *
     * @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);
    }

注意:步骤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缓存实践的部分内容,感谢阅读。

上一篇 下一篇

猜你喜欢

热点阅读