Redis7之缓存预热 + 缓存雪崩 + 缓存击穿 + 缓存穿透(八)

8.1 缓存预热

8.1.1 是什么

缓存预热就是系统上线后,提前将相关的缓存数据直接加载到缓存系统。避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!

8.1.2 解决

使用 @PostConstruct 初始化白名单数据

8.2 缓存雪崩

8.2.1 是什么

缓存雪崩就是瞬间过期数据量太大,导致对数据库服务器造成压力。

8.2.2 发生

  • redis 主机挂了, Redis全盘崩溃,偏硬件运维
  • redis 中有大量key 同时过期大面积失效,偏软件开发

8.2.3 预防 + 解决

  • redis 中 key 设置为永不过期 or 过期时间错开
  • redis 缓存集群实现高可用
    • 主从 + 哨兵
    • Redis 集群
    • 开启Redis 持久化机制 aof / rdb,尽快恢复缓存集群
  • 多缓存结合预防雪崩
    • ehcache 本地缓存 + redis缓存
  • 服务降级
    • Hystrix 或者 sentinel 限流 & 降级

8.3 缓存穿透

8.3.1 是什么

缓存穿透 就是请求去查询一条数据,先查redis,redis里面没有,再查mysql,mysql里面无,都查询不到该条记录,但是请求每次都会打到数据库上面去,导致后台数据库压力暴增

8.3.2 解决

在这里插入图片描述

Redis7之缓存预热 + 缓存雪崩 + 缓存击穿 + 缓存穿透(八)_第1张图片

1 空对象缓存或者缺省值

如果发生缓存穿透,可以针对要查询的数据,在Redis里存一个和业务部门商量后确定的缺省值(比如 零、负数、defaultNull等)

    public Customer findCustomerById(Integer customerId) {
        Customer customer = null;
        // 缓存redis的key名称
        String key = CACHE_KEY_CUSTOMER + customerId;
        // 1.去redis上查询
        customer = (Customer) redisTemplate.opsForValue().get(key);

        // 2. 如果redis有,直接返回  如果redis没有,在mysql上查询
        if (customer == null) {
            // 3.对于高QPS的优化,进来就先加锁,保证一个请求操作,让外面的redis等待一下,避免击穿mysql(大公司的操作 )
            synchronized (CustomerService.class) {
                // 3.1 第二次查询redis,加锁后
                customer = (Customer) redisTemplate.opsForValue().get(key);
                // 4.再去查询我们的mysql
                customer = customerMapper.selectByPrimaryKey(customerId);

                // 5.mysql有,redis无
                if (customer != null) {
                    // 6.把mysql查询到的数据会写到到redis, 保持双写一致性  7天过期
                    redisTemplate.opsForValue().set(key, customer, 7L, TimeUnit.DAYS);
                }else {
                    // defaultNull 规定为redis查询为空、MySQL查询也没有,缓存一个defaultNull标识为空,以防缓存穿透
                    redisTemplate.opsForValue().set(key, "defaultNull", 7L, TimeUnit.DAYS);
                }
            }
        }
        return customer;
    }

2 Google布隆过滤器Guava

案例:白名单过滤器

  • POM
        
        
            com.google.guava
            guava
            23.0
        
  • 业务类

    • GUavaBloomFilterController
    import com.xfcy.service.GuavaBloomFilterService;
    import io.swagger.annotations.Api;
    import io.swagger.annotations.ApiOperation;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    import org.springframework.web.bind.annotation.RestController;
    
    import javax.annotation.Resource;
    
    /**
     * @author 晓风残月Lx
     * @date 2023/3/29 19:06
     * guava版的布隆过滤器  谷歌开源
     */
    @Api(tags = "gogle工具Guava处理布隆过滤器")
    @RestController
    @Slf4j
    public class GuavaBloomFilterController {
    
        @Resource
        private GuavaBloomFilterService guavaBloomFilterService;
    
        @ApiOperation("guava布隆过滤器插入100万样本数据,额外10w(110w)测试是否存在")
        @RequestMapping(value = "/guavafilter", method = RequestMethod.GET)
        public void guavaBloomFilter() {
            guavaBloomFilterService.guavaBloomFilter();
        }
    }
    
    • GUavaBloomFilterService

      import com.google.common.hash.BloomFilter;
      import com.google.common.hash.Funnels;
      import lombok.extern.slf4j.Slf4j;
      import org.springframework.stereotype.Service;
      
      import java.util.ArrayList;
      
      /**
       * @author 晓风残月Lx
       * @date 2023/3/29 19:17
       */
      @Slf4j
      @Service
      public class GuavaBloomFilterService {
          // 1.定义一个常量
          public static final int _1W = 10000;
          // 2.定义我们guava布隆过滤器,初始容量
          public static final int SIZE = 100 * _1W;
          // 3.误判率,它越小误判的个数也越少(思考:是否可以无限小? 没有误判岂不是更好)
          public static double fpp = 0.01;  // 这个数越小所用的hash函数越多,bitmap占用的位越多  默认的就是0.03,5个hash函数   0.01,7个函数
          // 4.创建guava布隆过滤器
          private static BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), SIZE, fpp);
      
          public void guavaBloomFilter() {
              // 1.先让bloomFilter加入100w白名单数据
              for (int i = 0; i < SIZE; i++) {
                  bloomFilter.put(i);
              }
              // 2.故意取10w个不在合法范围内的数据,来进行误判率的演示
              ArrayList<Integer> list = new ArrayList<>(10 * _1W);
      
              // 3.验证
              for (int i = SIZE + 1; i < SIZE + (10 * _1W); i++){
                  if (bloomFilter.mightContain(i)){
                      log.info("被误判了:{}", i);
                      list.add(i);
                  }
              }
              log.info("误判总数量:{}", list.size());
          }
      }
      

    Redis7之缓存预热 + 缓存雪崩 + 缓存击穿 + 缓存穿透(八)_第2张图片

8.4 缓存击穿

8.4.1 是什么

缓存击穿就是大量请求同时查询一个key时,此时这个key正好失效了,就会导致大量的请求都打到数据库上面去,也就是热点key突然都失效了,MySQL承受高并发量

8.4.2 解决

在这里插入图片描述

1.差异失效时间,对于访问频繁的热点key,干脆就不设置过期时间

2.互斥更新,采用双检加锁

8.4.3 案例编码(防止缓存击穿)

对于分页显示数据,在高并发下,绝对不能使用mysql,可以用redis的list结构

差异失效时间 用在双缓存架构

Redis7之缓存预热 + 缓存雪崩 + 缓存击穿 + 缓存穿透(八)_第3张图片

  • Product类

    import io.swagger.annotations.ApiModel;
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    
    /**
     * @author 晓风残月Lx
     * @date 2023/3/30 9:40
     */
    @ApiModel(value = "聚划算活动product信息")
    @AllArgsConstructor
    @NoArgsConstructor
    @Data
    public class Product {
        // 产品id
        private Long id;
        // 产品名称
        private String name;
        // 产品价格
        private Integer price;
        // 产品详情
        private String detail;
    }
    
  • JHSTaskService(采用定时器将参加活动的商品加入redis)

    import com.xfcy.entities.Product;
    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;
    
    /**
     * @author 晓风残月Lx
     * @date 2023/3/30 9:43
     */
    @Service
    @Slf4j
    public class JHSTaskService {
    
        public static final String JHS_KEY = "jhs";
        public static final String JHS_KEY_A = "jhs:a";
        public static final String JHS_KEY_B = "jhs:b";
    
        @Autowired
        private RedisTemplate redisTemplate;
    
        /**
         * 模拟从数据库读取20件特价商品
         * @return
         */
        private List<Product> getProductsFromMysql() {
            List<Product> list = new ArrayList<>();
            for (int i = 0; i <= 20; i++) {
                Random random = new Random();
                int id = random.nextInt(10000);
                Product product = new Product((long) id, "product" + i, i, "detail");
                list.add(product);
            }
            return list;
        }
    
        //@PostConstruct    // 测试单缓存
        public void initJHS(){
            log.info("启动定时器 天猫聚划算模拟开始 ===============");
    
            // 1.用线程模拟定时任务,后台任务定时将mysql里面的特价商品刷新的redis
            new Thread(() -> {
                while (true){
                    // 2.模拟从mysql查到数据,加到redis并返回给页面
                    List<Product> list = this.getProductsFromMysql();
                    // 3.采用redis list数据结构的lpush命令来实现存储
                    redisTemplate.delete(JHS_KEY);
                    // 4.加入最新的数据给redis
                    redisTemplate.opsForList().leftPushAll(JHS_KEY, list);
                    // 5.暂停1分钟,间隔1分钟执行一次,模拟聚划算一天执行的参加活动的品牌
                    try {
                        TimeUnit.MINUTES.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            },"t1").start();
    
        }
    
        /**
         * 差异失效时间
         */
        @PostConstruct         // 测试双缓存
        public void initJHSAB(){
            log.info("启动AB的定时器 天猫聚划算模拟开始 ===============");
            // 1.用线程模拟定时任务,后台任务定时将mysql里面的特价商品刷新的redis
            new Thread(() -> {
                while (true){
                    // 2.模拟从mysql查到数据,加到redis并返回给页面
                    List<Product> list = this.getProductsFromMysql();
    
                    // 3.先更新B缓存且让B缓存过期时间超过缓存A时间,如果A突然失效了还有B兜底,防止击穿
                    redisTemplate.delete(JHS_KEY_B);
                    redisTemplate.opsForList().leftPushAll(JHS_KEY_B, list);
                    redisTemplate.expire(JHS_KEY_B, 86410L, TimeUnit.SECONDS);
                    // 4.再更新A缓存
                    redisTemplate.delete(JHS_KEY_A);
                    redisTemplate.opsForList().leftPushAll(JHS_KEY_A, list);
                    redisTemplate.expire(JHS_KEY_A, 86400L, TimeUnit.SECONDS);
    
                    // 5.暂停1分钟,间隔1分钟执行一次,模拟聚划算一天执行的参加活动的品牌
                    try {
                        TimeUnit.MINUTES.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            },"t1").start();
        }
    }
    
  • JHSProductController类

    import com.xfcy.entities.Product;
    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;
    
    /**
     * @author 晓风残月Lx
     * @date 2023/3/30 9:55
     */
    @RestController
    @Slf4j
    @Api(tags = "聚划算商品列表接口")
    public class JHSProductController {
    
        public static final String JHS_KEY = "jhs";
        public static final String JHS_KEY_A = "jhs:a";
        public static final String JHS_KEY_B = "jhs:b";
    
        @Autowired
        private RedisTemplate redisTemplate;
    
        /**
         * 分页查询:在高并发情况下,只能走redis查询,走db必定会把db打垮
         * @param page
         * @param size
         * @return
         */
        @RequestMapping(value = "/product/find", method = RequestMethod.GET)
        @ApiOperation("聚划算案例,每次1页每页5条显示")
        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结构里面的range命令来实现加载和分页
                list = redisTemplate.opsForList().range(JHS_KEY, start, end);
                if (CollectionUtils.isEmpty(list)) {
                    // TODO 走mysql查询
    
                }
                log.info("参加活动的商家: {}", list);
            }catch (Exception e){
                // 出异常了,一般redis宕机了或者redis网络抖动导致timeout
                log.error("jhs  exception{}", e);
                e.printStackTrace();
                // 。。。重试机制 再次查询mysql
    
            }
            return list;
        }
    
    
        @RequestMapping(value = "/product/findAB", method = RequestMethod.GET)
        @ApiOperation("AB双缓存架构,防止热点key突然消失")
        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结构里面的range命令来实现加载和分页
                list = redisTemplate.opsForList().range(JHS_KEY_A, start, end);
                if (CollectionUtils.isEmpty(list)) {
                    log.info("-----A缓存已经过期或活动结束了,记得人工修补,B缓存继续顶着");
    
                    // A没有来找B
                    list = redisTemplate.opsForList().range(JHS_KEY_B, start, end);
    
                    if (CollectionUtils.isEmpty(list)){
                        // TODO 走mysql查询
                    }
                }
                log.info("参加活动的商家: {}", list);
            }catch (Exception e){
                // 出异常了,一般redis宕机了或者redis网络抖动导致timeout
                log.error("jhs  exception{}", e);
                e.printStackTrace();
                // 。。。重试机制 再次查询mysql
            }
            return list;
        }
    }
    
    

8.5 总结

缓存问题 产生原因 解决方案
缓存更新不一致 数据变更、缓存时效性 同步更新、失效更新、异步更新、定时更新
缓存不一致 同步更新失败、异步更新 增加重试、补偿任务、最终一致
缓存穿透 恶意攻击 空对象缓存、bloomFilter 过滤器
缓存击穿 热点key失效 互斥更新、随即退避、差异失效时间
缓存雪崩 缓存挂掉 快速失败熔断、主从模式、集群模式

你可能感兴趣的:(redis,缓存,数据库,redis)