MENU

Redis中关于缓存预热 + 缓存雪崩 + 缓存击穿 + 缓存穿透

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

缓存预热

什么是预热?

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

解决方法

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

缓存雪崩

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

发生
  • redis 主机挂了, Redis全盘崩溃,偏硬件运维
  • redis 中有大量key 同时过期大面积失效,偏软件开发
如何预防
  • redis中key设置为永久不过期or过期时间错开
  • redis缓存集群实现高可用
  1. 主从 + 哨兵
  2. Redis 集群
  3. 开启Redis 持久化机制 aof / rdb,尽快恢复缓存集群
  • 多缓存结合预防雪崩
ehcache 本地缓存 + redis缓存
  • 服务降级
Hystrix或者阿里sentinel限流&降级

缓存穿透

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

image-20230613182702573

image-20230613170008933

解决方案

空对象缓存或者缺省值 ---但是还是防不住黑客恶意攻击

第一种解决方案,回写增强
如果发生了缓存穿透,我们可以针对要查询的数据,在Redis里存一个和业务部门商量后确定的缺省值(比如,零、负数、defaultNull等)。比如,键uid:abcdxxx,值defaultNull作为案例的key和value,先去redis查键uid:abcdxxx没有,再去mysql查没有获得,这就发生了一次穿透现象
but,可以增强回写机制mysql也查不到的话也让redis存入刚刚查不到的key并保护mysql。
第一次来查询uid:abcdxx,redis和mysql都没有,返回null给调用者,但是增强回写后第二次来查uid:abcdxxx,此时redis就有值了。可以直接从Redis中读取default缺省值返回给业务应用程序,避免了把大量请求发送给mysql处理,打爆mysql。
但是,此方法架不住黑客的恶意攻击,有缺陷.....,只能解决key相同的情况

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

白名单过滤器---架构说明
image-20230613171259588
误判问题,但是概率小可以接受,不能从布隆过滤器删除
全部合法的key都需要放入Guava版布隆过滤器+redis里面,不然数据就是返回null
案例
POM

<!--guawa Goole 开源的Guava 中自带的布隆过滤器-->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>23.0</version>
</dependency>

入门演示讲解

@Test
public void testGuavawithBloomFilter() {
       // 1创建guava版布隆过滤器
BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunneL()
expectedlnsertions:100);
       // 2判断指定的元素是否存徘
system.out.println(bloomFilter.mightcontain(object:1));
system.out.println(bloomFilter.mightcontain(object:2));
system.out.println();
       //3 讲元素新增进入bLoomfilterbloomFilter.put(object:1);
bloomFilter.put(object:2);
system.out.println(bloomFilter.mightcontain(object:1));
system.out.println(bloomFilter.mightcontain(object:2));
}

真实案例实践---取样本100w数据,查查不在100W范围内,其它10w数据是否存在
业务类

  • GuavaBloomFilterController
package cn.zzrg.redis7.controller;
import cn.zzrg.redis7.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 ZzRG
* @Date 2023/6/13 17:18
* @Version 1.0
*/
@Api(tags = "google工具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
package cn.zzrg.redis7.service;
/**
* @Author ZzRG
* @Date 2023/6/13 17:21
* @Version 1.0
*/
public interface GuavaBloomFilterService{
void guavaBloomFilter();
}
  • GuavaBloomFilterServiceImpl
package cn.zzrg.redis7.service.impl;
import cn.zzrg.redis7.service.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 ZzRG
* @Date 2023/6/13 17:22
* @Version 1.0
*/
@Service
@Slf4j
public class GuavaBloomFilterServiceImpl implements 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);
@Override
public void guavaBloomFilter() {
   // 1 先bloomFilter加入100W白名单那数据
   for (int i = 1; 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++) {
       //由于布隆过滤器可能存在一定的误判,当调用 mightContain 方法时:
       //如果返回 true ,则此元素可能存在过滤器中,实际生产中可能需要根据具体业务进一步判断;
       //如果返回 false ,则此元素一定不在过滤器中
       if (bloomFilter.mightContain(i)){
           //我们用的10W样本是在白名单外面的数据!所以产生了误判。
           log.info("被误判了:{}",i);
           list.add(i);
       }
       log.info("误判总数量:{}",list.size());
   }
}
}

image-20230613180515717

黑名单过滤器

image-20230613181020156

缓存击穿

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

危害

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

一般技术部门需要知道热点key是那些个?做到心里有数防止击穿。

解决

image-20230613212633860

热点key失效
时间到了自然清除但还被访问到
delete掉的key,刚巧又被访问
解决方案

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

方案2:互斥跟新,采用双检加锁策略

案例

天猫聚划算功能实现+防止缓存击穿

  • entities--Product
package cn.zzrg.redis7.entities;
import io.swagger.annotations.ApiModel;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @Author ZzRG
* @Date 2023/6/13 21:46
* @Version 1.0
*/
@ApiModel(value = "聚划算活动product信息")
@AllArgsConstructor
@NoArgsConstructor
@Data
public class Product {
  // 产品id
  private Long id;
  // 产品名称
  private String name;
  // 产品价格
  private Integer price;
  // 产品详情
  private String detail;
}
  • JHSTaskService
  • JHSTaskServiceImpl(采用定时器将参加活动的商品加入redis)----这样写3/4步骤时会出现热点key失效造成缓存击穿
package cn.zzrg.redis7.service.impl;
import cn.zzrg.redis7.entities.Product;
import cn.zzrg.redis7.service.JHSTaskService;
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 ZzRG
* @Date 2023/6/13 21:48
* @Version 1.0
*/
@Service
@Slf4j
public class JHSTaskServiceImpl implements 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 //@PostConstruct是Java自带的注解,在方法上加该注解会在项目启动的时候执行该方法,也可以理解为在spring容器初始化的时候执行该方法。
  public void initJHS(){
      log.info("启动定时器 天猫聚划算模拟开始");
      //1 用多线程模拟定时任务, 后台任务定时将mysql里面的参加活动的商品刷新到redis里
      // new Thread(new Runnable() {
      //     @Override
      //     public void run() {
      //     }
      // },"t1").start();
      //lamda 表达方法
      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秒钟线程,间隔一分钟执行一次,模拟聚划算一天执行的参加活动的品牌
              try {
                  TimeUnit.MINUTES.sleep(1);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
          }
      },"t1").start();
  }
}

解决缓存上述缓存击穿采用 两套缓存

image-20230613222916380

package cn.zzrg.redis7.service.impl;
import cn.zzrg.redis7.entities.Product;
import cn.zzrg.redis7.service.JHSTaskService;
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 ZzRG
* @Date 2023/6/13 21:48
* @Version 1.0
*/
@Service
@Slf4j
public class JHSTaskServiceImpl implements 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         // 测试双缓存 //@PostConstruct是Java自带的注解,在方法上加该注解会在项目启动的时候执行该方法,也可以理解为在spring容器初始化的时候执行该方法。
  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
package cn.zzrg.redis7.controller;
import cn.zzrg.redis7.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 ZzRG
* @Date 2023/6/13 22:02
* @Version 1.0
*/
@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;
  }
    /**
   * AB双层缓存
   * @param page
   * @param size
   * @return
   */
  @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;
  }
}

总结

image-20230613225437409

Leave a Comment