首先,在将缓存击穿前,大家先来回忆下自己写缓存的方案,这里我简单画了下流程图:
当我们缓存key设置过期时间,恰巧在这一刻这个key在某一刻被高并发的访问,把所有的请求都打到了DB中这就可能会导致DB挂了。这个跟后面说的缓存雪崩非常相似,这个和缓存雪崩的区别在于这里针对某一key缓存,但是雪崩则指的是多个key,要解决方案有很多,比如让一个线程构建缓存,另外线程等待知道构建好,或者redis维护timeout字段逻辑失效等等
1、使用同步方式锁,单机用synchronized,lock可重入锁等,分布式则用redis的setnx。
String get(String key) {
String value = redis.get(key);
if (value == null) {
String brokenKey = "broken_"+key;
if (redis.setnx(brokenKey, "1")) {
redis.expire(brokenKey, 60) //防止产生key不失效
value = db.get(key);
redis.set(key, value);
redis.del(brokenKey);
} else {
//其他线程休息50毫秒后重试
Thread.sleep(50);
get(key);
}
}
return value;
}
优点: 保证一致性
缺点:代码复杂度增大,存在死锁的风险
2、采用缓存物理不过期,逻辑过期方式,在key对应的value设置一个timeout字段。如果检测到存的时间超过过期时间则异步更新缓存。
String get(final String key) {
T t = redis.get(key);
String value = t.getValue();
long timeout = t.getTimeout();
// 当逻辑的超时时间到了时,异步构建缓存
if (timeout <= System.currentTimeMillis()) {
threadPool.execute(new Runnable() {
public void run() {
String brokenKey = "broken_"+key;
if (redis.setnx(brokenKey, "1")) { //redis加锁
redis.expire(brokenKey, 60); //防止产生key不失效
String dbValue = db.get(key);
redis.set(key, dbValue);
redis.del(brokenKey);
}
}
});
}
return value;
}
优点: 不会阻塞线程 高性能
缺点:数据库和缓存可能会存在数据不一致
看了上面介绍的缓存击穿了,现在怎么又出现了一个缓存穿透呢,感觉字面上穿透和击穿应该是一个意思啊,确实个人理解翻译上是差不多,但是对于redis的专业技术语上却是不同,缓存穿透指的是一些恶意人攻击,比如说登录的时候它请求一个一定不存在的用户名时,那么我们服务端正常应该是这样处理,首先到缓存中查询,没有在到数据查询,看看好像是没问题啊,查询一下应该很快,如果是这一刻恶意攻击发起几百万次请求,所以就会一直循环这个操作,一定不存在的用户名会一直查询数据库,那么你的数据库和缓存系统会不会挂呢?
1、布隆过滤器处理,把key放到布隆过滤器中,获取时查看是否存在,如果存在则获取缓存、获取数据库(把key放入缓存又2种方案,1、业务系统初始化 2、缓存中获取不到时单条插入到布隆过滤器)
优点: 高性能
缺点:布隆过滤器不支持删除,需要单独维护一个缓存key的集合
2、查询数据库返回NULL时,不在是不放到redis中,而是redis中设置查询结果为空的标示,比如:redis.set("key_none","NONE")
优点: 简单明了
缺点:需要为每个key再单独维护一个标示空的key
某个key访问非常频繁,当key在某个时刻失效的时候有大量线程来构建缓存,导致负载增加,系统崩溃。
热点key也是缓存失效打到数据库,同缓存击穿一样的解决方案
缓存雪崩上面又提到过和缓存击穿类似,雪崩指的是多个key同时失效,当高并发的请求过来所有请求都打到数据库导致挂了
1、尽量的把key的失效时间设置成不一样,这样可以减少并发带来的压力
2、同上缓存击穿方案