前言
緩存是幾乎所有應用程序性能的關鍵。很多時候需要分布式緩存(比如常用的 Redis、Codis),但在許多情況下,本地緩存也可以很好地工作,並且不需要分布式緩存的開銷和復雜性。
對於 DotNet 開發來說,本地 cache 很方便使用(比如 RuntimeCache 等); 對於 Java 說,也有很多優秀的本地 cache 庫(比如 Ehcache、GuavaCache 等),而 Java 這個帝國中,spring 是一個偉大且壟斷的存在。針對不同的緩存技術,Spring 定義了如下的 cacheManger 實現。
CacheManger | 描述 |
---|---|
SimpleCacheManager |
使用簡單的 Collection 來存儲緩存,主要用於測試 |
ConcurrentMapCacheManager |
使用 ConcurrentMap 作為緩存技術(默認),需要顯式的刪除緩存,無過期機制 |
NoOpCacheManager |
僅測試用途,不會實際存儲緩存 |
EhCacheCacheManager |
使用 EhCache 作為緩存技術,以前在 hibernate 的時候經常用 |
GuavaCacheManager |
使用 google guava 的 GuavaCache 作為緩存技術(1.5 版本已不建議使用) |
CaffeineCacheManager |
是使用 Java8 對 Guava 緩存的重寫,spring5(springboot2)開始用 Caffeine 取代 guava |
HazelcastCacheManager |
使用 Hazelcast 作為緩存技術 |
JCacheCacheManager |
使用 JCache 標准的實現作為緩存技術,如 Apache Commons JCS |
RedisCacheManager |
使用 Redis 作為緩存技術 |
因此,在許多應用程序中(包括普通的 Spring 和 Spring Boot),你都可以引入相應的依賴包后,簡單的把 @Cacheable
打在任何方法上使用它,以使其結果被緩存,以便下次調用該方法時,將返回緩存的結果。
Tips:spring cache 使用基於動態生成子類的代理機制來對方法的調用進行切面,如果緩存的方法是內部調用而不是外部引用,會導致代理失敗,切面失效。
雖然 Spring 有一些默認的緩存管理器實現,有時候,一些外部庫總是比簡單的實現更好、更靈活。例如,其中一個高性能的 Java 緩存庫 Caffeine 。
今天,我們主要目標就是:認識 Caffeine 以及如何實現多緩存靈活配置。
那么,簡單認識一下 Caffeine
Caffeine 是使用 Java8 對 Guava 緩存的重寫版本,緩存類似於 ConcurrentMap,但並不完全相同,可提供接近最佳的命中率。它基於 LRU 算法實現,支持多種緩存過期策略。在 Spring Boot 2.0 中將取代 GuavaCache。 特性如下:
- 自動將實體加載到緩存中,可以選擇異步加載;
- 基於頻率和新近度超過最大值時基於大小的淘汰;
- 自上次訪問或上次寫入以來的基於時間的過期;
- 基於第一個請求舊數據時的異步刷新(只放行一個請求去刷新數據);
- 其他
Caffeine 的一些參數,我們后續也會用到
參數 | 描述 |
---|---|
initialCapacity=[integer] |
初始的緩存空間大小(比較常用) |
maximumSize=[long] |
緩存的最大條數 (比較常用) |
maximumWeight=[long] |
緩存的最大權重 |
expireAfterAccess=[duration] |
最后一次寫入或訪問后經過固定時間過期 (比較常用) |
expireAfterWrite=[duration] |
最后一次寫入后經過固定時間過期(比較常用) |
refreshAfterWrite=[duration] |
創建緩存或者最近一次更新緩存后經過固定的時間間隔,刷新緩存 refreshAfterWrite requires a LoadingCache |
weakKeys |
打開 key 的弱引用 |
weakValues |
打開 value 的弱引用 |
softValues |
打開 value 的軟引用 |
recordStats |
開發統計功能 |
注意:
refreshAfterWrite
必須實現 LoadingCache,跟 expire 的區別是,指定時間過后 expire 是 remove 該 key,下次訪問是同步去獲取返回新值,而 refresh 則是指定時間后,不會 remove 該 key,下次訪問會觸發刷新,新值沒有回來時返回舊值。expireAfterWrite
和expireAfterAccess
同時存在時,以expireAfterWrite
為准。maximumSize
和maximumWeight
不可以同時使用。weakValues
和softValues
不可以同時使用。
簡單了解了 caffeine 是什么,有哪些屬性可用,那么我們回過頭來,你會發現,其實 SpringBoot 內部已經提供了一個默認實現 CaffeineCacheManager
(具體可以參見源碼 org.springframework.cache.caffeine.CaffeineCacheManager
)。這里不再過多的展開,可以自行閱讀一下源碼了解下~
因此,理想情況下,這就是你所需要的一切了:只需簡單的創建一個 CacheManager 的 bean,就可以為帶 @Cacheable
注釋的方法進行緩存。
到此,我們大概了解了 caffeine 是個什么,以及應該如何用,那么接下來,我們就用示例說話。看不到代碼瞎 BB 也是很讓人討厭的,不是么。
實戰
階段一目標:定義兩個 manager,實現不同的緩存配置。
舉例如下:
@Configuration public class CaffeineConfig extends CachingConfigurerSupport { @Override @Bean(name = "cacheManager") public CacheManager cacheManager() { CaffeineCacheManager cacheManager = new CaffeineCacheManager(); // 方案一(常用):定制化緩存Cache cacheManager.setCaffeine(Caffeine.newBuilder() .expireAfterWrite(10, TimeUnit.MINUTES) .initialCapacity(100) .maximumSize(10_000)); return cacheManager; } /** * 在@cacheable使用時,指定cacheManager=specCacheManager * * @return CacheManager */ @Bean(name = "specCacheManager") public CacheManager cacheManagerWithSpec() { CaffeineCacheManager cacheManager = new CaffeineCacheManager(); // 不允許空值 cacheManager.setAllowNullValues(false); // 傳入一個CaffeineSpec定制緩存,它的好處是可以把配置方便寫在配置文件里 cacheManager.setCaffeineSpec(CaffeineSpec.parse("initialCapacity=20,maximumSize=100,expireAfterWrite=10m")); // 指定使用該策略的CacheNames cacheManager.setCacheNames(new ArrayList<String>(Arrays.asList("fetchById", "fetchByName"))); return cacheManager; } }
使用起來也很簡單,畢竟 springBoot 這么牛 x 的框架提供了很好的集成和靈活性。
@Slf4j @Service public class UserServiceImpl implements UserService { @Override @Cacheable(cacheNames = "userSelectOrDefault", cacheManager = "cacheManager", key = "#userId") public MyUser selectOrDefault(Integer userId) { System.out.println("我要執行【selectOrDefault】方法的查詢邏輯啦~ userId=" + userId); System.out.println("當前時間:" + LocalDateTime.now().toString()); return new MyUser() .setUserId(userId) .setGender(userId % 2) .setUserName("userName_" + userId); } @Override @Cacheable(cacheNames = "fetchByName", cacheManager = "specCacheManager", key = "#userName") public MyUser fetchByName(String userName) { System.out.println("我要執行【fetchByName】方法的查詢邏輯啦~userName=" + userName); System.out.println("當前時間:" + LocalDateTime.now().toString()); int hashCode = userName.hashCode(); return new MyUser() .setUserId(hashCode) .setGender(hashCode % 2) .setUserName(userName); } }
通過指定 @Cacheable
的 cacheNames
、 cacheManager
就可以“靈活”的使用不同的緩存策略了。可能你覺得已經有點小滿足了,畢竟能靈活配置了嘛~~
然鵝~~
冷靜下,真的“靈活”么?
假如一個項目中有很多要緩存(而且也肯定很常見),並且緩存的策略規則也不盡相同時(比如重要的到期時間、初始容量、最大大小等),你是不是就覺得寫很多類似的 cacheManger
有點不爽?
筆者也翻閱了網上一些文章,但大多是告訴你如何使用自定義規范定義自定義緩存。但是,沒有一個實現了我希望的理想狀態。
我期望的是:既可以使用默認的一些策略規范自動創建緩存,又可以靈活的自定義設置你想要的緩存策略。
是不是聽起來有點貪心?其實,我個人覺得這是追求完美的人很正常的一個想法。
畢竟方法總比困那多~
那么,接下來,我們就要把這個想法落地。
階段二:目標:實現一個既可以手動配置,又可以默認的 CacheManger
更多實現細節,請通過移步公眾號~