封装Redis工具类(做到拿来即用)

开发板推荐:天空星STM32F407VET6开发板

超高性价比 STM32主控 | 超高主频 | 一板兼容百芯 | 比赛神器 | 沉金彩色丝印

1.封装Redis工具类

基于StringRedisTemplate封装一个缓存工具类,满足下列需求:

方法1:将任意Java对象序列化json并存储在string类型的key中,并且可以设置TTL过期时间
方法2:将任意Java对象序列化json并存储在string类型的key中,并且可以设置逻辑过期时间,用于处理缓存击穿问题

方法3:根据指定的key查询缓存,并反序列化指定类型,利用缓存空值的方式解决缓存穿透问题
方法4:根据指定的key查询缓存,并反序列化指定类型,需要利用逻辑过期解决缓存击穿问题

2.CacheClient工具类的实现

2.1各个方法的代码实现

@Slf4j
@Component
public class CacheClient {

    private final StringRedisTemplate stringRedisTemplate;
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

    //通过构造函数进行注入
    public CacheClient(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

 方法1:将任意Java对象序列化json并存储在string类型的key中,并且可以设置TTL过期

  1. set 方法:

    • 用于普通的缓存设置。
    • 可以指定缓存的过期时间和时间单位(如秒、分钟、小时等)。
    • 用途: 适用于普通的数据缓存,满足基本的缓存需求。
 /**
     * 将任意Java对象序列化为json并存储在string类型的key中,并且可以设置TTL过期时间
     * @param value
     * @param key
     * @param time
     * @param unit
     */
    public void set(Object value, String key, Long time, TimeUnit unit){
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value),time,unit);
    }

 方法2:将任意Java对象序列化json并存储在string类型的key中,并且可以设置逻辑过期时间

  1. setWithLogicalExpire 方法:

    • 用于设置带逻辑过期时间的缓存数据。
    • 在缓存中存储数据时同时包含一个逻辑过期时间字段。
    • 用途: 适用于防止缓存击穿,确保在缓存过期时能够返回旧数据并异步刷新。
 /**
     * 将任意Java对象序列化为json并存储在string类型的key中,并且可以设置逻辑过期时间,用于处理缓
     * @param value
     * @param key
     * @param time
     * @param unit
     */
    public void setWithLogicalExpire(Object value, String key, 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));
    }

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

  1. queryWithPassThrough 方法:

    • 实现了缓存穿透的解决方案。
    • 当缓存中没有数据时,从数据库中查询并缓存结果;如果数据库中也没有数据,则将空值写入缓存,避免缓存穿透。
    • 用途: 防止无效请求频繁穿透缓存访问数据库。
/**
     * 缓存穿透
     * @return
     */
    public <R,ID> R queryWithPassThrough(String pre, ID id, Class<R> type, Function<ID,R> dbFallback,Long time, TimeUnit unit){

        String key = pre + id;
        //1.从redis中查询缓存
        String json = stringRedisTemplate.opsForValue().get(key);
        //2.判断是否命中
        if (StrUtil.isNotBlank(json)) {
            //3.命中 返回商铺信息
            return JSONUtil.toBean(json,type);
        }
        //判断是否为null值
        if(json != null){
            return null;
        }
        //4.未命中 根据id查询数据库
        R r = dbFallback.apply(id);
        if (r == null) {
            //5.将空值存入redis
            stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL, TimeUnit.MINUTES);
            //5.存在 返回
            return null;
        }
        //6.存在 写入redis 返回商品信息
        this.set(r,pre,time,unit);
        return r;
    }

方法4:根据指定的key查询缓存,并反序列化指定类型,需要利用逻辑过期解决缓存击穿问题

  1. queryWithLogicalExpire 方法:

    • 使用逻辑过期解决缓存击穿问题。
    • 当缓存数据过期时,通过异步线程更新缓存,同时仍然返回旧数据,保证系统的高可用性。
    • 用途: 适用于热点数据,防止在缓存失效时大量请求同时涌向数据库。
 /**
     * 利用逻辑过期时间解决缓存击穿
     * @return
     */
    public <R,ID> R queryWithLogicalExpire(String pre,ID id,Class<R> type,Function<ID,R> dbFallback,Long time, TimeUnit unit){
        String key = pre + id;
        //1.从redis中查询缓存
        String Json = stringRedisTemplate.opsForValue().get(key);
        //2.不存在 直接返回空(不是热点数据)
        if(StrUtil.isBlank(Json)){
            return null;
        }
        //3.存在 需要先把Json反序列化为对象
        RedisData redisData = JSONUtil.toBean(Json, RedisData.class);
        JSONObject data =(JSONObject) redisData.getData();
        R r = JSONUtil.toBean(data,type);

        //4.判断缓存是否过期
        LocalDateTime expireTime = redisData.getExpireTime();

        if(expireTime.isAfter(LocalDateTime.now())){
            //5.未过期 返回商铺信息
            return r;
        }
        //6.已过期 缓存重建
        //6.1获取互斥锁
        String lockKey = LOCK_SHOP_KEY + id;
        boolean isTryLock = tryLock(lockKey);
        //6.2判断是否获取成功
        if (isTryLock) {
            //6.3成功

            //再次检查缓存中的数据是否过期,如果没有过期,无需重建
            //1.从redis中查询缓存
            Json = stringRedisTemplate.opsForValue().get(key);
            //2.不存在 直接返回空(不是热点数据)
            if(StrUtil.isBlank(Json)){
                return null;
            }
            //3.存在 需要先把Json反序列化为对象
            redisData = JSONUtil.toBean(Json, RedisData.class);
            data =(JSONObject) redisData.getData();
            r = JSONUtil.toBean(data, type);
            //4.判断缓存是否过期
            expireTime = redisData.getExpireTime();
            if(expireTime.isAfter(LocalDateTime.now())){
                //5.未过期 返回商铺信息
                return r;
            }
            //还过期 开启独立线程 缓存重建
            CACHE_REBUILD_EXECUTOR.submit(() -> {
                try {
                    //先查数据库
                    R r1 = dbFallback.apply(id);
                    //再写入Redis
                    this.setWithLogicalExpire(r1,key,time,unit);

                } catch (Exception e) {
                    throw new RuntimeException(e);
                }finally {
                    //释放锁
                    unLock(lockKey);
                }
            });


        }
        //6.4返回商铺信息
        return r;
    }
 /**
     * 获取锁
     * @param key
     * @return
     */
    private boolean tryLock(String key){
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }

    /**
     * 释放锁
     * @param key
     */
    private void unLock(String key){
        stringRedisTemplate.delete(key);
    }

2.2完整代码

 

@Slf4j
@Component
public class CacheClient {

    private final StringRedisTemplate stringRedisTemplate;
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

    //通过构造函数进行注入
    public CacheClient(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    /**
     * 将任意Java对象序列化为json并存储在string类型的key中,并且可以设置TTL过期时间
     * @param value
     * @param key
     * @param time
     * @param unit
     */
    public void set(Object value, String key, Long time, TimeUnit unit){
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value),time,unit);
    }

    /**
     * 将任意Java对象序列化为json并存储在string类型的key中,并且可以设置逻辑过期时间,用于处理缓
     * @param value
     * @param key
     * @param time
     * @param unit
     */
    public void setWithLogicalExpire(Object value, String key, 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));
    }
    /**
     * 缓存穿透
     * @return
     */
    public <R,ID> R queryWithPassThrough(String pre, ID id, Class<R> type, Function<ID,R> dbFallback,Long time, TimeUnit unit){

        String key = pre + id;
        //1.从redis中查询缓存
        String json = stringRedisTemplate.opsForValue().get(key);
        //2.判断是否命中
        if (StrUtil.isNotBlank(json)) {
            //3.命中 返回商铺信息
            return JSONUtil.toBean(json,type);
        }
        //判断是否为null值
        if(json != null){
            return null;
        }
        //4.未命中 根据id查询数据库
        R r = dbFallback.apply(id);
        if (r == null) {
            //5.将空值存入redis
            stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL, TimeUnit.MINUTES);
            //5.存在 返回
            return null;
        }
        //6.存在 写入redis 返回商品信息
        this.set(r,pre,time,unit);
        return r;
    }

    /**
     * 获取锁
     * @param key
     * @return
     */
    private boolean tryLock(String key){
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }

    /**
     * 释放锁
     * @param key
     */
    private void unLock(String key){
        stringRedisTemplate.delete(key);
    }

    /**
     * 利用逻辑过期时间解决缓存击穿
     * @return
     */
    public <R,ID> R queryWithLogicalExpire(String pre,ID id,Class<R> type,Function<ID,R> dbFallback,Long time, TimeUnit unit){
        String key = pre + id;
        //1.从redis中查询缓存
        String Json = stringRedisTemplate.opsForValue().get(key);
        //2.不存在 直接返回空(不是热点数据)
        if(StrUtil.isBlank(Json)){
            return null;
        }
        //3.存在 需要先把Json反序列化为对象
        RedisData redisData = JSONUtil.toBean(Json, RedisData.class);
        JSONObject data =(JSONObject) redisData.getData();
        R r = JSONUtil.toBean(data,type);

        //4.判断缓存是否过期
        LocalDateTime expireTime = redisData.getExpireTime();

        if(expireTime.isAfter(LocalDateTime.now())){
            //5.未过期 返回商铺信息
            return r;
        }
        //6.已过期 缓存重建
        //6.1获取互斥锁
        String lockKey = LOCK_SHOP_KEY + id;
        boolean isTryLock = tryLock(lockKey);
        //6.2判断是否获取成功
        if (isTryLock) {
            //6.3成功

            //再次检查缓存中的数据是否过期,如果没有过期,无需重建
            //1.从redis中查询缓存
            Json = stringRedisTemplate.opsForValue().get(key);
            //2.不存在 直接返回空(不是热点数据)
            if(StrUtil.isBlank(Json)){
                return null;
            }
            //3.存在 需要先把Json反序列化为对象
            redisData = JSONUtil.toBean(Json, RedisData.class);
            data =(JSONObject) redisData.getData();
            r = JSONUtil.toBean(data, type);
            //4.判断缓存是否过期
            expireTime = redisData.getExpireTime();
            if(expireTime.isAfter(LocalDateTime.now())){
                //5.未过期 返回商铺信息
                return r;
            }
            //还过期 开启独立线程 缓存重建
            CACHE_REBUILD_EXECUTOR.submit(() -> {
                try {
                    //先查数据库
                    R r1 = dbFallback.apply(id);
                    //再写入Redis
                    this.setWithLogicalExpire(r1,key,time,unit);

                } catch (Exception e) {
                    throw new RuntimeException(e);
                }finally {
                    //释放锁
                    unLock(lockKey);
                }
            });


        }
        //6.4返回商铺信息
        return r;
    }

}

3.ShopServiceImpl

@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 = queryWithPassThrough(id);

        //this::getById == id->getById(id)
        //Shop shop
        // = cacheClient
        //        .queryWithPassThrough(CACHE_SHOP_KEY, id, Shop.class, this::getById, CACHE_NULL_TTL, TimeUnit.MINUTES);

        //互斥锁解决缓存击穿
        //Shop shop = queryWithMutex(id);
        //逻辑过期解决缓存击穿
        //Shop shop = queryWithLogicalExpire(id)

        //逻辑过期解决缓存击穿
        Shop shop
                = cacheClient
                .queryWithLogicalExpire(CACHE_SHOP_KEY, id, Shop.class, this::getById, 20L, TimeUnit.SECONDS);

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

4.测试

TODO:使用逻辑过期解决缓存击穿问题,需要自行进行数据预热。

运行其中test2()即可

class HmDianPingApplicationTests {
    @Autowired
    private ShopServiceImpl shopService;

    @Resource
    private CacheClient cacheClient;
    @Test
    public void test() throws InterruptedException {
        shopService.saveShop2Redis(1L,10L);
    }
    @Test
    public void test2() throws InterruptedException {
        Shop shop = shopService.getById(1L);

        cacheClient.setWithLogicalExpire(shop, CACHE_SHOP_KEY + 1L,10L, TimeUnit.SECONDS);
    }

}

开发板推荐:天空星STM32F407VET6开发板

超高性价比 STM32主控 | 超高主频 | 一板兼容百芯 | 比赛神器 | 沉金彩色丝印

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值