Java本地高性能緩存的幾種常見實現方式
前言
Java緩存技術可分為遠端緩存和本地緩存,遠端緩存常用的方案有著名的redis和memcache,而本地緩存的代表技術主要有HashMap,Guava Cache,Caffeine和Encahche。本篇博文僅覆蓋了本地緩存,且突出探討高性能的本地緩存。
本篇博文將首先介紹常見的本地緩存技術,對本地緩存有個大概的了解;其次介紹本地緩存中號稱性能最好的Cache,可以探討看看到底有多好?怎么做到這么好?最后通過幾個實戰(zhàn)樣例,在日常工作中應用高性能的本地緩存。
一、 Java本地緩存技術介紹
1.1 使用List集合contains方法循環(huán)遍歷(有序) 1.1 HashMap
通過Map的底層方式,直接將需要緩存的對象放在內存中。
優(yōu)點:簡單粗暴,不需要引入第三方包,比較適合一些比較簡單的場景。
缺點:沒有緩存淘汰策略,定制化開發(fā)成本高。
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;
}
}1.2 Guava Cache
Guava Cache是由Google開源的基于LRU替換算法的緩存技術。但Guava Cache由于被下面即將介紹的Caffeine全面超越而被取代,因此不特意編寫示例代碼了,有興趣的讀者可以訪問Guava Cache主頁。
優(yōu)點:支持最大容量限制,兩種過期刪除策略(插入時間和訪問時間),支持簡單的統(tǒng)計功能。
缺點:springboot2和spring5都放棄了對Guava Cache的支持。
1.3 Caffeine
Caffeine采用了W-TinyLFU(LUR和LFU的優(yōu)點結合)開源的緩存技術。緩存性能接近理論最優(yōu),屬于是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最大緩存數
.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";
}
}1.4 Encache
Ehcache是一個純java的進程內緩存框架,具有快速、精干的特點。是hibernate默認的cacheprovider。
優(yōu)點:支持多種緩存淘汰算法,包括LFU,LRU和FIFO;緩存支持堆內緩存,堆外緩存和磁盤緩存;支持多種集群方案,解決數據共享問題。
缺點:性能比Caffeine差
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();
}
}
在Caffeine的官網介紹中,Caffeine在性能和功能上都與其他幾種方案相比具有優(yōu)勢,因此接下來主要探討Caffeine的性能和實現原理。
二、高性能緩存Caffeine
2.1 緩存類型
2.1.1 Cache
Cache<Key, Graph> cache = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(10_000)
.build();
// 查找一個緩存元素, 沒有查找到的時候返回null
Graph graph = cache.getIfPresent(key);
// 查找緩存,如果緩存不存在則生成緩存元素, 如果無法生成則返回null
graph = cache.get(key, k -> createExpensiveGraph(key));
// 添加或者更新一個緩存元素
cache.put(key, graph);
// 移除一個緩存元素
cache.invalidate(key);Cache 接口提供了顯式搜索查找、更新和移除緩存元素的能力。當緩存的元素無法生成或者在生成的過程中拋出異常而導致生成元素失敗,cache.get 也許會返回 null 。
2.1.2 Loading Cache
LoadingCache<Key, Graph> cache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build(key -> createExpensiveGraph(key));
// 查找緩存,如果緩存不存在則生成緩存元素, 如果無法生成則返回null
Graph graph = cache.get(key);
// 批量查找緩存,如果緩存不存在則生成緩存元素
Map<Key, Graph> graphs = cache.getAll(keys);一個LoadingCache是一個Cache 附加上 CacheLoader能力之后的緩存實現。
如果緩存不錯在,則會通過CacheLoader.load來生成對應的緩存元素。
2.1.3 Loading Cache 2.1.3 Async Cache
AsyncCache<Key, Graph> cache = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(10_000)
.buildAsync();
// 查找一個緩存元素, 沒有查找到的時候返回null
CompletableFuture<Graph> graph = cache.getIfPresent(key);
// 查找緩存元素,如果不存在,則異步生成
graph = cache.get(key, k -> createExpensiveGraph(key));
// 添加或者更新一個緩存元素
cache.put(key, graph);
// 移除一個緩存元素
cache.synchronous().invalidate(key);AsyncCache就是Cache的異步形式,提供了Executor生成緩存元素并返回CompletableFuture的能力。默認的線程池實現是 ForkJoinPool.commonPool() ,當然你也可以通過覆蓋并實現 Caffeine.executor(Executor)方法來自定義你的線程池選擇。
2.1.4 Async Loading Cache
AsyncLoadingCache<Key, Graph> cache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(10, TimeUnit.MINUTES)
// 你可以選擇: 去異步的封裝一段同步操作來生成緩存元素
.buildAsync(key -> createExpensiveGraph(key));
// 你也可以選擇: 構建一個異步緩存元素操作并返回一個future
.buildAsync((key, executor) -> createExpensiveGraphAsync(key, executor));
// 查找緩存元素,如果其不存在,將會異步進行生成
CompletableFuture<Graph> graph = cache.get(key);
// 批量查找緩存元素,如果其不存在,將會異步進行生成
CompletableFuture<Map<Key, Graph>> graphs = cache.getAll(keys);AsyncLoadingCache就是LoadingCache的異步形式,提供了異步load生成緩存元素的功能。
2.2 驅逐策略
基于容量
// 基于緩存內的元素個數進行驅逐
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
.maximumSize(10_000)
.build(key -> createExpensiveGraph(key));
// 基于緩存內元素權重進行驅逐
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
.maximumWeight(10_000)
.weigher((Key key, Graph graph) -> graph.vertices().size())
.build(key -> createExpensiveGraph(key));基于時間
// 基于固定的過期時間驅逐策略
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
.expireAfterAccess(5, TimeUnit.MINUTES)
.build(key -> createExpensiveGraph(key));
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.build(key -> createExpensiveGraph(key));
// 基于不同的過期驅逐策略
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
.expireAfter(new Expiry<Key, Graph>() {
public long expireAfterCreate(Key key, Graph graph, long currentTime) {
// Use wall clock time, rather than nanotime, if from an external resource
long seconds = graph.creationDate().plusHours(5)
.minus(System.currentTimeMillis(), MILLIS)
.toEpochSecond();
return TimeUnit.SECONDS.toNanos(seconds);
}
public long expireAfterUpdate(Key key, Graph graph,
long currentTime, long currentDuration) {
return currentDuration;
}
public long expireAfterRead(Key key, Graph graph,
long currentTime, long currentDuration) {
return currentDuration;
}
})
.build(key -> createExpensiveGraph(key));基于引用
// 當key和緩存元素都不再存在其他強引用的時候驅逐
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
.weakKeys()
.weakValues()
.build(key -> createExpensiveGraph(key));
// 當進行GC的時候進行驅逐
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
.softValues()
.build(key -> createExpensiveGraph(key));2.3 刷新機制
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
.maximumSize(10_000)
.refreshAfterWrite(1, TimeUnit.MINUTES)
.build(key -> createExpensiveGraph(key));只有在LoadingCache中可以使用刷新策略,與驅逐不同的是,在刷新的時候如果查詢緩存元素,其舊值將仍被返回,直到該元素的刷新完畢后結束后才會返回刷新后的新值。
2.4 統(tǒng)計
Cache<Key, Graph> graphs = Caffeine.newBuilder()
.maximumSize(10_000)
.recordStats()
.build();通過使用Caffeine.recordStats()方法可以打開數據收集功能。Cache.stats()方法將會返回一個CacheStats對象,其將會含有一些統(tǒng)計指標,比如:
hitRate(): 查詢緩存的命中率
evictionCount(): 被驅逐的緩存數量
averageLoadPenalty(): 新值被載入的平均耗時
配合SpringBoot提供的RESTful Controller,能很方便的查詢Cache的使用情況。
三、Caffeine在SpringBoot的實戰(zhàn)
按照Caffeine Github官網文檔的描述,Caffeine是基于Java8的高性能緩存庫。并且在Spring5(SpringBoot2.x)官方放棄了Guava,而使用了性能更優(yōu)秀的Caffeine作為默認的緩存方案。
SpringBoot使用Caffeine有兩種方式:
方式一:直接引入Caffeine依賴,然后使用Caffeine的函數實現緩存
方式二:引入Caffeine和Spring Cache依賴,使用SpringCache注解方法實現緩存
下面分別介紹兩種使用方式。
方式一:使用Caffeine依賴
首先引入maven相關依賴:
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>其次,設置緩存的配置選項
@Configuration
public class CacheConfig {
@Bean
public Cache<String, Object> caffeineCache() {
return Caffeine.newBuilder()
// 設置最后一次寫入或訪問后經過固定時間過期
.expireAfterWrite(60, TimeUnit.SECONDS)
// 初始的緩存空間大小
.initialCapacity(100)
// 緩存的最大條數
.maximumSize(1000)
.build();
}
}最后給服務添加緩存功能
@Slf4j
@Service
public class UserInfoServiceImpl {
/**
* 模擬數據庫存儲數據
*/
private HashMap<Integer, UserInfo> userInfoMap = new HashMap<>();
@Autowired
Cache<String, Object> caffeineCache;
public void addUserInfo(UserInfo userInfo) {
userInfoMap.put(userInfo.getId(), userInfo);
// 加入緩存
caffeineCache.put(String.valueOf(userInfo.getId()),userInfo);
}
public UserInfo getByName(Integer id) {
// 先從緩存讀取
caffeineCache.getIfPresent(id);
UserInfo userInfo = (UserInfo) caffeineCache.asMap().get(String.valueOf(id));
if (userInfo != null){
return userInfo;
}
// 如果緩存中不存在,則從庫中查找
userInfo = userInfoMap.get(id);
// 如果用戶信息不為空,則加入緩存
if (userInfo != null){
caffeineCache.put(String.valueOf(userInfo.getId()),userInfo);
}
return userInfo;
}
public UserInfo updateUserInfo(UserInfo userInfo) {
if (!userInfoMap.containsKey(userInfo.getId())) {
return null;
}
// 取舊的值
UserInfo oldUserInfo = userInfoMap.get(userInfo.getId());
// 替換內容
if (!StringUtils.isEmpty(oldUserInfo.getAge())) {
oldUserInfo.setAge(userInfo.getAge());
}
if (!StringUtils.isEmpty(oldUserInfo.getName())) {
oldUserInfo.setName(userInfo.getName());
}
if (!StringUtils.isEmpty(oldUserInfo.getSex())) {
oldUserInfo.setSex(userInfo.getSex());
}
// 將新的對象存儲,更新舊對象信息
userInfoMap.put(oldUserInfo.getId(), oldUserInfo);
// 替換緩存中的值
caffeineCache.put(String.valueOf(oldUserInfo.getId()),oldUserInfo);
return oldUserInfo;
}
@Override
public void deleteById(Integer id) {
userInfoMap.remove(id);
// 從緩存中刪除
caffeineCache.asMap().remove(String.valueOf(id));
}
}方式二:使用Spring Cache注解
首先引入maven相關依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>其次,配置緩存管理類
@Configuration
public class CacheConfig {
/**
* 配置緩存管理器
*
* @return 緩存管理器
*/
@Bean("caffeineCacheManager")
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCaffeine(Caffeine.newBuilder()
// 設置最后一次寫入或訪問后經過固定時間過期
.expireAfterAccess(60, TimeUnit.SECONDS)
// 初始的緩存空間大小
.initialCapacity(100)
// 緩存的最大條數
.maximumSize(1000));
return cacheManager;
}
}最后給服務添加緩存功能
@Slf4j
@Service
@CacheConfig(cacheNames = "caffeineCacheManager")
public class UserInfoServiceImpl {
/**
* 模擬數據庫存儲數據
*/
private HashMap<Integer, UserInfo> userInfoMap = new HashMap<>();
@CachePut(key = "#userInfo.id")
public void addUserInfo(UserInfo userInfo) {
userInfoMap.put(userInfo.getId(), userInfo);
}
@Cacheable(key = "#id")
public UserInfo getByName(Integer id) {
return userInfoMap.get(id);
}
@CachePut(key = "#userInfo.id")
public UserInfo updateUserInfo(UserInfo userInfo) {
if (!userInfoMap.containsKey(userInfo.getId())) {
return null;
}
// 取舊的值
UserInfo oldUserInfo = userInfoMap.get(userInfo.getId());
// 替換內容
if (!StringUtils.isEmpty(oldUserInfo.getAge())) {
oldUserInfo.setAge(userInfo.getAge());
}
if (!StringUtils.isEmpty(oldUserInfo.getName())) {
oldUserInfo.setName(userInfo.getName());
}
if (!StringUtils.isEmpty(oldUserInfo.getSex())) {
oldUserInfo.setSex(userInfo.getSex());
}
// 將新的對象存儲,更新舊對象信息
userInfoMap.put(oldUserInfo.getId(), oldUserInfo);
// 返回新對象信息
return oldUserInfo;
}
@CacheEvict(key = "#id")
public void deleteById(Integer id) {
userInfoMap.remove(id);
}
}總結
到此這篇關于Java本地高性能緩存的幾種常見實現方式的文章就介紹到這了,更多相關Java本地高性能緩存實現內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
SpringBoot集成ShardingSphere-JDBC實現分庫分表
本文主要介紹了SpringBoot集成ShardingSphere-JDBC實現分庫分表,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2025-07-07
將java程序打成jar包在cmd命令行下執(zhí)行的方法
這篇文章主要給大家介紹了關于將java程序打成jar包在cmd命令行下執(zhí)行的相關資料,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧。2018-01-01

