前言
缓存(内存 or Memcached or Redis.....)在互联网项目中广泛应用,本篇博客将讨论下缓存击穿这一个话题,涵盖缓存击穿的现象、解决的思路、以及通过代码抽象方式来处理缓存击穿。
什么是缓存击穿?
缓存击穿上面的代码,是一个典型的写法:当查询的时候,先从Redis集群中取,如果没有,那么再从DB中查询并设置到Redis集群中。
注意,在实际开发中,我们一般在缓存中,存储的数据结构是JSON。(JDK提供的序列化方式效率稍微比JSON序列化低一些;而且JDK序列化非常严格,字段的增减,就很可能导致反序列失败,而JSON这方面兼容性较好)
假设从DB中查询需要2S,那么显然这段时间内过来的请求,在上述的代码下,会全部走DB查询,相当于缓存被直接穿透,这样的现象就称之为“缓存击穿”!
避免缓存击穿的思路分析
加synchronized?
同步方式如果synchronized加在方法上,使得查询请求都得排队,本来我们的本意是让并发查询走缓存。也就是现在synchronized的粒度太大了。
缩小synchronized的粒度?
缩小粒度上面代码,在缓存有数据时,让查询缓存的请求不必排队,减小了同步的粒度。但是,仍然没有解决缓存击穿的问题。
虽然,多个查询DB的请求进行排队,但是即便一个DB查询请求完成并设置到缓存中,其他查询DB的请求依然会继续查询DB!
synchronized+双重检查机制
双重检查
通过synchronized+双重检查机制:
在同步块中,继续判断检查,保证不存在,才去查DB。
代码抽象
发现没有,其实我们处理缓存的代码,除了具体的查询DB逻辑外,其他都是模板化的。下面我们就来抽象下!
一个查询DB的接口:
CacheLoader既然查询具体的DB是由业务来决定的,那么暴露这个接口让业务去实现它。
一个模板:
TemplateSpring不是有很多Template类么?我们也可以通过这种思想对代码进行一个抽象,让外界来决定具体的业务实现,而把模板步骤写好。(有点类似AOP的概念)
改进后的代码:
改进后的调用代码从这里可以看出,我们并不关心缓存的数据从哪里加载,而是交给具体的使用方,而且使用方在使用时再也不必关注缓存击穿的问题,因为我们都给抽象了。
好了,到这里,关于缓存击穿就讨论到这里。
2017.10.28 zhangfengzhe
网友评论
或许可以这样改进,我们的查询始终是到缓存里面取,如果取不到,不要去db里面查了;存在另一个线程负责定时更新缓存(从db同步至缓存);这样即便db中不存在对应查询项,那么即便存在攻击,从缓存查询也很快,问题不大,实际应用中,对于攻击,很多时候在nginx这层就拦截住了,不会让攻击传导到查询缓存这个步骤了。
/**
* Cache loader <br/>
* Load data from basic record system,such as mysql , hbase ...
*
* @Author: yicai.liu
*/
public abstract class CacheLoader<T> {
public TypeReference<T> rawType() {
return new TypeReference<T>() {
};
}
/**
* Load
*
* @Return data
*/
public abstract T load();
}
在方法内获取:
TypeReference<V> clazz = cacheLoader.rawType();
不知道我有没有描述清楚,希望您回答一下。
谢谢
Striped<Lock> striped = Striped.lazyWeakLock(1024);
Lock lock = striped.get(key);
try{
lock.lock();
//process business
}finally{
lock.unlock();
}
类似的文章:
https://stackoverflow.com/questions/11124539/how-to-acquire-a-lock-by-a-key
https://stackoverflow.com/questions/5639870/simple-java-name-based-locks/28723518#28723518