前言
對一個java后臺開發(fā)者而言,提到緩存,第一反應(yīng)就是redis和memcache。利用這類緩存足以解決大多數(shù)的性能問題了,并且java針對這兩者也都有非常成熟的api可供使用。但是我們也要知道,這兩種都屬于remote cache(分布式緩存),應(yīng)用的進(jìn)程和緩存的進(jìn)程通常分布在不同的服務(wù)器上,不同進(jìn)程之間通過RPC或HTTP的方式通信。這種緩存的優(yōu)點是緩存和應(yīng)用服務(wù)解耦,支持大數(shù)據(jù)量的存儲,缺點是數(shù)據(jù)要經(jīng)過網(wǎng)絡(luò)傳輸,性能上會有一定損耗。
與分布式緩存對應(yīng)的是本地緩存,緩存的進(jìn)程和應(yīng)用進(jìn)程是同一個,數(shù)據(jù)的讀寫都在一個進(jìn)程內(nèi)完成,這種方式的優(yōu)點是沒有網(wǎng)絡(luò)開銷,訪問速度很快。缺點是受JVM內(nèi)存的限制,不適合存放大數(shù)據(jù)。
本篇文章我們主要主要討論Java本地緩存的的一些常用方案。
本地緩存常用技術(shù)
本地緩存和應(yīng)用同屬于一個進(jìn)程,使用不當(dāng)會影響服務(wù)穩(wěn)定性,所以通常需要考慮更多的因素,例如容量限制、過期策略、淘汰策略、自動刷新等。常用的本地緩存方案有:
- 根據(jù)HashMap自實現(xiàn)本地緩存
- Guava Cache
- Caffeine
- Encache
下面分別進(jìn)行介紹:
1. 根據(jù)HashMap自定義實現(xiàn)本地緩存
緩存的本質(zhì)就是存儲在內(nèi)存中的KV數(shù)據(jù)結(jié)構(gòu),對應(yīng)的就是jdk中的HashMap,但是要實現(xiàn)緩存,還需要考慮并發(fā)安全性、容量限制等策略,下面簡單介紹一種利用LinkedHashMap實現(xiàn)緩存的方式:
public class LRUCache extends LinkedHashMap {
/**
* 可重入讀寫鎖,保證并發(fā)讀寫安全性
*/
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維持了一個鏈表結(jié)構(gòu),用來存儲節(jié)點的插入順序或者訪問順序(二選一),并且內(nèi)部封裝了一些業(yè)務(wù)邏輯,只需要覆蓋removeEldestEntry方法,便可以實現(xiàn)緩存的LRU淘汰策略。此外我們利用讀寫鎖,保障緩存的并發(fā)安全性。需要注意的是,這個示例并不支持過期時間淘汰的策略。
自實現(xiàn)緩存的方式,優(yōu)點是實現(xiàn)簡單,不需要引入第三方包,比較適合一些簡單的業(yè)務(wù)場景。缺點是如果需要更多的特性,需要定制化開發(fā),成本會比較高,并且穩(wěn)定性和可靠性也難以保障。對于比較復(fù)雜的場景,建議使用比較穩(wěn)定的開源工具。
2. 基于Guava Cache實現(xiàn)本地緩存
Guava是Google團(tuán)隊開源的一款 Java 核心增強庫,包含集合、并發(fā)原語、緩存、IO、反射等工具箱,性能和穩(wěn)定性上都有保障,應(yīng)用十分廣泛。Guava Cache支持很多特性:
- 支持最大容量限制
- 支持兩種過期刪除策略(插入時間和訪問時間)
- 支持簡單的統(tǒng)計功能
- 基于LRU算法實現(xiàn)
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 {
//創(chuàng)建guava cache
Cache<String, String> loadingCache = CacheBuilder.newBuilder()
//cache的初始容量
.initialCapacity(5)
//cache最大緩存數(shù)
.maximumSize(10)
//設(shè)置寫緩存后n秒鐘過期
.expireAfterWrite(17, TimeUnit.SECONDS)
//設(shè)置讀寫緩存后n秒鐘過期,實際很少用到,類似于expireAfterWrite
//.expireAfterAccess(17, TimeUnit.SECONDS)
.build();
String key = "key";
// 往緩存寫數(shù)據(jù)
loadingCache.put(key, "v");
// 獲取value的值,如果key不存在,調(diào)用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是一款十分優(yōu)異的緩存工具,功能豐富,線程安全,足以滿足工程化使用,以上代碼只介紹了一般的用法,實際上springboot對guava也有支持,利用配置文件或者注解可以輕松集成到代碼中。
3. Caffeine
Caffeine是基于java8實現(xiàn)的新一代緩存工具,緩存性能接近理論最優(yōu)??梢钥醋魇荊uava Cache的增強版,功能上兩者類似,不同的是Caffeine采用了一種結(jié)合LRU、LFU優(yōu)點的算法:W-TinyLFU,在性能上有明顯的優(yōu)越性。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 {
//創(chuàng)建guava cache
Cache<String, String> loadingCache = Caffeine.newBuilder()
//cache的初始容量
.initialCapacity(5)
//cache最大緩存數(shù)
.maximumSize(10)
//設(shè)置寫緩存后n秒鐘過期
.expireAfterWrite(17, TimeUnit.SECONDS)
//設(shè)置讀寫緩存后n秒鐘過期,實際很少用到,類似于expireAfterWrite
//.expireAfterAccess(17, TimeUnit.SECONDS)
.build();
String key = "key";
// 往緩存寫數(shù)據(jù)
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無論從功能上和性能上都有明顯優(yōu)勢。同時兩者的API類似,使用Guava Cache的代碼很容易可以切換到Caffeine,節(jié)省遷移成本。需要注意的是,SpringFramework5.0(SpringBoot2.0)同樣放棄了Guava Cache的本地緩存方案,轉(zhuǎn)而使用Caffeine。
4. Encache
Encache是一個純Java的進(jìn)程內(nèi)緩存框架,具有快速、精干等特點,是Hibernate中默認(rèn)的CacheProvider。同Caffeine和Guava Cache相比,Encache的功能更加豐富,擴(kuò)展性更強:
- 支持多種緩存淘汰算法,包括LRU、LFU和FIFO
- 緩存支持堆內(nèi)存儲、堆外存儲、磁盤存儲(支持持久化)三種
- 支持多種集群方案,解決數(shù)據(jù)共享問題
Encache的使用,首先需要導(dǎo)入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的堆內(nèi)緩存
.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();
}
}
總結(jié)
- 從易用性角度,Guava Cache、Caffeine和Encache都有十分成熟的接入方案,使用簡單。
- 從功能性角度,Guava Cache和Caffeine功能類似,都是只支持堆內(nèi)緩存,Encache相比功能更為豐富
- 從性能上進(jìn)行比較,Caffeine最優(yōu)、GuavaCache次之,Encache最差(下圖是三者的性能對比結(jié)果)
總體來說,對于本地緩存的方案中,筆者比較推薦Caffeine,性能上遙遙領(lǐng)先。雖然Encache功能更為豐富,甚至提供了持久化和集群的功能,但是這些功能完全可以依靠其他方式實現(xiàn)。真實的業(yè)務(wù)工程中,建議使用Caffeine作為本地緩存,另外使用redis或者memcache作為分布式緩存,構(gòu)造多級緩存體系,保證性能和可靠性。