当前位置: 首页 > article >正文

Redis7——进阶篇(五)

 前言:此篇文章系本人学习过程中记录下来的笔记,里面难免会有不少欠缺的地方,诚心期待大家多多给予指教。


基础篇:

  1. Redis(一)
  2. Redis(二)
  3. Redis(三)
  4. Redis(四)
  5. Redis(五)
  6. Redis(六)
  7. Redis(七)
  8. Redis(八)

进阶篇:

  1. Redis(九)
  2. Redis(十)
  3. Redis(十一)
  4. Redis(十二)

接上期内容:上期完成了相关案例的学习。下面学习缓存穿透、预热、雪崩、击穿,话不多说,直接发车。


一、缓存预热

(一)、定义

缓存预热是一种在系统启动阶段或者特定时间点,将一些经常访问或者关键的数据提前加载到缓存中的操作,以减少对数据源(如数据库)的访问次数,从而提高系统的响应速度和性能。避免在用户首次请求时才去加载数据而导致的性能延迟。


(二)、功能

  1. 减少首次请求延迟:当用户首次访问某些数据时,如果没有进行缓存预热,系统需要从数据库等数据源中查询数据,这个过程可能会比较耗时。
  2. 减轻数据库压力:在系统运行初期,如果大量用户同时发起请求,这些请求都直接访问数据库,会给数据库带来巨大的压力,甚至可能导致数据库性能下降或者崩溃。缓存预热可以将部分数据提前加载到缓存中,后续的请求优先从缓存中获取数据,从而减少了对数据库的访问频率,降低了数据库的负载
  3. 提高系统性能和稳定性:由于缓存的读写速度通常比数据库等数据源快很多,缓存预热可以让系统在处理请求时更快地获取数据,从而提高系统的整体性能。同时,减少了对数据库的依赖,降低了因数据库故障或性能问题导致系统不可用的风险,增强了系统的稳定性。

(三)、常用方案

  1.  硬编码(不大推荐):在代码中直接明确地指定需要加载到缓存的数据和逻辑,不通过外部配置或动态计算来改变。在系统启动时,按照预先编写好的代码逻辑将数据加载到缓存中。
  2. @PostConstruct注解: Java 中的一个注解,用于标记一个方法,该方法会在依赖注入完成之后、对象正式投入使用之前被自动调用。
  3. 定时器任务:通过定时任务框架(如 Spring 的@Scheduled注解、Quartz 等),按照预设的时间间隔(如每天凌晨、每小时等)自动执行缓存预热操作。
  4. 数据脚本:使用脚本语言(如 Python、Shell 等)编写脚本,通过脚本连接到缓存系统,将数据加载到缓存中。 

二、缓存雪崩

(一)、名词解释

缓存雪崩是指在某一时刻,缓存中大量的键在同一时间点或者在极短的时间内集中过期失效,或者缓存服务器发生故障导致缓存服务不可用,此时大量原本可以从缓存中获取数据的请求,都直接涌向了数据库等后端数据源,给数据库带来巨大的压力,甚至可能导致数据库不堪重负而崩溃,进而使整个系统出现性能急剧下降、服务不可用等严重问题。


(二)、发生场景

1、硬件方面

缓存服务器的硬件设备(如硬盘、内存、网卡等)出现故障,可能会导致缓存服务无法正常运行,从而发生缓存雪崩现象。


2、业务方面

大量的业务key同时过期,比如在进行缓存预热时,为大量缓存数据设置了相同的过期时间,当这个过期时间到达时,这些缓存数据会同时失效,从而引起缓存雪崩现象。


(三)、预防与解决措施

硬件方面无法把控,主要从业务方面来解决。

业务方面:

  1. 避免大量缓存键同时过期:①、设置随机时间:在设置缓存键的过期时间时,为每个键的过期时间添加一个随机的偏移量,避免它们集中在同一时刻过期。②、设置key用不过期

  2. redis集群实现服务高可用:使用缓存服务器的集群模式,集群模式可以将数据分散存储在多个节点上,当某个节点出现故障时,其他节点仍然可以正常提供服务,保证缓存服务的可用性。

  3. 多缓存结合:采用多级缓存架构,例如同时使用本地缓存(ehcache)+redis缓存。

  4. 服务降级:在应用层对请求进行限流,当请求量超过一定阈值时,直接拒绝部分请求,避免过多的请求直接访问数据库。


三、缓存穿透

(一)、名词解释

缓存穿透是指客户端请求的数据在缓存中不存在,同时在数据库中也不存在,这样每次该请求都会穿透缓存,直接访问数据库。如果有大量这样的无效请求持续涌入,会对数据库造成极大的压力,甚至可能导致数据库不堪重负而崩溃。例如,黑客可能会故意发起大量不存在的键的请求,以消耗数据库资源。


(二)、发生场景

  1. 黑客恶意攻击:攻击者可能会利用系统的漏洞,构造大量不存在的请求,如不存在的用户 ID、商品 ID 等,向系统发起请求。由于这些请求对应的数据在缓存和数据库中都不存在,会导致大量请求直接穿透缓存访问数据库,从而影响系统的正常运行。
  2. 业务数据异常:在业务系统中,可能会出现数据不一致或者数据删除不及时的情况。
  3. 错误的用户输入:如果系统没有对用户输入进行严格的验证,用户可能会输入错误的查询条件,如输入一个不存在的订单号、手机号码等。这些无效请求会直接穿透缓存访问数据库。

(三)、预防与解决措施

1、方案一

缓存空值或默认值,当查询的数据在数据库中不存在时,在缓存中存储一个空值或者默认值,并设置一个较短的过期时间。这样下次相同的请求就可以直接从缓存中获取空值或默认值,而不会再穿透到数据库。


2、方案二

使用布隆过滤器,①、自研布隆过滤器。②、使用Google  Guava 库实现布隆过滤器。


(四)、案例演示

自研简略版布隆过滤器在上一篇已经学习过了,下面将学习Google Guava实现方式。Guava源码地址:GitHub - google/guava: Google core libraries for Java

1、需求说明

模拟使用Guava布隆过滤器拦截掉非法数字,对于合法的数字放行。


2、导入依赖

<!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>32.1.2-jre</version>
</dependency>

3、编码实现

public class GuavaBloomFilterDemo {
    public static void main(String[] args) {
        //误判率,它越小误判的个数也就越少(思考,是不是可以设置的无限小,没有误判岂不更好)
        //fpp the desired false positive probability 0.0 < fpp < 1.0,误判率越低,消耗的资源越多,哈希函数也用的越多,误判率也就越低
        // 默认值 0.03
        BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), 1000000, 0.03);
        //1 先往布隆过滤器里面插入100万的样本数据
        for (int i = 1; i <= 1000000; i++) {
            bloomFilter.put(i);
        }

        List<Integer> list1 = Arrays.asList(120000000, 111111, 10, 222222222, 42575, 123457);

        list1.forEach(e -> {
            boolean result = bloomFilter.mightContain(e);
            if (result) {
                // TODO 查redis → 查数据库 → .....
                System.out.println("存在,放行" + e);
            } else {
                // 直接返回结果
                System.out.println("不存在,拦截" + e);
            }
        });

        //故意取10万个不在过滤器里的值,看看有多少个会被认为在过滤器里
        List<Integer> list = new ArrayList<>(1000000);
        for (int i = 1000000 + 1; i <= 1100000; i++) {
            if (bloomFilter.mightContain(i)) {
                list.add(i);
            }
        }
        System.out.println("误判的总数量::{}" + list.size());
    }
}


四、缓存击穿

(一)、名词解释

缓存击穿是指在高并发的场景下,一个非常热点的 key 在缓存中过期失效的瞬间,大量针对该 key 的请求同时涌入。由于此时缓存中没有该 key 对应的数据,这些请求就会全部转向数据库去查询。数据库在短时间内需要处理大量的查询请求,从而承受巨大的压力,可能会导致数据库性能急剧下降,甚至出现崩溃的情况,进而影响整个系统的正常运行。


(二)、发生场景

  1. 热点数据过期:在电商系统中,一款热门商品的信息会被大量用户频繁访问,为了减轻数据库压力,会将该商品信息缓存起来并设置过期时间。当这个过期时间到达,缓存中的数据失效,而此时恰好有大量用户同时发起对该商品信息的请求,就会出现缓存击穿的情况。
  2. 流量突发:一些突发的热点事件会导致瞬间产生大量的请求。比如,某明星突然发布一条微博,引发大量粉丝同时访问其个人主页,而该主页信息在缓存中过期,大量请求就会直接冲向数据库。
  3. 数据预热不正确:比如新闻网站在每天早上进行缓存预热,但遗漏了当天的一条重大热点新闻,当用户大量访问该新闻时,就会出现问题。

(三)、预防与解决措施

1、方案一

使用双检加锁策略。前面已经学习过了,不在阐述。

public String get(String key) {
        String value = redis.get(key);// 查询缓存
        if (value != null) {
            //缓存存在直接返回
            return value;
        } else {
            //缓存不存在则对方法加锁
            //假设请求量很大,缓存过期
            synchronized (this) {
                value = redis.get(key); // 在查一遍redis
                if (value == null) {
                    // 从数据库获取数据
                    value = dao.get(key);
                    // 设置过期时间并回写到缓存
                    redis.setex(key, time, value);
                }
                return value;
            }
        }
    }

2、方案二

随机退避策略。当发现缓存中热点 key 失效时,让各个请求不要立即去访问数据库,而是各自随机等待一段不同的时间后再去尝试获取数据。这样可以避免大量请求在同一时刻集中访问数据库,将请求的时间分散开,减轻数据库在短时间内的压力。

public String randomBackoffMethod(String key) {
        try {
            Jedis jedis = RedisUtils.getJedis();
            String data = jedis.get(key);
            if (data == null) {
                try {
                    // 生成随机退避时间 毫秒
                    int backoffTime = new Random().nextInt(2000);
                    Thread.sleep(backoffTime);
                    // 再次尝试从缓存获取数据,
                    // 但是在高并发场景下,可能线程随机退避时间会一样,
                    // 为了避免造成缓存双写不一致问题,使用双检锁策略来防止
                    String value = jedis.get(key);// 查询缓存
                    if (value != null) {
                        //缓存存在直接返回
                        return value;
                    } else {
                        //缓存不存在则对方法加锁
                        //假设请求量很大,缓存过期
                        synchronized (this) {
                            value = jedis.get(key); // 在查一遍redis
                            if (value == null) {
                                // 从数据库获取数据
                                value = dao.get(key);
                                // 设置过期时间并回写到缓存
                                jedis.setex(key, time, value);
                            }
                            return value;
                        }
                    }
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
            jedis.close();
            return data;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

3、方案三

差异失效策略。它的核心思想是避免多个热点 Key 在同一时刻同时失效,从而防止大量请求在瞬间全部涌向数据库,给数据库造成过大压力。

在缓存击穿场景中的实现方式为一个热点key拷贝两份,两份缓存过期时间不一样,将缓存失效的时间分散开来,以此保障系统的稳定性与性能。

/**
     * 差异失效策略查询
     */
    public String differentialFailureSelectMethod(String key) {
        // 同一个热点key的前缀
        String prefixA = "keyA:";
        String prefixB = "keyB:";
        try {
            Jedis jedis = RedisUtils.getJedis();
            String data = jedis.get(prefixA + key);
            if (data == null) {
                System.out.println("=========A缓存已经失效");
                //用户先查询缓存A(上面的代码),如果缓存A查询不到(例如,更新缓存的时候删除了),再查询缓存B
                data = jedis.get(prefixB + key);
                if (data == null) {
                    System.out.println("=========B缓存已经失效");
                    //TODO 查数据库 → 回写redis
                }
                return data;
            }
            System.out.println("查询结果:{}" + data);
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return null;
    }

    /**
     * 差异失效策略更新
     */
    public void differentialFailureUpdateMethod(String key) {
        try {
            // 以前的某个热点key
            String oldHotKeyA = "oldHotKey:" + key;
            String oldHotKeyB = "oldHotKey:" + key;

            // 新热点key前缀
            String prefixA = "newKeyA:";
            String prefixB = "newKeyB:";
            //模拟从数据库查数据
            Jedis jedis = RedisUtils.getJedis();
            Object o = dao.get(key);
            //先更新B缓存
            jedis.del(oldHotKeyB);
            jedis.set(prefixB + o.getId());
            jedis.expire(prefixB + o.getId(), 2000);
            //再更新A缓存
            jedis.del(oldHotKeyA);
            jedis.set(prefixA + o.getId());
            jedis.expire(prefixA + o.getId(), 1000);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

五、总结 

一图总结,四个问题以及解决办法:

问题类型核心问题解决方案典型场景
缓存预热数据未提前加载启动加载、定时任务、脚本电商大促前的商品信息加载
缓存雪崩大量缓存失效随机过期、集群、高可用、限流大量key过期、缓存服务器宕机时
缓存穿透无效请求攻击布隆过滤器、缓存空值恶意攻击场景
缓存击穿热点 Key 失效双检锁、差异失效、随机退避秒杀活动中的商品信息访问

ps:努力到底,让持续学习成为贯穿一生的坚守。学习笔记持续更新中。。。。


http://www.kler.cn/a/580832.html

相关文章:

  • Consensus 大会全观察:政策、生态与技术交汇,香港能否抢占 Web3 先机?
  • 【网络编程】WSAAsyncSelect 模型
  • redis 用来实现排行榜的功能
  • Qt 中实现自定义控件子类化
  • scala传递匿名函数简化的原则
  • Android 低功率蓝牙之BluetoothGattCharacteristic详解
  • linux下文件读写操作
  • 探索CAMEL:揭开多智能体系统的神秘面纱
  • upload-labs(1-20)详解(专业版)
  • JVM参数调整
  • Linux——基础IO【3万字大章】
  • 第四次CCF-CSP认证(含C++源码)
  • 构建服务器--在线单词查询
  • Ubuntu 22.04 升级到 Ubuntu 24.04 全流程指南
  • tcc编译器教程6 进一步学习编译gmake源代码
  • Unity2017打包出来后的场景一片红
  • P8630 [蓝桥杯 2015 国 B] 密文搜索--map、substr
  • 大型语言模型为何看不懂电路图:局限性分析
  • OpenHarmony 5.0.0 Release
  • 【算法 C/C++】二维前缀和