缓存处理流程
前台请求数据,先到缓存(Redis)中去取,如果有则直接返回数据;如果没有,则进一步到数据库中去获取数据。在高并发的场景之下,使用缓存可以快很多;但如果大量的请求越过缓存,请求数据库,那必然会给数据库带来很大的压力,会出现下面所说的缓存穿透、缓存击穿和缓存雪崩的问题。
缓存穿透
缓存穿透是指查询一个不存在的数据,由于缓存是不命中时被动写的。如果从数据库中查不到数据,则不会写入缓存,所以这将导致不存在的数据每次请求都要到DB中去查询,失去了缓存的意义。特别是在流量大的时候,可能数据库就挂掉了。
解决方法:
- 缓存空值,这样不会查询数据库;
- 采用布隆过滤器:将所有可能存在的数据哈希到一个足够大的 bitmap 中,如果查询不存在的数据,则会被这个 bitmap 拦截掉,从而避免了对数据库的查询压力。
布隆过滤器原理:当一个元素被加入集合时,通过K个散列函数将这个元素映射成一个位数组中的K个点,把他们置成1.查询时,将元素通过散列函数映射后得到K个点,如果这些点在位图中有任意一个是0,则被检测元素不存在,直接拦截掉并返回;如果都是1,则查询元素可能存在,就会进一步查询Redis和数据库。
缓存雪崩
缓存雪崩是指我们在设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,此时请求全部转发给数据库,数据库瞬时压力过大挂掉。
解决方法:
- 过期时间打散:在原有的失效时间的基础上增加一个随机值,使得过期时间分散一点,不会在同一个时间失效;
- 设置热点数据不过期:热点数据不过期,就会减小发生雪崩的可能性;
缓存击穿
当大量的请求同时查询一个key的时候,此时正好这个key在缓存中失效了,就会导致大量的请求都落到数据库,致使数据库宕机。
解决方法:
- 设置热点数据不过期;
- 加锁:第一个请求的线程可以拿到锁,拿到锁的线程查询到数据之后设置缓存,其他的线程获取锁失败后会等待50ms,然后重新到缓存中获取数据,这样就可以避免大量的请求落到数据库中。
加锁的代码思想如下:
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的数据,如果在redis中得到了对应的value,直接返回;如果value为空(即数据不在缓存中),则需要进一步查询数据库。此时想查询数据库的线程就要去争夺key的锁,第一个率先得到锁的就会在数据库中得到value并返回,并设置缓存;没有得到锁的则sleep 50ms,然后继续试图从缓存中得到key对应的值,往复循环,直到得到value。