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()    缓存穿透分析

// 缓存穿透
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.存在,直接返回
        return JSONUtil.toBean(json, type);
    }
    // 判断命中的是否是空值
    if (json != null) {
        // 返回一个错误信息
        return null;
    }
    // 4.不存在,根据id查询数据库
    R r = dbFallback.apply(id);
    // 5.不存在,返回错误
    if (r == null) {
        // 将空值写入redis
        stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
        // 返回错误信息
        return null;
    }
    // 6.存在,写入redis
    this.set(key, r, time, unit);
    return r;
}

(1)泛型 

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

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

调用queryWithPassThrough() 

(2)Function 函数接口

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

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

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

Function<ID, R> 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 类

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

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

CacheClient 工具类

package com.hmdp.utils;

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

import static com.hmdp.utils.RedisConstants.CACHE_NULL_TTL;
import static com.hmdp.utils.RedisConstants.LOCK_SHOP_KEY;

/**
 * 1. 将任意的Java对象序列化为json并储存在string类型的key中,并且可以设置TTL过期时间。
 * 2. 将任意的Java对象序列化为json并储存在string类型的key中,并且可以设置逻辑过期时间,用于缓存击穿。
 * 3. 根据指定的key查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透。
 * 4. 根据指定的key查询缓存,并反序列化为指定类型,利用逻辑过期解决缓存击穿。
 */

@Slf4j
@Component
public class CacheClient {
    private final StringRedisTemplate stringRedisTemplate;

    // stringRedisTemplate 构造函数注入 !
    public CacheClient(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    // 存入redis的ket-value,并设计过期时间
    public void set(String key, Object value, Long time, TimeUnit unit) {
        // stringRedisTemplate要求是string类型,value直接拿下来是一个object
        // 使用 JSONUtil将 object 序列化为 string
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);
    }

    // 逻辑过期
    public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit) {
        // 设置逻辑过期
        RedisData redisData = new RedisData();
        redisData.setData(value);
        // LocalDateTime.now() 获取当前时间
        // plusSeconds 添加秒数
        // 使用TimeUnit包的 toSeconds将时间转换为秒数
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
        // 写入Redis
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
    }

    // 缓存穿透
    public <R,ID> R queryWithPassThrough(
            String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit){
        // Java 8 定义了 Function 接口,apply()可以接受一个泛型 T 对象,返回一个泛型 R 对象
        // keyPrefix Key的前缀
        String key = keyPrefix + id;
        // 1.从redis查询商铺缓存
        String json = stringRedisTemplate.opsForValue().get(key);
        // 2.判断是否存在
        if (StrUtil.isNotBlank(json)) {
            // 3.存在,直接返回
            return JSONUtil.toBean(json, type);
        }
        // 判断命中的是否是空值
        if (json != null) {
            // 返回一个错误信息
            return null;
        }
        // 4.不存在,根据id查询数据库
        // Shop shop = getById(id); 但是需要使用到缓存穿透的场景有很多,可能是查shop,可能是user
        // 所以执行的orm是不一样的!
        // Function<ID, R> dbFallback : ID是参数、R是返回值。
        R r = dbFallback.apply(id);
        // 5.不存在,返回错误
        if (r == null) {
            // 将空值写入redis
            stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
            // 返回错误信息
            return null;
        }
        // 6.存在,写入redis
        this.set(key, r, time, unit);
        return r;
    }

    // 定义线程池
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
    // 逻辑过期
    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);
        R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);
        LocalDateTime expireTime = redisData.getExpireTime();
        // 5.判断是否过期
        if(expireTime.isAfter(LocalDateTime.now())) {
            // 5.1.未过期,直接返回店铺信息
            return r;
        }
        // 5.2.已过期,需要缓存重建
        // 6.缓存重建
        // 6.1.获取互斥锁
        String lockKey = LOCK_SHOP_KEY + id;
        boolean isLock = tryLock(lockKey);
        // 6.2.判断是否获取锁成功
        if (isLock){
            // 6.3.成功,开启独立线程,实现缓存重建
            CACHE_REBUILD_EXECUTOR.submit(() -> {
                try {
                    // 查询数据库
                    R newR = dbFallback.apply(id);
                    // 重建缓存
                    this.setWithLogicalExpire(key, newR, time, unit);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }finally {
                    // 释放锁
                    unlock(lockKey);
                }
            });
        }
        // 6.4.返回过期的商铺信息
        return r;
    }

    public <R, ID> R queryWithMutex(
            String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
        String key = keyPrefix + id;
        // 1.从redis查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        // 2.判断是否存在
        if (StrUtil.isNotBlank(shopJson)) {
            // 3.存在,直接返回
            return JSONUtil.toBean(shopJson, type);
        }
        // 判断命中的是否是空值
        if (shopJson != null) {
            // 返回一个错误信息
            return null;
        }

        // 4.实现缓存重建
        // 4.1.获取互斥锁
        String lockKey = LOCK_SHOP_KEY + id;
        R r = null;
        try {
            boolean isLock = tryLock(lockKey);
            // 4.2.判断是否获取成功
            if (!isLock) {
                // 4.3.获取锁失败,休眠并重试
                Thread.sleep(50);
                return queryWithMutex(keyPrefix, id, type, dbFallback, time, unit);
            }
            // 4.4.获取锁成功,根据id查询数据库
            r = dbFallback.apply(id);
            // 5.不存在,返回错误
            if (r == null) {
                // 将空值写入redis
                stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
                // 返回错误信息
                return null;
            }
            // 6.存在,写入redis
            this.set(key, r, time, unit);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }finally {
            // 7.释放锁
            unlock(lockKey);
        }
        // 8.返回
        return r;
    }

    private boolean tryLock(String key) {
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }

    private void unlock(String key) {
        stringRedisTemplate.delete(key);
    }
}

ShopServiceImpl 类

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

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

package com.hmdp.service.impl;

import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.dto.Result;
import com.hmdp.entity.Shop;
import com.hmdp.mapper.ShopMapper;
import com.hmdp.service.IShopService;
import com.hmdp.utils.CacheClient;
import com.hmdp.utils.RedisData;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;

import java.time.LocalDateTime;
import java.util.concurrent.TimeUnit;

import static com.hmdp.utils.RedisConstants.*;

@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Resource
    private CacheClient cacheClient;


    @Override
    public Result queryById(Long id) {

        // 缓存穿透
        // Shop shop = cacheClient.queryWithPassThrough(CACHE_SHOP_KEY,id,Shop.class,this::getById,CACHE_SHOP_TTL,TimeUnit.MINUTES);

        // 1.互斥锁解决缓存击穿问题
        // 注意!!!
        // 如果使用了逻辑过期由于使用了 RedisData,所以存入的redis的类型发生了改变。
        // Shop shop = cacheClient.queryWithMutex(CACHE_SHOP_KEY, id, Shop.class, this::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES);

        // 2.逻辑过期解决缓存击穿问题
        Shop shop = cacheClient.queryWithLogicalExpire(CACHE_SHOP_KEY, id, Shop.class, this::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES);

        if (shop == null) {
            return Result.fail("店铺不存在");
        }
        System.out.println(shop);
        return Result.ok(shop);
    }

    /**
     * 重建缓存,先缓存预热一下,否则queryWithLogicalExpire() 的expire为null
     * @param id
     * @param expireSeconds
     */
    public void saveShopRedis(Long id, Long expireSeconds) {
        // 1.查询店铺数据
        Shop shop = getById(id);
        // 2.封装逻辑过期时间
        RedisData redisData = new RedisData();
        redisData.setData(shop);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));  // 过期时间
        // 3.写入redis
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
    }


    @Override
    @Transactional
    public Result update(Shop shop) {
        System.out.println("up");
        Long id = shop.getId();
        if (id == null) {
            return Result.fail("店铺id不能为空!");
        }
        // 1.更新数据库
        updateById(shop);

        // 2.删除缓存
        stringRedisTemplate.delete(CACHE_SHOP_KEY + id);

        return Result.ok();
    }
}

Redis缓存问题小结 

  • 如何添加缓存

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

  • 缓存更新策略

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

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

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

  • redis工具类的编写

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

posted @   金鳞踏雨  阅读(237)  评论(0编辑  收藏  举报  
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)
点击右上角即可分享
微信分享提示