缓存在应用中的处理流程
前台请求,后台先从缓存中取数据,取到直接返回结果,取不到时再从数据库中取,数据库取到更新缓存,并返回结果,数据库也没取到,那直接返回空结果
看似简单的处理流程;然而在一个分布式应用中,只要是引入了缓存,为了保证系统的健壮性,在缓存系统设计时,就必须要考虑缓存击穿,缓存穿透,缓存雪崩的问题及解决方案
缓存击穿
缓存击穿:缓存中没有数据,但数据库中有数据。(一般是缓存时间到期导致缓存失效)这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力,甚至可能压垮数据库
解决方案
- 设置热点数据不过期:直接将缓存设置为不过期,然后由定时任务去异步加载数据,更新缓存。这种方式适用于比较极端的场景,例如流量特别特别大的场景,使用时需要考虑业务能接受数据不一致的时间,还有就是异常情况的处理,不要到时候缓存刷新不上,一直是脏数据,那就凉了
- 加互斥锁:在并发的多个请求中,只有第一个请求(或前几个请求)线程能拿到锁并执行数据库查询操作,其他的线程拿不到锁就阻塞等着,等到第一个线程将数据写入缓存后,后面的请求直接从缓存中取数据。对于互斥锁的选择,可以是
Java API
层面的Lock
或是JVM
的synchronized
或是分布式锁,只要保证数据库的请求能大大降低即可。注意加锁的维度要按key
维度去加锁
使用分布式锁的伪代码,仅供参考
public Object getData(String key) throws InterruptedException {
Object value = redis.get(key);
// 缓存值过期
if (value == null) {
// lockRedis:专门用于加锁的redis;
// "empty":加锁的值随便设置都可以
if (lockRedis.set(key, "empty", "PX", lockExpire, "NX")) {
try {
// 查询数据库,并写到缓存,让其他线程可以直接走缓存
value = getDataFromDb(key);
redis.set(key, value, "PX", expire);
} catch (Exception e) {
// 异常处理
} finally {
// 释放锁
lockRedis.delete(key);
}
} else {
// sleep50ms后,进行重试
Thread.sleep(50);
return getData(key);
}
}
return value;
}
缓存穿透
缓存穿透:查询一个数据,发现缓存中没有;于是查询数据库,发现也没有。而后面用户不断发起请求,缓存没有命中,于是都去请求了数据库。这给数据库造成很大的压力,引起数据库压力瞬间增大,造成过大压力,甚至可能压垮数据库
解决方案
- 接口校验:在正常业务流程中可能会存在少量访问不存在
key
的情况,但是一般不会出现大量的情况,所以这种场景最大的可能性是遭受了非法攻击。可以在最外层先做一层校验:用户鉴权、数据合法性校验等,例如用户鉴权校验,id
做基础校验,id<=0
的直接拦截 - 缓存空值:从缓存取不到数据,在数据库中也没有取到,这时也可以将
key-value
对写为key-null
,缓存有效时间可以设置短点,如30
秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id
暴力攻击 - 布隆过滤器:使用布隆过滤器存储所有可能访问的
key
,不存在的key
直接被过滤,存在的key
则再进一步查询缓存和数据库
布隆过滤器
布隆过滤器的特点是判断不存在的,则一定不存在;判断存在的,大概率存在,但也有小概率不存在。并且这个概率是可控的,我们可以让这个概率变小或者变高,取决于用户本身的需求
布隆过滤器由一个 bitSet
和 一组 Hash
函数(算法)组成,是一种空间效率极高的概率型算法和数据结构,主要用来判断一个元素是否在集合中存在
在初始化时,bitSet
的每一位被初始化为 0
,同时会定义 Hash
函数,例如有 3
组 Hash
函数:hash1、hash2、hash3
写入流程
当我们要写入一个值时,过程如下,以 jionghui
为例
- 首先将
jionghui
跟3
组Hash
函数分别计算,得到bitSet
的下标为:1、7、10
- 将
bitSet
的这3
个下标标记为1
假设我们还有另外两个值:java
和 diaosi
,按上面的流程跟 3
组 Hash
函数分别计算,结果如下
java
:Hash
函数计算bitSet
下标为:1、7、11
diaosi
:Hash
函数计算bitSet
下标为:4、10、11
查询流程
当我们要查询一个值时,过程如下,同样以 jionghui
为例
- 首先将
jionghui
跟3
组Hash
函数分别计算,得到bitSet
的下标为:1、7、10
- 查看
bitSet
的这3
个下标是否都为1
,如果这3
个下标不都为1
,则说明该值必然不存在,如果这3
个下标都为1
,则只能说明可能存在,并不能说明一定存在
其实上图的例子已经说明了这个问题了,当我们只有值 jionghui
和 diaosi
时,bitSet
下标为 1
的有:1、4、7、10、11
。当我们又加入值 java
时,bitSet
下标为 1
的还是这 5
个,所以当 bitSet
下标为 1
的为:1、4、7、10、11
时,我们无法判断值 java
存不存在
其根本原因是,不同的值在跟 Hash
函数计算后,可能会得到相同的下标,所以某个值的标记位,可能会被其他值给标上了。这也是为啥布隆过滤器只能判断某个值可能存在,无法判断必然存在的原因。但是反过来,如果该值根据 Hash
函数计算的标记位没有全部都为 1
,那么则说明必然不存在,这个是肯定的
缓存雪崩
缓存雪崩:当某一时刻发生大规模的缓存失效的情况,比如你的缓存服务宕机了,会有大量的请求进来直接打到 DB
上,这样可能导致整个系统的崩溃,称为雪崩
解决方案
- 过期时间打散:既然是大量缓存集中失效,那最容易想到就是让他们不集中生效。可以给缓存的过期时间时加上一个随机值时间,使得每个
key
的过期时间分布开来,不会集中在同一时刻失效 - 热点数据不过期:该方式和缓存击穿一样,也是要着重考虑刷新的时间间隔和数据异常如何处理的情况
参考:https://blog.csdn.net/v123411739/article/details/115058811