Java本地高性能緩存的幾種常見實現(xiàn)方式
前言
Java緩存技術可分為遠端緩存和本地緩存,遠端緩存常用的方案有著名的redis和memcache,而本地緩存的代表技術主要有HashMap,Guava Cache,Caffeine和Encahche。本篇博文僅覆蓋了本地緩存,且突出探討高性能的本地緩存。
本篇博文將首先介紹常見的本地緩存技術,對本地緩存有個大概的了解;其次介紹本地緩存中號稱性能最好的Cache,可以探討看看到底有多好?怎么做到這么好?最后通過幾個實戰(zhàn)樣例,在日常工作中應用高性能的本地緩存。
一、 Java本地緩存技術介紹
1.1 使用List集合contains方法循環(huán)遍歷(有序) 1.1 HashMap
通過Map的底層方式,直接將需要緩存的對象放在內(nèi)存中。
優(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最大緩存數(shù) .maximumSize(10) //設置寫緩存后n秒鐘過期 .expireAfterWrite(17, TimeUnit.SECONDS) //設置讀寫緩存后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"; } }
1.4 Encache
Ehcache是一個純java的進程內(nèi)緩存框架,具有快速、精干的特點。是hibernate默認的cacheprovider。
優(yōu)點:支持多種緩存淘汰算法,包括LFU,LRU和FIFO;緩存支持堆內(nèi)緩存,堆外緩存和磁盤緩存;支持多種集群方案,解決數(shù)據(jù)共享問題。
缺點:性能比Caffeine差
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(); } }
在Caffeine的官網(wǎng)介紹中,Caffeine在性能和功能上都與其他幾種方案相比具有優(yōu)勢,因此接下來主要探討Caffeine的性能和實現(xiàn)原理。
二、高性能緩存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能力之后的緩存實現(xiàn)。
如果緩存不錯在,則會通過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的能力。默認的線程池實現(xiàn)是 ForkJoinPool.commonPool() ,當然你也可以通過覆蓋并實現(xiàn) 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 驅(qū)逐策略
基于容量
// 基于緩存內(nèi)的元素個數(shù)進行驅(qū)逐 LoadingCache<Key, Graph> graphs = Caffeine.newBuilder() .maximumSize(10_000) .build(key -> createExpensiveGraph(key)); // 基于緩存內(nèi)元素權重進行驅(qū)逐 LoadingCache<Key, Graph> graphs = Caffeine.newBuilder() .maximumWeight(10_000) .weigher((Key key, Graph graph) -> graph.vertices().size()) .build(key -> createExpensiveGraph(key));
基于時間
// 基于固定的過期時間驅(qū)逐策略 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)); // 基于不同的過期驅(qū)逐策略 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和緩存元素都不再存在其他強引用的時候驅(qū)逐 LoadingCache<Key, Graph> graphs = Caffeine.newBuilder() .weakKeys() .weakValues() .build(key -> createExpensiveGraph(key)); // 當進行GC的時候進行驅(qū)逐 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中可以使用刷新策略,與驅(qū)逐不同的是,在刷新的時候如果查詢緩存元素,其舊值將仍被返回,直到該元素的刷新完畢后結束后才會返回刷新后的新值。
2.4 統(tǒng)計
Cache<Key, Graph> graphs = Caffeine.newBuilder() .maximumSize(10_000) .recordStats() .build();
通過使用Caffeine.recordStats()方法可以打開數(shù)據(jù)收集功能。Cache.stats()方法將會返回一個CacheStats對象,其將會含有一些統(tǒng)計指標,比如:
hitRate(): 查詢緩存的命中率
evictionCount(): 被驅(qū)逐的緩存數(shù)量
averageLoadPenalty(): 新值被載入的平均耗時
配合SpringBoot提供的RESTful Controller,能很方便的查詢Cache的使用情況。
三、Caffeine在SpringBoot的實戰(zhàn)
按照Caffeine Github官網(wǎng)文檔的描述,Caffeine是基于Java8的高性能緩存庫。并且在Spring5(SpringBoot2.x)官方放棄了Guava,而使用了性能更優(yōu)秀的Caffeine作為默認的緩存方案。
SpringBoot使用Caffeine有兩種方式:
方式一:直接引入Caffeine依賴,然后使用Caffeine的函數(shù)實現(xiàn)緩存
方式二:引入Caffeine和Spring Cache依賴,使用SpringCache注解方法實現(xiàn)緩存
下面分別介紹兩種使用方式。
方式一:使用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() // 設置最后一次寫入或訪問后經(jīng)過固定時間過期 .expireAfterWrite(60, TimeUnit.SECONDS) // 初始的緩存空間大小 .initialCapacity(100) // 緩存的最大條數(shù) .maximumSize(1000) .build(); } }
最后給服務添加緩存功能
@Slf4j @Service public class UserInfoServiceImpl { /** * 模擬數(shù)據(jù)庫存儲數(shù)據(jù) */ 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()); // 替換內(nèi)容 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() // 設置最后一次寫入或訪問后經(jīng)過固定時間過期 .expireAfterAccess(60, TimeUnit.SECONDS) // 初始的緩存空間大小 .initialCapacity(100) // 緩存的最大條數(shù) .maximumSize(1000)); return cacheManager; } }
最后給服務添加緩存功能
@Slf4j @Service @CacheConfig(cacheNames = "caffeineCacheManager") public class UserInfoServiceImpl { /** * 模擬數(shù)據(jù)庫存儲數(shù)據(jù) */ 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()); // 替換內(nèi)容 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本地高性能緩存的幾種常見實現(xiàn)方式的文章就介紹到這了,更多相關Java本地高性能緩存實現(xiàn)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Mybatis Mapper接口和xml綁定的多種方式、內(nèi)部實現(xiàn)原理和過程解析
在Mybatis中,我們需要創(chuàng)建一個與實體類對應的Mapper接口,然后在該接口上添加方法,這些方法對應著SQL語句,這篇文章主要介紹了Mybatis Mapper接口和xml綁定的多種方式、內(nèi)部實現(xiàn)原理和過程,需要的朋友可以參考下2023-11-11SpringBoot Controller返回圖片的三種方式
在互聯(lián)網(wǎng)的世界里,圖片無處不在,它們是信息傳遞的重要媒介,也是視覺盛宴的一部分,而在Spring Boot項目中,如何優(yōu)雅地處理和返回圖片數(shù)據(jù),則成為了開發(fā)者們不得不面對的問題,今天,就讓我們一起來探索Spring Boot Controller的神奇轉(zhuǎn)換,需要的朋友可以參考下2024-07-07Springboot整合ActiveMQ實現(xiàn)消息隊列的過程淺析
昨天仔細研究了activeMQ消息隊列,也遇到了些坑,下面這篇文章主要給大家介紹了關于SpringBoot整合ActiveMQ的相關資料,文中通過實例代碼介紹的非常詳細,需要的朋友可以參考下2023-02-02SpringBoot之@ConditionalOnProperty注解使用方法
在平時業(yè)務中,我們需要在配置文件中配置某個屬性來決定是否需要將某些類進行注入,讓Spring進行管理,而@ConditionalOnProperty能夠?qū)崿F(xiàn)該功能,文中有詳細的代碼示例,需要的朋友可以參考下2023-05-05