05 缓存预热+缓存雪崩+缓存击穿+缓存穿透

缓存血崩

发生

  • redis主机挂了,Redis 全盘崩溃
  • 比如缓存中有大量数据同时过期

解决

在这里插入图片描述

  • redis缓存集群实现高可用
    • 主从+哨兵
    • Redis Cluster
  • ehcache本地缓存 + Hystrix或者阿里sentinel限流&降级
  • 开启Redis持久化机制aof/rdb,尽快恢复缓存集群

缓存穿透

  • 是什么

    • 请求去查询一条记录,先redis后mysql发现都查询不到该条记录,但是请求每次都会打到数据库上面去,导致后台数据库压力暴增,这种现象我们称为缓存穿透,这个redis变成了一个摆设。。。。。。
    • 简单说就是本来无一物,既不在Redis缓存中,也不在数据库中
  • 危害

    • 第一次来查询后,一般我们有回写redis机制
    • 第二次来查的时候redis就有了,偶尔出现穿透现象一般情况无关紧要

解决

方案1:空对象缓存或者缺省值

  • 在这里插入图片描述
  • 黑客或者恶意攻击
    • 黑客会对你的系统进行攻击,拿一个不存在的id 去查询数据,会产生大量的请求到数据库去查询。可能会导致你的数据库由于压力过大而宕掉
    • id相同打你系统:第一次打到mysql,空对象缓存后第二次就返回null了,避免mysql被攻击,不用再到数据库中去走一圈了
    • id不同打你系统:由于存在空对象缓存和缓存回写(看自己业务不限死),redis中的无关紧要的key也会越写越多(记得设置redis过期时间)

方案2:Google布隆过滤器Guava解决缓存穿透

  • Guava 中布隆过滤器的实现算是比较权威的,所以实际项目中我们不需要手动实现一个布隆过滤器
  • Guava’s BloomFilter 源码剖析
  • Coding实战
    • 建Module:bloomfilter_demo

    • 改POM

      <?xml version="1.0" encoding="UTF-8"?>
      <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
               xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
          <modelVersion>4.0.0</modelVersion>
      
          <parent>
              <groupId>org.springframework.boot</groupId>
              <artifactId>spring-boot-starter-parent</artifactId>
              <version>2.3.10.RELEASE</version>
              <relativePath/> <!-- lookup parent from repository -->
          </parent>
      
          <groupId>com.atguigu.redis.bloomfilter</groupId>
          <artifactId>bloomfilter_demo</artifactId>
          <version>0.0.1-SNAPSHOT</version>
      
      
          <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>
              <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>
      
    • 写YML

      server.port=6666
      # ========================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/db2021?useUnicode=true&characterEncoding=utf-8&useSSL=false
      spring.datasource.username=root
      spring.datasource.password=123456
      spring.datasource.druid.test-while-idle=false
      
      # ========================redis相关配置=====================
      # Redis数据库索引(默认为0)
      spring.redis.database=0
      # Redis服务器地址
      spring.redis.host=192.168.111.147
      # Redis服务器连接端口
      spring.redis.port=6379
      # 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
      
    • 主启动

    • 业务类

      package com.learn.bloomfilter;
      
      import com.google.common.hash.BloomFilter;
      import com.google.common.hash.Funnels;
      
      import java.util.ArrayList;
      import java.util.List;
      
      /**
       * @author YSK
       * @since 2023/5/31 14:25
       */
      public class GuavaBloomFilterDemo {
              
              
          public static final int _1W = 10000;
          //布隆过滤器里预计要插入多少数据
          public static int size = 100 * _1W;
          //误判率,它越小误判的个数也就越少(思考,是不是可以设置的无限小,没有误判岂不更好)
          //默认是0.03
          public static double fpp = 0.01;
      
          /**
           * helloworld入门
           */
          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));
      
          }
      
          /**
           * 误判率演示+源码分析
           */
          public void bloomFilter2() {
              
              
              // 构建布隆过滤器
              BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), size, fpp);
      
              //1 先往布隆过滤器里面插入100万的样本数据
              for (int i = 0; i < size; i++) {
              
              
                  bloomFilter.put(i);
              }
             /* List<Integer> listSample = new ArrayList<>(size);
              //2 这100万的样本数据,是否都在布隆过滤器里面存在?
              for (int i = 0; i < size; i++)
              {
                  if (bloomFilter.mightContain(i)) {
                      listSample.add(i);
                      continue;
                  }
              }
              System.out.println("存在的数量:" + listSample.size());*/
      
              //3 故意取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());
          }
      
          public static void main(String[] args) {
              
              
              new GuavaBloomFilterDemo().bloomFilter();
          }
      }
      
  • 布隆过滤器说明
  • 在这里插入图片描述

方案3:Redis布隆过滤器解决缓存穿透

  • Guava缺点说明:Guava 提供的布隆过滤器的实现还是很不错的 (想要详细了解的可以看一下它的源码实现),但是它有一个重大的缺陷就是只能单机使用 ,而现在互联网一般都是分布式的场景。
案例:白名单过滤器
  • 白名单架构说明
  • 在这里插入图片描述
  • 误判问题,但是概率小可以接受,不能从布隆过滤器删除
  • 全部合法的key都需要放入过滤器+redis里面,不然数据就是返回null
  • code
package com.learn.bloomfilter;

import org.redisson.Redisson;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RBucket;
import org.redisson.api.RedissonClient;
import org.redisson.client.codec.StringCodec;
import org.redisson.config.Config;
import org.springframework.data.redis.core.RedisTemplate;

import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;

/**
 * @author YSK
 * @since 2023/5/31 15:48
 */
public class RedissonBloomFilterDemo {
    
    
    public static final int _1W = 10000;

    //布隆过滤器里预计要插入多少数据
    public static int size = 100 * _1W;
    //误判率,它越小误判的个数也就越少
    public static double fpp = 0.03;

    static RedissonClient redissonClient = null;//jedis
    static RBloomFilter rBloomFilter = null;//redis版内置的布隆过滤器

    @Resource
    RedisTemplate redisTemplate;


    static {
    
    
        Config config = new Config();
        config.useSingleServer().setAddress("redis://192.168.111.147:6379").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");

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

        //3 测试 ,布隆过滤器无+redis无

    }

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


    public static void main(String[] args) {
    
    
        //String phoneListById = getPhoneListById("10086");
        //String phoneListById = getPhoneListById("10087"); //请测试执行2次
        String phoneListById = getPhoneListById("10088");
        System.out.println("------查询出来的结果: " + phoneListById);

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

  • 总结
  • 在这里插入图片描述

缓存击穿

是什么

  • 大量的请求同时查询一个 key 时,此时这个key正好失效了,就会导致大量的请求都打到数据库上面去
  • 简单说就是热点key突然失效了,暴打mysql
  • 危害:会造成某一时刻数据库请求量过大,压力剧增。

解决

  • 对于访问频繁的热点key,干脆就不设置过期时间
  • 互斥独占锁防止击穿
    • 多个线程同时去查询数据库的这条数据,那么我们可以在第一个查询数据的请求上使用一个 互斥锁来锁住它。
    • 其他的线程走到这一步拿不到锁就等着,等第一个线程查询到了数据,然后做缓存。后面的线程进来发现已经有缓存了,就直接走缓存。
    • 在这里插入图片描述

高并发的淘宝聚划算案例落地

分析过程

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

redis数据类型选型

在这里插入图片描述

springboot+redis实现高并发的淘宝聚划算业务

 
package com.atguigu.redis.service;

import cn.hutool.core.date.DateUtil;
import com.atguigu.redis.entities.Product;
import com.atguigu.redis.util.Constants;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeUnit;

/**
 * @auther zzyy
 * @create 2021-05-09 14:47
 */
@Service
@Slf4j
public class JHSTaskService
{
    
    
    @Autowired
    private RedisTemplate redisTemplate;

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

                log.info("runJhs定时刷新..............");
            }
        },"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;
    }
}
 
package com.atguigu.redis.controller;

import com.atguigu.redis.entities.Product;
import com.atguigu.redis.util.Constants;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

/**
 * @auther zzyy
 * @create 2021-05-09 14:56
 */
@RestController
@Slf4j
@Api(description = "聚划算商品列表接口")
public class JHSProductController
{
    
    
    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 分页查询:在高并发的情况下,只能走redis查询,走db的话必定会把db打垮
     * http://localhost:5555/swagger-ui.html#/jhs-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.JHS_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;
    }

}

Bug和隐患说明

  • QPS上1000后导致可怕的缓存击穿
  • 在这里插入图片描述
  • 在这里插入图片描述

进一步升级加固案例

  • 定时轮询,互斥更新,差异失效时间
  • 在这里插入图片描述
 
package com.atguigu.redis.service;

import cn.hutool.core.date.DateUtil;
import com.atguigu.redis.entities.Product;
import com.atguigu.redis.util.Constants;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeUnit;

/**
 * @auther zzyy
 * @create 2021-05-09 15:54
 */
@Service
@Slf4j
public class JHSABTaskService
{
    
    
    @Autowired
    private RedisTemplate redisTemplate;

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

                log.info("runJhs定时刷新..............");
            }
        },"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;
    }
}
 
package com.atguigu.redis.controller;

import com.atguigu.redis.entities.Product;
import com.atguigu.redis.util.Constants;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

/**
 * @auther zzyy
 * @create 2021-05-09 15:58
 */
@RestController
@Slf4j
@Api(description = "聚划算商品列表接口AB")
public class JHSABProductController
{
    
    
    @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.JHS_KEY_A, start, end);
            if (CollectionUtils.isEmpty(list)) {
    
    
                log.info("=========A缓存已经失效了,记得人工修补,B缓存自动延续5天");
                //用户先查询缓存A(上面的代码),如果缓存A查询不到(例如,更新缓存的时候删除了),再查询缓存B
                this.redisTemplate.opsForList().range(Constants.JHS_KEY_B, start, end);
            }
            log.info("查询结果:{}", list);
        } catch (Exception ex) {
    
    
            //这里的异常,一般是redis瘫痪 ,或 redis网络timeout
            log.error("exception:", ex);
            //TODO 走DB查询
        }
        return list;
    }

}

总结

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/m0_56709616/article/details/130967530