在实际的开发过程中,我们经常需要用到缓存。使用缓存常见的一个场景就是key不在缓存中,这个时候我们会去读取这个key对应的值,然后把这个值放到缓存中,代码如下:
public class CacheNoFuture {
private ConcurrentHashMap<String, Object> cache = new ConcurrentHashMap<>();
public Object get(String key) {
Object o = cache.get(key);
if (o == null) {
o = readFromDB(key);
cache.put(key, o);
}
return o;
}
private Object readFromDB(String key) {
return new Object();
}
这个代码有一个比较大的问题就是:如果同一时刻大量的请求发现o是空,都会去调用readFromDB,导致缓存被击穿了,可能的后果就是数据库直接被冲垮。理想的情况是同一个key同一时间只有一个thread去调用readFromDB,其他的thread等待它的结果。我们看一下Cglib包下面的LoadingCache是怎么做的。
1.代码位置
目前我使用的cglib的maven配置如下:
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.2.5</version>
</dependency>
LoadingCache代码在net.sf.cglib.core.internal包下面。
2.代码分析
先看一下它的get方法,还是比较好理解的,有点不一样的是它判断了从map里面取到的内容是不是FutureTask,这个会在后面介绍。接下来我们看一下当从缓存里面读到的数据为空或者为FutureTask的时候,它做了什么。
public V get(K key) {
final KK cacheKey = keyMapper.apply(key);
Object v = map.get(cacheKey);
if (v != null && !(v instanceof FutureTask)) {
return (V) v;
}
return createEntry(key, cacheKey, v);
}
这段代码还是比较好理解的,我觉得理一下我标注的5行基本就差不多了。
- line 1: 从 get 我们知道进入createEntry的条件是v不为空并且v不是FutureTask ,这一行判断v不为空,那只能说明v是FutureTask,所以把v赋值给task,表示目前已经有一个线程在加载数据了。
- line 2: 很多线程正在竞争的去加载数据,但是只有putIfAbsent返回为空的那个成为creator
- line 3、4: 结合line 2的解释,没有竞争成功的,会获得creator的FutureTask(加载没有完成)或者V(加载已经完成)
- line 5:加载完成之后放回到cache里面,这也就是为什么有line 4的原因了。
protected V createEntry(final K key, KK cacheKey, Object v) {
FutureTask<V> task;
boolean creator = false;
if (v != null) { //line 1
// Another thread is already loading an instance
task = (FutureTask<V>) v;
} else {
task = new FutureTask<V>(new Callable<V>() {
public V call() throws Exception {
return loader.apply(key);
}
});
Object prevTask = map.putIfAbsent(cacheKey, task);
if (prevTask == null) { //line 2
// creator does the load
creator = true;
task.run();
} else if (prevTask instanceof FutureTask) { //line 3
task = (FutureTask<V>) prevTask;
} else { //line 4
return (V) prevTask;
}
}
V result;
try {
result = task.get();
} catch (InterruptedException e) {
throw new IllegalStateException("Interrupted while loading cache item", e);
} catch (ExecutionException e) {
Throwable cause = e.getCause();
if (cause instanceof RuntimeException) {
throw ((RuntimeException) cause);
}
throw new IllegalStateException("Unable to load cache item", cause);
}
if (creator) { //line 5
map.put(cacheKey, result);
}
return result;
}
3.总结
这种做法也有明显的不足:
- 加载成功之后,key对应的值就不会再变了,即使我们数据源头发生了变化。