SpringBoot整合Redis以及缓存穿透缓存雪崩缓存击穿的理解如何添加锁解决缓存击穿问题?分布式情况下如何添加分布式锁

Posted Mr.Aholic

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了SpringBoot整合Redis以及缓存穿透缓存雪崩缓存击穿的理解如何添加锁解决缓存击穿问题?分布式情况下如何添加分布式锁相关的知识,希望对你有一定的参考价值。

文章目录

1、步骤

前提条件:已经安装了Redis

  • 1、pom中引入依赖
  • 2、配置文件中配置
  • 3、项目中使用

2、具体过程

1、引入pom依赖

版本由父工程管理

        <!--引入redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

2、修改配置文件

spring:
  redis:
    host: 192.168.202.211
    port: 6379

3、单元测试

这里有关stringRedisTemplate的使用、请自行查阅

    @Autowired
    StringRedisTemplate stringRedisTemplate;

    @Test
    public void testRedis()
        ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
        //保存
        ops.set("hello", UUID.randomUUID().toString());
        //查询
        String hello = ops.get("hello");
        System.out.println("之前保存的数据是:"+hello);
        
    

4、测试结果

3、redis运行情况

我这里用docker安装redis、查看容器运行情况

4、项目中实际应用

代码逻辑

测试

访问接口数据

redis可视化工具查看

5、加锁解决缓存击穿问题

代码一(存在问题)

    @Override
    @Cacheable(value = "category",key = "#root.methodName",sync = true)
    public Map<String, List<Catalog2Vo>> getCatalogJsonDbWithSpringCache() 
        //1、缓存中放入json字符串,拿出json字符串,需要逆转为能用的对象类型【序列化和反序列化】
        String catalogJSON = stringRedisTemplate.opsForValue().get("catalogJSON");
        if(StringUtils.isEmpty(catalogJSON))
            //2、缓存中没有,查询数据库
            Map<String, List<Catalog2Vo>> calogJsonFromDb = getCategoriesDb();
            //3、将查到的数据放入缓存,将对象转为json放在缓存中
            String s = JSON.toJSONString(calogJsonFromDb);
            stringRedisTemplate.opsForValue().set("catalogJSON",s,1, TimeUnit.DAYS);
        
        System.out.println("直接取的缓存数据");
        //转为指定的对象
        Map<String, List<Catalog2Vo>> result = JSON.parseObject(catalogJSON,new TypeReference<Map<String, List<Catalog2Vo>>>());
        return result;
    


  //从数据库中查出三级分类
    private  Map<String, List<Catalog2Vo>> getCategoriesDb() 
        synchronized (this)
            //得到锁以后,应该再去缓存中确定一次,如果缓存中没有需要继续查询
            String catalogJSON = stringRedisTemplate.opsForValue().get("catalogJSON");
            if(!StringUtils.isEmpty(catalogJSON))
                //缓存中存在数据、直接返回
                Map<String, List<Catalog2Vo>> result = JSON.parseObject(catalogJSON,new TypeReference<Map<String, List<Catalog2Vo>>>());
                return result;

            

            System.out.println("缓存中没有数据,查询了数据库");
            //优化业务逻辑,仅查询一次数据库
            List<CategoryEntity> categoryEntities = this.list();
            //查出所有一级分类
            List<CategoryEntity> level1Categories = getCategoryByParentCid(categoryEntities, 0L);
            Map<String, List<Catalog2Vo>> listMap = level1Categories.stream().collect(Collectors.toMap(k->k.getCatId().toString(), v -> 
                //遍历查找出二级分类
                List<CategoryEntity> level2Categories = getCategoryByParentCid(categoryEntities, v.getCatId());
                List<Catalog2Vo> catalog2Vos=null;
                if (level2Categories!=null)
                    //封装二级分类到vo并且查出其中的三级分类
                    catalog2Vos = level2Categories.stream().map(cat -> 
                        //遍历查出三级分类并封装
                        List<CategoryEntity> level3Catagories = getCategoryByParentCid(categoryEntities, cat.getCatId());
                        List<Catalog2Vo.Catalog3Vo> catalog3Vos = null;
                        if (level3Catagories != null) 
                            catalog3Vos = level3Catagories.stream()
                                    .map(level3 -> new Catalog2Vo.Catalog3Vo(level3.getParentCid().toString(), level3.getCatId().toString(), level3.getName()))
                                    .collect(Collectors.toList());
                        
                        Catalog2Vo catalog2Vo = new Catalog2Vo(v.getCatId().toString(), cat.getCatId().toString(), cat.getName(), catalog3Vos);
                        return catalog2Vo;
                    ).collect(Collectors.toList());
                
                return catalog2Vos;
            ));
            return listMap;

        

    

使用jmeter对其进行压测

查看控制台情况,理想情况是数据库只查询一次。实际上查询了多次,出现这个问题的原因就是一个用户查询完数据后,就释放了锁。还未将数据写入缓存的时候,第二个用户又拿到了锁。这个时候缓存中还未进行数据缓存,导致再次查询数据库。需要优化代码逻辑

代码二(问题解决)

优化代码逻辑,压测过程同上。

6、新问题

本地锁在分布式情况下是锁不住的

7、分布式锁

待编辑

Redis缓存穿透,缓存击穿,缓存雪崩解决方案以及封装Redis工具类

参考自黑马程序员

缓存穿透

缓存穿透: 数据在数据库和redis中都不存在,请求直接打在数据库中。

解决方案:

方案一:


方案二:


代码案例实现:

 	// 缓存穿透
    private Shop cacheBreakDown(Long id)
        // 查询redis
        String jsonShop = (String) redisTemplate.opsForValue().get(RedisConstants.SHOP_CACHE + id);
        // 既不是""也不是null,命中直接返回
        if (StrUtil.isNotBlank(jsonShop)) 
            return JSONUtil.toBean(jsonShop, Shop.class);
        
        // 判断是否命中空值"",命中直接返回
        if (jsonShop != null) 
            return null;
        
        // 查询数据库
        Shop shop = this.getById(id);
        if (shop == null) 
            // 将空值""写入redis,防止缓存穿透
            redisTemplate.opsForValue().set(RedisConstants.SHOP_CACHE + id, "", RedisConstants.SHOP_CACHE_TTL, TimeUnit.SECONDS);
            return null;
        
        jsonShop=JSONUtil.toJsonStr(shop);
        redisTemplate.opsForValue().set(RedisConstants.SHOP_CACHE + id, jsonShop, RedisConstants.SHOP_CACHE_TTL, TimeUnit.MINUTES);
        return shop;
    

缓存击穿


互斥锁时序图

代码实现:

	// 加锁
    private boolean tryLock(String key)
        Boolean ifAbsent = redisTemplate.opsForValue().setIfAbsent(key, "1", Duration.ofMillis(10000));
        return BooleanUtil.isTrue(ifAbsent);
    
    // 解锁
    private void unLock(String key)
        redisTemplate.delete(key);
    

	 // 缓存击穿
    private Shop cachePenetrate(long id)
        // 查询redis
        String jsonShop = (String) redisTemplate.opsForValue().get(RedisConstants.SHOP_CACHE + id);
        if (StrUtil.isNotBlank(jsonShop)) 
            // 直接返回
            return JSONUtil.toBean(jsonShop, Shop.class);
        
        // 判断是否命中空值 ""
        if (jsonShop != null) 
            return null;
        
        Shop shop = null;
        try 
            // 实现缓存重建
            if (!tryLock(RedisConstants.LOCK_KEY+id))
                // 未获得锁,休眠重试
                Thread.sleep(100);
                return cachePenetrate(id);
            
            // 查询数据库
            shop = this.getById(id);
            if (shop == null) 
                // 将空值写入redis,防止缓存穿透
                redisTemplate.opsForValue().set(RedisConstants.SHOP_CACHE + id, "", RedisConstants.SHOP_CACHE_TTL, TimeUnit.SECONDS);
                return null;
            
            jsonShop=JSONUtil.toJsonStr(shop);
            redisTemplate.opsForValue().set(RedisConstants.SHOP_CACHE + id, jsonShop, RedisConstants.SHOP_CACHE_TTL, TimeUnit.MINUTES);
         catch (InterruptedException e) 
            throw new RuntimeException("被打断");
         finally 
            // 释放锁
            unLock(RedisConstants.LOCK_KEY+id);
        
        return shop;
    

逻辑过期时序图

代码实现:

@Data
public class RedisData 
	// 逻辑过期时间
    private LocalDateTime expireTime;
    // 要存入redis的数据
    private Object data;

// 重建的线程池
private ThreadPoolExecutor cacheThreadPool=new ThreadPoolExecutor(
            5,10,1,TimeUnit.MINUTES,new ArrayBlockingQueue<>(10));
            
 private Shop cacheLogicExpire(long id)
        // 查询redis
        String jsonShop = (String) redisTemplate.opsForValue().get(RedisConstants.SHOP_CACHE + id);
        // 没有命中直接返回null
        if (StrUtil.isBlank(jsonShop)) 
            return null;
        
        // 反序列化
        RedisData redisData= JSONUtil.toBean(jsonShop, RedisData.class);
        JSONObject jsonObject= (JSONObject) redisData.getData();
        Shop shop = JSONUtil.toBean(jsonObject, Shop.class);
        //  命中了判断过期时间是否过期,时间没有过期直接返回数据
        if (redisData.getExpireTime().isAfter(LocalDateTime.now())) 
            return shop;
        
        // 过期了尝试获得锁
        if (tryLock(RedisConstants.LOCK_KEY+id)) 
            // 开启线程缓存重建
            cacheThreadPool.execute(()->
                try 
                    saveData2Redis(id,1800);
                 catch (Exception e) 
                    throw new RuntimeException(e);
                 finally 
                    // 释放锁
                    unLock(RedisConstants.LOCK_KEY+id);
                
            );
        
        // 返回过期数据
        return shop;
    

    public void saveData2Redis(long id,long time)
        Shop shop = getById(id);
        RedisData redisData = new RedisData();
        redisData.setData(shop);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(time));
        redisTemplate.opsForValue().set(RedisConstants.SHOP_CACHE,JSONUtil.toJsonStr(redisData));
    

缓存雪崩

封装工具类


方法1:

    /**
     *
     * @param key key
     * @param value value
     * @param expireTime key过期时间
     * @param unit 时间单位
     */
    public void set(String key, Object value, long expireTime, TimeUnit unit)
        redisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(value),expireTime,unit);
    

方法2:

  /**
     *
     * @param key key
     * @param value value
     * @param expireTime key过期时间
     * @param unit 时间单位
     */
    public void setWithLogicalTime(String key, Object value, long expireTime, TimeUnit unit)
        RedisData redisData = new RedisData();
        long seconds = unit.toSeconds(expireTime);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(seconds));
        redisData.setData(value);
        redisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
    

方法3:

 /**
     *
     * @param keyPrefix 要查询数据的key前缀
     * @param id 要查询的数据id
     * @param type 数据类型
     * @param dbOpe 查询数据方法
     * @param expireTime 过期时间
     * @param unit 时间单位
     * @param <R> 返回的数据类型
     * @param <ID> 要查询数据的ID类型
     * @return 要查询的数据
     */
    public <R,ID> R getCache(String keyPrefix, ID id, Class<R> type, Function<ID,R> dbOpe,long expireTime,TimeUnit unit)
        String key=keyPrefix+id;
        // 先查询Redis
        String value = (String) redisTemplate.opsForValue().get(key);
        // 不为空且不为""直接返回
        if (StrUtil.isNotBlank(value))
            return JSONUtil.toBean(value,type);
        
        // 如果为""不为null返回null
        if (value!=null)
            return null;
        
        // 根据id查询数据库
        R result = dbOpe.apply(id);
        // 数据库也为空
        if (result == null) 
            // 缓存""防止缓存穿透
            redisTemplate.opsForValue().set(key,"",expireTime,unit);
            return null;
        
        // 不为空缓存重建
        redisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(result),expireTime,unit);
        return result;
    

方法4:

 /**
     *
     * @param keyPrefix 要查询数据的key前缀
     * @param id 要查询的数据id
     * @param type 数据类型
     * @param dbOpe 查询数据方法
     * @param expireTime 过期时间
     * @param unit 时间单位
     * @param <R> 返回的数据类型
     * @param <ID> 要查询数据的ID类型
     * @return 要查询的数据
     */
    public <R,ID> R getCacheWithLogicExpire(String keyPrefix, ID id, Class<R> type, Function<ID,R> dbOpe, long expireTime, TimeUnit unit)
        String key=keyPrefix+id;
        // 查询redis
        String value = (String) redisTemplate.opsForValue().get(key);
        // 为空返回null
        if (StrUtil.isBlank(value))
            return null;
        
        // 反序列化为RedisData
        RedisData redisData = JSONUtil.toBean(value, RedisData.class);
        R result = JSONUtil.toBean((JSONObject) redisData.getData(), type);
        // 判断是否过期,没有过期直接返回
        if (LocalDateTime.now().isAfter(redisData.getExpireTime()))
            return result;
        
        // 过期了尝试获得锁
        if (tryLock(key))
            // 开启线程缓存重建
            cacheThreadPool.execute(()->
                try 
                    R r = dbOpe.apply(id);
                    RedisData redis_data = new RedisData();
                    redis_data.setData(r);
                    redis_data.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(expireTime)));
                    redisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(redis_data));
                 catch (Exception e) 
                    throw new RuntimeException(e);
                 finally 
                    // 释放锁
                    unLock(RedisConstants.LOCK_KEY+id);
                
            );
        
        return result;
    

以上是关于SpringBoot整合Redis以及缓存穿透缓存雪崩缓存击穿的理解如何添加锁解决缓存击穿问题?分布式情况下如何添加分布式锁的主要内容,如果未能解决你的问题,请参考以下文章

重学SpringBoot系列之EhCache缓存,缓存问题

Redis分片主从哨兵集群,原理详解,集群的配置安装,8大数据类型,springboot整合使用

Redis分片主从哨兵集群,原理详解,集群的配置安装,8大数据类型,springboot整合使用

Redis缓存穿透,缓存击穿,缓存雪崩解决方案以及封装Redis工具类

Redis缓存穿透,缓存击穿,缓存雪崩解决方案以及封装Redis工具类

Redis缓存穿透,缓存击穿,缓存雪崩解决方案以及封装Redis工具类