前段時間,運營反饋某管理後臺偶發響應時間超長。經過查看日誌,定位到問題主要是在權限驗證時發生了大量的數據庫查詢,該後臺的權限管理使用了Guava中的Cache組件緩存用戶信息,同時設置了緩存過時時間爲20s,當緩存過時後的第一個請求會觸發緩存刷新,從數據庫中獲取大量的用戶信息到內存中,這就致使了響應時間過長。所以趁着這個機會,閱讀下Guava Cache更新緩存的相關源碼。數據庫
經過查閱Guava Cache的官方文檔瞭解到,更新緩存的方式有兩種緩存
而這兩種方法的具體實現原理及區別是什麼呢,下面咱們經過閱讀源碼進行了解。異步
經過調用Cache的get方法進行debug,逐行往下看,咱們不難定位到下面截取的源碼的第10行這裏,前面先是獲取了該key值對應的entry,而後將該entry和當前時間now做爲參數調用getLiveValue方法,這個方法的做用是從entry中獲取value,若已過時,則返回null。ui
1 V get(K key, int hash, CacheLoader<? super K, V> loader) throws ExecutionException { 2 checkNotNull(key); 3 checkNotNull(loader); 4 try { 5 if (count != 0) { 6 ReferenceEntry<K, V> e = getEntry(key, hash); 7 if (e != null) { 8 long now = map.ticker.read(); 9 // 從entry中獲取value,若已過時,則返回null 10 V value = getLiveValue(e, now); 11 if (value != null) { 12 recordRead(e, now); 13 statsCounter.recordHits(1); 14 // 定時刷新 15 return scheduleRefresh(e, key, hash, value, now, loader); 16 } 17 ValueReference<K, V> valueReference = e.getValueReference(); 18 if (valueReference.isLoading()) { 19 return waitForLoadingValue(e, key, valueReference); 20 } 21 } 22 } 23 // 返回null或者緩存過時時會調用這個方法,加鎖獲取或load 24 return lockedGetOrLoad(key, hash, loader); 25 }
咱們接着往下走,發現方法下面的在第13行進行了是否過時的判斷,接着進入isExpired(ReferenceEntry<K, V> entry, long now)方法中,這個方法比較簡單,先判斷是否有設置expireAfterAccess或expireAfterWrite,而後經過當前時間和對應的訪問或寫操做時間的時間差值與設置的過時時間進行對比。當緩存過時時,getLiveValue返回null,最後會調用lockedGetOrLoad方法加載新值。spa
1 V getLiveValue(ReferenceEntry<K, V> entry, long now) { 2 if (entry.getKey() == null) { 3 tryDrainReferenceQueues(); 4 return null; 5 } 6 V value = entry.getValueReference().get(); 7 if (value == null) { 8 tryDrainReferenceQueues(); 9 return null; 10 } 11 12 // 判斷entry是否過時 13 if (map.isExpired(entry, now)) { 14 tryExpireEntries(now); 15 return null; 16 } 17 return value; 18 }
1 boolean isExpired(ReferenceEntry<K, V> entry, long now) { 2 checkNotNull(entry); 3 if (expiresAfterAccess() && (now - entry.getAccessTime() >= expireAfterAccessNanos)) { 4 return true; 5 } 6 if (expiresAfterWrite() && (now - entry.getWriteTime() >= expireAfterWriteNanos)) { 7 return true; 8 } 9 return false; 10 }
當緩存沒過時或沒設置過時時間時,getLiveValue返回不爲null,這時會調用scheduleRefresh()方法。這個方法首先去判斷是否設置了定時刷新和是否超過了設定的刷新時間,而後判斷當前的ValueReference是否爲LoadingValueReference,條件都成立的話,會調用refresh()方法,這個方法首先會將該entry的ValueReference設爲上面提到的LoadingValueReference,表示該緩存項處於loading狀態,以後進行load的操做。從這裏能夠看出,loading標識保證了同一緩存項,只會存在一個線程進行refresh時的load操做,在load未完成期間,其餘訪問該緩存項的線程都會直接返回oldValue。線程
1 V scheduleRefresh( 2 ReferenceEntry<K, V> entry, 3 K key, 4 int hash, 5 V oldValue, 6 long now, 7 CacheLoader<? super K, V> loader) { 8 if (map.refreshes() 9 && (now - entry.getWriteTime() > map.refreshNanos) 10 && !entry.getValueReference().isLoading()) { 11 V newValue = refresh(key, hash, loader, true); 12 if (newValue != null) { 13 return newValue; 14 } 15 } 16 return oldValue; 17 }
1 V refresh(K key, int hash, CacheLoader<? super K, V> loader, boolean checkTime) { 2 final LoadingValueReference<K, V> loadingValueReference = 3 insertLoadingValueReference(key, hash, checkTime); 4 if (loadingValueReference == null) { 5 return null; 6 } 7 8 ListenableFuture<V> result = loadAsync(key, hash, loadingValueReference, loader); 9 if (result.isDone()) { 10 try { 11 return Uninterruptibles.getUninterruptibly(result); 12 } catch (Throwable t) { 13 // don't let refresh exceptions propagate; error was already logged 14 } 15 } 16 return null; 17 }
從上面的源碼閱讀能夠知道,緩存定時過時和緩存定時刷新這兩種更新緩存的方式的主要區別是,前者緩存過時時,可能會存在多個線程同時進行load操做,阻塞多個線程,然後者更新緩存時,會標記loading狀態,這時只會阻塞一個線程,其餘線程繼續返回舊值,同時咱們能夠經過CacheLoader的reload(K key, V oldValue)方法來進行異步刷新。debug