https://www.bilibili.com/video/av96504549?p=14
前言
缓存是我们项目应用肯定会使用,是我们数据库的守护神,能够保证数据库的稳定,能够提高整个系统的性能。一般我们采用市面上的redis、memcahce方案;redis已经非常强大了,每秒支持几万的连接时不成问题。设计一个缓存系统,不得不要考虑的问题就是:缓存穿透、缓存击穿与失效时的雪崩效应。
原文链接:https://blog.youkuaiyun.com/weter_drop/article/details/84556736
一、缓存穿透
缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。在流量大时,可能DB就挂掉了,要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞。例如:如商品id=10000,系统就不存在id为10000的数据。按照缓存设计的原理,缓存中找不到此商品,就会到数据库中查询,也没有找到。这样的请求都会打到数据库中,缓存在这个场景中就相当于没有。恶意的人利用这个漏洞发起攻击,直接把数据库搞崩溃。
解决方案
有很多种方法可以有效地解决缓存穿透问题:
解决方案一:
最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被 这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。
解决方案二:
另外也有一个更为简单粗暴的方法(我们采用的就是这种),如果一个查询返回的数据为空(不管是数 据不存在,还是系统故障),我们仍然把这个空结果进行缓存。但是会存在两个问题:
问题一:
空值做了缓存,这样就无形了增加了很多没有意义的缓存,内存吃紧。比较有效的方式就是在这些值上面设置一个过期时间,让起自动删除,一般设置不超过5分钟
问题二:
有可能与业务存在不一致的窗口时间,如:虽然商品ID=10000现在不存在,我们现在在redis中保存了一个空值,但业务后来就真的有了商品id为10000的数据,这样数据库和缓存的信息就不一致,导致业务出现问题。此时就可以利用消息中间件等方式系统剔除掉此缓存数据
二、缓存雪崩
雪崩是指我们一般在设置缓存的时候都会设置一个缓存过期时间,如果一些缓存过期时间在同一刻,也就是缓存同时过期了。这个时候有大量请求过来,发现缓存中不存在,就直接到数据库中获取,就像雪崩一样同时打到数据库上面,这样就会导致数据库负载高,很有可能就会崩溃。
解决方案
解决方案一:
发现缓存不存在,在读取数据库时,再保存到redis缓存中,采用队列的方式或加锁的方式,进行排队访问数据库,这样就能够避免同时打到数据库上。
解决方案二:
这个方案就比较简单,雪崩是因为过期时间在同一刻导致的,那我们就可以在设置过期时间分散开。如在原有的过期时间的基础上加上一个 随机数,比如:1~10分钟,这样就不会放生在同一刻缓存失效了。
解决方案三:
设置二级缓存机制,这样就能够保证即时第一层缓存失效了,还有第二级缓存的保障;不过就增加了系统复杂性。
三、缓存击穿
对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:缓存被“击穿”的问题,这个和缓存雪崩的区别在于这里针对某一key缓存,前者则是很多key。
缓存在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。
解决方案一:检测更新
最容易想到的是这些热点数据设置永不过期,就不存在此问题。这样就会出现一个问题,那缓存怎么去更新呢?
方法一:
可以做一个后台服务,专门用来做数据缓存的更新。每隔30分钟更新一下这些热点缓存数据。不过这样就增加了系统复杂度。
方法二:
方法一过于复杂,我们可以改造一下redis的缓存结构,在缓存业务数据的时候,再保存跟业务数据关联的过期时间key。每次请求过来时,判断一下此业务数据的是否要到过期时间了,如:此值还有1分钟就过期了,这样就服务本身主动去数据库查询一次,做一个缓存的更新。
解决方案二:分级缓存
分级缓存,可采用二级缓存的方式缓存此热点数据。
采用 L1 (一级缓存)和 L2(二级缓存) 缓存方式,L1 缓存失效时间短,L2 缓存失效时间长。 请求优先从 L1 缓存获取数据,如果 L1缓存未命中则加锁,只有 1 个线程获取到锁,这个线程再从数据库中读取数据并将数据再更新到到 L1 缓存和 L2 缓存中,而其他线程依旧从 L2 缓存获取数据并返回。
这种方式,主要是通过避免缓存同时失效并结合锁机制实现。所以,当数据更新时,只能淘汰 L1 缓存,不能同时将 L1 和 L2 中的缓存同时淘汰。L2 缓存中可能会存在脏数据,需要业务能够容忍这种短时间的不一致。而且,这种方案可能会造成额外的缓存空间浪费。
解决方案三:加锁
也就是在获取数据时,redis缓存不存在,在访问数据库时,通过加锁的方式获取数据库的值,这样就保证了访问数据库只有一个请求过来,从而保证数据库的负载。具体如何加锁,下篇文章中会介绍。
————————————————
版权声明:本文为优快云博主「ratelFu」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.youkuaiyun.com/weter_drop/article/details/84556736