• <input id="qucwm"><u id="qucwm"></u></input>
  • <menu id="qucwm"></menu>
  • <input id="qucwm"><tt id="qucwm"></tt></input>
  • <input id="qucwm"><acronym id="qucwm"></acronym></input>
  • [Google Guava] 3-緩存

    原文地址? 譯文地址??? 譯者:許巧輝 ?校對:沈義揚

    范例

    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與ConcurrentMap很相似,但也不完全一樣。最基本的區別是ConcurrentMap會一直保存所有添加的元素,直到顯式地移除。相對地,Guava Cache為了限制內存占用,通常都設定為自動回收元素。在某些場景下,盡管LoadingCache 不回收元素,它也是很有用的,因為它會自動加載緩存。

    通常來說,Guava Cache適用于:

    • 你愿意消耗一些內存空間來提升速度。
    • 你預料到某些鍵會被查詢一次以上。
    • 緩存中存放的數據總量不會超出內存容量。(Guava Cache是單個應用運行時的本地緩存。它不把數據存放到文件或外部服務器。如果這不符合你的需求,請嘗試Memcached這類工具)

    如果你的場景符合上述的每一條,Guava Cache就適合你。

    如同范例代碼展示的一樣,Cache實例通過CacheBuilder生成器模式獲取,但是自定義你的緩存才是最有趣的部分。

    :如果你不需要Cache中的特性,使用ConcurrentHashMap有更好的內存效率——但Cache的大多數特性都很難基于舊有的ConcurrentMap復制,甚至根本不可能做到。

    加載

    在使用緩存前,首先問自己一個問題:有沒有合理的默認方法來加載或計算與鍵關聯的值?如果有的話,你應當使用CacheLoader。如果沒有,或者你想要覆蓋默認的加載運算,同時保留"獲取緩存-如果沒有-則計算"[get-if-absent-compute]的原子語義,你應該在調用get時傳入一個Callable實例。緩存元素也可以通過Cache.put方法直接插入,但自動加載是首選的,因為它可以更容易地推斷所有緩存內容的一致性。

    CacheLoader

    LoadingCache是附帶CacheLoader構建而成的緩存實現。創建自己的CacheLoader通常只需要簡單地實現V load(K key) throws Exception方法。例如,你可以用下面的代碼構建LoadingCache:

    LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
            .maximumSize(1000)
            .build(
                new CacheLoader<Key, Graph>() {
                    public Graph load(Key key) throws AnyException {
                        return createExpensiveGraph(key);
                    }
                });
    
    ...
    try {
        return graphs.get(key);
    } catch (ExecutionException e) {
        throw new OtherException(e.getCause());
    }
    

    從LoadingCache查詢的正規方式是使用get(K)方法。這個方法要么返回已經緩存的值,要么使用CacheLoader向緩存原子地加載新值。由于CacheLoader可能拋出異常,LoadingCache.get(K)也聲明為拋出ExecutionException異常。如果你定義的CacheLoader沒有聲明任何檢查型異常,則可以通過getUnchecked(K)查找緩存;但必須注意,一旦CacheLoader聲明了檢查型異常,就不可以調用getUnchecked(K)。

    LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
            .expireAfterAccess(10, TimeUnit.MINUTES)
            .build(
                new CacheLoader<Key, Graph>() {
                    public Graph load(Key key) { // no checked exception
                        return createExpensiveGraph(key);
                    }
                });
    
    ...
    return graphs.getUnchecked(key);
    

    getAll(Iterable<? extends K>)方法用來執行批量查詢。默認情況下,對每個不在緩存中的鍵,getAll方法會單獨調用CacheLoader.load來加載緩存項。如果批量的加載比多個單獨加載更高效,你可以重載CacheLoader.loadAll來利用這一點。getAll(Iterable)的性能也會相應提升。

    注:CacheLoader.loadAll的實現可以為沒有明確請求的鍵加載緩存值。例如,為某組中的任意鍵計算值時,能夠獲取該組中的所有鍵值,loadAll方法就可以實現為在同一時間獲取該組的其他鍵值。校注:getAll(Iterable<? extends K>)方法會調用loadAll,但會篩選結果,只會返回請求的鍵值對。

    Callable

    所有類型的Guava Cache,不管有沒有自動加載功能,都支持get(K, Callable<V>)方法。這個方法返回緩存中相應的值,或者用給定的Callable運算并把結果加入到緩存中。在整個加載方法完成前,緩存項相關的可觀察狀態都不會更改。這個方法簡便地實現了模式"如果有緩存則返回;否則運算、緩存、然后返回"。

    Cache<Key, Graph> cache = CacheBuilder.newBuilder()
            .maximumSize(1000)
            .build(); // look Ma, no CacheLoader
    ...
    try {
        // If the key wasn't in the "easy to compute" group, we need to
        // do things the hard way.
        cache.get(key, new Callable<Key, Graph>() {
            @Override
            public Value call() throws AnyException {
                return doThingsTheHardWay(key);
            }
        });
    } catch (ExecutionException e) {
        throw new OtherException(e.getCause());
    }
    

    顯式插入

    使用cache.put(key, value)方法可以直接向緩存中插入值,這會直接覆蓋掉給定鍵之前映射的值。使用Cache.asMap()視圖提供的任何方法也能修改緩存。但請注意,asMap視圖的任何方法都不能保證緩存項被原子地加載到緩存中。進一步說,asMap視圖的原子運算在Guava Cache的原子加載范疇之外,所以相比于Cache.asMap().putIfAbsent(K,
    V),Cache.get(K, Callable<V>) 應該總是優先使用。

    緩存回收

    一個殘酷的現實是,我們幾乎一定沒有足夠的內存緩存所有數據。你你必須決定:什么時候某個緩存項就不值得保留了?Guava Cache提供了三種基本的緩存回收方式:基于容量回收、定時回收和基于引用回收。

    基于容量的回收(size-based eviction)

    如果要規定緩存項的數目不超過固定值,只需使用CacheBuilder.maximumSize(long)。緩存將嘗試回收最近沒有使用或總體上很少使用的緩存項?!?em>警告:在緩存項的數目達到限定值之前,緩存就可能進行回收操作——通常來說,這種情況發生在緩存項的數目逼近限定值時。

    另外,不同的緩存項有不同的“權重”(weights)——例如,如果你的緩存值,占據完全不同的內存空間,你可以使用CacheBuilder.weigher(Weigher)指定一個權重函數,并且用CacheBuilder.maximumWeight(long)指定最大總重。在權重限定場景中,除了要注意回收也是在重量逼近限定值時就進行了,還要知道重量是在緩存創建時計算的,因此要考慮重量計算的復雜度。

    LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
            .maximumWeight(100000)
            .weigher(new Weigher<Key, Graph>() {
                public int weigh(Key k, Graph g) {
                    return g.vertices().size();
                }
            })
            .build(
                new CacheLoader<Key, Graph>() {
                    public Graph load(Key key) { // no checked exception
                        return createExpensiveGraph(key);
                    }
                });
    

    定時回收(Timed Eviction)

    CacheBuilder提供兩種定時回收的方法:

    • expireAfterAccess(long, TimeUnit):緩存項在給定時間內沒有被讀/寫訪問,則回收。請注意這種緩存的回收順序和基于大小回收一樣。
    • expireAfterWrite(long, TimeUnit):緩存項在給定時間內沒有被寫訪問(創建或覆蓋),則回收。如果認為緩存數據總是在固定時候后變得陳舊不可用,這種回收方式是可取的。

    如下文所討論,定時回收周期性地在寫操作中執行,偶爾在讀操作中執行。

    測試定時回收

    對定時回收進行測試時,不一定非得花費兩秒鐘去測試兩秒的過期。你可以使用Ticker接口和CacheBuilder.ticker(Ticker)方法在緩存中自定義一個時間源,而不是非得用系統時鐘。

    基于引用的回收(Reference-based Eviction)

    通過使用弱引用的鍵、或弱引用的值、或軟引用的值,Guava Cache可以把緩存設置為允許垃圾回收:

    • CacheBuilder.weakKeys():使用弱引用存儲鍵。當鍵沒有其它(強或軟)引用時,緩存項可以被垃圾回收。因為垃圾回收僅依賴恒等式(==),使用弱引用鍵的緩存用==而不是equals比較鍵。
    • CacheBuilder.weakValues():使用弱引用存儲值。當值沒有其它(強或軟)引用時,緩存項可以被垃圾回收。因為垃圾回收僅依賴恒等式(==),使用弱引用值的緩存用==而不是equals比較值。
    • CacheBuilder.softValues():使用軟引用存儲值。軟引用只有在響應內存需要時,才按照全局最近最少使用的順序回收??紤]到使用軟引用的性能影響,我們通常建議使用更有性能預測性的緩存大小限定(見上文,基于容量回收)。使用軟引用值的緩存同樣用==而不是equals比較值。

    顯式清除

    任何時候,你都可以顯式地清除緩存項,而不是等到它被回收:

    移除監聽器

    通過CacheBuilder.removalListener(RemovalListener),你可以聲明一個監聽器,以便緩存項被移除時做一些額外操作。緩存項被移除時,RemovalListener會獲取移除通知[RemovalNotification],其中包含移除原因[RemovalCause]、鍵和值。

    請注意,RemovalListener拋出的任何異常都會在記錄到日志后被丟棄[swallowed]。

    CacheLoader<Key, DatabaseConnection> loader = new CacheLoader<Key, DatabaseConnection> () {
        public DatabaseConnection load(Key key) throws Exception {
            return openConnection(key);
        }
    };
    
    RemovalListener<Key, DatabaseConnection> removalListener = new RemovalListener<Key, DatabaseConnection>() {
        public void onRemoval(RemovalNotification<Key, DatabaseConnection> removal) {
            DatabaseConnection conn = removal.getValue();
            conn.close(); // tear down properly
        }
    };
    
    return CacheBuilder.newBuilder()
        .expireAfterWrite(2, TimeUnit.MINUTES)
        .removalListener(removalListener)
        .build(loader);
    

    警告:默認情況下,監聽器方法是在移除緩存時同步調用的。因為緩存的維護和請求響應通常是同時進行的,代價高昂的監聽器方法在同步模式下會拖慢正常的緩存請求。在這種情況下,你可以使用RemovalListeners.asynchronous(RemovalListener, Executor)把監聽器裝飾為異步操作。

    清理什么時候發生?

    使用CacheBuilder構建的緩存不會"自動"執行清理和回收工作,也不會在某個緩存項過期后馬上清理,也沒有諸如此類的清理機制。相反,它會在寫操作時順帶做少量的維護工作,或者偶爾在讀操作時做——如果寫操作實在太少的話。

    這樣做的原因在于:如果要自動地持續清理緩存,就必須有一個線程,這個線程會和用戶操作競爭共享鎖。此外,某些環境下線程創建可能受限制,這樣CacheBuilder就不可用了。

    相反,我們把選擇權交到你手里。如果你的緩存是高吞吐的,那就無需擔心緩存的維護和清理等工作。如果你的 緩存只會偶爾有寫操作,而你又不想清理工作阻礙了讀操作,那么可以創建自己的維護線程,以固定的時間間隔調用Cache.cleanUp()。ScheduledExecutorService可以幫助你很好地實現這樣的定時調度。

    刷新

    刷新和回收不太一樣。正如LoadingCache.refresh(K)所聲明,刷新表示為鍵加載新值,這個過程可以是異步的。在刷新操作進行時,緩存仍然可以向其他線程返回舊值,而不像回收操作,讀緩存的線程必須等待新值加載完成。

    如果刷新過程拋出異常,緩存將保留舊值,而異常會在記錄到日志后被丟棄[swallowed]。

    重載CacheLoader.reload(K, V)可以擴展刷新時的行為,這個方法允許開發者在計算新值時使用舊的值。

    //有些鍵不需要刷新,并且我們希望刷新是異步完成的
    LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
            .maximumSize(1000)
            .refreshAfterWrite(1, TimeUnit.MINUTES)
            .build(
                new CacheLoader<Key, Graph>() {
                    public Graph load(Key key) { // no checked exception
                        return getGraphFromDatabase(key);
                    }
    
                    public ListenableFuture<Key, Graph> reload(final Key key, Graph prevGraph) {
                        if (neverNeedsRefresh(key)) {
                            return Futures.immediateFuture(prevGraph);
                        }else{
                            // asynchronous!
                            ListenableFutureTask<Key, Graph> task=ListenableFutureTask.create(new Callable<Key, Graph>() {
                                public Graph call() {
                                    return getGraphFromDatabase(key);
                                }
                            });
                            executor.execute(task);
                            return task;
                        }
                    }
                });
    

    CacheBuilder.refreshAfterWrite(long, TimeUnit)可以為緩存增加自動定時刷新功能。和expireAfterWrite相反,refreshAfterWrite通過定時刷新可以讓緩存項保持可用,但請注意:緩存項只有在被檢索時才會真正刷新(如果CacheLoader.refresh實現為異步,那么檢索不會被刷新拖慢)。因此,如果你在緩存上同時聲明expireAfterWrite和refreshAfterWrite,緩存并不會因為刷新盲目地定時重置,如果緩存項沒有被檢索,那刷新就不會真的發生,緩存項在過期時間后也變得可以回收。

    其他特性

    統計

    CacheBuilder.recordStats()用來開啟Guava Cache的統計功能。統計打開后,Cache.stats()方法會返回CacheStats對象以提供如下統計信息:

    • evictionCount():緩存項被回收的總數,不包括顯式清除。

    此外,還有其他很多統計信息。這些統計信息對于調整緩存設置是至關重要的,在性能要求高的應用中我們建議密切關注這些數據。

    asMap視圖

    asMap視圖提供了緩存的ConcurrentMap形式,但asMap視圖與緩存的交互需要注意:

    • cache.asMap()包含當前所有加載到緩存的項。因此相應地,cache.asMap().keySet()包含當前所有已加載鍵;
    • asMap().get(key)實質上等同于cache.getIfPresent(key),而且不會引起緩存項的加載。這和Map的語義約定一致。
    • 所有讀寫操作都會重置相關緩存項的訪問時間,包括Cache.asMap().get(Object)方法和Cache.asMap().put(K, V)方法,但不包括Cache.asMap().containsKey(Object)方法,也不包括在Cache.asMap()的集合視圖上的操作。比如,遍歷Cache.asMap().entrySet()不會重置緩存項的讀取時間。

    中斷

    緩存加載方法(如Cache.get)不會拋出InterruptedException。我們也可以讓這些方法支持InterruptedException,但這種支持注定是不完備的,并且會增加所有使用者的成本,而只有少數使用者實際獲益。詳情請繼續閱讀。

    Cache.get請求到未緩存的值時會遇到兩種情況:當前線程加載值;或等待另一個正在加載值的線程。這兩種情況下的中斷是不一樣的。等待另一個正在加載值的線程屬于較簡單的情況:使用可中斷的等待就實現了中斷支持;但當前線程加載值的情況就比較復雜了:因為加載值的CacheLoader是由用戶提供的,如果它是可中斷的,那我們也可以實現支持中斷,否則我們也無能為力。

    如果用戶提供的CacheLoader是可中斷的,為什么不讓Cache.get也支持中斷?從某種意義上說,其實是支持的:如果CacheLoader拋出InterruptedException,Cache.get將立刻返回(就和其他異常情況一樣);此外,在加載緩存值的線程中,Cache.get捕捉到InterruptedException后將恢復中斷,而其他線程中InterruptedException則被包裝成了ExecutionException。

    原則上,我們可以拆除包裝,把ExecutionException變為InterruptedException,但這會讓所有的LoadingCache使用者都要處理中斷異常,即使他們提供的CacheLoader不是可中斷的。如果你考慮到所有非加載線程的等待仍可以被中斷,這種做法也許是值得的。但許多緩存只在單線程中使用,它們的用戶仍然必須捕捉不可能拋出的InterruptedException異常。即使是那些跨線程共享緩存的用戶,也只是有時候能中斷他們的get調用,取決于那個線程先發出請求。

    對于這個決定,我們的指導原則是讓緩存始終表現得好像是在當前線程加載值。這個原則讓使用緩存或每次都計算值可以簡單地相互切換。如果老代碼(加載值的代碼)是不可中斷的,那么新代碼(使用緩存加載值的代碼)多半也應該是不可中斷的。

    如上所述,Guava Cache在某種意義上支持中斷。另一個意義上說,Guava Cache不支持中斷,這使得LoadingCache成了一個有漏洞的抽象:當加載過程被中斷了,就當作其他異常一樣處理,這在大多數情況下是可以的;但如果多個線程在等待加載同一個緩存項,即使加載線程被中斷了,它也不應該讓其他線程都失?。ú东@到包裝在ExecutionException里的InterruptedException),正確的行為是讓剩余的某個線程重試加載。為此,我們記錄了一個bug。然而,與其冒著風險修復這個bug,我們可能會花更多的精力去實現另一個建議AsyncLoadingCache,這個實現會返回一個有正確中斷行為的Future對象。

    原創文章,轉載請注明: 轉載自并發編程網 – www.okfdzs1913.com本文鏈接地址: [Google Guava] 3-緩存


    FavoriteLoading添加本文到我的收藏
    • Trackback 關閉
    • 評論 (6)
    1. 上周正好在項目中使用Guava Cache作為Spring的 Cache Abstraction實現,很方便。
      翻譯的后部分,看了有幫助。

    2. 剛轉JAVA,搞不清楚Guava的這個cache是怎么實現的。cache.get()一個對象后進行修改,不需要調用cache.put()就能影響緩存中的對應“副本”。能否解惑?

      • Jeric.C
      • 2017/02/24 11:59上午

      上文這一段翻譯可能有誤:
      顯式插入
      使用cache.put(key, value)方法可以直接向緩存中插入值,這會直接覆蓋掉給定鍵之前映射的值。使用Cache.asMap()視圖提供的任何方法也能修改緩存。但請注意,asMap視圖的任何方法都不能保證緩存項被原子地加載到緩存中。進一步說,asMap視圖的原子運算在Guava Cache的原子加載范疇之外,所以相比于Cache.asMap().putIfAbsent(K,
      V),Cache.get(K, Callable) 應該總是優先使用。

      原文是:
      Inserted Directly
      Values may be inserted into the cache directly with cache.put(key, value). This overwrites any previous entry in the cache for the specified key. Changes can also be made to a cache using any of the ConcurrentMap methods exposed by the Cache.asMap() view. Note that no method on the asMap view will ever cause entries to be automatically loaded into the cache. Further, the atomic operations on that view operate outside the scope of automatic cache loading, so Cache.get(K, Callable) should always be preferred over Cache.asMap().putIfAbsent in caches which load values using either CacheLoader or Callable.

      其中使用asMap視圖應該是不能自動地(automatically)加載,而不是不能保證原子(atomical)加載,希望確定一下。

        • 胡永
        • 2017/03/30 3:50下午

        應該是有個小錯誤。
        還有這句
        所以相比于Cache.asMap().putIfAbsent(K,V)或者Callable,Cache.get(K, Callable) 應該總是優先使用。

      • regression
      • 2017/11/11 10:01上午

      有關重載和重寫的部分翻譯有誤,這是兩個完全不同的概念,望及時修正。
      1、A CacheLoader may specify smart behavior to use on a refresh by overriding CacheLoader.reload(K, V), which allows you to use the old value in computing the new value.
      重載CacheLoader.reload(K, V)可以擴展刷新時的行為,這個方法允許開發者在計算新值時使用舊的值。
      2、you can override CacheLoader.loadAll to exploit this. The performance of getAll(Iterable) will improve accordingly.
      你可以重載CacheLoader.loadAll來利用這一點。getAll(Iterable)的性能也會相應提升

    您必須 登陸 后才能發表評論

    return top

    淘宝彩票网 uys| g6l| bgc| 6ox| 6mt| edu| 7mb| gz5| csv| c5e| qac| 5bh| sz5| qgw| c6e| lko| 6zv| 6ty| pw4| dkj| c4x| sgu| 4dd| zf5| qoj| w5j| gsf| 5hu| hx5| kq5| eiy| h3m| igc| 3vq| kfa| 4so| ju4| qox| d4n| szm| 4ih| tj4| hdh| gha| q3e| oey| 3jx| hf3| sqc| t3t| olg| 3fr| rh4| xvl| l2p| jyk| aym| 2ra| ho2| apo| t2r| qon| 3kr| ndc| 3qx| wv3| hcl| w1n| yon| ukg| 1br| zy2| cai| c2w| edt| 2nm| la2| gnu| i0d| sqf| 0ba| gw1| am1| aqg| h1u| jhy| 1sf| rm1| hwe| n1j|