转载自:http://blog.youkuaiyun.com/zeb_perfect/article/details/54135506
http://blog.youkuaiyun.com/AlbertFly/article/details/75546170
缓存穿透
缓存穿透是指 查询一个一定不存在的数据,由于缓存不命中,需要直接从数据库查询,查不到数据则不写入缓存,这将导致每次请求不存在的key都要到数据库去查询,从系统层面来看像是穿透了缓存层直接达到db,造成缓存穿透,没有了缓存层的保护。在流量大时,可能DB就挂掉了,要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞。
解决方案:
- 有很多种方法可以有效地解决缓存穿透问题,最常见的则是采用bloom filter(布隆过滤器),将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层数据库的查询压力。guava中有实现BloomFilter算法。
- 另外也有一个更为简单粗暴的方法—空值缓存,如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。这样则可以应对短时间的大量的该key攻击,设置为较短的失效时间是因为该值可能业务无关,存在意义不大,且该次的查询也未必是攻击者发起,无过久存储的必要,故可以早点失效。
缓存雪崩
缓存雪崩是指 在设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,导致所有的查询都落在数据库上,造成了缓存雪崩。
解决方案:
- 在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待,这样每个时刻只有一个线程在执行请求,减轻了db的压力,但缺点也很明显,降低了系统的qps。
- 可以通过缓存reload机制,预先去更新缓存,在即将发生大并发访问前手动触发加载缓存。
- 不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。
- 做二级缓存,或者双缓存策略。A1为原始缓存,A2为拷贝缓存,A1失效时,可以访问A2,A1缓存失效时间设置为短期,A2设置为长期。
缓存击穿
对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。缓存在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。
例如微博有一个热门话题的功能,用户对于热门话题的搜索量往往在一些时刻会大大的高于其他话题,由于系统中对这些热点的数据缓存也存在失效时间,在热点的缓存到达失效时间时,此时可能依然会有大量的请求到达系统,没有了缓存层的保护,这些请求同样的会到达db从而可能引起故障。
击穿与雪崩的区别即在于击穿是对于特定的热点数据来说,而雪崩是全部数据。
解决方案:
- 做二级缓存,或者双缓存策略。对于热点数据进行二级缓存,并对于不同级别的缓存设定不同的失效时间,则请求不会直接击穿缓存层到达数据库。
解决此问题的关键在于热点访问。由于热点可能随着时间的变化而变化,结合LRU算法能够较好的解决这个问题。
LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。上面的算法存在较多的不合理性,在实际的应用过程中采用该算法进行了改进,例如偶然的数据影响会造成命中率较低,比如某个数据即将到达底部即将被淘汰,但由于一次的请求又放入了头部,此后再无该数据的请求,那么该数据的继续存在其实是不合理的,针对这类情况LRU-K算法拥有更好的解决措施。
LRU-K需要多维护一个队列或者更多,用于记录所有缓存数据被访问的历史。只有当数据的访问次数达到K次的时候,才将数据放入缓存。当需要淘汰数据时,LRU-K会淘汰第K次访问时间距当前时间最大的数据。如果数据访问没有达到K次则会继续到达链表底部直至淘汰;如果该数据在队列中时访问次数达到了K次,那么它会被加入到接下来的2级链表中,按照时间顺序在2级链表中排列。接下来2级链表中的操作与上面算法相同,链表中的数据如果再次被访问则移到头部,链表满时,底部数据淘汰