代碼地址:https://github.com/vikde/demo-guava-cache 一、簡介 guava cache是google guava中的一個記憶體緩存模塊,用於將數據緩存到JVM記憶體中.實際項目開發中經常將一些比較公共或者常用的數據緩存起來方便快速訪問. 記憶體緩存最常見的就是基於H ...
代碼地址:https://github.com/vikde/demo-guava-cache
一、簡介
guava cache是google guava中的一個記憶體緩存模塊,用於將數據緩存到JVM記憶體中.實際項目開發中經常將一些比較公共或者常用的數據緩存起來方便快速訪問.
記憶體緩存最常見的就是基於HashMap實現的緩存,為瞭解決併發問題也可能也會用到ConcurrentHashMap等併發集合,但是記憶體緩存需要考慮很多問題,包括併發問題、緩存過期機制、緩存移除機制、緩存命中統計率等.
guava cache已經考慮到這些問題,可以上手即用.通過CacheBuilder創建緩存、然後設置緩存的相關參數、設置緩存的載入方法等.本例子主要講解guava cache的基本用法,詳細的說明已在代碼中說明.
二、代碼示例
1 package com.vikde.demo.guava.cache; 2 3 import com.google.common.cache.CacheBuilder; 4 import com.google.common.cache.CacheLoader; 5 import com.google.common.cache.LoadingCache; 6 7 import java.text.SimpleDateFormat; 8 import java.util.Date; 9 import java.util.Random; 10 import java.util.concurrent.TimeUnit; 11 12 /** 13 * google guava cache 緩存demo 14 * 15 * @author vikde 16 * @date 2017/12/14 17 */ 18 public class DemoGuavaCache { 19 public static void main(String[] args) throws Exception { 20 LoadingCache<Integer, String> cache = CacheBuilder.newBuilder() 21 //設置併發級別為8,併發級別是指可以同時寫緩存的線程數 22 .concurrencyLevel(8) 23 //設置緩存容器的初始容量為10 24 .initialCapacity(10) 25 //設置緩存最大容量為100,超過100之後就會按照LRU最近雖少使用演算法來移除緩存項 26 .maximumSize(100) 27 //是否需要統計緩存情況,該操作消耗一定的性能,生產環境應該去除 28 .recordStats() 29 //設置寫緩存後n秒鐘過期 30 .expireAfterWrite(17, TimeUnit.SECONDS) 31 //設置讀寫緩存後n秒鐘過期,實際很少用到,類似於expireAfterWrite 32 //.expireAfterAccess(17, TimeUnit.SECONDS) 33 //只阻塞當前數據載入線程,其他線程返回舊值 34 //.refreshAfterWrite(13, TimeUnit.SECONDS) 35 //設置緩存的移除通知 36 .removalListener(notification -> { 37 System.out.println(notification.getKey() + " " + notification.getValue() + " 被移除,原因:" + notification.getCause()); 38 }) 39 //build方法中可以指定CacheLoader,在緩存不存在時通過CacheLoader的實現自動載入緩存 40 .build(new DemoCacheLoader()); 41 42 //模擬線程併發 43 new Thread(() -> { 44 //非線程安全的時間格式化工具 45 SimpleDateFormat simpleDateFormat = new SimpleDateFormat("HH:mm:ss"); 46 try { 47 for (int i = 0; i < 15; i++) { 48 String value = cache.get(1); 49 System.out.println(Thread.currentThread().getName() + " " + simpleDateFormat.format(new Date()) + " " + value); 50 TimeUnit.SECONDS.sleep(3); 51 } 52 } catch (Exception ignored) { 53 } 54 }).start(); 55 56 new Thread(() -> { 57 SimpleDateFormat simpleDateFormat = new SimpleDateFormat("HH:mm:ss"); 58 try { 59 for (int i = 0; i < 10; i++) { 60 String value = cache.get(1); 61 System.out.println(Thread.currentThread().getName() + " " + simpleDateFormat.format(new Date()) + " " + value); 62 TimeUnit.SECONDS.sleep(5); 63 } 64 } catch (Exception ignored) { 65 } 66 }).start(); 67 //緩存狀態查看 68 System.out.println(cache.stats().toString()); 69 } 70 71 /** 72 * 隨機緩存載入,實際使用時應實現業務的緩存載入邏輯,例如從資料庫獲取數據 73 */ 74 public static class DemoCacheLoader extends CacheLoader<Integer, String> { 75 @Override 76 public String load(Integer key) throws Exception { 77 System.out.println(Thread.currentThread().getName() + " 載入數據開始"); 78 TimeUnit.SECONDS.sleep(8); 79 Random random = new Random(); 80 System.out.println(Thread.currentThread().getName() + " 載入數據結束"); 81 return "value:" + random.nextInt(10000); 82 } 83 } 84 }
三、策略分析
expireAfterWrite 寫緩存後多久過期
expireAfterAccess 讀寫緩存後多久過期
refreshAfterWrite 寫入數據後多久過期,只阻塞當前數據載入線程,其他線程返回舊值
這幾個策略時間可以單獨設置,也可以組合配置
expireAfterWrite與refreshAfterWrite單獨使用與混合使用的策略分析
已知配置條件:
Thread-1 每 3 秒獲取一次緩存id=1的數據
Thread-2 每 5 秒獲取一次緩存id=1的數據
載入一次緩存載入數據耗時 8 秒
1、expireAfterWrite單獨使用
expireAfterWrite=17
Thread-2 載入數據開始
Thread-2 載入數據結束
Thread-1 01:04:07 value:6798
Thread-2 01:04:07 value:6798
Thread-1 01:04:10 value:6798
Thread-2 01:04:12 value:6798
Thread-1 01:04:13 value:6798
Thread-1 01:04:16 value:6798
Thread-2 01:04:17 value:6798
Thread-1 01:04:19 value:6798
Thread-1 01:04:22 value:6798
Thread-2 01:04:22 value:6798
1 value:6798 被移除,原因:EXPIRED
Thread-1 載入數據開始
Thread-1 載入數據結束
Thread-1 01:04:33 value:7836
Thread-2 01:04:33 value:7836
Thread-1 01:04:36 value:7836
Thread-2 01:04:38 value:7836
Thread-1 01:04:39 value:7836
說明:
啟動時Thread-2載入數據,此時緩存中無數據,Thread-1阻塞等待Thread-2載入完成數據. 在設置的時間數據過期後Thread-1載入數據,Thread-2本應該01:04:22後的5秒載入數據,但是Thread-1等待3秒後載入,數據載入耗時8秒,所以Thread-2在01:04:33時載入數據成功.
結論:
當其他線程在載入數據的時候,當前線程會一直阻塞等待其他線程載入數據完成.
2、refreshAfterWrite單獨使用
refreshAfterWrite=17
Thread-2 載入數據開始
Thread-2 載入數據結束
Thread-1 01:13:32 value:551
Thread-2 01:13:32 value:551
Thread-1 01:13:35 value:551
Thread-2 01:13:37 value:551
Thread-1 01:13:38 value:551
Thread-1 01:13:41 value:551
Thread-2 01:13:42 value:551
Thread-1 01:13:44 value:551
Thread-1 01:13:47 value:551
Thread-2 01:13:47 value:551
Thread-1 載入數據開始
Thread-2 01:13:52 value:551
Thread-2 01:13:57 value:551
Thread-1 載入數據結束
1 value:551 被移除,原因:REPLACED
Thread-1 01:13:58 value:827
Thread-1 01:14:01 value:827
Thread-2 01:14:02 value:827
Thread-1 01:14:04 value:827
Thread-2 01:14:07 value:827
說明:
啟動時Thread-2載入數據,此時緩存中無數據,Thread-1阻塞等待Thread-2載入完成數據. 在設置的時間數據過期後Thread-1載入數據,Thread-2仍然按照策略獲取到舊數據成功.
結論:
當沒有數據的時候,其他線程在載入數據的時候,當前線程會一直阻塞等待其他線程載入數據完成;如果有數據的情況下其他線程正在載入數據,當前線程返回舊數據.
3、expireAfterWrite與refreshAfterWrite一起使用情況一
expireAfterWrite=13
refreshAfterWrite=17
Thread-2 載入數據開始
Thread-2 載入數據結束
Thread-1 01:18:32 value:5901
Thread-2 01:18:32 value:5901
Thread-1 01:18:35 value:5901
Thread-2 01:18:37 value:5901
Thread-1 01:18:38 value:5901
Thread-1 01:18:41 value:5901
Thread-2 01:18:42 value:5901
Thread-1 01:18:44 value:5901
1 value:5901 被移除,原因:EXPIRED
Thread-1 載入數據開始
Thread-1 載入數據結束
Thread-2 01:18:55 value:1300
Thread-1 01:18:55 value:1300
Thread-1 01:18:58 value:1300
Thread-2 01:19:00 value:1300
Thread-1 01:19:01 value:1300
說明:
啟動時Thread-2載入數據,此時緩存中無數據,Thread-1阻塞等待Thread-2載入完成數據. 在設置的時間數據過期後Thread-1載入數據,Thread-2本應該01:18:42後的5秒載入數據,但是Thread-1等待3秒後載入,數據載入耗時8秒,所以Thread-2在01:18:55時載入數據成功.
結論:
當其他線程在載入數據的時候,當前線程會一直阻塞等待其他線程載入數據完成,與單獨使用expireAfterWrite一樣的效果.
4、expireAfterWrite與refreshAfterWrite一起使用情況二
expireAfterWrite=17
refreshAfterWrite=13
Thread-2 載入數據開始
Thread-2 載入數據結束
Thread-1 01:20:25 value:1595
Thread-2 01:20:25 value:1595
Thread-1 01:20:28 value:1595
Thread-2 01:20:30 value:1595
Thread-1 01:20:31 value:1595
Thread-1 01:20:34 value:1595
Thread-2 01:20:35 value:1595
Thread-1 01:20:37 value:1595
Thread-2 載入數據開始
Thread-1 01:20:40 value:1595
Thread-2 載入數據結束
Thread-1 01:20:48 value:2277
1 value:1595 被移除,原因:EXPIRED
Thread-2 01:20:48 value:2277
Thread-1 01:20:51 value:2277
Thread-2 01:20:53 value:2277
Thread-1 01:20:54 value:2277
Thread-1 01:20:57 value:2277
Thread-2 01:20:58 value:2277
Thread-1 01:21:00 value:2277
Thread-1 載入數據開始
Thread-2 01:21:03 value:2277
Thread-1 載入數據結束
Thread-2 01:21:11 value:3750
1 value:2277 被移除,原因:EXPIRED
Thread-1 01:21:11 value:3750
Thread-1 01:21:14 value:3750
Thread-2 01:21:16 value:3750
Thread-1 01:21:17 value:3750
Thread-1 01:21:20 value:3750
Thread-2 01:21:21 value:3750
說明:
啟動時Thread-2載入數據,此時緩存中無數據,Thread-1阻塞等待Thread-2載入完成數據. 在設置的時間數據過期後Thread-2載入數據,Thread-1仍然按照策略在01:20:40獲取到舊數據成功,但是本應該01:20:45繼續獲取一次數據但是等到01:20:48才獲取成功.
結論:
當沒有數據的時候,其他線程在載入數據的時候,當前線程會一直阻塞等待其他線程載入數據完成; 如果有數據的情況下其他線程正在載入數據,已經超過refreshAfterWrite設置時間但是沒有超過expireAfterWrite設置的時間時當前線程返回舊數據. 如果有數據的情況下其他線程正在載入數據,已經超過expireAfterWrite設置的時間時當前線程阻塞等待其他線程載入數據完成. 這種情況適合與設置一個載入緩衝區的情況,既能保證過期後載入數據,又能保證長時間沒訪問多個線程併發時獲取到過期舊數據的情況.