缓存穿透
key对应的数据在数据源并不存在,每次针对此key的请求从缓存获取不到,请求都会到数据源,从而可能压垮数据源。比如用一个不存在的用户id获取用户信息,不论缓存还是数据库都没有,若黑客利用此漏洞进行攻击可能压垮数据库。
解决方案:如果是mysql中没有相关数据,那么针对这个key在redis中给这个key设置一个"NULL",相当于标识数据库中没有这条数据,不要再去做无用的查询了,直接返回结果给方法调用方,同时在mysql插入这条数据的时候要及时清理掉这条缓存,否则这条缓存将成为脏数据,其次,再设置这条缓存的时候也要设置过期时间,防止这个缓存一直存在
缓存击穿
某一时间缓存过期,如果期间大量的请求到来,且遇到了慢sql查询,那么会使得一瞬间mysql压力陡增
解决方案:使用分布式锁,使用业务锁标识 + 缓存中的key拼接作为锁,高并发场景下,第一个线程抢占了这个锁,即通过setnx对这个锁设置了值,才能去查mysql,并将结果存到缓存,其余线程如果是要使用同样的key查询,那么再获取锁的,即通过setnx对这个锁设置值的时候失败,这种时候可以睡眠一段时间重新递归调用,可以限制最大重试次数,如果超过最大次数说明次数分布式锁出现异常,打印日志并避开缓存直接去查mysql获取结果
缓存雪崩
当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,也会给后端系统(比如DB)带来很大压力。有一个简单方案就时将缓存失效时间分散开,比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。或者Redis查DB数据的时候将请求加一个随机延迟时间,让请求不要集中。
标准代码
private static Jedis jedis;
/**
*
* @param key:redis缓存中的键
* @param supplier:供给者对象,它的匿名实现类的get方法中执行sql方法,并返回查询mysql的结果
* @param expiredTime:过去时间,不宜太短,否则遇到慢sql
* @return
*/
public static <T> Object getData(String key, Supplier<T> supplier, long expiredTime, int count) throws InterruptedException {
Object data = jedis.get(key);
if (data != null) {
// 如果在缓存中找到数据NULL,说明当前数据库中也没有这个值,直接返回,预防缓存穿透
if ("NULL".equals(data)) {
return null;
} else {
log.info("get data from redis:{}", key);
return data;
}
} else {
// 此处相当于利用setnx的特征模拟出一个类似于争夺锁的场景,只有获取到锁的线程,才会去数据库中取数据,并设置到缓存,
// 其它线程统一先睡一段时间再去尝试去缓存中取数据,用来预防缓存击穿
// 此处key的过期时间不宜太短,否则遇到慢sql,再未执行完慢sql时,且当前线程执行方法因为循环没有返回null,那么这个线程
// 会获取到这个锁,再去执行慢sql,这样即使之前的线程已经执行完毕了,但循环睡眠中的线程依然获取不到锁,那这样预防,这样也会一定程度上
if ("OK".equals(jedis.set(key, "lock", new SetParams().nx().ex(180l)))) {
//执行sql
T t = supplier.get();
log.info("get data from db key:{}", key);
// 如果在缓存中没有找到数据,查询数据库
if (t != null) {
String json = new Gson().toJson(t);
// 缓存时间在设置的时间expiredTime的基础上加上一个上限为1/10过期时间的随机数。防止缓存雪崩
long time = expiredTime + new Random(expiredTime / 10).nextLong();
// 如果在数据库中找到数据,将数据缓存到Redis,并返回数据
jedis.setex(key, time, json);
return json;
} else {
// 如果在数据库中也没有找到数据,将"NULL"缓存到Redis,并返回null,这样可以防止缓存击透
jedis.setex(key, 60, "NULL");
return null;
}
} else {
Thread.sleep(50);
data = jedis.get(key);
if (Objects.nonNull(data)) {
return data;
}
while (count-- > 0) {
getData(key, supplier, expiredTime, count);
}
// 这种情况可能分布式锁存在问题,或执行可能是存在问题,打印报警日志,并尝试去库中获取结果
if (count == -1) {
T t = supplier.get();
log.warn()
}
}
}
return data;
}