欢迎来到“雪碧聊技术”CSDN博客!

在这里,您将踏入一个专注于Java开发技术的知识殿堂。无论您是Java编程的初学者,还是具有一定经验的开发者,相信我的博客都能为您提供宝贵的学习资源和实用技巧。作为您的技术向导,我将不断探索Java的深邃世界,分享最新的技术动态、实战经验以及项目心得。

让我们一同在Java的广阔天地中遨游,携手提升技术能力,共创美好未来!感谢您的关注与支持,期待在“雪碧聊技术”与您共同成长!

目录

一、缓存雪崩

1、什么是缓存雪崩?

2、解决方案

二、缓存击穿

1、什么是缓存击穿?

2、解决方案

①互斥锁

②逻辑过期

3、对比两种方案

4、案例:基于“互斥锁”,解决缓存击穿问题

​编辑

①如何实现互斥锁呢?

②编写代码

③重启项目,使用Jmeter工具测试该后端接口

5、案例:基于“逻辑过期”,解决缓存击穿问题


一、缓存雪崩

1、什么是缓存雪崩?

缓存雪崩:是指在同一时段,大量的缓存key同时失效或者Redis服务宕机,导致大量请求打到数据库,带来巨大压力。

2、解决方案

二、缓存击穿

1、什么是缓存击穿?

        缓存击穿:也叫热点key问题,就是一个被高并发访问缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间打到数据库,给数据库带来巨大的压力。

        举例:热点key的TTL突然到期了,线程1从redis中没命中,然后去数据库查询,并将数据重新写会redis(由于该热点key的缓存重建业务较复杂,因此重建时间比较久),在重建期间,由于该热点key是高并发的,因此此时无数线程请求该key,redis没命中,于是这些请求都打到数据库,导致数据库压力巨大。

2、解决方案

①互斥锁

核心思想:线程1发现redis不存在该热点key,于是获取互斥锁,然后查询数据库并重建该缓存,等把该热点key的缓存成功写到了redis中,才会释放这个互斥锁。这期间,其他线程,查询redis也没命中,于是也尝试获取互斥锁,但是获取失败了(因为互斥锁被线程1占用),于是无法进行查询数据库并重建缓存的动作,于是只能等待。等到线程1重建好缓存,才能命中。

优点:缓存和数据库,是数据一致的。

缺点:其他线程需要等待,导致性能不佳(服务可用性差)。

②逻辑过期

核心思想:往redis存数据时,我就不设置TTL,于是该热点key永远不会过期,因此不可能出现热点key失效的现象。

但是为了保证缓存、数据库的数据一致性,因此还是要往热点key的value当中添加一个逻辑过期时间,每次访问该热点缓存时,都要主动判断是否过期,如果过期,就获取互斥锁,然后开启一个新线程(等于叫来一个帮手),帮我去完成缓存重建的动作,我自己这个线程还是返回旧的缓存,这样的确有点数据不一致,但是也无伤大雅。

优点:无需等待,性能好(服务可用性好)。

缺点:导致轻微的数据不一致问题。

3、对比两种方案

综上:这两种方式的数据一致性、服务可用性(性能)不可兼得。

一致性和可用性的抉择,也是分布式系统里面,常常面临的一个问题。

这两种,也没有孰优孰劣,应当根据应用场景来进行抉择。

4、案例:基于“互斥锁”,解决缓存击穿问题

①如何实现互斥锁呢?

我们此处借助redis的机制,来实现互斥锁。

也就是,我们获取互斥锁时,就执行setnx lock 1,即:向redis中,添加一个key为lock,value为1的键值对;释放互斥锁时,就执行del lock,即:将key为lock的数据删除。

②编写代码

先编写获取、释放互斥锁的方法。

注意:redis中的setnx命令,对应stringRedisTemplate中的setIfAbsent方法。

    //自定义方法:获取互斥锁
    private boolean tryLock(String key){
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);//防止Boolean自动拆箱成boolean时出现空指针异常
    }
    //自定义方法:释放互斥锁
    private void unlock(String key){
        stringRedisTemplate.delete(key);
    }

编写“互斥锁”解决缓存击穿的代码(带*号的,是本次要学的)

    @Override
    public Result queryById(Long id) {
        String key = CACHE_SHOP_KEY + id;
        //1、从Redis中查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        //2、判断redis是否存在
        if(StrUtil.isNotBlank(shopJson)){
            //3、存在,则直接返回
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return Result.ok(shop);
        }
        //判断命中的是否是空值
        if(shopJson != null && shopJson.equals("")){
            //返回一个错误信息
            return Result.fail("店铺信息不存在!防止缓存穿透!");
        }

        //4、redis中不存在,进行缓存重建 *
        //4.1获取互斥锁 *
        String lockKey = "lock:shop:"+id;
        Shop shop = null;
        try {
            boolean isLock = tryLock(lockKey);
            //4.2判断是否获取成功 *
            if(!isLock){
                //4.3失败,则休眠并重试查询redis *
                Thread.sleep(50);
                return queryById(id);//递归本方法,表示重新查询redis
            }
            //4.4成功,根据id查询数据库 *
            shop = getById(id);//该方法,来自mybatisPlus
            //模拟重建缓存的延时 *
            Thread.sleep(200);
            //5、数据库也不存在,返回错误 *
            if(shop == null){
                //将空值写入redis
                stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
                return Result.fail("店铺不存在!");
            }
            //6、数据库中存在,则写入redis,并返回给前端 *
            stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            //7、释放互斥锁 *
            unlock(lockKey);
        }
        if(shop==null){ //*
            return Result.fail("店铺不存在!");
        }
        //8、返回
        return Result.ok(shop);
    }

③重启项目,使用Jmeter工具测试该后端接口

可见此时没毛病,使用“互斥锁”的方式,成功解决了“缓存击穿”的问题。

5、案例:基于“逻辑过期”,解决缓存击穿问题

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部