Java應用緩存介紹與LRU(Least Recently Used)算法

引言

對於大規模的Java Web應用來說,會有大量的數據和大量的用戶,對於如何提升網站的響應速度成爲了我們開發人員的一個挑戰。對於緩存來說,它可以解決其中的一部分問題。在這篇文章中,我會介紹什麼是緩存,它的工作流程是什麼樣的,被緩存的數據應該具有什麼樣的特性。最後我會介紹一個非常受歡迎的緩存算法 - LRU,並給出了一個具體的實現。

cache

緩存的大致工作流程

緩存就是把我們頻繁訪問的數據(並且這些數據比較消耗時間計算或讀取)放到內存中,以便我們下次用到這樣的數據時,就不用重新計算或者重新讀取。比如從數據庫中查詢出的結果、IO讀取一些配置文件的信息等。Java中的緩存接口通常如下所示:

public interface Cache {
    Object get(final Object key);
    Object put(final Object key, final Object value);
}

一個普通應用緩存的大致流程如下圖:

application cache

應用程序用一個key從緩存中取數據(get方法),如果在緩存中並沒有發現這個key,那麼應用程序從一個非常耗時的數據源中取數據並把獲取到的數據放到緩存中(put方法)。因此,接下來的請求就可以從緩存中取得數據了。

緩存數據應該具有的特性

由於緩存需要用到應用的內存,因此我們需要去控制緩存的大小,把一些不常用的數據從緩存中清除。被緩存的數據應該具有 Temporal locality 和 Spatial locality,下面我把wikipedia 中對這2條性質的定義翻譯一下。

Spatial locality

如果某一特定時間的某個特定存儲位置被引用到,那麼在不久的將來它附近的位置很可能會被再次引用到。

Temporal locality

如果某一特定時間的某個特定存儲位置被引用到,那麼在不久的將來它很可能會被再次引用到。Temporal locality 是 Spatial locality 的一個特例,即未來將要被訪問到的位置和當前的位置是相同的。

如果我們緩存的數據並不滿足上面的兩個性質,緩存命中率非常低,因此會導致緩存的元素很快地被清除出去。由於增大維護緩存的開銷,並沒有達到緩存應有的效果,反而會導致應用程序的性能下降。

緩存的性能評價指標

hit/miss ratio 是一個主要的評價緩存性能的指標。我們計算它通過緩存命中的次數 / 緩存沒有命中的次數

當我們評估緩存的性能時,我們可以讓程序運行一段時間後來計算上面的hit/miss ratio,如果這個ratio很高,則證明緩存性能很好; 如果這個ratio的值很小,則表示應用緩存的數據不應該被緩存,或者同樣有可能是因爲我們的緩存size太小,導致不能捕獲到緩存數據的temporal locality.

LRU算法

緩存 Eviction Policy

緩存 Eviction Policy 就是當一個新的元素加入到緩存中時,如果導致緩存大小已經超過了所允許的範圍,它會從緩存中移除一個已經存在的元素。eviction policy 確保緩存的大小不會超過一個我們自己指定的闕值。

LRU是一個很受歡迎的具有Eviction Policy功能的一個算法。LRU受歡迎的原因就是它同時捕獲了數據訪問的2個性質 - temporal locality 和 spatial locality

LRU算法實現

一個典型的LRU緩存實現由一個Map和一個linked list組成,Map存儲緩存的元素,linked list 保持追蹤最近最少使用的緩存元素。當一個緩存元素被更新時,把它從linked list中移除,並把它放入到linked list中的頭部。如果當一個新的元素加入到列表中時,LRU會把這個元素加入到linked list的頭部,同時LRU也會判斷緩存大小是否超過了限制,如果超過了限制,則位於linked list尾部的元素將被移除。

下面我用LinkedHashMap來實現一個簡單的LRU緩存:

import java.util.LinkedHashMap;
import java.util.Map;

public LRUCache<K, V> extends LinkedHashMap<K, V> {
  private int cacheSize;

  public LRUCache(int cacheSize) {
    super(16, 0.75, true);
    this.cacheSize = cacheSize;
  }

  // This method is called just after a new entry has been added
  protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
    return size() >= cacheSize;
  }
}

上面的代碼僅僅是一個簡單的實現,並不足以用到真實的應用中。如果大家想要一個可以在真實應用中使用的產品,建議大家看看caffeine,基於Java 8的一個高性能緩存庫。或者可以參考一下Apache Commons的LRUMap

二級緩存

在object-relational mapping (ORM)框架(Hibernate)或者data mapping (DM) 框架(iBatis)都提供了二級緩存的功能。二級緩存封裝了緩存邏輯的複雜性,不讓客戶端看見。通過減小不必要的數據庫,二級緩存提高了ORM 或者 DM框架的性能,下圖解釋了二級緩存的工作流程:

Level-2 Cache

從上圖我們可以看到,客戶端應用並沒有直接去訪問緩存,而是訪問ORM 或者 DM框架提供的更高級的接口。

總結

恰當的使用緩存可以大大提高我們應用程序的性能。但是,在選擇是否使用緩存之前,我們必須要看看被緩存的數據是否符合temporal locality 或者 spatial locality,如果數據並不符合這樣的性質,緩存不僅不會使我們的應用程序性能提升,而且還會導致性能下降,這是因爲維護緩存的開銷要比從緩存中獲益的開銷要小。因此,在使用緩存之前,我們一定要認真想一想,數據是否具有上面的2個特性。

發佈了106 篇原創文章 · 獲贊 213 · 訪問量 59萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章