Redis高级(六)、一套打通缓存预热、雪崩、击穿、穿透原理与解决方案

128 阅读12分钟

觉得对你有益的小伙伴记得点个赞+关注

后续完整内容持续更新中

希望一起交流的欢迎发邮件至javalyhn@163.com

再来回顾一遍BloomFilter作用。 image.png

1. 缓存预热

1.1 缓存预热介绍

缓存预热这个概念就如它字面意思。当系统上线时,缓存内还没有数据,如果直接提供给用户使用,每个请求都会穿过缓存去访问底层数据库,如果并发大的话,很有可能在上线当天就会宕机,因此我们需要在上线前先将数据库内的热点数据缓存至Redis内再提供出去使用,这种操作就成为"缓存预热"。

缓存预热的实现方式有很多,比较通用的方式是写个批任务,在启动项目时或定时去触发将底层数据库内的热点数据加载到缓存内。

2. 缓存雪崩

2.1 缓存雪崩介绍

造成缓存雪崩的两种主要情况

  1. 同一时段大量的缓存key集体失效
  2. Redis主机挂了,Redis全盘崩溃

2.2 缓存雪崩解决方案

  • 给不同的Key的TTL添加随机值

  • 利用Redis集群提高服务的可用性

    1. 主从+哨兵
    2. Redis Cluster
  • 给缓存业务添加降级限流策略

    ehcache本地缓存 + Hystrix或者阿里sentinel限流&降级

  • 给业务添加多级缓存

  • 开启Redis持久化机制aof/rdb,尽快恢复集群

3. 缓存穿透

3.1 缓存穿透介绍

缓存穿透是指Redis与数据库中都没有请求需要的数据,此时数据打在Redis上没有意义,仍然会去查询数据库,导致数据库压力剧增,这种现象我们叫做缓存穿透。此时这个Redis相当于一个摆设。

3.2 缓存穿透的危害

在正常情况下,在请求查询Redis未查询到数据时会从mysql查询数据,并且回写到Redis中,但是此时mysql无数据,不能回写数据,导致每次都要查询数据库,数据路压力暴增!!

3.3 缓存穿透的解决方案

3.3.1 空对象缓存或者缺省值

这个方案一般情况下是可以的,步骤就是

一旦发生缓存穿透,我们就可以针对查询的数据,在Redis中缓存一个空值或者是和业务层协商确定的缺省值(比如库存的缺省值可以设为0)。紧接着,应用发送的后续请求再进行查询时,就可以直接从Redis中读取到空值或者缺省值,返回给业务层了,避免了把大量请求发送给数据库处理,保持了数据库的正常运行。

但是!!如果有一名hacker或者是恶意攻击对你的系统进行gj,拿一个不存在的id去查询数据,此时有两种情况

  1. id相同gj系统:第一次会穿过Redis打到mysql,空对象缓存后第二次就返回null了,这样避免了mysql被大量访问,保护了系统。
  2. id不同gj系统:由于存在空对象的缓存和缓存的回写(看自己的业务),Redis中无关紧要的key也会越来越多,(一定要给Redis的每一个key设置过期时间)

3.3.2 布隆过滤器

有关布隆过滤器的安装+操作+原理知识,我放到了 juejin.cn/post/719137… ,这一讲知识就专注于BloomFilter的实战了。

在这篇文章,我采用两种布隆过滤器来实现

3.3.3 Google布隆过滤器Guava解决缓存穿透

Guava 中布隆过滤器的实现算是比较权威的,所以实际项目中我们不需要手动实现一个布隆过滤器。

如果有兴趣想去看Guava's BloomFilter源码的,可以参照 github.com/google/guav…

Coding实战

  1. 建modul bloomfilter_demo
  2. 改pom
<properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <junit.version>4.12</junit.version>
        <log4j.version>1.2.17</log4j.version>
        <lombok.version>1.16.18</lombok.version>
        <mysql.version>5.1.47</mysql.version>
        <druid.version>1.1.16</druid.version>
        <mybatis.spring.boot.version>1.3.0</mybatis.spring.boot.version>
    </properties>

    <dependencies>
        <!--guava Google 开源的 Guava 中自带的布隆过滤器-->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>23.0</version>
        </dependency>
        <!-- redisson -->
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.13.4</version>
        </dependency>
        <!--web+actuator-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <!--SpringBoot与Redis整合依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
        <!-- jedis -->
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>3.1.0</version>
        </dependency>
        <!-- springboot-aop 技术-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <!--Mysql数据库驱动-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.47</version>
        </dependency>
        <!--集成druid连接池-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.10</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>${druid.version}</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>${druid.version}</version>
        </dependency>
        <!--mybatis和springboot整合-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>${mybatis.spring.boot.version}</version>
        </dependency>
        <!-- 添加springboot对amqp的支持 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
        <!--通用基础配置-->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>${junit.version}</version>
        </dependency>
        <!-- redisson -->
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.13.4</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>${log4j.version}</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>${lombok.version}</version>
            <optional>true</optional>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
  1. 写配置文件 application.properties
server.port=6666
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.url=jdbc:mysql://localhost:3306/db2022?useUnicode=true&characterEncoding=utf-8&useSSL=false
spring.datasource.username=root
spring.datasource.password=admin
spring.datasource.druid.test-while-idle=false

# ========================redis相关配置=====================
# Redis数据库索引(默认为0)
spring.redis.database=0
# Redis服务器地址
spring.redis.host=
# Redis服务器连接端口
spring.redis.port=
# Redis服务器连接密码(默认为空)
spring.redis.password=
# 连接池最大连接数(使用负值表示没有限制) 默认 8
spring.redis.lettuce.pool.max-active=8
# 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1
spring.redis.lettuce.pool.max-wait=-1
# 连接池中的最大空闲连接 默认 8
spring.redis.lettuce.pool.max-idle=8
# 连接池中的最小空闲连接 默认 0
spring.redis.lettuce.pool.min-idle=0
  1. 主启动
  2. 业务类

首先新建一个测试类

    @Test
    public void bloomFilter()
    {
// 创建布隆过滤器对象
        BloomFilter<Integer> filter = BloomFilter.create(Funnels.integerFunnel(), 100);
// 判断指定元素是否存在
        System.out.println(filter.mightContain(1));
        System.out.println(filter.mightContain(2));
// 将元素添加进布隆过滤器
        filter.put(1);
        filter.put(2);
        System.out.println(filter.mightContain(1));
        System.out.println(filter.mightContain(2));
    }

image.png

  1. 新建BloomFilterDemo 接着我们往BloomFilter里面添加100w数据
public class BloomfilterDemo
{
    public static final int _1W = 10000;

    //布隆过滤器里预计要插入多少数据
    public static int size = 100 * _1W;
    //误判率,它越小误判的个数也就越少(思考,是不是可以设置的无限小,没有误判岂不更好)
    public static double fpp = 0.03;

    // 构建布隆过滤器
    private static BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), size,fpp);


    public static void main(String[] args)
    {
        //1 先往布隆过滤器里面插入100万的样本数据
        for (int i = 0; i < size; i++) {
            bloomFilter.put(i);
        }

        //故意取10万个不在过滤器里的值,看看有多少个会被认为在过滤器里
        List<Integer> list = new ArrayList<>(10 * _1W);

        for (int i = size+1; i < size + 100000; i++) {
            if (bloomFilter.mightContain(i)) {
                System.out.println(i+"\t"+"被误判了.");
                list.add(i);
            }
        }
        System.out.println("误判的数量:" + list.size());

    }

}
 

image.png

看到上述结果,有3033个元素被误判了,我们拿计算器计算一下

image.png 也就是3%,这不就是我们之前设置的误判率吗。 那结余所给代码中误判率的提示,是不是越小越好呢? 来实践一下。

image.png

image.png

由上述两图可知,误判的却是少了(说没了太绝对),但是效率却大大降低。 所以这是一个双刃剑,有得必有失。

3.3.4 将上一个的demo误判率恢复成0.03debug一下

我们给create方法打上断点

image.png

上图中的numBits表示表示存一百万个int类型数字,需要的位数为7298440,700多万位。理论上存一百万个数,一个int是4字节32位,需要3200万位。如果使用HashMap去存,按HashMap50%的存储效率,需要6400万位。可以看出BloomFilter的存储空间很小,只有HashMap的1/10左右。

上面的numHashFunctions,表示需要5个函数去存这些数字(put时,会使用每个hash函数对值进行hash,然后分别存到大的bit数组中,mightContain的时候,也会通过该方式取出来,只要发现某一位上没有,则为false)。

同样的,此时将fpp误判率改为0.0003debug一下

image.png

看到此结果大家并会惊讶。

和上面对比可以看出,错误率越大,所需空间和时间越小,错误率越小,所需空间和时间约大

3.3.5 Guava布隆过滤器缺点

Guava 提供的布隆过滤器的实现还是很不错的 (想要详细了解的可以看一下它的源码实现),但是它有一个重大的缺陷就是只能单机使用 ,而现在互联网一般都是分布式的场景。

为了解决这个问题,我们就需要用到 Redis 中的布隆过滤器了

3.3.6 Redis布隆过滤器解决缓存穿透(白名单案例模拟)

  1. 我们来看一下白名单架构

image.png

  1. 注意点
  • 误判问题,但是概率小可以接受,不能从布隆过滤器删除。

  • 全部合法的key都需要放入过滤器+redis里面,不然数据就是返回null。

  1. Coding实现
// 1测试  布隆过滤器有+redis有

public class BloomFilterDemo2 {
    public static final int _1W = 10000;

    //布隆过滤器里面预计要添加多少元素
    public static int size = 100 * _1W;

    //误判率,他越小误判个数也就越少
    public static double fpp = 0.0003;

    static RedissonClient redissonClient = null;
    static RBloomFilter rBloomFilter = null;

    static{
        Config config = new Config();
        ```
config.useSingleServer().setAddress("redis://xxxxx:6379").setPassword("xxxx").setDatabase(0);
        //构造Redisson
        redissonClient = Redisson.create(config);
        //通过redisson构造rBloomFilter
        rBloomFilter = redissonClient.getBloomFilter("phoneListBloomFilter",new StringCodec());

        rBloomFilter.tryInit(size,fpp);

        // 1测试  布隆过滤器有+redis有
        rBloomFilter.add("10086");
        redissonClient.getBucket("10086",new StringCodec()).set("chinamobile10086");


    }
    public static void main(String[] args)
    {
        String phoneListById = getPhoneListById("10086");
        System.out.println("------查询出来的结果: "+phoneListById);

        //暂停几秒钟线程
        try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
        redissonClient.shutdown();
    }

    private static String getPhoneListById(String IDNumber)
    {
        String result = null;

        if (IDNumber == null) {
            return null;
        }
        //1 先去布隆过滤器里面查询
        if (rBloomFilter.contains(IDNumber)) {
            //2 布隆过滤器里有,再去redis里面查询
            RBucket<String> rBucket = redissonClient.getBucket(IDNumber, new StringCodec());
            result = rBucket.get();
            if(result != null)
            {
                return "i come from redis: "+result;
            }else{
                result = getPhoneListByMySQL(IDNumber);
                if (result == null) {
                    return null;
                }
                // 重新将数据更新回redis
                redissonClient.getBucket(IDNumber,new StringCodec()).set(result);
            }
            return "i come from mysql: "+result;
        }
        return result;
    }

    private static String getPhoneListByMySQL(String IDNumber)
    {
        //此处就不操作数据库了,给随机值替代
        return "chinamobile"+IDNumber;
    }

}

image.png image.png

// 2测试  布隆过滤器有+redis无
static{
        Config config = new Config();
        ```
config.useSingleServer().setAddress("redis://xxxxx:6379").setPassword("xxxx").setDatabase(0);
        //构造Redisson
        redissonClient = Redisson.create(config);
        //通过redisson构造rBloomFilter
        rBloomFilter = redissonClient.getBloomFilter("phoneListBloomFilter",new StringCodec());

        rBloomFilter.tryInit(size,fpp);


        // 2测试  布隆过滤器有+redis无
        rBloomFilter.add("10086");


    }
    

image.png image.png

//3 测试 ,都没有
static{
        Config config = new Config();
        ```
config.useSingleServer().setAddress("redis://xxxxx:6379").setPassword("xxxx").setDatabase(0);
        //构造Redisson
        redissonClient = Redisson.create(config);
        //通过redisson构造rBloomFilter
        rBloomFilter = redissonClient.getBloomFilter("phoneListBloomFilter",new StringCodec());

        rBloomFilter.tryInit(size,fpp);


        // 3测试都没有
    }

image.png

3.4 布隆过滤器重要总结

image.png

3.5 在centos7下布隆过滤器2种安装方式

juejin.cn/post/719137…

3.6 解决缓存穿透开发人员需要注意的有哪些

  • 增强id的复杂度,避免被猜测id规律
  • 做好数据的基础格式校验
  • 加强用户权限校验
  • 做好热点参数的限流

4. 缓存击穿

4.1 缓存击穿介绍

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

简单说就是热点key突然失效了,暴打mysql。

4.2 缓存击穿危害

会造成某一时刻数据库请求量过大,压力剧增。

4.3 缓存击穿解决方案

4.3.1 对于访问频繁的热点key,干脆就不设置过期时间

4.3.2 互斥独占锁防止击穿

多个线程同时去查询数据库的这条数据,那么我们可以在第一个查询数据的请求上使用一个 互斥锁来锁住它。

其他的线程走到这一步拿不到锁就等着,等第一个线程查询到了数据,然后做缓存。后面的线程进来发现已经有缓存了,就直接走缓存。

利用双端检索完成 image.png

4.3.3 逻辑过期

这个方法是在方案1的基础上优化的,如果我们不给热点key设置失效时间,那么热点key随着时间的增多,内存占用的就多了,而且基于Redis的淘汰策略,不回去淘汰没有设置过期时间的key,这也就是设置key时候的重点一定要加上过期时间!!!!!

我们把过期时间设置在value中,注意:这个过期时间并不会直接作用于redis,而是我们后续通过逻辑去处理。

假设线程1去查询缓存,然后从value中判断出来当前的数据已经过期了,此时线程1去获得互斥锁,那么其他线程会进行阻塞,获得了锁的线程他会开启一个 线程去进行 以前的重构数据的逻辑,直到新开的线程完成这个逻辑后,才释放锁, 而线程1直接进行返回,假设现在线程3过来访问,由于线程线程2持有着锁,所以线程3无法获得锁,线程3也直接返回数据,只有等到新开的线程2把重建数据构建完后,其他线程才能走返回正确的数据。

这种方案巧妙在于,异步的构建缓存,缺点在于在构建完缓存之前,返回的都是脏数据

image.png

4.3.4 互斥更新+随机退避+差异失效时间

我们用一个模拟72小时高并发+定时更新+分栏显示业务来解释这个解决方法。

4.3.4.1 分析过程

  1. 100%高并发,绝对不用mysql实现
  2. 先把mysql里面参加活动的数据抽取进redis,一般采用定时器扫描来决定上线活动还是下线取消。
  3. 支持分页功能,一页20条记录

请思考一下,Redis中的什么数据类型可以解决这个问题

答案是list

image.png

4.3.4.2 coding实战

  1. 建module
  2. 改pom
 <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <junit.version>4.12</junit.version>
        <log4j.version>1.2.17</log4j.version>
        <lombok.version>1.16.18</lombok.version>
        <mysql.version>5.1.47</mysql.version>
        <druid.version>1.1.16</druid.version>
        <mapper.version>4.1.5</mapper.version>
        <mybatis.spring.boot.version>1.3.0</mybatis.spring.boot.version>
    </properties>

    <dependencies>
        <!--SpringBoot通用依赖模块-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <!--swagger2-->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.9.2</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.9.2</version>
        </dependency>
        <!--SpringBoot与Redis整合依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!--springCache-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-cache</artifactId>
        </dependency>
        <!--springCache连接池依赖包-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
        <!-- jedis -->
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>3.1.0</version>
        </dependency>
        <!--Mysql数据库驱动-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.47</version>
        </dependency>
        <!--SpringBoot集成druid连接池-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.10</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>${druid.version}</version>
        </dependency>
        <!--mybatis和springboot整合-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>${mybatis.spring.boot.version}</version>
        </dependency>
        <!-- 添加springboot对amqp的支持 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
        <dependency>
            <groupId>commons-codec</groupId>
            <artifactId>commons-codec</artifactId>
            <version>1.10</version>
        </dependency>
        <!--通用基础配置junit/devtools/test/log4j/lombok/hutool-->
        <!--hutool-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.2.3</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>${junit.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>${log4j.version}</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>${lombok.version}</version>
            <optional>true</optional>
        </dependency>
        <!--persistence-->
        <dependency>
            <groupId>javax.persistence</groupId>
            <artifactId>persistence-api</artifactId>
            <version>1.0.2</version>
        </dependency>
        <!--通用Mapper-->
        <dependency>
            <groupId>tk.mybatis</groupId>
            <artifactId>mapper</artifactId>
            <version>${mapper.version}</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
  1. 写配置文件
server.port=5555

spring.application.name=redis0511

# ========================logging 日志相关的配置=====================
#系统默认,全局root配置的日志形式,可以注释掉
logging.level.root=warn
#开发人员自己设置的包结构,对那个package进行什么级别的日志监控
logging.level.com.redis=info
#开发人员自定义日志路径和日志名称
logging.file.name=
#%d{HH:mm:ss.SSS}――日志输出时间
#%thread――输出日志的进程名字,这在Web应用以及异步任务处理中很有用
#%-5level――日志级别,并且使用5个字符靠左对齐
#%logger- ――日志输出者的名字
#%msg――日志消息
#%n――平台的换行符
#logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger- %msg%n
logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger- %msg%n 
logging.pattern.file=%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger- %msg%n

# ========================alibaba.druid相关配置=====================
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/db2022?useUnicode=true&characterEncoding=utf-8&useSSL=false
spring.datasource.username=
spring.datasource.password=
spring.datasource.druid.test-while-idle=false

# ========================redis相关配置=====================
# Redis数据库索引(默认为0)
spring.redis.database=0
# Redis服务器地址
#spring.redis.host=
spring.redis.host=
# Redis服务器连接端口
spring.redis.port=
# Redis服务器连接密码(默认为空)
spring.redis.password=
# 连接池最大连接数(使用负值表示没有限制) 默认 8
spring.redis.lettuce.pool.max-active=8
# 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1,记得加入单位ms,不然idea报红色
spring.redis.lettuce.pool.max-wait=-1ms
# 连接池中的最大空闲连接 默认 8
spring.redis.lettuce.pool.max-idle=8
# 连接池中的最小空闲连接 默认 0
spring.redis.lettuce.pool.min-idle=0

# ========================mybatis相关配置===================
mybatis.mapper-locations=classpath:mapper/*.xml
mybatis.type-aliases-package=com.redis.entities

# ========================swagger=====================
spring.swagger2.enabled=true
  1. 主启动
@SpringBootApplication
@MapperScan("com.redis.mapper") //import tk.mybatis.spring.annotation.MapperScan;
public class Redis20210511Application
{

    public static void main(String[] args)
    {
        SpringApplication.run(Redis20210511Application.class, args);
    }

}
  1. 配置RedisConfig
@Configuration
public class RedisConfig
{
    /**
     * @param lettuceConnectionFactory
     * @return
     *
     * redis序列化的工具配置类,下面这个请一定开启配置
     * 127.0.0.1:6379> keys *
     * 1) "ord:102"  序列化过
     * 2) "\xac\xed\x00\x05t\x00\aord:102"   野生,没有序列化过
     */
    @Bean
    public RedisTemplate<String,Serializable> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory)
    {
        RedisTemplate<String,Serializable> redisTemplate = new RedisTemplate<>();

        redisTemplate.setConnectionFactory(lettuceConnectionFactory);
        //设置key序列化方式string
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        //设置value的序列化方式json
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());

        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());

        redisTemplate.afterPropertiesSet();

        return redisTemplate;
    }
}
  1. 实体类
@Data
@ApiModel(value = "活roducet信息")
public class Product {

    private Long id;
    /**
     * 产品名称
     */
    private String name;
    /**
     * 产品价格
     */
    private Integer price;
    /**
     * 产品详情
     */
    private String detail;

    public Product() {
    }

    public Product(Long id, String name, Integer price, String detail) {
        this.id = id;
        this.name = name;
        this.price = price;
        this.detail = detail;
    }
}
  1. 常量类
public class Constants {

    public  static final String HD_KEY="hd";

    public  static final String HD_KEY_A="hd:a";

    public  static final String HD_KEY_B="hd:b";

}
 
  1. 采用定时器将参与活动的特价商品新增进入redis中
@Service
@Slf4j
public class HDTaskService
{
    @Autowired
    private RedisTemplate redisTemplate;

    @PostConstruct
    public void initHD(){
        log.info("启动定时器.........."+DateUtil.now());
        new Thread(() -> {
            //模拟定时器,定时把数据库的特价商品,刷新到redis中
            while (true){
                //模拟从数据库读取100件特价商品,用于加载到页面中
                List<Product> list=this.products();
                //采用redis list数据结构的lpush来实现存储
                this.redisTemplate.delete(Constants.HD_KEY);
                //lpush命令
                this.redisTemplate.opsForList().leftPushAll(Constants.HD_KEY,list);
                //间隔一分钟 执行一遍
                try { TimeUnit.MINUTES.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }

                log.info("runHD定时刷新..............");
            }
        },"t1").start();
    }

    /**
     * 模拟从数据库读取100件特价商品,用于加载到的页面中
     */
    public List<Product> products() {
        List<Product> list=new ArrayList<>();
        for (int i = 1; i <=20; i++) {
            Random rand = new Random();
            int id= rand.nextInt(10000);
            Product obj=new Product((long) id,"product"+i,i,"detail");
            list.add(obj);
        }
        return list;
    }
}
  1. controller
@RestController
@Slf4j
@Api(description = "商品列表接口")
public class HDProductController
{
    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 分页查询:在高并发的情况下,只能走redis查询,走db的话必定会把db打垮
     * http://localhost:5555/swagger-ui.html#/hd-product-controller/findUsingGET
     */
    @RequestMapping(value = "/pruduct/find",method = RequestMethod.GET)
    @ApiOperation("按照分页和每页显示容量,点击查看")
    public List<Product> find(int page, int size) {
        List<Product> list=null;
        long start = (page - 1) * size;
        long end = start + size - 1;
        try {
            //采用redis list数据结构的lrange命令实现分页查询
            list = this.redisTemplate.opsForList().range(Constants.HD_KEY, start, end);
            if (CollectionUtils.isEmpty(list)) {
                //TODO 走DB查询
            }
            log.info("查询结果:{}", list);
        } catch (Exception ex) {
            //这里的异常,一般是redis瘫痪 ,或 redis网络timeout
            log.error("exception:", ex);
            //TODO 走DB查询
        }
        return list;
    }

}

4.3.4.3 至此步骤,上述功能算是完成,请思考在高并发下有什么经典生产问题?

QPS上1000后导致可怕的缓存击穿 image.png

image.png

4.3.4.4 升级加固案例

定时轮询+互斥更新+差异失效时间 我们设置两个缓存A和B,主A从B,都来存放数据。

有几个要点

  1. 主缓存A后更新,从缓存B先更新
  2. 先查询主缓存A,再查询从缓存B
  3. 主缓存A的key的过期时间设置的要比从缓存B的key的过期时间要短

image.png

service层

@Service
@Slf4j
public class HDABTaskService
{
    @Autowired
    private RedisTemplate redisTemplate;

    @PostConstruct
    public void initHDAB(){
        log.info("启动AB定时器.........."+DateUtil.now());
        new Thread(() -> {
            //模拟定时器,定时把数据库的特价商品,刷新到redis中
            while (true){
                //模拟从数据库读取100件特价商品,用于加载到页面中
                List<Product> list=this.products();
                //先更新B缓存
                this.redisTemplate.delete(Constants.HD_KEY_B);
                this.redisTemplate.opsForList().leftPushAll(Constants.HD_KEY_B,list);
                this.redisTemplate.expire(Constants.HD_KEY_B,20L,TimeUnit.DAYS);
                //再更新A缓存
                this.redisTemplate.delete(Constants.HD_KEY_A);
                this.redisTemplate.opsForList().leftPushAll(Constants.HD_KEY_A,list);
                this.redisTemplate.expire(Constants.HD_KEY_A,15L,TimeUnit.DAYS);
                //间隔一分钟 执行一遍
                try { TimeUnit.MINUTES.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }

                log.info("runHD定时刷新..............");
            }
        },"t1").start();
    }

    /**
     * 模拟从数据库读取100件特价商品,用于加载到页面中
     */
    public List<Product> products() {
        List<Product> list=new ArrayList<>();
        for (int i = 1; i <=20; i++) {
            Random rand = new Random();
            int id= rand.nextInt(10000);
            Product obj=new Product((long) id,"product"+i,i,"detail");
            list.add(obj);
        }
        return list;
    }
}

controller层

@RestController
@Slf4j
@Api(description = "商品列表接口AB")
public class HDABProductController
{
    @Autowired
    private RedisTemplate redisTemplate;

    @RequestMapping(value = "/pruduct/findab",method = RequestMethod.GET)
    @ApiOperation("按照分页和每页显示容量,点击查看AB")
    public List<Product> findAB(int page, int size) {
        List<Product> list=null;
        long start = (page - 1) * size;
        long end = start + size - 1;
        try {
            //采用redis list数据结构的lrange命令实现分页查询
            list = this.redisTemplate.opsForList().range(Constants.HD_KEY_A, start, end);
            if (CollectionUtils.isEmpty(list)) {
                log.info("=========A缓存已经失效了,记得人工修补,B缓存自动延续5天");
                //用户先查询缓存A(上面的代码),如果缓存A查询不到(例如,更新缓存的时候删除了),再查询缓存B
                this.redisTemplate.opsForList().range(Constants.HD_KEY_B, start, end);
            }
            log.info("查询结果:{}", list);
        } catch (Exception ex) {
            //这里的异常,一般是redis瘫痪 ,或 redis网络timeout
            log.error("exception:", ex);
            //TODO 走DB查询
        }
        return list;
    }

}

5. 总结

image.png

至此缓存预热、雪崩、击穿、穿透原理与解决方案已全部讲完,有关缓存更新方式,缓存不一致,我放在下面讲缓存双写一致性的文章里。

希望对小伙伴有帮助的,不忘点赞加关注!!