Cglib中LoadingCache原始碼分析

Lorenzo君發表於2018-08-13

  在實際的開發過程中,我們經常需要用到快取。使用快取常見的一個場景就是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的解釋,沒有競爭成功的,會獲得creatorFutureTask(載入沒有完成)或者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對應的值就不會再變了,即使我們資料來源頭髮生了變化。

相關文章