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

程序员黑哥 2023-08-09 15:59 发表于湖南

收录于合集

#数据库21个

#redis22个

#编程语言3个

#计算机网络10个

#编程64个

一、缓存穿透

1、什么是缓存穿透

  • 缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远都不会生效,这些请求都会打到数据库。

  • 当有人恶意频繁地访问在缓存中和数据库中都不存在的数据时,整个系统就可能出现问题。

2、解决方案

  • 方案一:缓存空对象。即我们把在缓存中和数据库中都不存在的数据缓存到Redis中,并设置过期时间;

    图片

  • 方案二:布隆过滤。客户端请求的数据会先请求布隆过滤器,如果存在则放行;如果不存在则拒绝客户端的请求。

图片

上个图就明白了:

图片

代码实现:

  • 首先查看是否命中缓存,如果没有命中,则查看数据库判断是否存在商铺,存在则写入缓存,不存在则将空值("")写入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);    }

流程分析图:

图片

二、缓存雪崩

  • 缓存雪崩是指在同一时段大量的缓存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);    }

流程分析图:

图片

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);

流程分析图:

图片

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值