• Redis从理论到实战:用Redis解决缓存穿透、缓存击穿问题(提供解决方案)



    加油加油,不要过度焦虑(*^▽^*)

    一、缓存穿透

    1、什么是缓存穿透

    • 缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远都不会生效,这些请求都会打到数据库。
    • 当有人恶意频繁地访问在缓存中和数据库中都不存在的数据时,整个系统就可能出现问题。

    2、解决方案

    • 方案一:缓存空对象。即我们把在缓存中和数据库中都不存在的数据缓存到Redis中,并设置过期时间;
    优点实现简单,维护方便
    缺点额外的内存消耗、也可能会造成短期的数据不一致
    • 方案二:布隆过滤。客户端请求的数据会先请求布隆过滤器,如果存在则放行;如果不存在则拒绝客户端的请求。
    优点内存占用较少,没有多余key
    缺点实现复杂、存在误判的可能

    上个图就明白了:

    在这里插入图片描述

    代码实现:

    • 首先查看是否命中缓存,如果没有命中,则查看数据库判断是否存在商铺,存在则写入缓存,不存在则将空值("")写入redis;如果命中缓存,则查看是否是空值,如果是空值则返回给用户店铺不存在的提示,如果不是空值则返回数据给客户端。
        @Override
        public Result queryShopById(Long id) {
            String key = CACHE_SHOP_KEY + id;
            String shopCache = redisTemplate.opsForValue().get(key);
            //如果在缓存中查询到商户,则返回数据给前端
            if (StrUtil.isNotBlank(shopCache)) {
                Shop shop = JSONUtil.toBean(shopCache, Shop.class);
                return Result.ok(shop);
            }
            //判断命中的是否是空值
            if (shopCache != null){
                return Result.fail("店铺不存在");
            }
            //不存在则根据id在数据库中查找
            Shop shop = shopMapper.selectById(id);
            if (shop == null) {
                //将空值写入redis,设置过期时间为2分钟
                redisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
                return Result.fail("店铺不存在");
            }
            //店铺存在,写入缓存,过期时间设置为30分钟
            redisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
            return Result.ok(shop);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    流程分析图:

    在这里插入图片描述


    二、缓存雪崩

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

    解决方案:

    • 给不同的key的过期时间设置随机值
    • 利用redis集群提高服务的可用性

    在这里插入图片描述


    三、缓存击穿

    1、什么是缓存击穿

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

    2、解决方案

    常见的解决方案有两种:互斥锁和逻辑过期。

    • 互斥锁:当线程1查询缓存未命中时,会获取一个互斥锁,然后查询数据库并重建缓存数据;在此期间,如果线程2查询缓存也未命中,并不会成功获取互斥锁(因为线程1正在使用),线程2会休眠并重试,直到线程1写入缓存释放锁;线程2缓存命中。

    • 逻辑过期:当线程1查询缓存发现逻辑时间已过期时,会成功获取一个互斥锁,并开启一个新线程,这个新线程会进行查询数据库重建缓存数据的操作,写入缓存时重置逻辑过期时间,最后释放锁;线程1此时返回过期数据。如果有线程3在线程1获取互斥锁后查询缓存数据,发现逻辑时间已过期,就会获取互斥锁但失败,最后也返回过期数据,可以说是不争不抢。

    解决方案优点缺点
    互斥锁没有额外的内存消耗、保证一致性、实现比较简单线程需要等待,性能会受到影响、可能有死锁风险
    逻辑过期线程无需等待,性能较好不保证一致性、有额外的内存消耗、实现复杂

    上图:

    在这里插入图片描述


    3、互斥锁解决缓存击穿问题

    @Override
        public Result queryShopById(Long id) {
            Shop shop = queryWithMutex(id);
            if (shop == null) {
                return Result.fail("店铺不存在");
            }
            return Result.ok(shop);
        }
        //封装缓存击穿方法
        public Shop queryWithMutex(Long id) {
            String key = CACHE_SHOP_KEY + id;
            String shopCache = redisTemplate.opsForValue().get(key);
            if (StrUtil.isNotBlank(shopCache)) {
                Shop shop = JSONUtil.toBean(shopCache, Shop.class);
                return shop;
            }
            if (shopCache != null) {
                return null;
            }
            //实现缓存重键
            //获取互斥锁
            String lockKey = LOCK_SHOP_KEY + id;
            Shop shop = null;
            try {
                boolean isLock = tryLock(lockKey);
                if (!isLock) {
                    Thread.sleep(50);
                    //没有拿到锁则重试
                    return queryWithMutex(id);
                }
                //成功
                shop = shopMapper.selectById(id);
                if (shop == null) {
                    redisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
                    return null;
                }
                redisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                //释放互斥锁
                unlock(lockKey);
            }
            return shop;
        }
        //获取锁,并设置过期时间为5秒
        private boolean tryLock(String key) {
            Boolean flag = redisTemplate.opsForValue().setIfAbsent(key, "1", LOCK_SHOP_TTL, TimeUnit.SECONDS);
            return BooleanUtil.isTrue(flag);
        }
        //释放锁
        private void unlock(String key) {
            redisTemplate.delete(key);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54

    流程分析图:

    在这里插入图片描述


    4、逻辑删除解决缓存击穿问题

        @Override
        public Result queryShopById(Long id) {
            Shop shop = queryWithLogicalExpire(id);
            if (shop == null) {
                return Result.fail("店铺不存在");
            }
            return Result.ok(shop);
        }
        public Shop queryWithLogicalExpire(Long id) {
            String key = CACHE_SHOP_KEY + id;
            String shopCache = redisTemplate.opsForValue().get(key);
            if (StrUtil.isBlank(shopCache)) {
                return null;
            }
            //命中,需要先把JSON反序列化为对象
            RedisData redisData = JSONUtil.toBean(shopCache, RedisData.class);
            JSONObject data = (JSONObject) redisData.getData();
            Shop shop = JSONUtil.toBean(data, Shop.class);
            LocalDateTime expireTime = redisData.getExpireTime();
            if (expireTime.isAfter(LocalDateTime.now())) {
                //未过期则直接返回店铺信息
                return shop;
            }
            //已过期,需要缓存重建
            //获取互斥锁
            String lockKey = LOCK_SHOP_KEY + id;
            boolean isLock = tryLock(lockKey);
            if (isLock) {
                //成功,开启独立线程,实现缓存重建
                CACHE_REBUILD_EXECUTOR.submit(() -> {
                    try {
                        //重建缓存
                        this.saveShopToRedis(id, CACHE_REBUILD_TTL);
                    } catch (Exception e) {
                        throw new RuntimeException(e);
                    } finally {
                        unlock(lockKey);
                    }
                });
            }
            return shop;
        }
        /**
         * @param id            店铺id
         * @param expireSeconds 设置的过期时间
         */
        public void saveShopToRedis(Long id, Long expireSeconds) {
            //查询店铺数据
            Shop shop = shopMapper.selectById(id);
            //封装逻辑过期时间
            RedisData redisData = new RedisData();
            redisData.setData(shop);
            redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
            //写入redis
            redisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
        }
        //获取锁,并设置过期时间为5秒
        private boolean tryLock(String key) {
            Boolean flag = redisTemplate.opsForValue().setIfAbsent(key, "1", LOCK_SHOP_TTL, TimeUnit.SECONDS);
            return BooleanUtil.isTrue(flag);
        }
        //释放锁
        private void unlock(String key) {
            redisTemplate.delete(key);
        }
        //设置10个线程池
        private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68

    流程分析图:

    在这里插入图片描述


    总结完了,有些难度,需要花时间细细琢磨

  • 相关阅读:
    第29章_瑞萨MCU零基础入门系列教程之改进型环形缓冲区
    [软件工具]opencv-svm快速训练助手教程解决opencv C++ SVM模型训练与分类实现任务支持C# python调用
    IDEA Docker插件远程连接Docker,并打包部署启动SpringBoot项目
    读书笔记:软件工程(11) - 传统方法学 - 软件需求分析
    【AGC】SDK未经用户同意获取AndroidID问题
    docker学习--最详细的docker run 各子命令解释与应用
    【备忘】ChromeDriver 官方下载地址 Selenium,pyppetter依赖
    Leetcode Hot100之双指针
    vue项目嵌套(vue2嵌套vue3)
    C++练习:人员信息管理程序计算不同职员的每月工资。
  • 原文地址:https://blog.csdn.net/weixin_59654772/article/details/127836464