一、前言
一些数据我们没有必要每次查询时都去查询到数据库,特别是高 QPS的系统,每次都去查询数据库,对于数据库来说就是灾难。我们使用缓存时,我们的业务系统大概的调用流程如下图:
二、缓存穿透
1、概念
缓存穿透是指查询一个一定不存在的数据,这将导致这个不存在的数据每次请求都要到存储层去查询,在流量大时,可能DB就挂掉了,如果有人利用不存在的key频繁攻击我们的应用,会导致系统无法正常访问。
2、解决思路
1)缓存空值
当存储层不命中后,即使返回的空对象也将其缓存起来,同时会设置一个过期时间。通常这个时间较短(如5分钟),这个词可能不是热点词汇,较短的缓存时间可以节省内存空间;如果是恶意攻击,之后再访问这个数据将会从缓存中获取,保护了后端数据源;
弊端:
- 空值做了缓存,意味着缓存层中存了更多的键,需要更多的内存空间 ( 如果是攻击,问题更严重 ),比较有效的方法是针对这类数据设置一个较短的过期时间,让其自动剔除。
- 不宜与正常值共用空间,否则当空间不足时,缓存系统的LRU算法可能会先剔除正常值,再剔除空值——这个漏洞可能会受到攻击。
- 缓存层和存储层的数据会有一段时间窗口的不一致,可能会对业务有一定影响。例如过期时间设置为 5分钟,如果此时存储层添加了这个数据,此段时间会出现缓存层和存储层数据不一致,此时可以利用消息系统或者其他方式清除掉缓存层中的空对象。
2)布隆过滤器(Bloom Filter)
布隆过滤器的原理是,当一个元素被加入集合时,通过K个散列函数将这个元素映射成一个位数组中的K个点,把它们置为1。检索时,我们只要看看这些点是不是都是1就(大约)知道集合中有没有它了:如果这些点有任何一个0,则被检元素一定不在;如果都是1,则被检元素很可能在。这就是布隆过滤器的基本思想。
Bloom Filter跟单哈希函数Bit-Map不同之处在于:Bloom Filter使用了k个哈希函数,每个字符串跟k个bit对应。从而降低了冲突的概率。
对所有可能查询的参数以hash形式存储,当用户想要查询的时候,使用布隆过滤器发现不在集合中,就直接丢弃,不再对持久层查询。
弊端:
Bloom Filter之所以能做到在时间和空间上的效率比较高,是因为牺牲了判断的准确率、删除的便利性
- 存在误判,可能要查到的元素并没有在容器中,但是hash之后得到的k个位置上值都是1。如果bloom filter中存储的是黑名单,那么可以通过建立一个白名单来存储可能会误判的元素。
- 删除困难。一个放入容器的元素映射到bit数组的k个位置上是1,删除的时候不能简单的直接置为0,可能会影响其他元素的判断。可以采用Counting Bloom Filter。
3)代码过滤
接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截等;
三、缓存击穿
1、概念
缓存击穿实际上是缓存雪崩的一个特例,微博有一个热门话题的功能,用户对于热门话题的搜索量往往在一些时刻会大大的高于其他话题,这种我们成为系统的“热点“,由于系统中对这些热点的数据缓存也存在失效时间,在热点的缓存到达失效时间时,此时可能依然会有大量的请求到达系统,没有了缓存层的保护,这些请求同样的会到达db从而可能引起故障。击穿与雪崩的区别即在于击穿是对于特定的热点数据来说,而雪崩是全部数据。
2、解决思路
1)热点数据永不过期
从缓存上看,确实没有设置过期时间,这就保证了,不会出现热点key过期问题,也就是“物理”不过期。
从功能上看,如果不过期,那不就成静态的了吗?所以我们把过期时间存在key对应的value里,如果发现要过期了,通过一个后台的异步线程进行缓存的重新构建,将数据刷新,也就是“逻辑”过期.
弊端:
- 不保证一致性
- 代码复杂度增大(每个value都要维护一个timekey)
- 占用一定的内存空间(每个value都要维护一个timekey)。
2)使用互斥锁(mutex key)
就是只让一个线程构建缓存,其他线程等待构建缓存的线程执行完,重新从缓存获取数据。
如果是单机,可以用synchronized或者lock来处理,如果是分布式环境可以用分布式锁就可以了(分布式锁,可以用memcache的add, redis的setnx, zookeeper的添加节点操作)。
弊端:
- 降低了系统的qps
- 代码复杂性增加
- 有出现死锁的可能性, 存在线程池阻塞的风险
- “分布式缓存加锁”通常是一个反模式,如果持有锁的实例不稳定导致没及时释放,就会浪费这个锁,直到锁过期。
3)"提前"使用互斥锁(mutex key)
方法同使用互斥锁,但在value内部设置1个超时值(timeout1), timeout1比实际的缓存到期时间(timeout2)小。当从cache读取到timeout1发现它已经过期时候,马上延长timeout并重新从数据库加载数据并设置到cache中。
弊端:
- 同使用互斥锁
4)多级缓存
对于热点数据进行二级缓存,且过期时间错开,则请求基本不会直接击穿缓存层到达数据库。
四、缓存雪崩
1、概念
缓存雪崩的情况是说,当某一时刻发生大规模的缓存失效的情况,比如你的缓存服务宕机了、大量热点缓存同时过期导致大量缓存击穿,会有大量的请求进来直接打到DB上面。
2、解决思路
缓存击穿中的解决思路在缓存雪崩同样适用,但是互斥锁在大量热点缓存同时过期时需要同时大量构建新的缓存,可能会造成服务器性能问题。
1)设置不同的失效时间
为了避免这些热点的数据集中失效,那么我们在设置缓存过期时间的时候,我们让他们失效的时间错开。比如在一个基础的时间上加上或者减去一个范围内的随机值。
2)redis高可用
优化redis性能,搭建集群
3)数据预热
可以通过缓存reload机制,预先去更新缓存,再即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。
4)限流降级
限流算法
- 计数
- 滑动窗口
- 令牌桶Token Bucket
- 漏桶 leaky bucket
限流组件设置限流数值,超出的请求会走开发好的降级组件,返回配置好的默认值。
5)排队加锁
在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
2 comments
嗯 看了博主的文章犹如 醍醐灌顶 我觉得 我在 服务器处理问题的思维上 有了 飞跃似的进步 NICE 写得好好哟,我要给你生猴子!::funny:04::
boom