言
對一個java後臺開發者而言,提到緩存,第一反應就是redis和memcache。利用這類緩存足以解決大多數的性能問題了,並且java針對這兩者也都有非常成熟的api可供使用。但是我們也要知道,這兩種都屬於remote cache(分佈式緩存),應用的進程和緩存的進程通常分佈在不同的服務器上,不同進程之間通過RPC或HTTP的方式通信。這種緩存的優點是緩存和應用服務解耦,支持大數據量的存儲,缺點是數據要經過網絡傳輸,性能上會有一定損耗。
與分佈式緩存對應的是本地緩存,緩存的進程和應用進程是同一個,數據的讀寫都在一個進程內完成,這種方式的優點是沒有網絡開銷,訪問速度很快。缺點是受JVM內存的限制,不適合存放大數據。
本篇文章我們主要主要討論Java本地緩存的的一些常用方案。
本地緩存常用技術
本地緩存和應用同屬於一個進程,使用不當會影響服務穩定性,所以通常需要考慮更多的因素,例如容量限制、過期策略、淘汰策略、自動刷新等。常用的本地緩存方案有:
- 根據HashMap自實現本地緩存
- Guava Cache
- Caffeine
- Encache
下面分別進行介紹:
1. 根據HashMap自定義實現本地緩存
緩存的本質就是存儲在內存中的KV數據結構,對應的就是jdk中的HashMap,但是要實現緩存,還需要考慮併發安全性、容量限制等策略,下面簡單介紹一種利用LinkedHashMap實現緩存的方式:
public class LRUCache extends LinkedHashMap {
/**
* 可重入讀寫鎖,保證併發讀寫安全性
*/
private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private Lock readLock = readWriteLock.readLock();
private Lock writeLock = readWriteLock.writeLock();
/**
* 緩存大小限制
*/
private int maxSize;
public LRUCache(int maxSize) {
super(maxSize + 1, 1.0f, true);
this.maxSize = maxSize;
}
@Override
public Object get(Object key) {
readLock.lock();
try {
return super.get(key);
} finally {
readLock.unlock();
}
}
@Override
public Object put(Object key, Object value) {
writeLock.lock();
try {
return super.put(key, value);
} finally {
writeLock.unlock();
}
}
@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
return this.size() > maxSize;
}
}
複製代碼
LinkedHashMap維持了一個鏈表結構,用來存儲節點的插入順序或者訪問順序(二選一),並且內部封裝了一些業務邏輯,只需要覆蓋removeEldestEntry方法,便可以實現緩存的LRU淘汰策略。此外我們利用讀寫鎖,保障緩存的併發安全性。需要注意的是,這個示例並不支持過期時間淘汰的策略。
自實現緩存的方式,優點是實現簡單,不需要引入第三方包,比較適合一些簡單的業務場景。缺點是如果需要更多的特性,需要定製化開發,成本會比較高,並且穩定性和可靠性也難以保障。對於比較複雜的場景,建議使用比較穩定的開源工具。
2. 基於Guava Cache實現本地緩存
Guava是Google團隊開源的一款 Java 核心增強庫,包含集合、併發原語、緩存、IO、反射等工具箱,性能和穩定性上都有保障,應用十分廣泛。Guava Cache支持很多特性:
- 支持最大容量限制
- 支持兩種過期刪除策略(插入時間和訪問時間)
- 支持簡單的統計功能
- 基於LRU算法實現
Guava Cache的使用非常簡單,首先需要引入maven包:
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>18.0</version>
</dependency>
複製代碼
一個簡單的示例代碼如下:
public class GuavaCacheTest {
public static void main(String[] args) throws Exception {
//創建guava cache
Cache<String, String> loadingCache = CacheBuilder.newBuilder()
//cache的初始容量
.initialCapacity(5)
//cache最大緩存數
.maximumSize(10)
//設置寫緩存後n秒鐘過期
.expireAfterWrite(17, TimeUnit.SECONDS)
//設置讀寫緩存後n秒鐘過期,實際很少用到,類似於expireAfterWrite
//.expireAfterAccess(17, TimeUnit.SECONDS)
.build();
String key = "key";
// 往緩存寫數據
loadingCache.put(key, "v");
// 獲取value的值,如果key不存在,調用collable方法獲取value值加載到key中再返回
String value = loadingCache.get(key, new Callable<String>() {
@Override
public String call() throws Exception {
return getValueFromDB(key);
}
});
// 刪除key
loadingCache.invalidate(key);
}
private static String getValueFromDB(String key) {
return "v";
}
}
複製代碼
總體來說,Guava Cache是一款十分優異的緩存工具,功能豐富,線程安全,足以滿足工程化使用,以上代碼只介紹了一般的用法,實際上springboot對guava也有支持,利用配置文件或者註解可以輕鬆集成到代碼中。
3. Caffeine
Caffeine是基於java8實現的新一代緩存工具,緩存性能接近理論最優。可以看作是Guava Cache的增強版,功能上兩者類似,不同的是Caffeine採用了一種結合LRU、LFU優點的算法:W-TinyLFU,在性能上有明顯的優越性。Caffeine的使用,首先需要引入maven包:
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.5.5</version>
</dependency>
複製代碼
使用上和Guava Cache基本類似:
public class CaffeineCacheTest {
public static void main(String[] args) throws Exception {
//創建guava cache
Cache<String, String> loadingCache = Caffeine.newBuilder()
//cache的初始容量
.initialCapacity(5)
//cache最大緩存數
.maximumSize(10)
//設置寫緩存後n秒鐘過期
.expireAfterWrite(17, TimeUnit.SECONDS)
//設置讀寫緩存後n秒鐘過期,實際很少用到,類似於expireAfterWrite
//.expireAfterAccess(17, TimeUnit.SECONDS)
.build();
String key = "key";
// 往緩存寫數據
loadingCache.put(key, "v");
// 獲取value的值,如果key不存在,獲取value後再返回
String value = loadingCache.get(key, CaffeineCacheTest::getValueFromDB);
// 刪除key
loadingCache.invalidate(key);
}
private static String getValueFromDB(String key) {
return "v";
}
}
複製代碼
相比Guava Cache來說,Caffeine無論從功能上和性能上都有明顯優勢。同時兩者的API類似,使用Guava Cache的代碼很容易可以切換到Caffeine,節省遷移成本。需要注意的是,SpringFramework5.0(SpringBoot2.0)同樣放棄了Guava Cache的本地緩存方案,轉而使用Caffeine。
4. Encache
Encache是一個純Java的進程內緩存框架,具有快速、精幹等特點,是Hibernate中默認的CacheProvider。同Caffeine和Guava Cache相比,Encache的功能更加豐富,擴展性更強:
- 支持多種緩存淘汰算法,包括LRU、LFU和FIFO
- 緩存支持堆內存儲、堆外存儲、磁盤存儲(支持持久化)三種
- 支持多種集羣方案,解決數據共享問題
Encache的使用,首先需要導入maven包:
<dependency>
<groupId>org.ehcache</groupId>
<artifactId>ehcache</artifactId>
<version>3.8.0</version>
</dependency>
複製代碼
以下是一個簡單的使用案例:
public class EncacheTest {
public static void main(String[] args) throws Exception {
// 聲明一個cacheBuilder
CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder()
.withCache("encacheInstance", CacheConfigurationBuilder
//聲明一個容量爲20的堆內緩存
.newCacheConfigurationBuilder(String.class,String.class, ResourcePoolsBuilder.heap(20)))
.build(true);
// 獲取Cache實例
Cache<String,String> myCache = cacheManager.getCache("encacheInstance", String.class, String.class);
// 寫緩存
myCache.put("key","v");
// 讀緩存
String value = myCache.get("key");
// 移除換粗
cacheManager.removeCache("myCache");
cacheManager.close();
}
}
複製代碼
總結
- 從易用性角度,Guava Cache、Caffeine和Encache都有十分成熟的接入方案,使用簡單。
- 從功能性角度,Guava Cache和Caffeine功能類似,都是隻支持堆內緩存,Encache相比功能更爲豐富
- 從性能上進行比較,Caffeine最優、GuavaCache次之,Encache最差(下圖是三者的性能對比結果)
總體來說,對於本地緩存的方案中,筆者比較推薦Caffeine,性能上遙遙領先。雖然Encache功能更爲豐富,甚至提供了持久化和集羣的功能,但是這些功能完全可以依靠其他方式實現。真實的業務工程中,建議使用Caffeine作爲本地緩存,另外使用redis或者memcache作爲分佈式緩存,構造多級緩存體系,保證性能和可靠性。