Guava Cache

年糕媽媽技術團隊發表於2019-03-01
當專案中需要使用local cache的時候,一般都會通過基於 ConcurrentHashMap或者LinkedHashMap來實現自己的LRU Cache。在造輪子過程中,一般都需要解決一下問題:
1. 記憶體是有限了,所以需要限定快取的最大容量.
2. 如何清除“太舊”的快取entry.
3. 如何應對併發讀寫.
4.快取資料透明化:命中率、失效率等.
cache的優劣基本取決於如何優雅高效地解決上面這些問題。Guava cache很好地解決了這些問題,是一個非常好的本地快取,執行緒安全,功能齊全,簡單易用,效能好。整體上來說Guava cache 是本地快取的不二之選。
下面是一個簡單地例子:
LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
                .maximumSize(1000).expireAfterWrite(10, TimeUnit.MINUTES)
                .removalListener(MY_LISTENER)                .build(new CacheLoader<Key, Graph>() {
                    public Graph load(Key key) throws AnyException {
                        return createExpensiveGraph(key);                    }                });複製程式碼
接下來,我們從多個角度來介紹如何使用Guava cache。
一、建立cache:
一般來說,在工作中我們一般這樣使用remote cache或者local cache:
User user = cache.get(usernick); if(user == null){ user = userDao.getUser(usernick); cache.put(usernick, user);
} return user;
即if cached, return; otherwise create/load/compute, cache and return。
而Guava cache 通過下面兩種方式以一種更優雅的方式實現了這個邏輯:
1. From A CacheLoader
2. From A Callable
通過這兩種方法建立的cache,和普通的用map快取相比,不同在於,都實現了上面提到的——“if cached, return; otherwise create/load/compute, cache and return”。但不同的在於cacheloader的定義比較寬泛,是針對整個cache定義的,可以認為是統一的根據key值load value的方法。而callable的方式較為靈活,允許你在get的時候指定。舉兩個栗子來介紹如何使用這兩種方式
From CacheLoader:
LoadingCache<String, String> graphs = CacheBuilder.newBuilder().maximumSize(1000)
                .build(new CacheLoader<String, String>() {
                    public String load(String key) {
                        // 這裡是key根據實際去取值的方法,例如根據這個key去資料庫或者通過複雜耗時的計算得出
                        System.out.println("no cache,load from db");                        return "123";                    }                });        String val1 = graphs.get("key");        System.out.println("1 value is: " + val1);        String val2 = graphs.get("key");        System.out.println("2 value is: " + val2);From Callable:               Cache<String, String> cache = CacheBuilder.newBuilder()
                .maximumSize(1000).build();        String val1 = cache.get("key", new Callable<String>() {
            public String call() {
                // 這裡是key根據實際去取值的方法,例如根據這個key去資料庫或者通過複雜耗時的計算得出
                System.out.println("val call method is invoked");                return "123";            }        });        System.out.println("1 value is: " + val1);        String val2 = cache.get("testKey", new Callable<String>() {
            public String call() {
                // 這裡是key根據實際去取值的方法,例如根據這個key去資料庫或者通過複雜耗時的計算得出
                System.out.println("val call method is invoked");                return "123";            }        });        System.out.println("2 value is: " + val2);複製程式碼
需要注意的是,所有的Guava caches,不論是否是loader模式,都支援get(Key,Callable<V>)方法。
另外,除了上述這兩種方式來更新快取外,Guava cache當然也支援Inserted Directly:Values也可以通過cache.put(key,value)直接將值插入到cache中。該方法將覆蓋key對應的entry。
二、快取移除
記憶體是有限,所以不能把所有的東西都載入到記憶體中,過大的local cache對任何java應用來說都是噩夢。因此local cache必須提供不同的機制來清除“不必要”的快取entry,平衡記憶體使用率和命中率。Guava Cache提供了3中快取清除策略:size-based eviction, time-based eviction, and reference-based eviction.
size-based eviction:基於cache容量的移除。如果你的cache不允許擴容,即不允許超過設定的最大值,那麼使用CacheBuilder.maxmuSize(long)即可。在這種條件下,cache會自己釋放掉那些最近沒有或者不經常使用的entries記憶體。這裡需要注意一下兩點:
1.並不是在超過限定時才會刪除掉那些entries,而是在即將達到這個限定值時,那麼你就要小心考慮這種情況了,因為很明顯即使沒有達到這個限定值,cache仍然會進行刪除操作。
2.如果一個key-entry已經被移除了,當你再次呼叫get(key)時,如果CacheBuilder採用的是CacheLoader模式,那依然會從cacheLoader中載入一次。
此外,如果你的cache裡面的entries有著截然不同的記憶體佔用如果你的cache values有著截然不同的記憶體佔用,你可以通過CacheBuilder.weigher(Weigher)來為不同的entry設定weigh,然後使用CacheBuilder.maximumWeight(long)設定一個最大值。在tpn會通過local cache快取使用者對訊息類目的訂閱資訊,有的使用者訂閱的訊息類目比較多,所佔的記憶體就比較多,有的使用者訂閱的訊息類目比較少,自然佔用的記憶體就比較少。那麼我就可以通過下面的方法來根據使用者訂閱的訊息類目數量設定不同的weight,這樣就可以在不更改cache大小的情況下,使得快取儘量覆蓋更多地使用者:
LoadingCache<Key, User> Users= CacheBuilder.newBuilder()
       .maximumWeight(100000)       .weigher(new Weigher<Key, User>() {
          public int weigh(Key k, User u) {
               if(u.categories().szie() >5){
                    return 2;               }else{                    return 1;               }          }        })       .build(           new CacheLoader<Key, User>() {
             public Userload(Key key) { // no checked exception
               return createExpensiveUser(key);             }           });複製程式碼
PS:這個例子可能不是很恰當,當時足以說明weight的用法。
time-based eviction:基於時間的移除。Guava cache提供了兩種方法來實現這個邏輯:
1. expireAfterAccess(long, TimeUnit)
從最後一次訪問(讀或者寫)開始計時,過了這段指定的時間就會釋放掉該entries。注意:那些被刪掉的entries的順序時和size-based eviction是十分相似的。
2. expireAfterWrite(long,TimeUnit)
從entries被建立或者最後一次被修改值的點來計時的,如果從這個點開始超過了那段指定的時間,entries就會被刪除掉。這點設計的很精明,因為資料會隨著時間變得越來越陳舊。
如果想要測試Timed Eviction,使用Ticker interface和CacheBuilder.ticker(Ticker)方法對你的cache設定一個時間即可,那麼你就不需要去等待系統時間了。
reference-based eviction:基於引用的移除。Guava為你準備了entries的垃圾回收器,對於keys或者values可以使用weak reference ,對於values可以使用soft reference.
1. CacheBuilder.weakKeys(): 通過weak reference儲存keys。在這種情況下,如果keys沒有被strong或者soft引用,那麼entries會被垃圾回收。
2. CacheBuilder.weakValues() : 通過weak referene 儲存values.在這種情況下,如果valves沒有被strong或者soft引用,那麼entries會被垃圾回收。
需要注意的是:這種條件下的垃圾回收器是建立在引用之上的,那麼這會造成整個cache是使用==來比較倆個key的,而不是equals();
除了上面這三種方式來移除cache的enties外,還可以通過以下3個方法來主動釋放一些enties:
1. 單獨移除用: Cache.invalidate(key)
2. 批量移除用 :Cache.invalidateAll(keys)
3. 移除所有用 :Cache.invalidateAll()
如果需要在移除資料的時候有所動作還可以定義Removal Listener,但是有點需要注意的是預設Removal Listener中的行為是和移除動作同步執行的,如果需要改成非同步形式,可以考慮使用RemovalListeners.asynchronous(RemovalListener, Executor)。
最後我們來看一下Guava Cache是什麼時候執行清理動作的。通過CacheBuilder建立的cache既不會自動執行清理和移除entry,也不會在entry過期後立馬執行清除操作。相反,其在執行寫操作或者讀操作的時候(在寫操作非常少的情況下)來通過少量的操作來執行清理工作。這樣做的原因是:如果我們要不斷進行快取的清理和移除,我們需要建立一個執行緒,其業務將與使用者的操作來爭奪共享鎖。此外,某些環境限制清理執行緒的建立,這將使CacheBuilder無法使用在該環境中。 因此,Guava cache將何時清理的選擇權交給使用者。如果你的快取是面向高吞吐量應用的,那麼你不必擔心執行快取維護,清理過期的entries等。如果你的快取是面向讀多寫少的應用,為了避免影響快取讀取,那麼就可以建立自己的維護執行緒每隔一段時間就呼叫Cache.cleanUp(),如可以用ScheduledExecutorService安排維修。
如果你想安排定期維護快取的快取中很少有寫,只是用ScheduledExecutorService安排維修。
三、統計功能:
統計功能是Guava cache一個非常實用的特性。可以通過CacheBuilder.recordStats() 方法啟動了 cache的資料收集:
1. Cache.stats(): 返回了一個CacheStats物件, 提供一些資料方法
2. hitRate(): 請求點選率
3. averageLoadPenalty(): 載入new value,花費的時間, 單位nanosecondes
4. evictionCount(): 清除的個數 

相關文章