当前位置: 首页 > article >正文

Redis三剑客:缓存雪崩、缓存穿透、缓存击穿


文章目录

  • 缓存雪崩
  • 缓存穿透
  • 缓存击穿

缓存雪崩

缓存雪崩产生原因:由于缓存在同一时间大面积失效或者Redis宕机导致大量请求落入数据库,给数据库造成巨大的压力
解决方案:
1、当将数据添加到缓存中时,给缓存时间添加随机的过期时间。可以防止缓存在同一时间大面积失效。
2、使用Redis集群。当有节点宕机时使用其他节点恢复数据。
3、做好限级限流的策略
4、使用多级缓存

缓存穿透

缓存穿透产生原因:被恶意者攻击,发送大量不存在的数据请求,导致请求不会命中缓存,直接落到数据库,给数据库带来巨大的压力
解决方案:
1、缓存空值键到缓存中,适用于大量请求所带的id相同。为了避免内存的浪费,可以给空值键设置一个过期时间
优点:实现简单
缺点:当大量请求所带的id不同时,需要缓存多个空值键,占用额外的内存空间

保证空键值的实现:

    @Autowired
    private StringRedisTemplate redisTemplate;

    public void set(String key, Object o, Long time, TimeUnit unit) {
        redisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(o), time, unit);
    }
    /**
     * 缓存空值键(解决缓存穿透)
     */
    public  <R, Id> R querySaveNull(String prefixKey, Id id, Class<R> type, Function<Id, R> dbFunction, Long time, TimeUnit unit) {
        System.out.println("缓存空值");
        String cacheKey = prefixKey + id;
        String objectStr = redisTemplate.opsForValue().get(cacheKey);
        if (StrUtil.isNotBlank(objectStr)) {
            return JSONUtil.toBean(objectStr, type);
        }
//        shopStr不为null说明为'',即命中空值键的缓存
        if (objectStr != null) {
            return null;
        }
        R r = dbFunction.apply(id);
//        当查出来的数据为null时,往redis中添加空值的键
        if (r == null) {
            redisTemplate.opsForValue().set(cacheKey, "", CACHE_NULL_TTL,TimeUnit.SECONDS);
            return null;
        }
        set(cacheKey,r,time,unit);
        return r;
    }

2、适用布隆过滤器。将数据库所有数据的id放入布隆过滤器中,当有请求到达时,先经过布隆过滤器,判断布隆过滤器中是否存在请求id,如果存在则通过,如果不存在则直接返回。
缺点:实现复杂,存在误差
3、可以增加id的复杂性,添加请求所带id的检验,可以防止被攻击者猜到id并发出攻击

缓存击穿

缓存击穿产生原因:存在高并发的热点键,并且其重建缓存业务复杂,重建缓存耗时长,当热点键过期失效时,大量线程进入重建缓存,查询数据库,导致数据库压力暴涨
解决方案:
1、基于互斥锁的实现。
基于Redis的setnx命令实现互斥锁,当数据不存在时可以添加数据成功,数据存在时添加数据失败,当线程适用setnx命令添加成功数据时则成功抢到了锁,添加失败则获取锁失败,线程执行结束后需要及时释放锁,为了防止因为其他原因锁没有被释放,可以给锁添加一个过期时间。


    /**
     * 获取互斥锁
     */
    private Boolean getLock(String lockKey) {
//        返回的布尔类型使包装型,可能为null值,调用布尔工具类,当包装类为true时才返回true,否则返回false
        Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", LOCK_SHOP_TTL, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(aBoolean);
    }

    /**
     * 释放互斥锁
     */
    private void releaseLock(String lockKey) {
        redisTemplate.delete(lockKey);
    }

当多线程到达时,首先查看能否命中缓存,如果缓存失效我们尝试让这些线程竞争锁,竞争到锁的线程负责进行缓存重建的业务,其他的线程则使其休眠一段时间后重新执行业务。以下是基于Shop对象的实现。

  private Shop queryWithMutex(Long id) {
//       首先从缓存中查询数据
        String cacheKey = CACHE_SHOP_KEY + id;
        String shopStr = redisTemplate.opsForValue().get(cacheKey);
        if (StrUtil.isNotBlank(shopStr)) {
            return JSONUtil.toBean(shopStr, Shop.class);
        }
        String lockKey = LOCK_SHOP_KEY + id;
        Boolean aBoolean = getLock(lockKey);
        Shop shop = null;
//       如果获取锁失败,则进行递归重新执行
        try {
            if (!aBoolean) {
      
                Thread.sleep(50);
                return queryWithMutex(id);
            }
            Thread.sleep(500);
            shop = getById(id);
//        当查出来的数据为null时,往redis中添加空值的键
            if (shop == null) {
                redisTemplate.opsForValue().set(cacheKey, "", CACHE_NULL_TTL, TimeUnit.SECONDS);
                return null;
            }
            redisTemplate.opsForValue().set(cacheKey, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
//            当抛出异常或者业务执行完毕之后释放锁
            releaseLock(lockKey);
        }
        return shop;
    }

优点:保证了数据的高一致性,所有请求都能够返回最新的数据
缺点:重建缓存耗时较长,其他线程都需要等待缓存重建消耗的时间

2、基于逻辑过期实现。我们不给键添加过期时间,让其一直存在缓存中,但是我们在将数据其添加进缓存中时,会给其添加一个逻辑过期的字段,当多线程到达时,我们首先命中缓存,取出逻辑过期的字段,判断数据是否过期,如果数据过期,让线程竞争锁,获取到锁的线程新创建一个线程来进行缓存重建的业务,然后主线程和其他未竞争到锁的线程直接返回过期的数据。以下时基于Shop对象的实现

@Data
public class RedisData {
    private LocalDateTime expireTime;
    private Object data;
}
private Shop queryWithLogicalExpire(Long id) {
        String key = CACHE_SHOP_KEY + id;
        String redisDataStr = redisTemplate.opsForValue().get(key);
        if (StrUtil.isBlank(redisDataStr)) {
            return null;
        }
        RedisData redisData = JSONUtil.toBean(redisDataStr, RedisData.class);
        Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
//        逻辑时间未过期
        LocalDateTime expireTime = redisData.getExpireTime();
        System.out.println(expireTime);
        System.out.println(LocalDateTime.now());
        if (expireTime.isAfter(LocalDateTime.now())) {
            return shop;
        }
//        逻辑时间过期后,竞争锁
        String lockKey = LOCK_SHOP_KEY + id;
//        竞争成功锁的线程new线程来进行缓存重建
        Boolean isLock = getLock(lockKey);
        if (isLock){
                executorService.execute(()->{
                    try {
                        Thread.sleep(500);
                        System.out.println("缓存重建");
                        saveShopRedis(id,CACHE_SHOP_TTL);
                    } catch (Exception e) {
                        e.printStackTrace();
                    } finally {
                        releaseLock(lockKey);
                    }
                });
            }
        return shop;
    }

    /**
      逻辑过期进行缓存重建
     */
    public void saveShopRedis(Long id, Long time) {
        String key = CACHE_SHOP_KEY + id;
        Shop shop = getById(id);
        RedisData redisData = new RedisData();
        redisData.setData(shop);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(time));
        redisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
    }

优点:线程无需等待,直接返回数据
缺点:不保证数据的高一致性,部分线程可能返回以及过期的数据


http://www.kler.cn/a/405085.html

相关文章:

  • Shell编程-8
  • python oa服务器巡检报告脚本的重构和修改(适应数盾OTP)有空再去改
  • Java中的TreeSet集合解析
  • 美畅物联丨智能分析,安全管控:视频汇聚平台助力智慧工地建设
  • 【前端】JavaScript中的indexOf()方法详解:基础概念与背后的应用思路
  • 将网站地址改成https地址需要哪些材料
  • 国标GB28181设备管理软件EasyGBS国标GB28181视频平台:RTMP和GB28181两种视频上云协议的区别
  • RNN简单理解;为什么出现Transformer:传统RNN的问题;Attention(注意力机制)和Self-Attention(自注意力机制)区别;
  • SQLAlchemy,ORM的Python标杆!
  • 嵌入式硬件电子电路设计(六)LDO低压差线性稳压器全面详解
  • 音视频入门基础:MPEG2-TS专题(6)——FFmpeg源码中,获取MPEG2-TS传输流每个transport packet长度的实现
  • 开源许可协议
  • 【Swift】字符串和字符
  • springboot第83集:理解SaaS多租户应用的架构和设计,设备介入,网关设备,安全,实时实现,序列化...
  • python-自定义排序函数sorted()
  • OpenCV基本图像处理操作(六)——直方图与模版匹配
  • 二叉树路径相关算法题|带权路径长度WPL|最长路径长度|直径长度|到叶节点路径|深度|到某节点的路径非递归(C)
  • 一篇文章了解机器学习(下)
  • linux命令面试题及参考答案
  • 5G NR:TDD和FDD的技术差异
  • 数据结构 ——— 判断一棵树是否是完全二叉树
  • 数学建模学习(137):使用Python进行频数分析
  • c#基本数据类型占用字节长度/取值范围/对应.net类型
  • 【机器学习】聚类算法原理详解
  • python: generator model using sql server 2019
  • linux命令之netstat用法