缓存一致性、缓存穿透击穿、缓存雪崩、热点数据集中失效等问题入门

缓存一致性、缓存穿透击穿、缓存雪崩、热点数据集中失效等问题入门,第1张

这问题大概1年半之前一次面试碰到的.看起来很高大上,其实呵呵.平时忙着业务,根本没时间去理一些理论概念性问题.

而事实上,做再多业务都不能从本质提高自己认识.所以会花几天时间彻底理一理这个问题

临时想起来个事,做个记录呗

域名如:alibaba.com (URL地址)
IP地址为:xx.233.xxs.12 (访问)

首先,第一步浏览器会请求DNS返回域名对应的IP,浏览器还提供了DNS数据缓存服务,如果某个域名已经被解析过了,浏览器就会缓存解析的结构,下次查询时直接使用,减少一次网络请求。

拿到IP后,就需要获取端口号,如果url没有明确指出端口号,HTTP协议默认是80端口。

在实际情况中,因为用户和服务器之间存在代理服务器比如nginx,这个解析到的域名一般是代理服务器的 IP地址(或者IP地址 :80端口 ),代理服务再去转发请求到真正的业务服务器,业务服务器和IP和端口号就跟访问的域名没啥关系了。

域名和端口号是怎么对应起来的?
客户端输入域名,通过DNS将域名解析成为服务器ip,找到代理服务器,因为http协议服务所占用的端口默认为80端口,所以会访问服务器的80端口,然后再通过代理服务器将请求转发到不同的服务器以及端口中.

如图:域名和端口号的对应

进入主题

缓存减少数据库访问压力

mybatis有自己的缓存机制

 mybatis层面宏观上sqlSession会话属于一级缓存,而在.XML映射文件中配置

即maybatis的二级缓存机制

但是只要有缓存就会存在数据不一致的问题

mybatis层面sqlSession会话不存在共享情况,不存在线程安全问题

而二级缓存由多个SqlSession共享,因此引出了SynchronizedCache

为什么有SOFT(软引用)和WEAK(若引用)适用于缓存,软引用在内存不足时会自动清理

弱引用在调用GC时就会清理。(自己感觉也没理的很清楚)

 

本地缓存:指的是在应用中的缓存组件,其最大的优点是应用和 cache是在同一个进程内部,请求缓存非常快速,没有过多的网络开销等,在单应用不需要集群支持或者集群情况下各节点无需互相通知的场景下使用本地缓存较合适;同时,它的缺点也是应为缓存跟应用程序耦合,多个应用程序无法直接的共享缓存,各应用或集群的各节点都需要维护自己的单独缓存,对内存是一种浪费。
分布式缓存:指的是与应用分离的缓存组件或服务,其最大的优点是自身就是一个独立的应用,与本地应用隔离,多个应用可直接的共享缓存。

比如上篇文章Shiro:SpringBoot项目整合Shiro_fhrui的博客-CSDN博客

将用户信息保存起来用到的就是本地缓冲.但是如何保证数据库信息与缓存一致呢

经常有LRUCache一说,其实LRU指的是一种算法而Cache指的就是缓存

还有FIFOCache

  • LRU – 最近最少使用:移除最长时间不被使用的对象。
  • FIFO – 先进先出:按对象进入缓存的顺序来移除它们。

用个小Demo解释下LRU和FIFO

LinkedHashMap 可以记录插入顺序和访问顺序,有序,非线程安全

 所谓的FIFO先进先出算法

LRU – 最近最少使用

以AOP形式自定义本地缓存

@Component
@Aspect
public class SpringAOP {


    /**
     * 需求:用户第一次查询走目标方法
     * 用户第二次查询走缓存,不执行目标方法
     * 如何判断用户是否为第一次查询:
     * 通过Map集合判断,有数据,证明不是第一次执行
     * 执行步骤:
     * 1.获取用户查询参数
     * 2.判断map集合中是否有该数据.
     *      true:从Map集合中get之后返回
     *      false:执行目标方法,之后将user对象保存到Map中
     * @param proceedingJoinPoint
     * @return
     */
    private static Map map=new HashMap<>();

    //首选环绕通知
    @Around("execution(* com.xxx.service..*(..))")//该包下所有类所有方法随意参数
    public Object around(ProceedingJoinPoint proceedingJoinPoint){
        Object o=null;

        User args=(User)proceedingJoinPoint.getArgs()[0];
        if(map.containsKey(args.getId())){
            //一般是返回用户数据  为了测试简单  接口里返回void
            return map.get(((User)proceedingJoinPoint.getArgs()[0]).getId());
        }else{
            try {
                o=proceedingJoinPoint.proceed();
            } catch (Throwable throwable) {
                throwable.printStackTrace();
            }
            map.put(((User)proceedingJoinPoint.getArgs()[0]).getId(),(User)proceedingJoinPoint.getArgs()[0]);
        }

        return o;
    }
}

而像redis就是分布式缓存.redis又称为键值存储数据库(非关系型数据库),键值存储数据库有很多不单指redis

缓存一致性:比如缓存中有一定数据,但是数据库被更新,即缓存中数据与数据库数据的一致性问题.

在于分布式(系统)架构关于CAP理论.C指一致性.A指可用性.P指分区容错性.可以是CP,AP.CA三者

只能满足两个,不能同时都满足.

1种解决方案就是在update数据库的同时,也update缓存.通常这种更新是异步的

第二种解决方案.所有的读都从redis里读.写与更新或删除到数据库的同时更新redis.在写或更新或删除数据库的时候给数据库和redis加锁.就是写或更新或删除时,不让你读redis.写或更新或删除完成时,才让你读.mysql通过binlog异步更新redis来解决缓存一致性.我这写的好尴尬呀......

缓存穿透/击穿:

缓存穿透是指查询一个根本不存在的数据,缓存层和持久层都不会命中。在日常工作中出于容错的考虑,如果从持久层查不到数据则不写入缓存层,缓存穿透将导致不存在的数据每次请求都要到持久层去查询,失去了缓存保护后端持久的意义。(对于系统A,假设一秒 5000 个请求,结果其中 4000 个请求是黑客发出的恶意攻击。黑客发出的那 4000 个攻击,缓存中查不到,每次你去数据库里查,也查不到。)
缓存穿透示意图:

 缓存穿透问题可能会使后端存储负载加大,由于很多后端持久层不具备高并发性,甚至可能造成后端存储宕机。通常可以在程序中统计总调用数、缓存层命中数、如果同一个Key的缓存命中率很低,可能就是出现了缓存穿透问题。
  造成缓存穿透的基本原因有两个。第一,自身业务代码或者数据出现问题(例如:set 和 get 的key不一致),第二,一些恶意攻击、爬虫等造成大量空命中(爬取线上商城商品数据,超大循环递增商品的ID)

大白话就是每次缓存中取不到数据,就会查询数据库,那么有人就利用这一点不断的将请求的KEY就是缓存中没有的,频繁访问数据库.让你的数据库奔溃.

解决方案 1.缓存空对象

缓存空对象:是指在持久层没有命中的情况下,对key进行set (key,null)

缓存空对象会有两个问题:
   第一,value为null 不代表不占用内存空间,空值做了缓存,意味着缓存层中存了更多的键,需要更多的内存空间,比较有效的方法是针对这类数据设置一个较短的过期时间,让其自动剔除。
   第二,缓存层和存储层的数据会有一段时间窗口的不一致,可能会对业务有一定影响。例如过期时间设置为5分钟,如果此时存储层添加了这个数据,那此段时间就会出现缓存层和存储层数据的不一致,此时可以利用消息系统或者其他方式清除掉缓存层中的空对象。

2.布隆过滤器拦截(额滴娘我已经懵逼了)

   在访问缓存层和存储层之前,将存在的key用布隆过滤器提前保存起来,做第一层拦截,当收到一个对key请求时先用布隆过滤器验证是key否存在,如果存在在进入缓存层、存储层。可以采用布隆过滤器,使用一个足够大的bitmap,用于存储可能访问的key,不存在的key直接被过滤;访问key未在DB查询到值,也将空值写进缓存,但可以设置较短过期时间。这种方法适用于数据命中不高、数据相对固定、实时性低的应用场景,代码维护较为复杂,但是缓存空间占用少。

   布隆过滤器实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。

它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。

   布隆过滤器拦截的算法描述:

   初始状态时,BloomFilter是一个长度为m的位数组,每一位都置为0。

   添加元素x时,x使用k个hash函数得到k个hash值,对m取余,对应的bit位设置为1。

   判断y是否属于这个集合,对y使用k个哈希函数得到k个哈希值,对m取余,所有对应的位置都是1,则认为y属于该集合(哈希冲突,可能存在误判),否则就认为y不属于该集合。可以通过增加哈希函数和增加二进制位数组的长度来降低错报率。

缓存击穿 缓存击穿的理解

   缓存击穿,就是说某个 key 非常热点,访问非常频繁,处于集中式高并发访问的情况,当这个 key 在失效的瞬间,大量的请求就击穿了缓存,直接请求数据库,就像是在一道屏障上凿开了一个洞。

解决方案 分布式互斥锁

   只允许一个线程重建缓存,其他线程等待重建缓存的线程执行完,重新从缓存获取数据即可。set(key,value,timeout)

永不过期

   从缓存层面来看,确实没有设置过期时间,所以不会出现热点key过期后产生的问题,也就是“物理”不过期。

   从功能层面来看,为每个value设置一个逻辑过期时间,当发现超过逻辑过期时间后,会使用单独的线程去更新缓

两种方案对比:

  分布式互斥锁:这种方案思路比较简单,但是存在一定的隐患,如果在查询数据库 + 和 重建缓存(key失效后进行了大量的计算)时间过长,也可能会存在死锁和线程池阻塞的风险,高并发情景下吞吐量会大大降低!但是这种方法能够较好地降低后端存储负载,并在一致性上做得比较好。

  永远不过期:这种方案由于没有设置真正的过期时间,实际上已经不存在热点key产生的一系列危害,但是会存在数据不一致的情况,同时代码复杂度会增大。

缓存雪崩 概念理解

  如果缓存集中在一段时间内失效,发生大量的缓存穿透,所有的查询都落在数据库上,造成了缓存雪崩。

  这个没有完美解决办法,但可以分析用户行为,尽量让失效时间点均匀分布。大多数系统设计者考虑用加锁或者队列的方式保证缓存的单线程(进程)写,从而避免失效时大量的并发请求落到底层存储系统上。

解决方案 缓存层高可用:

   可以把缓存层设计成高可用的,即使个别节点、个别机器、甚至是机房宕掉,依然可以提供服务。利用sentinel或cluster实现。

做二级缓存,或者双缓存策略:

   采用多级缓存,本地进程作为一级缓存,redis作为二级缓存,不同级别的缓存设置的超时时间不同,即使某级缓存过期了,也有其他级别缓存兜底

数据预热:

   可以通过缓存reload机制,预先去更新缓存,再即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀

加锁排队. 限流-- 限流算法. 1.计数 2.滑动窗口 3. 令牌桶Token Bucket 4.漏桶 leaky bucket [1]

   在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
  业界比较常用的做法,是使用mutex。简单地来说,就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db,而是先使用缓存工具的某些带成功 *** 作返回值的 *** 作(比如Redis的SETNX或者Memcache的ADD)去set一个mutex key,当 *** 作返回成功时,再进行load db的 *** 作并回设缓存;否则,就重试整个get缓存的方法。

从概念上讲缓存击穿指某些热点数据的Key失效,而缓存雪崩指一大堆热点数据的key失效

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/920090.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-16
下一篇 2022-05-16

发表评论

登录后才能评论

评论列表(0条)

保存