什么是缓存穿透、缓存雪崩、缓存击穿?

打印 上一主题 下一主题

主题 1789|帖子 1789|积分 5367

 什么是缓存?

缓存就是数据互换的缓冲区,是存贮数据的暂时地方,一样平常读写性能较高。
怎么防止缓存穿透?

缓存穿透是指客户端哀求的数据在缓存中和数据库中都不存在,这样缓存永久不会生效,这些哀求都会打到数据库,给数据库带来巨大压力。
常见的办理方案有两种:
缓存空对象
客户端第一次哀求是,发现数据库中数据不存在,在缓存中设置该值为空串,后面的哀求中若发现缓存中存储的值为空串直接返回空串,不在查询数据库。(缓存须要设置肯定时间内过期,防止数据库中有数据后缓存仍旧为空。或者在插入数据时,清空缓存)


  •         长处:实现简单,维护方便
  •         缺点:额外的内存斲丧,可能造成短期的不一致

  1.     /**
  2.      * 设置空值解决缓存穿透
  3.      */
  4.     public Shop queryWithPassThrough(Long id){
  5.         //先从redis查询商铺缓存,若存在,从redis中返回,否则查询数据库,存在写入redis,并返回
  6.         String shopJson = stringRedisTemplate.opsForValue().get("cache:shop:" + id);
  7.         if(StringUtils.isNotBlank(shopJson)){
  8.             Shop shop = JSONUtil.toBean(shopJson, Shop.class);
  9.             return shop;
  10.         }
  11.         //判断命中的是否为空,防止缓存穿透
  12.         if(shopJson==null){ //若为null,说明redis中数据为空字符串,说明mysql数据库也没有数据
  13.             return null;
  14.         }
  15.         //redis不存在,查询数据库
  16.         Shop shop = getById(id);
  17.         if(shop==null){
  18.             //将空值写入redis
  19.             stringRedisTemplate.opsForValue().set("cache:shop:"+id,"",30, TimeUnit.MINUTES);
  20.             return null;
  21.         }
  22.         stringRedisTemplate.opsForValue().set("cache:shop:"+id,JSONUtil.toJsonStr(shop));
  23.         return shop;
  24.     }
复制代码

布隆过滤


  •      长处:内存占用较少,没有多余key
  •      缺点:实现复杂、存在误判可能
其他方案:
        •增强id的复杂度,制止被推测id规律
        •做好数据的基础格式校验
        •加强用户权限校验
        •做好热点参数的限流
为什么会出现缓存雪崩?

缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量哀求到达数据库,带来巨大压力。

办理方案:


  • 给不同的Key的TTL添加随机值
  • 使用Redis集群进步服务的可用性
  • 给缓存业务添加降级限流计谋
  • 给业务添加多级缓存
如何办理缓存击穿?

缓存击穿标题也叫热点Key标题,就是一个被高并发访问而且缓存重修业务较复杂的key突然失效了,无数的哀求访问会在瞬间给数据库带来巨大的冲击。
常见的办理方案有两种:
互斥锁
 在高并发情况下,当有一个线程得到锁访问数据库时,其他线程等待。假如业务A须要获取缓存A和缓存B,而业务B须要获取缓存B和缓存A。此时业务A已经获取了缓存A的锁正在等待缓存B,而业务B获取了缓存B的锁等待缓存A,就出现了互相称待的情况,产生死锁。
        长处:没有额外的内存斲丧,包管一致性,实现简单
        缺点:线程须要等待,性能受影响,可能有死锁风险

  1.    /**
  2.      * 在设置空值,已经解决缓存穿透的基础上,添加互斥锁解决缓存击穿
  3.      */
  4.     public Shop queryWithMutex (Long id){
  5.         //先从redis查询商铺缓存,若存在,从redis中返回,否则查询数据库,存在写入redis,并返回
  6.         String shopJson = stringRedisTemplate.opsForValue().get("cache:shop:" + id);
  7.         if(StringUtils.isNotBlank(shopJson)){
  8.             Shop shop = JSONUtil.toBean(shopJson, Shop.class);
  9.             return shop;
  10.         }
  11.         //判断命中的是否为空,房子缓存穿透
  12.         if(shopJson==null){ //部位null,说明redis中数据为空字符串,说明mysql数据库也没有数据
  13.             return null;
  14.         }
  15.         //redis不存在,失效缓存重建
  16.         //获取互斥锁,每个店铺创建一个锁
  17.         String LockKey = "lock:shop:"+id;
  18.         Shop shop = null;
  19.         try {
  20.             boolean isLock = tryLock(LockKey);
  21.             //获取锁失败,休眠重试
  22.             if(!isLock){
  23.                 Thread.sleep(50);
  24.                 return queryWithMutex(id);
  25.             }
  26.             //获取到锁,查询数据库
  27.             shop = getById(id);
  28.             if(shop==null){
  29.                 //将空值写入redis
  30.                 stringRedisTemplate.opsForValue().set("cache:shop:"+id,"",30, TimeUnit.MINUTES);
  31.                 return null;
  32.             }
  33.             stringRedisTemplate.opsForValue().set("cache:shop:"+id,JSONUtil.toJsonStr(shop));
  34.         }catch (InterruptedException e){
  35.             throw  new RuntimeException("系统异常");
  36.         }finally {
  37.             //释放锁
  38.             unLock(LockKey);
  39.         }
  40.         return shop;
  41.     }
  42.     /**
  43.      *获取锁
  44.      */
  45.     private boolean tryLock(String key){
  46.         Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
  47.         return BooleanUtil.isTrue(flag);
  48.     }
  49.     /**
  50.      * 释放锁
  51.      */
  52.     private void unLock(String key){
  53.         stringRedisTemplate.delete(key);
  54.     }
复制代码

逻辑过期:
        长处:线程无需等待,性能较好
        缺点:不包管一致性,有额外内存斲丧,实现复杂


  1. //线程池
  2. private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
  3.     /**
  4.      * 使用逻辑过期解决缓存击穿(实际上数据不会过期,故不需要考虑缓存穿透问题),使用时需要先缓存热点数据
  5.      */
  6.     public Shop queryWithLogicalExpire (Long id){
  7.         //先从redis查询商铺缓存,若存在,从redis中返回,否则查询数据库,存在写入redis,并返回
  8.         String shopJson = stringRedisTemplate.opsForValue().get("cache:shop:" + id);
  9.         //缓存中不存在,直接返回null。(热点数据,通过一般需要自行初始化到redis缓存中,一般不会出现null的情况)
  10.         if(StringUtils.isBlank(shopJson)){
  11.             return null;
  12.         }
  13.         RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
  14.         Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);//获取缓存数据
  15.         LocalDateTime expireTime = redisData.getExpireTime();//获取缓存过期时间
  16.         //判断是否过期
  17.         if(expireTime.isAfter(LocalDateTime.now())){
  18.             //未过期,直接返回
  19.             return shop;
  20.         }
  21.         //过期,需要缓存重建
  22.         //获取互斥锁
  23.         String LockKey = "lock:shop:"+id;
  24.         if(tryLock(LockKey)){
  25.             //获取锁成功,开启独立线程,实现缓存重建
  26.             CACHE_REBUILD_EXECUTOR.submit(() ->{
  27.                 //重建缓存
  28.                 try{
  29.                     this.saveShop2Redis(id,20L);
  30.                 }catch (Exception e){
  31.                     throw new RuntimeException(e);
  32.                 }finally {
  33.                     //释放锁
  34.                     unLock(LockKey);
  35.                 }
  36.             });
  37.         }
  38.         return shop;
  39.     }
  40.     public void saveShop2Redis(Long id,Long expireSeconds){
  41.         //查询店铺数据
  42.         Shop shop = getById(id);
  43.         //封装逻辑过期时间
  44.         RedisData redisData = new RedisData();
  45.         redisData.setData(shop);
  46.         redisData.setExpireTime(LocalDateTime.now().plusMinutes(expireSeconds));
  47.         //写入redis
  48.         stringRedisTemplate.opsForValue().set("cache:shop:"+id,JSONUtil.toJsonStr(redisData));
  49.     }
  50.     /**
  51.      *获取锁
  52.      */
  53.     private boolean tryLock(String key){
  54.         Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
  55.         return BooleanUtil.isTrue(flag);
  56.     }
  57.     /**
  58.      * 释放锁
  59.      */
  60.     private void unLock(String key){
  61.         stringRedisTemplate.delete(key);
  62.     }
复制代码
全局ID天生器

假如使用数据库自增ID就存在一些标题:



  • id的规律性太明显
  • 受单表数据量的限制
全局ID天生器,是一种在分布式体系下用来天生全局唯一ID的工具,一样平常要满足下列特性:


为了增加ID的安全性,我们可以不直接使用Redis自增的数值,而是拼接一些别的信息:

ID的构成部门:



  • 符号位:1bit,永久为0
  • 时间戳:31bit,以秒为单元,可以使用69年
  • 序列号:32bit,秒内的计数器,支持每秒产生2^32个不同ID


Redis自增ID计谋:



  • 每天一个key,方便统计订单量
  • ID构造是 时间戳 + 计数器
  
  1. import org.springframework.data.redis.core.StringRedisTemplate;
  2. import org.springframework.stereotype.Component;
  3. import java.time.LocalDateTime;
  4. import java.time.ZoneOffset;
  5. import java.time.format.DateTimeFormatter;
  6. @Component
  7. public class RedisIdWorker {
  8.     private StringRedisTemplate stringRedisTemplate;
  9.     public RedisIdWorker(StringRedisTemplate stringRedisTemplate) {
  10.         this.stringRedisTemplate = stringRedisTemplate;
  11.     }
  12.     // 2022年开始时间戳
  13.     private static final long BEGIN_TIMESTAMP = 1640995200L;
  14.     // 序列号位数
  15.     private static final int COUNT_BITS = 32;
  16.     public long nextId(String KeyPrefix) {
  17.         // 1.生成时间戳
  18.         LocalDateTime now = LocalDateTime.now();
  19.         long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
  20.         long timestamp = nowSecond-BEGIN_TIMESTAMP;
  21.         // 2.生成序列号
  22.         String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
  23.         //自增长,返回自增序列号,key不存在会自动创建一个key
  24.         long count = stringRedisTemplate.opsForValue().increment("icr:" + KeyPrefix + ":" + date);
  25.         // 3.拼接并返回
  26.         // 时间戳左移32位,通过|运算,拼接序列号
  27.         return timestamp << COUNT_BITS | count;
  28.     }
  29. }
复制代码


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

莫张周刘王

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表