本文介绍: 本质都是Redis的命中率降低,导致大量请求直接访问数据库缓存穿透解决方法包括设置带TTL的空值 & 使用布隆过滤器

一、缓存穿透

1.1 产生原因

客户端请求数据缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会访问数据库。导致DB的压力瞬间变大而卡死或者宕机。

缓存穿透发生的场景一般有两类:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jACPoikM-1688634946898)(【Redis】缓存穿透、缓存击穿、缓存雪崩的原因及解决方案/image-20230705214531919.png)]

1.2 解决方法

接口校验

类似于用户权限的拦截,对于id = -3872这些无效访问就直接拦截,不允许这些请求到达Redis、DB上。

空值进行缓存

比如,虽然数据库没有id = 1022用户数据,但是在redis中对其进行缓存(key=1022, value=null),这样当请求到达redis的时候就会直接返回一个null的值给客户端,避免了大量无法访问数据直接打在DB上。

需要注意:

使用布隆过滤器

一个很清晰的视频,十分钟以内就能看完:布隆过滤器Bilibili视频讲解
在这里插入图片描述

布隆过滤器基本原理使用BitMap结构多个哈希函数,向其中加入数据

x

x

x时,将

h

a

s

h

1

(

x

)

hash_1(x)

hash1(x)

h

a

s

h

2

(

x

)

hash_2(x)

hash2(x)

h

a

s

h

3

(

x

)

hash_3(x)

hash3(x)位置bit值置为1。查询时,如果相应的位置均为1,则认为该数存在布隆过滤器中。性能较高但也有缺点:

如果它告诉你不存在,则一定不存在;如果它告诉你存在,则可能不存在。
插一句:很适合判断海量元素元素是否存在,比如设置网站黑名单

使用BitMap作为布隆过滤器,将目前所有可以访问到的资源通过简单映射关系放入到布隆过滤器中(哈希计算),当一个请求来临的时候先进行布隆过滤器判断,如果有那么才进行放行,否则就直接拦截。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XmeWjaTb-1688634946899)(【Redis】缓存穿透、缓存击穿、缓存雪崩的原因及解决方案/image-20230705220925615.png)]

实时监控

对redis进行实时监控,当发现redis中的命中率下降的时候进行原因的排查,配合运维人员访问对象访问数据进行分析查询,从而进行黑名单设置限制服务(拒绝黑客攻击)。

二、缓存雪崩

redis中的大量key集体过期可以理解为Redis中的大部分数据都清空 / 失效了,这时候如果有大量并发的请求来到,Redis就无法进行有效的响应(命中率急剧下降),也会导致DB先生的绝望。

缓存雪崩场景通常有两个

2.2 解决方法

失效时间分散开

常用且易于实现通过使用自动生成随机数使得key过期时间TTL是随机,防止集体过期

业务添加多级缓存

使用nginx缓存 + redis缓存 + 其他缓存,不同层使用不同的缓存,可靠性更强。

构建缓存高可用集群

主要针对缓存服务故障的情景,使用Redis集群提高服务可用性

使用锁或者队列方式

如果查不到就加上排它锁,其他请求只能进行等待,但这种方式可能影响并发量。

设置缓存标记

点数可以考虑失效后台异步更新缓存,适用于严格要求缓存一致性的情景。

三、缓存击穿

Redis中的某个热点key过期,但是此时有大量的用户访问该过期key

可以看成缓存雪崩的一个特殊子集

比如xxx塌房哩、xxx商品活动,这时候大量用户都在访问该热点事件,但是可能优于某种原因,redis的这个热点key过期了,那么这时候大量高并发对于该key的请求就得不到redis的响应,那么就会将请求直接打在DB服务器上,导致整个DB瘫痪。

3.2 解决方法

使用互斥

只有一个请求可以获取互斥锁,然后到DB中将数据查询并返回到Redis,之后所有请求就可以从Redis中得到响应。【缺点:所有线程的请求需要一同等待

”提前“使用互斥锁 / 逻辑过期

value内部设置一个比缓存(Redis)过期时间短的过期时间标识,当异步线程发现该值快过期时,马上延长内置的这个时间,并重新从数据库加载数据,设置到缓存中去。【缺点:不保证一致性实现相较互斥锁更复杂

提前对热点数据进行设置

类似于新闻、某博等软件需要对热点数据进行预先设置在Redis中,或者适当延长Redis中的Key过期时间。

监控数据,适时调整

监控哪些数据是热门数据,实时的调整key的过期时长。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3p28T6My-1688634946899)(【Redis】缓存穿透、缓存击穿、缓存雪崩的原因及解决方案/image-20230705223722624.png)]

3.3 实现

1 互斥锁

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vebCChYc-1688634946900)(【Redis】缓存穿透、缓存击穿、缓存雪崩的原因及解决方案/image-20230705224115044.png)]

使用setnx作为Redis中的锁。

    /**
     * 根据id查找商户,先到redis中找,再到MySQL中找
     * @param id
     * @return
     */
    @Override
    public Result queryShopById(Long id) {

        // 用String形式存储JSON
        String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);

        // 如果查询结果不为null,直接返回
        if (StrUtil.isNotBlank(shopJson)) {
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return Result.ok(shop);
        }

        // 否则Redis中查询结果为空判断是否为“”
        if (shopJson != null) {
            return Result.fail("店铺不存在,请确认id是否正确");
        }

        // 尝试获取锁,
        // 如果没有得到锁,Sleep一段时间
        if (!tryLock(LOCK_SHOP_KEY + id)) {
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 从开始重试
            return queryShopById(id);
        }

        // 获得了锁,从MySQl中查找
        Shop shop = this.getById(id);
        // 模拟重建的延时
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 不在MySQL中
        if (shop == null) {
            // 将空值写入Redis
            stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
            // 释放锁
            unLock(LOCK_SHOP_KEY + id);
            return Result.fail("店铺不存在,请确认id是否正确");
        }
        else {
            // 在MySQL中,存入redis
            stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
            // 释放锁
            unLock(LOCK_SHOP_KEY + id);
            return Result.ok(shop);
        }
    }

    public boolean tryLock(String key) {
        // 尝试获取锁,set成功返回true,否则返回false
        Boolean getLock = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        // 避免getLock为null,使用工具
        return BooleanUtil.isTrue(getLock);
    }

    public void unLock(String key) {
        stringRedisTemplate.delete(key);
    }


测试

F:JmeterbinApacheJMeter.jar

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Uo5t40vQ-1688634946900)(【Redis】缓存穿透、缓存击穿、缓存雪崩的原因及解决方案/image-20230706153638543.png)]

对应地,MySQL只执行了1次SQL:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6nAJ3Gg8-1688634946900)(【Redis】缓存穿透、缓存击穿、缓存雪崩的原因及解决方案/image-20230706153739245.png)]

2 逻辑过期

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WsYMPuEj-1688634946900)(【Redis】缓存穿透、缓存击穿、缓存雪崩的原因及解决方案/image-20230706160509947.png)]

    /**
     * 线程池
     */
    private static final ThreadFactory NAMED_THREAD_FACTORY = new ThreadFactoryBuilder().build();

    private static final ExecutorService POOL = new ThreadPoolExecutor(5, 200,
            0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable&gt;(1024), NAMED_THREAD_FACTORY,
            new ThreadPoolExecutor.AbortPolicy());


    public Result queryWithExpire(Long id) {
        String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);

        // 如果查询结果为null,直接失败
        if (StrUtil.isBlank(shopJson)) {
            return Result.fail("您查询的数据不存在,请检查您的输入");
        }

        RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
        Shop shop = JSONUtil.toBean((JSONObject)redisData.getData(), Shop.class);
        // 判断缓存是否过期
        LocalDateTime time = redisData.getExpireTime();
        // 未过期,直接返回信息
        if (time.isAfter(LocalDateTime.now())) {
            return Result.ok(shop);
        }
        // 过期,获取互斥锁失败,返回过期信息
        if (!tryLock(LOCK_SHOP_KEY + id)) {
            unLock(LOCK_SHOP_KEY + id);
            return Result.ok(shop);
        }
        // 过期,获取互斥锁成功,开启新线程,重建数据库
       POOL.submit(() -> {
           this.saveShop2Redis(id, 20L);
           unLock(LOCK_SHOP_KEY + id);
       });
       // 返回过期信息
       return Result.ok(shop);
    }

	/**
     * 在MySQL中查找id的shop,写入Redis并更新虚拟过期时间
     * @param id
     * @param expireSeconds
     */
    private void saveShop2Redis(Long id, Long expireSeconds) {
        // 获取
        Shop shop = this.getById(id);

        // 封装
        RedisData redisData = new RedisData();
        redisData.setData(shop);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));

        // 写入Redis
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
    }

原文地址:https://blog.csdn.net/qq_43787197/article/details/131581410

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任

如若转载,请注明出处:http://www.7code.cn/show_20374.html

如若内容造成侵权/违法违规/事实不符,请联系代码007邮箱suwngjj01@126.com进行投诉反馈,一经查实,立即删除

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注