前言
Caffeine是基於Java8的高效能快取庫,參考了Google guava的API,基於Guava Cache和ConcurrentLinkedHashMap的經驗改進而來。
效能對比以下是官方的效能測試對比,官方地址:https://github.com/ben-manes/caffeine/wiki/Benchmarks
1. 8個執行緒讀,100%的讀操作2. 6個執行緒讀,2個執行緒寫,也就是75%的讀操作,25%的寫操作。3. 8個執行緒寫,100%的寫操作結論:從以上對比來看,其他快取框架相比較Caffeine就是個渣渣。Caffeine特性1.限制快取大小2.透過非同步自動載入實體到快取中3.基於大小的回收策略4.基於時間的回收策略5.基於引用的回收策略6.當向快取中一個已經過時的元素進行訪問的時候將會進行非同步重新整理7.key自動封裝虛引用8.value自動封裝弱引用或軟引用9.實體過期或被刪除的通知10.寫入通知,可以將其傳播到其他資料來源中11.統計累計訪問快取
最佳實踐新增依賴
<dependency> <groupId>com.github.ben-manes.caffeine</groupId> <artifactId>caffeine</artifactId> <version>2.8.2</version> </dependency>
1. 載入策略
Caffeine提供了四種快取新增策略:手動載入,自動載入,手動非同步載入和自動非同步載入。
(1) 手動新增 public static void manualLoad() { Cache<String, String> cache = Caffeine.newBuilder() .expireAfterWrite(10, TimeUnit.MINUTES) .maximumSize(10_000) .build(); //查詢一個快取元素,沒有查詢到的時候返回null String name = cache.getIfPresent("name"); System.out.println("name:" + name); //查詢快取,如果快取不存在則生成快取元素,如果無法生成則返回null name = cache.get("name", k -> "小明"); System.out.println("name:" + name); //新增或者更新一個快取元素 cache.put("address", "深圳"); String address = cache.getIfPresent("address"); System.out.println("address:" + address); }
(2) 自動載入
private static void autoLoad() throws InterruptedException { LoadingCache<String, String> cache = Caffeine.newBuilder() .maximumSize(10_000) .expireAfterWrite(10, TimeUnit.MINUTES) .build(new CacheLoader<String, String>() { @Nullable @Override public String load(@NonNull String s) throws Exception { System.out.println("load:" + s); return "小明"; } @Override public @NonNull Map<String, String> loadAll(@NonNull Iterable<? extends String> keys) throws Exception { System.out.println("loadAll:" + keys); Map<String, String> map = new HashMap<>(); map.put("phone", "13866668888"); map.put("address", "深圳"); return map; } }); //查詢快取,如果快取不存在則生成快取元素,如果無法生成則返回null String name = cache.get("name"); System.out.println("name:" + name); //批次查詢快取,如果快取不存在則生成快取元素 Map<String, String> graphs = cache.getAll(Arrays.asList("phone", "address")); System.out.println(graphs); }
(3) 手動非同步載入private static void manualAsynLoad() throws ExecutionException, InterruptedException { AsyncCache<String, String> cache = Caffeine.newBuilder() .expireAfterWrite(10, TimeUnit.MINUTES) .maximumSize(10_000) //可以用指定的執行緒池 .executor(Executors.newSingleThreadExecutor()) .buildAsync(); //查詢快取元素,如果不存在,則非同步生成 CompletableFuture<String> graph = cache.get("name", new Function<String, String>() { @SneakyThrows @Override public String apply(String key) { System.out.println("key:" + key+",當前執行緒:"+Thread.currentThread().getName()); //模仿從資料庫獲取值 Thread.sleep(1000); return "小明"; } }); System.out.println("獲取name之前_time:"+System.currentTimeMillis()/1000); String name = graph.get(); System.out.println("獲取name:"+name+",time:"+System.currentTimeMillis()/1000); }
(4) 自動非同步載入private static void autoAsynLoad() throws ExecutionException, InterruptedException { AsyncLoadingCache<String, String> cache = Caffeine.newBuilder() .maximumSize(10_000) .expireAfterWrite(10, TimeUnit.MINUTES) //你可以選擇:去非同步的封裝一段同步操作來生成快取元素 .buildAsync(new AsyncCacheLoader<String, String>() { @Override public @NonNull CompletableFuture<String> asyncLoad(@NonNull String key, @NonNull Executor executor) { System.out.println("自動非同步載入_key:" + key+",當前執行緒:"+Thread.currentThread().getName()); return CompletableFuture.completedFuture("小明"); } }); //也可以選擇:構建一個非同步快取元素操作並返回一個future //.buildAsync((key, executor) -> createExpensiveGraphAsync(key, executor)); //查詢快取元素,如果其不存在,將會非同步進行生成 cache.get("name").thenAccept(name->{ System.out.println("name:" + name); }); } private static CompletableFuture<String> createExpensiveGraphAsync(String key, Executor executor) { return CompletableFuture.supplyAsync(new Supplier<String>() { @Override public String get() { System.out.println(executor); System.out.println("key:" + key+",當前執行緒:"+Thread.currentThread().getName()); return "小明"; } }, executor);}
2. 回收策略Caffeine提供了三種回收策略:基於容量回收、基於時間回收、基於引用回收。
(1) 基於容量回收策略基於大小回收策略有兩種:一種是基於容量大小,一種是基於權重大小。兩者只能取其一。
① 基於容量--maximumSize為快取容量指定特定的大小,Caffeine.maximumSize(long)。當快取容量超過指定的大小,快取將嘗試逐出最近或經常未使用的條目。
public static void main(String[] args) throws InterruptedException { Cache<String, String> cache = Caffeine.newBuilder() .maximumSize(1) .removalListener((String key, Object value, RemovalCause cause) -> System.out.printf("Key %s was removed (%s)%n", key, cause)) .build(); cache.put("name", "小明"); System.out.println("name:" + cache.getIfPresent("name") + ",快取容量:" + cache.estimatedSize()); cache.put("address", "中國"); Thread.sleep(2000); System.out.println("name:" + cache.getIfPresent("name") + ",快取容量:" + cache.estimatedSize());}
② 基於權重--maximumWeight用Caffeine.maximumWeight(long)指定權重大小,透過Caffeine.weigher(Weigher)方法自定義計算權重方式。
public static void main(String[] args) throws InterruptedException { //初始化快取,設定最大權重為20 Cache<Integer, Integer> cache = Caffeine.newBuilder() .maximumWeight(20) .weigher(new Weigher<Integer, Integer>() { @Override public @NonNegative int weigh(@NonNull Integer key, @NonNull Integer value) { System.out.println("weigh:"+value); return value; } }) .removalListener((Integer key, Object value, RemovalCause cause) -> System.out.printf("Key %s was removed (%s)%n", key, cause)) .build(); cache.put(100, 10); //列印快取個數,結果為1 System.out.println(cache.estimatedSize()); cache.put(200, 20); //稍微休眠一秒 Thread.sleep(1000); //列印快取個數,結果為1 System.out.println(cache.estimatedSize());}
(2) 基於時間策略① 寫入時間--expireAfterWrite在最後一次寫入開始計時,到達指定的時間後過期清除。如果一直寫入,那麼一直不會過期。
private static void writeFixedTime() throws InterruptedException { //在最後一次訪問或者寫入後開始計時,在指定的時間後過期。 LoadingCache<String, String> graphs = Caffeine.newBuilder() .expireAfterWrite(1, TimeUnit.SECONDS) .removalListener((String key, Object value, RemovalCause cause) -> System.out.printf("Key %s was removed (%s)%n", key, cause)) .build(key -> createExpensiveGraph(key)); String name = graphs.get("name"); System.out.println("第一次獲取name:" + name); name = graphs.get("name"); System.out.println("第二次獲取name:" + name); Thread.sleep(2000); name = graphs.get("name"); System.out.println("第三次延遲2秒後獲取name:" + name);}private static String createExpensiveGraph(String key) { System.out.println("重新自動載入資料"); return "小明";}
② 寫入和訪問時間--expireAfterAccess在最後一次寫入或訪問開始計時,在指定時間後過期清除。如果一直訪問或寫入,那麼一直不會過期。
private static void accessFixedTime() throws InterruptedException { //在最後一次訪問或者寫入後開始計時,在指定的時間後過期。 LoadingCache<String, String> graphs = Caffeine.newBuilder() .expireAfterAccess(3, TimeUnit.SECONDS) .removalListener((String key, Object value, RemovalCause cause) -> System.out.printf("Key %s was removed (%s)%n", key, cause)) .build(key -> createExpensiveGraph(key)); String name = graphs.get("name"); System.out.println("第一次獲取name:" + name); name = graphs.get("name"); System.out.println("第二次獲取name:" + name); Thread.sleep(2000); name = graphs.get("name"); System.out.println("第三次延遲2秒後獲取name:" + name);}private static String createExpensiveGraph(String key) { System.out.println("重新自動載入資料"); return "小明";}
③ 自定義時間--expireAfter自定義策略,由Expire實現獨自計算時間。分別計算新增、更新、讀取時間。
private static void customTime() throws InterruptedException { LoadingCache<String, String> graphs = Caffeine.newBuilder() .removalListener((String key, Object value, RemovalCause cause) -> System.out.printf("Key %s was removed (%s)%n", key, cause)) .expireAfter(new Expiry<String, String>() { @Override public long expireAfterCreate(@NonNull String key, @NonNull String value, long currentTime) { //這裡的currentTime由Ticker提供,預設情況下與系統時間無關,單位為納秒 System.out.println(String.format("expireAfterCreate----key:%s,value:%s,currentTime:%d", key, value, currentTime)); return TimeUnit.SECONDS.toNanos(10); } @Override public long expireAfterUpdate(@NonNull String key, @NonNull String value, long currentTime, @NonNegative long currentDuration) { //這裡的currentTime由Ticker提供,預設情況下與系統時間無關,單位為納秒 System.out.println(String.format("expireAfterUpdate----key:%s,value:%s,currentTime:%d,currentDuration:%d", key, value, currentTime,currentDuration)); return TimeUnit.SECONDS.toNanos(3); } @Override public long expireAfterRead(@NonNull String key, @NonNull String value, long currentTime, @NonNegative long currentDuration) { //這裡的currentTime由Ticker提供,預設情況下與系統時間無關,單位為納秒 System.out.println(String.format("expireAfterRead----key:%s,value:%s,currentTime:%d,currentDuration:%d", key, value, currentTime,currentDuration)); return currentDuration; } }) .build(key -> createExpensiveGraph(key)); String name = graphs.get("name"); System.out.println("第一次獲取name:" + name); name = graphs.get("name"); System.out.println("第二次獲取name:" + name); Thread.sleep(5000); name = graphs.get("name"); System.out.println("第三次延遲5秒後獲取name:" + name); Thread.sleep(5000); name = graphs.get("name"); System.out.println("第五次延遲5秒後獲取name:" + name);} private static String createExpensiveGraph(String key) { System.out.println("重新自動載入資料"); return "小明";}
(3) 基於引用策略非同步載入的方式不支援引用回收策略
① 軟引用當GC並且記憶體不足時,會觸發軟引用回收策略。
設定jvm啟動時-XX:+PrintGCDetails -Xmx100m 引數,可以看GC日誌列印會觸發軟引用的回收策略。
private static void softValues() throws InterruptedException { //當進行GC的時候進行驅逐 LoadingCache<String, byte[]> cache = Caffeine.newBuilder() .softValues() .removalListener((String key, Object value, RemovalCause cause) -> System.out.printf("Key %s was removed (%s)%n", key, cause)) .build(key -> loadDB(key)); System.out.println("1"); cache.put("name1", new byte[1024 * 1024*50]); System.gc(); System.out.println("2"); Thread.sleep(5000); cache.put("name2", new byte[1024 * 1024*50]); System.gc(); System.out.println("3"); Thread.sleep(5000); cache.put("name3", new byte[1024 * 1024*50]); System.gc(); System.out.println("4"); Thread.sleep(5000); cache.put("name4", new byte[1024 * 1024*50]); System.gc(); Thread.sleep(5000);}private static byte[] loadDB(String key) { System.out.println("重新自動載入資料"); return new byte[1024*1024];}
② 弱引用當GC時,會觸發弱引用回收策略。
設定jvm啟動時-XX:+PrintGCDetails -Xmx100m 引數,可以看GC日誌列印會觸發弱引用的回收策略。
private static void weakKeys() throws InterruptedException { LoadingCache<String, byte[]> cache = Caffeine.newBuilder() .weakKeys() .weakValues() .removalListener((String key, Object value, RemovalCause cause) -> System.out.printf("Key %s was removed (%s)%n", key, cause)) .build(key -> loadDB(key)); System.out.println("新增name1"); cache.put("name1", new byte[1024 * 1024]); System.gc(); System.out.println("新增name2"); Thread.sleep(5000); cache.put("name2", new byte[1024 * 1024]); System.gc(); System.out.println("新增name3"); Thread.sleep(5000); cache.put("name3", new byte[1024 * 1024]); System.gc(); Thread.sleep(5000);}private static byte[] loadDB(String key) { System.out.println("重新自動載入資料"); return new byte[1024*1024];}
未完待續