缓存击穿
缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,此时如果你的代码没有实现同步机制,会造成小部分的请求直接打到数据库上,给数据库带来一定的压力。
例如,有1000人在淘宝上要查询一个商品信息,但是缓存中没有这条数据或缓存时间到期,所以要从数据库中查找。这不能1000个人都去访问数据库,这会给数据库造成太大的压力。
解决方案
- synchronized
使用同步机制来进行线程的限制,但有缺点:在分布式系统/集群下是无法确保各节点同步,也就是说如果是秒杀等保证库存不超卖的情景下,不能用此方案。当然也可以使用互斥锁(mutex key),两者的机制是一样的。在缓存失效的时候(判断拿出来的值为空),不是立即去load db,而是先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一个mutex key,当操作返回成功时,再进行load db的操作并回设缓存;否则,就重试整个get缓存的方法。 - redis分布式锁
分布式锁实现方式有一般有3种,本文使用Redis来实现
- 数据库乐观锁;
- 基于Redis的分布式锁;
- 基于ZooKeeper的分布式锁
但是锁有几点要求:
- 互斥性。在任意时刻,只有一个客户端能持有锁。
- 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
- 具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
- 一致性。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。
当然还有其他方法,热点数据永不过期;缓存预热,比如秒杀活动开始前,先在redis初始化数据;做好熔断、降级,防止系统崩溃。
缓存穿透
缓存穿透是去访问一个在缓存和数据库中都不存在的数据信息,这时大量的访问可能会压垮数据源。一般情况是黑客攻击,拿着不存在的ID去发送大量的请求。但它的过期时间会很短,最长不超过五分钟。
解决方案
-
设置空值
明白了原理之后,就是在缓存中找不到数据,那么可以将这些key对应的值设置为null或空字符串丢到缓存里面去。 -
布隆过滤器
方法一虽然能解决,但是遇到大量的redis请求,会导致redis内存爆掉,所以就提出了另一钟方法,布隆过滤器。将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被 这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。
布隆过滤器的工作原理:
- 添加key时,每个哈希函数都利用这个key计算出一个哈希值,再根据哈希值计算一个位置,并将位数组中这个位置的值设置为1。
- 询问key时,每个哈希函数都利用这个key计算出一个哈希值,再根据哈希值计算一个位置。然后对比这些哈希函数在位数组中对应位置的数值:
- 如果这几个位置中,有一个位置的值是0,就说明这个布隆过滤器中,不存在这个key。
- 如果这几个位置中,所有位置的值都是1,就说明这个布隆过滤器中,极有可能存在这个key。之所以不是百分之百确定,是因为也可能是其他的key运算导致该位置为1。
缓存雪崩
缓存雪崩是指当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,大量的数据查询会给后端系统(比如DB)带来很大压力。和缓存击穿不同的是:缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。
解决方案
- 增加随机数
一个简单方案就时讲缓存失效时间分散开,比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。 - 队列或者锁
用加锁或者队列的方式保证来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上,但这种方案可能会影响并发量。它只是为了减轻数据库的压力,并没有提高系统吞吐量。所以并不推荐。