Redis--缓存雪崩,缓存穿透,缓存击穿的解决方案

快速游览

- 缓存雪崩
- 缓存穿透
- 缓存击穿
- 封装工具类

缓存雪崩

黑马的视频讲的挺形象的我就套图了
Redis--缓存雪崩,缓存穿透,缓存击穿的解决方案_第1张图片
因为程序原因或者突发事件导致宕机,大量缓存数据无法被击中,直接到数据库进行读写操作
解决方案:

  • 给不同的Key的TTL添加随机值

  • 利用Redis集群提高服务的可用性

  • 给缓存业务添加降级限流策略

  • 给业务添加多级缓存

缓存穿透

缓存穿透是指当用户在查询一条数据的时候,而此时数据库和缓存却没有关于这条数据的任何记录,而这条数据在缓存中没找到就会向数据库请求获取数据。用户拿不到数据时,就会一直发请求,查询数据库,这样会对数据库的访问造成很大的压力。

如:用户查询一个 id =aa的商品信息,一般数据库 id 值都是数字0开始自增,很明显这条信息是不在数据库中,当没有信息返回时,会一直向数据库查询,大量的io操作会给当前数据库的造成很大的访问压力。
一般项目的数据读写操作:

  1. 客户端发起一个查询请求,我们会先查询redis缓存
  2. 命中直接返回,如果没有命中,穿透缓存,再去查询数据库
  3. 如果数据库查询到对应数据返回客户端,并且写入缓存以及TTL(定时清理)
  4. 当数据库数据更改,把缓存清空,下次客户端访问时写入缓存
    缓存穿透的发生一般是受到 “黑客攻击” 所导致的,所以应该进行监控,如果真的是黑客攻击,及时添加黑名单。

解决后思路 :

  1. 客户端发起一个查询请求,判断是否符合该接口参数范围,我们会先查询redis缓存,
  2. 命中直接返回,如果没有命中,穿透缓存,再去查询数据库
  3. 如果数据库查询到对应数据返回客户端,并且写入缓存以及TTL(定时清理)
  4. 如果数据库也没有该信息 时,为避免客户端大量请求继续访问,应该在缓存写入一个生命周期很短的定值,防止来连续访问数据库
  5. 当数据库数据更改,把缓存清空,下次客户端访问时写入缓存
    Redis--缓存雪崩,缓存穿透,缓存击穿的解决方案_第2张图片
    前端根据id获取信息
    代码演示:
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
   @Resource
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 根据id查询商铺信息
     * 添加redis缓存和数据库同步功能
     * 1.更新数据时候 先更新数据库
     * 2.数据库发生改变 时候删除缓存 (用户只能在数据库访问 然后更新缓存避免多余无效更新)
     * 3.给数据设置定时时间 万一发生并发数据到点清除 用户访问时候 缓存更新
     *
     * @param id
     * @return
     */

    @Override
    public Result queryById(Long id) {
        //1.从redis缓存中查寻缓存
        /**
         * 因为这里使用的是forvalue操作对象
         * 获取出来的对象是java字符串
         * 所以我们需要反序列化成java对象
         */
        String s = stringRedisTemplate.opsForValue().get(HmConstants.SHOP_PREX + id);//店铺对应的key
//        判断是否纯在 并且判断是否为空值缓存穿透策略
        if (StrUtil.isNotBlank(s)){//该方法空值也会执行 肯定汇报错 所以写一个空值处理 空串会返回fasle 不执行if

            //2.如果存在返回数据 先解析(反序列化)为java对象
            Shop shop = JSONUtil.toBean(s, Shop.class);
            return Result.ok(shop);
        }
        System.out.println(s);
//        判断是否是否为我们设置的防穿透值
if (s!=null) {//这里不能使用.equals("") 这样很会报错null指针异常
    //或者s!=null 上一个if判断是否有值,这里不能是null null会去执行下一步
    

    return  Result.fail("店铺信息不存在");
}
/**
 * 缓存没命中 查询数据库
 */
        //3.不纯在则去mysql数据进行查找
        Shop shop = getBaseMapper().selectById(id);
        //4.mysql中查到
        if (Objects.isNull(shop)){
            //4.若在数据库中没有发现则返回错误    message

            // 给redis 写"null"   防止有人恶意读写数据库(io)崩坏 并且数据自动销毁 避免管理员数据库更新数据后 而用户看到的还是空值
            stringRedisTemplate.opsForValue().set(HmConstants.SHOP_PREX + id,"",5,TimeUnit.MINUTES);//什么都不设是null    ,''叫做empty
            return  Result.fail("没有该店铺信息");
        }
        //5.查到到对应数据即可返回 并且更新缓存区
//        字符串存储 存储java对象 需要转化为json字符串
        String shopjson = JSONUtil.toJsonStr(shop);
//6. 设置缓存数据生命周期 自动删除  确保数据无论是否手动更新成功都清楚 让用户下次访问时写入缓存
        stringRedisTemplate.opsForValue().set(HmConstants.SHOP_PREX+id,shopjson,30, TimeUnit.MINUTES);
        return  Result.ok(shop);

    }

这里使用""表示数据库没有这个数据,所以如果缓存中命中的是该数据,直接返回错误提示信息
ps:,""是empty空字符和null不同

这里提一个点,.equals(“”),会把"“和null等同起来,使用的是hutool工具包也是,所以当我访问不纯在的信息时,redis查出数据是”",而传入该方法过后,报错空指针异常,

此外为了保证数据的一直性,当数据库发生更改,应删除对应数据,避免数据不一致

  /**
     * 更新数据库
     * 添加redis缓存和数据库同步功能
     * 1.更新数据时候 先更新数据库
     * 2.数据库发生改变 时候删除缓存 (用户只能在数据库访问 然后更新缓存避免多余无效更新)
     * 3.给数据设置定时时间 万一发生并发数据到点清除 用户访问时候 缓存更新
     *

     * @return
     */
    @Override
    @Transactional//定位事务
    public Result update(Shop shop) {
//        1.更新数据库
        updateById(shop);
//        2.删除缓存
        if (shop.getId()==null) {
        return  Result.fail("店铺id 不能为空");
        }
        stringRedisTemplate.delete(HmConstants.SHOP_PREX+shop.getId());
        //3.将数据库操作和缓存操作设置为一个事务一个操作出错发生回滚 确保同步
        return Result.ok();

    }

发送测试:
Redis--缓存雪崩,缓存穿透,缓存击穿的解决方案_第3张图片
redis结果:
Redis--缓存雪崩,缓存穿透,缓存击穿的解决方案_第4张图片
还有中方式是使用布隆过滤器,但是布隆过滤器不会提供删除方法,在代码维护上比较困难。所以需要的伙伴可以自行了解
Redis--缓存雪崩,缓存穿透,缓存击穿的解决方案_第5张图片

总结:Redis--缓存雪崩,缓存穿透,缓存击穿的解决方案_第6张图片

缓存击穿

线程安全定义
当多个线程同时访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那就称这个对象时线程安全的。

缓存击穿和雪崩类似 ,但是突然部分高并发,经常被擦查询的缓存数据丢失或者失效,无数个线程携带的量请求,修改部分数据,是及其容易造成线程不安全,数据未知

解决方案:

Redis--缓存雪崩,缓存穿透,缓存击穿的解决方案_第7张图片
分为俩种解决方式:

1.互斥锁:

互斥锁的工作原理,多个线程访问数据时,若缓存中未命中数据,进行判断,该线程是否拥有锁,拥有锁的权限即可查询数据库,对缓存进行重建,并且释放锁(避免死锁!!),没有获取到锁的线程经行睡眠,在重新尝试集中缓存

这么一描述,是不是感觉和redis有个指令很像,对就是setnx(k存在即不设置,不纯在k才能设置)
Redis--缓存雪崩,缓存穿透,缓存击穿的解决方案_第8张图片

代码实现

@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
   @Resource
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 根据id查询商铺信息
     * 添加redis缓存和数据库同步功能
     * 1.更新数据时候 先更新数据库
     * 2.数据库发生改变 时候删除缓存 (用户只能在数据库访问 然后更新缓存避免多余无效更新)
     * 3.给数据设置定时时间 万一发生并发数据到点清除 用户访问时候 缓存更新
     *
     * @param id
     * @return
     */

    @Override
    public Result queryById(Long id) {
     //        1演示缓存击穿
        Shop shop = queryWithMutex(id);
        if (shop == null) {
            return Result.fail("没有店铺信息");
        }
        System.out.println(shop);
        return  Result.ok(shop);
    }
    /**
     * 更新数据库
     * 添加redis缓存和数据库同步功能
     * 1.更新数据时候 先更新数据库
     * 2.数据库发生改变 时候删除缓存 (用户只能在数据库访问 然后更新缓存避免多余无效更新)
     * 3.给数据设置定时时间 万一发生并发数据到点清除 用户访问时候 缓存更新
     *

     * @return
     */
    @Override
    @Transactional//定位事务
    public Result update(Shop shop) {
//        1.更新数据库
        updateById(shop);
//        2.删除缓存
        if (shop.getId()==null) {
        return  Result.fail("店铺id 不能为空");
        }
        stringRedisTemplate.delete(HmConstants.SHOP_PREX+shop.getId());
        //3.将数据库操作和缓存操作设置为一个事务一个操作出错发生回滚 确保同步
        return Result.ok();

    }
    /**
     * 尝试获取锁(模仿互斥锁)
     */
    private  boolean trylock(String id){
//        使用redis充当锁 setnx操作
        String lockey = "lock:shop:";
//        当前线程如果没有锁则设置成功 返回true 有锁则返回false 设置到期时间防止锁在某个线程很久不结束
        Boolean rflog = stringRedisTemplate.opsForValue().setIfAbsent(lockey+id, "huchilock", 5, TimeUnit.SECONDS);
       return   BooleanUtil.isTrue(rflog);//直接返回布尔值 实际调用拆箱(booleanvalue)方法 如果为null 会报指针异常

    }
    //释放锁 执行完防止缓存击穿过后把锁的数据删除
    private void unlock(String id){
       stringRedisTemplate.delete("lock:shop:"+id);

    }
/*    将演示焕春击穿的代码封装成一个方法
在进行修改;便于保存
    */
public Shop queryWithMutex(Long id){
    //1.从redis缓存中查寻缓存
    /**
     * 因为这里使用的是forvalue操作对象
     * 获取出来的对象是java字符串
     * 所以我们需要反序列化成java对象
     */
    String s = stringRedisTemplate.opsForValue().get(HmConstants.SHOP_PREX + id);//店铺对应的key
//        判断是否命中 并且判断是否为空字符值缓存穿透策略
    if (StrUtil.isNotBlank(s)){//该方法空值也会执行空字符和null返回false
    不执行if

        //2.如果存在返回数据 先解析(反序列化)为java对象
        Shop shop = JSONUtil.toBean(s, Shop.class);
        return shop;
    }

//        判断是否是否为我们设置的防穿透值
    if (s!=null) {//这里不能使用.equals("") 这样很会报错null指针异常
        //或者s!=null 上一个if判断是否有值,这里判断不是null null会去数据库查询 能ull和empty不一样

        return null;//外层在失败封装
    }
/**
 * 缓存没命中 查询数据库重建缓存
 *
 */
    //3.不存在在则去mysql数据进行查找
//    3.1获取互斥锁

    boolean istrylock = trylock( id.toString());
//    3.2判断使否互获取成功

    Shop shop = null;
    try {
        if (!istrylock){
            //    3.3失败则当前该线程休眠并且重试(避免多线程同时操作 导致数据不一致)
    //        休眠
            Thread.sleep(50);
    //        重试递归
            return queryWithMutex(id);

        }

//    3.4获取成功再次查询redis做了递归数据以及重建是否
        String s2 = stringRedisTemplate.opsForValue().get(HmConstants.SHOP_PREX + id);
        if (s2!= null){
            Shop shop1 = JSONUtil.toBean(s2, Shop.class);
            return shop1;
        }
//   3.5 则数据重建redis
        shop = getBaseMapper().selectById(id);
        Thread.sleep(200);//模拟数据模拟重建的线程
        //4.开始重建 mysql中未查到
        if (Objects.isNull(shop)){
            //4.若在数据库中没有发现则返回null
            // 给redis 写"null"   防止有人恶意读写数据库(io)崩坏 并且数据自动销毁 避免管理员数据库更新数据后 而用户看到的还是空值
            stringRedisTemplate.opsForValue().set(HmConstants.SHOP_PREX + id,"",5,TimeUnit.MINUTES);//什么都不设是null,''叫做empty
            return null;
        }
        //5.若查到到对应数据即可返回 并且更新缓存区
        String shopjson = JSONUtil.toJsonStr(shop);
//6. 设置缓存数据生命周期 自动删除  确保数据无论是否手动更新成功都清楚 让用户下次访问时写入缓存
        stringRedisTemplate.opsForValue().set(HmConstants.SHOP_PREX+id,shopjson,30, TimeUnit.MINUTES);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);//异常抛出
    }finally {
        //   7. 释放互斥锁
        unlock(id.toString());//无论异常如何都要把这个锁删了
    } 
//返回数据

    return shop;

}

}

测试

把缓存区对应数据清空,使用jmeter5秒进行2000次请求测试
Redis--缓存雪崩,缓存穿透,缓存击穿的解决方案_第9张图片
输出日志,只进行了一次数据库的读写 说明防止击穿成功并且线程线程安全
Redis--缓存雪崩,缓存穿透,缓存击穿的解决方案_第10张图片

2.逻辑过期

和Mybatis中的逻辑删除类似,给常用的·热点数据设置一个过期状态码,设置过期但是并不会在缓存中消失
实现逻辑
Redis--缓存雪崩,缓存穿透,缓存击穿的解决方案_第11张图片
逻辑过期时间:

代码实现

封装带有逻辑过期时间的实体
/*
逻辑删除的封装类型
 */
@Data
public class RedisData {
    private LocalDateTime expireTime;//逻辑过期时间
    private Object data;//存入redis的数据
}

这里分别把缓存击穿的俩种解决方式定义为俩种方法:

service实现类

@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
   @Resource
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 根据id查询商铺信息
     * 添加redis缓存和数据库同步功能
     * 1.更新数据时候 先更新数据库
     * 2.数据库发生改变 时候删除缓存 (用户只能在数据库访问 然后更新缓存避免多余无效更新)
     * 3.给数据设置定时时间 万一发生并发数据到点清除 用户访问时候 缓存更新
     *
     * @param id
     * @return
     */

    @Override
    public Result queryById(Long id) {
     //        用互斥锁的方式演示缓存击穿
//        Shop shop = queryWithMutex(id);
//        用逻辑删除 方式演示缓存击穿
        Shop shop = queryWithLogicExpire(id);
        if (shop == null) {
            return Result.fail("没有店铺信息");
        }

        return  Result.ok(shop);
    }

    /**
     *
     * 用逻辑删除格式重建缓存数据
     * @param id
     * @param expireSeconds
     */
      public  void   saveShop2Redis(Long id,Long expireSeconds) {
//       1.查询店铺数据
          Shop shop = getById(id);
          try {
              Thread.sleep(200L);//模拟重建缓存的延时
          } catch (InterruptedException e) {
              e.printStackTrace();
          }
//       2. 封装逻辑过期时间
          RedisData redisdata = new RedisData();
          redisdata.setData(shop);
          redisdata.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));//设置传入过期时间

//        3.写入缓存
          stringRedisTemplate.opsForValue().set(HmConstants.SHOP_PREX + id,JSONUtil.toJsonStr(redisdata));

    }
    /**
     * 更新数据库
     * 添加redis缓存和数据库同步功能
     * 1.更新数据时候 先更新数据库
     * 2.数据库发生改变 时候删除缓存 (用户只能在数据库访问 然后更新缓存避免多余无效更新)
     * 3.给数据设置定时时间 万一发生并发数据到点清除 用户访问时候 缓存更新
     *

     * @return
     */
    @Override
    @Transactional//定位事务
    public Result update(Shop shop) {
//        1.更新数据库
        updateById(shop);
//        2.删除缓存
        if (shop.getId()==null) {
        return  Result.fail("店铺id 不能为空");
        }
        stringRedisTemplate.delete(HmConstants.SHOP_PREX+shop.getId());
        //3.将数据库操作和缓存操作设置为一个事务一个操作出错发生回滚 确保同步
        return Result.ok();

    }
    /**
     * 尝试获取锁(模仿互斥锁)
     */
    private  boolean trylock(String id){
//        使用redis充当锁 setnx操作
        String lockey = "lock:shop:";
//        当前线程如果没有锁则设置成功 返回true 有锁则返回false 设置到期时间防止锁在某个线程很久不结束
        Boolean rflog = stringRedisTemplate.opsForValue().setIfAbsent(lockey+id, "huchilock", 5, TimeUnit.SECONDS);
       return   BooleanUtil.isTrue(rflog);//直接返回布尔值 实际调用拆箱(booleanvalue)方法 如果为null 会报指针异常

    }
    //释放锁 执行完防止缓存击穿过后把锁的数据删除
    private void unlock(String id){
       stringRedisTemplate.delete("lock:shop:"+id);

    }
//    缓存穿透缓存击穿实现
    //
public Shop queryWithMutex(Long id){
    //1.从redis缓存中查寻缓存
    /**
     * 因为这里使用的是forvalue操作对象
     * 获取出来的对象是java字符串
     * 所以我们需要反序列化成java对象
     */
    String s = stringRedisTemplate.opsForValue().get(HmConstants.SHOP_PREX + id);//店铺对应的key
//        判断是否纯在 并且判断是否为空值缓存穿透策略
    if (StrUtil.isNotBlank(s)){//该方法空值也会执行空字符和null返回false

        //2.如果存在返回数据 先解析(反序列化)为java对象
        Shop shop = JSONUtil.toBean(s, Shop.class);
        return shop;
    }

//        判断是否是否为我们设置的防穿透值
    if (s!=null) {//这里不能使用.equals("") 这样很会报错null指针异常
        //或者s!=null 上一个if判断是否有值,这里不能是null null会去数据库查询 能ull和empty不一样

        return null;//外层在做循环
    }
/**
 * 缓存没命中 查询数据库重建缓存
 *
 */
    //3.不存在在则去mysql数据进行查找
//    3.1获取互斥锁

    boolean istrylock = trylock( id.toString());
//    3.2判断使否互获取成功

    Shop shop = null;
    try {
        if (!istrylock){
            //    3.3失败则当前该线程休眠并且重试(避免多线程同时操作 导致数据不一致)
    //        休眠
            Thread.sleep(50);
    //        重试递归
            return queryWithMutex(id);

        }

//    3.4获取成功再次查询redis做了递归数据以及重建是否
        String s2 = stringRedisTemplate.opsForValue().get(HmConstants.SHOP_PREX + id);
        if (s2!= null){
            Shop shop1 = JSONUtil.toBean(s2, Shop.class);
            return shop1;
        }
//   3.5 则数据重建redis
        shop = getBaseMapper().selectById(id);
        Thread.sleep(200);//模拟数据模拟重建的线程
        //4.开始重建 mysql中未查到
        if (Objects.isNull(shop)){
            //4.若在数据库中没有发现则返回null
            // 给redis 写"null"   防止有人恶意读写数据库(io)崩坏 并且数据自动销毁 避免管理员数据库更新数据后 而用户看到的还是空值
            stringRedisTemplate.opsForValue().set(HmConstants.SHOP_PREX + id,"",5,TimeUnit.MINUTES);//什么都不设是null,''叫做empty
            return null;
        }
        //5.查到到对应数据即可返回 并且更新缓存区
        String shopjson = JSONUtil.toJsonStr(shop);
//6. 设置缓存数据生命周期 自动删除  确保数据无论是否手动更新成功都清楚 让用户下次访问时写入缓存
        stringRedisTemplate.opsForValue().set(HmConstants.SHOP_PREX+id,shopjson,30, TimeUnit.MINUTES);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);//异常抛出
    }finally {
        //   7. 释放互斥锁
        unlock(id.toString());//无论异常如何都要把这个锁删了
    }
//返回数据

    return shop;

}
/**
 * 热点数据
 * 用逻辑过期解决缓存击穿问题
 */
//线程执行器 cache_rebuild_executor
private static  final ExecutorService cache_rebuild_executor = Executors.newFixedThreadPool(10);//新建10个线程
public Shop queryWithLogicExpire(Long id) {
//   1 查询缓存
    String s = stringRedisTemplate.opsForValue().get(HmConstants.SHOP_PREX + id);//店铺对应的key
//     2.   判断是否命中
    if (StrUtil.isBlank(s)) {
//      3.  没命中返回null
        return null;
    }
//  4. 命中,需要把json转化为对象
    RedisData redisData = JSONUtil.toBean(s, RedisData.class);
//    取出数据 json转化二次封装的object 是josnObjecr
    Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
//    取出过期时间
    LocalDateTime expireTime = redisData.getExpireTime();
    // 5. 判断是否逻辑过期 过期时间是否当前时间之后
    if (expireTime.isAfter(LocalDateTime.now())) {
        //5.1 未过期返回店铺信息
        return shop;
    }
// 5.2 过期 需要缓存重建
//    6. 缓存重建
//    6.1 获取互斥锁
    boolean istrylock = trylock(id.toString());
//     6.2 判断获取成功与否
//  6.3 获取锁成功开启独立线程 实现缓存重建: 查询,封装,写入缓存 定义在方法saveShop2Redis
    if (istrylock) {
        cache_rebuild_executor.submit(
                () -> {
                    try {
                        this.saveShop2Redis(id, 30L);//写入缓存并且添加逻辑过期时间
                    } finally {
                        //释放锁 避免思索
                        this.unlock(id.toString());
                    }

                }

        );//创建的10个线程添加任务
    }
    //    6.4 失败直接返回过期商铺信息
    return shop;
        }
}

测试结果

提前在数据库中,更改数据,并逻辑过期时间到期
用1s 100个线程进行获取请求接口

Redis--缓存雪崩,缓存穿透,缓存击穿的解决方案_第12张图片

只要有俩个线程拿到了锁进行了 sql语句的查询 重建过期数据的缓存说明线程安全 没有堵塞
Redis--缓存雪崩,缓存穿透,缓存击穿的解决方案_第13张图片

俩种方式对比

Redis--缓存雪崩,缓存穿透,缓存击穿的解决方案_第14张图片

工具类的封装

未来解决以上常见问题 所以进行封装一个简单的工具类,以求能够达到:

Redis--缓存雪崩,缓存穿透,缓存击穿的解决方案_第15张图片
方法 1,3 解决常见数据的缓存穿透,2,4解决热点高并发数据的缓存击穿,使用springdata序列化好的Stringredistemplate为基础,不适用默认的redistemplate(读写数据需要自己序列化).

代码实现

封装的解决常见问题的缓存工具类
CacheClient

//封装一个redis缓存工具类
@Slf4j//日志
@Component//注入ioc管理  也避免注解失效
public class CacheClient {
//   不能使用注解注入静态变量
    /**
     */
    @Autowired
    private    StringRedisTemplate stringRedisTemplate ;



    /**
     * 放缓存穿透
      * @param key
     * @param value
     * @param Time
     * @param unit
     */
public void set(String key, Object value, Long Time, TimeUnit unit){

    stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value),Time,unit);
}

    /**
     *设置封装逻辑删除的对象
     * @param key
     * @param value
     * @param time 逻辑删除时间有效期
     * @param unit  时间单位
     */
    public  void setWithLogicExpire(String key, Object value,Long time,TimeUnit unit){
    //new 逻辑过期对象
        RedisData data = new RedisData();
        //封装属性和逻辑过期时间
        data.setData(value);
        data.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
//        把包含逻辑过期的数据写入redis
      stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(data));

    }

    /**
     * 用于查询缓存并且防止缓存穿透
     *
     * 根据缓存前缀和id 获取缓存
     * @param keyprefix 缓存前缀 因为不知道查询哪个功能相关的缓存
     * @param id
     * @param bean
     * @param  自定义返回类型
     * @param 
     * @return
     * //    获取返回值 因为不知道返回值是什么 所以使用泛型指定
     * //定义返回值泛型·和id 泛型
     */
    public   <R,ID> R querywithPassThrough(String keyprefix, ID id, Class<R> bean, Function<ID,R> dbFallback,  Long time, TimeUnit unit) {
        String key = keyprefix+id;
//        1从redis 查询商品缓存
        String json = stringRedisTemplate.opsForValue().get(key);
//        2. 判断是否存在
        if (Strings.isNotBlank(json)) {
            //        3.存在返回直接
            return  JSONUtil.toBean(json,bean); //字节码反射
        }

//        4.既然不纯在值 则判断是否为""空值
        if (json!= null){
//            再次强调 null!="" 但是equals 和大多方法会将俩种数据混成一个
//            为防止穿透设置的空值""
            return null;
        }
        // 5.为null 说明未命中缓存 从数据库中查询 作为工具类并不知道
        //5.1  作为工具类并不知道 查询那一张表 故此定义函数让调用者自己传入方法
       R r=dbFallback.apply(id);
        // 6.对数据库的查询结果经行判断
        if (r== null){
//           6.2数据库没有该数据,做空值处理 防止穿透
            //6.3Long time,TimeUnit unit加入时间 单位参数 让用户可以定义空白值存在时间 平且可以为空即默认时间
        stringRedisTemplate.opsForValue().set(key,"",time,unit);
        return null;//返回错误

        }
        //7. 数据库中存在该数据 写入很缓存
        set(key,r,time,unit);
        return r;
    }



    //线程池执行器 cache_rebuild_executor
    private   final ExecutorService cache_rebuild_executor = Executors.newFixedThreadPool(10);//新建10个线程

    /**
     * 查询热点数据
     * 用逻辑删除的方式
     * 防止缓存击穿
     * @param cacheprefix 逻辑删除缓存的前缀
     * @param id
     * @param ResultType
     * @param dbFallback  数据库操作函数
     * @param time
     * @param unit
     * @param  返回类型
     * @param  id
     * @return
     */
    public   <R,ID> R queryWithLogicExpire(String cacheprefix,ID id,Class<R> ResultType,Function<ID,R> dbFallback, Long time, TimeUnit unit) {
        String key=cacheprefix+id;
//   1 查询缓存
        String s = stringRedisTemplate.opsForValue().get(key);//店铺对应的key
//     2.   判断
        if (StrUtil.isBlank(s)) {
//      3.  没命中返回null
            return null;
        }
//  4. 命中,需要把json转化为逻辑删除的封装对象
     RedisData redisData = JSONUtil.toBean(s, RedisData.class);
//    取出数据 反序列化成传入字节码类型
      R r = JSONUtil.toBean((JSONObject) redisData.getData(),ResultType);
//    取出过期时间
        LocalDateTime expireTime = redisData.getExpireTime();
        // 5. 判断是否逻辑过期 过期时间是否当前时间之后
        if (expireTime.isAfter(LocalDateTime.now())) {
            //5.1 未过期返回店铺信息
            return r;
        }
// 5.2 过期 需要缓存重建
//    6. 缓存重建
//    6.1 获取互斥锁
        boolean istrylock = trylock(id.toString());
//     6.2 判断获取成功与否
//  6.3 获取锁成功开启独立线程 实现缓存重建: 查询,封装,写入缓存
        if (istrylock) {
            cache_rebuild_executor.submit(
                    () -> {
                        try {
//                            查询,封装,写入缓存 对任意数据表操作,所以将操作过程作为参数传入
                            R DB_r = dbFallback.apply(id);
                            setWithLogicExpire(key,DB_r,time,unit);
                        } finally {
                            //释放锁 避免思索
                           unlock(id.toString());
                        }

                    }

            );//创建的10个线程添加任务
        }
        //    6.4 获取锁失败直接返回过期商铺信息
        return r;
    }

    /**
     * 尝试获取互斥锁
     * @param id
     * @return
     */

    private  boolean  trylock(String id){
//        使用redis充当锁
        String lockey = "lock:shop:";//锁前缀
//        当前线程如果没有锁则设置成功 返回true 有锁则返回false 设置到期时间防止锁在某个线程很久不结束
//        setnx操作
        Boolean rflog = stringRedisTemplate.opsForValue().setIfAbsent(lockey+id, "huchilock", 5, TimeUnit.SECONDS);
        return   BooleanUtil.isTrue(rflog);//直接返回布尔值 实际调用拆箱(booleanvalue)方法 如果为null 会报指针异常

    }
    //释放锁 执行完防止缓存击穿过后把锁的数据删除
    private  void unlock(String id){
        stringRedisTemplate.delete("lock:shop:"+id);

    }

}

其中的Redis Data 其实只是封装了时间是数据的类
Redis Data代码:

/*
逻辑删除的封装类型
 */
@Data
public class RedisData {
    private LocalDateTime expireTime;//逻辑过期时间
    private Object data;//存入redis的数据
}

演示jumeter 用上工具类的缓存击穿
1秒,1000个线程 才读写了 一次数据库 说明缓存没有被击穿 并没有线程安全问题
在这里插入图片描述
自定义工具类调用:

  Shop shop = cacheClient.querywithPassThrough(HmConstants.SHOP_PREX, id, Shop.class, this::getById, 10L, TimeUnit.SECONDS);

你可能感兴趣的:(Redis,缓存,redis,数据库)