在實際的開發過程中,我們經常需要用到快取。使用快取常見的一個場景就是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對應的值就不會再變了,即使我們資料來源頭髮生了變化。