簡介
前面剛說到Guava Cache,他的優勢是封裝了get,put操做;提供線程安全的緩存操做;提供過時策略;提供回收策略;緩存監控。當緩存的數據超過最大值時,使用LRU算法替換。這一篇咱們將要談到一個新的本地緩存框架:Caffeine Cache。它也是站在巨人的肩膀上-Guava Cache,藉着他的思想優化了算法發展而來。java
按 Caffeine Github 文檔描述,Caffeine 是基於 JAVA 8 的高性能緩存庫。而且在 spring5 (springboot 2.x) 後,spring 官方放棄了 Guava,而使用了性能更優秀的 Caffeine 做爲默認緩存組件。git
Caffine Cache
Caffeine Cache:https://github.com/ben-manes/caffeinegithub
1. 緩存填充策略
Caffeine Cache提供了三種緩存填充策略:手動、同步加載和異步加載。web
手動加載
在每次get key的時候指定一個同步的函數,若是key不存在就調用這個函數生成一個值。算法
/** * 手動加載 * @param key * @return */ public Object manulOperator(String key) { Cache<String, Object> cache = Caffeine.newBuilder() .expireAfterWrite(1, TimeUnit.SECONDS) .expireAfterAccess(1, TimeUnit.SECONDS) .maximumSize(10) .build(); //若是一個key不存在,那麼會進入指定的函數生成value Object value = cache.get(key, t -> setValue(key).apply(key)); cache.put("hello",value); //判斷是否存在若是不存返回null Object ifPresent = cache.getIfPresent(key); //移除一個key cache.invalidate(key); return value; } public Function<String, Object> setValue(String key){ return t -> key + "value"; }
同步加載
構造Cache時候,build方法傳入一個CacheLoader實現類。實現load方法,經過key加載value。spring
/** * 同步加載 * @param key * @return */ public Object syncOperator(String key){ LoadingCache<String, Object> cache = Caffeine.newBuilder() .maximumSize(100) .expireAfterWrite(1, TimeUnit.MINUTES) .build(k -> setValue(key).apply(key)); return cache.get(key); } public Function<String, Object> setValue(String key){ return t -> key + "value"; }
異步加載
AsyncLoadingCache是繼承自LoadingCache類的,異步加載使用Executor去調用方法並返回一個CompletableFuture。異步加載緩存使用了響應式編程模型。數據庫
若是要以同步方式調用時,應提供CacheLoader。要以異步表示時,應該提供一個AsyncCacheLoader,並返回一個CompletableFuture。編程
/** * 異步加載 * @param key * @return */ public Object asyncOperator(String key){ AsyncLoadingCache<String, Object> cache = Caffeine.newBuilder() .maximumSize(100) .expireAfterWrite(1, TimeUnit.MINUTES) .buildAsync(k -> setAsyncValue(key).get()); return cache.get(key); } public CompletableFuture<Object> setAsyncValue(String key){ return CompletableFuture.supplyAsync(() -> { return key + "value"; }); }
2. 回收策略
Caffeine提供了3種回收策略:基於大小回收,基於時間回收,基於引用回收。緩存
基於大小的過時方式
基於大小的回收策略有兩種方式:一種是基於緩存大小,一種是基於權重。安全
// 根據緩存的計數進行驅逐 LoadingCache<String, Object> cache = Caffeine.newBuilder() .maximumSize(10000) .build(key -> function(key)); // 根據緩存的權重來進行驅逐(權重只是用於肯定緩存大小,不會用於決定該緩存是否被驅逐) LoadingCache<String, Object> cache1 = Caffeine.newBuilder() .maximumWeight(10000) .weigher(key -> function1(key)) .build(key -> function(key)); maximumWeight與maximumSize不能夠同時使用。
基於時間的過時方式
// 基於固定的到期策略進行退出 LoadingCache<String, Object> cache = Caffeine.newBuilder() .expireAfterAccess(5, TimeUnit.MINUTES) .build(key -> function(key)); LoadingCache<String, Object> cache1 = Caffeine.newBuilder() .expireAfterWrite(10, TimeUnit.MINUTES) .build(key -> function(key)); // 基於不一樣的到期策略進行退出 LoadingCache<String, Object> cache2 = Caffeine.newBuilder() .expireAfter(new Expiry<String, Object>() { @Override public long expireAfterCreate(String key, Object value, long currentTime) { return TimeUnit.SECONDS.toNanos(seconds); } @Override public long expireAfterUpdate(@Nonnull String s, @Nonnull Object o, long l, long l1) { return 0; } @Override public long expireAfterRead(@Nonnull String s, @Nonnull Object o, long l, long l1) { return 0; } }).build(key -> function(key));
Caffeine提供了三種定時驅逐策略:
- expireAfterAccess(long, TimeUnit):在最後一次訪問或者寫入後開始計時,在指定的時間後過時。假如一直有請求訪問該key,那麼這個緩存將一直不會過時。
- expireAfterWrite(long, TimeUnit):在最後一次寫入緩存後開始計時,在指定的時間後過時。
- expireAfter(Expiry):自定義策略,過時時間由Expiry實現獨自計算。
緩存的刪除策略使用的是惰性刪除和定時刪除。這兩個刪除策略的時間複雜度都是O(1)。
基於引用的過時方式
// 當key和value都沒有引用時驅逐緩存 LoadingCache<String, Object> cache = Caffeine.newBuilder() .weakKeys() .weakValues() .build(key -> function(key)); // 當垃圾收集器須要釋放內存時驅逐 LoadingCache<String, Object> cache1 = Caffeine.newBuilder() .softValues() .build(key -> function(key)); 注意:AsyncLoadingCache不支持弱引用和軟引用。
Caffeine.weakKeys(): 使用弱引用存儲key。若是沒有其餘地方對該key有強引用,那麼該緩存就會被垃圾回收器回收。因爲垃圾回收器只依賴於身份(identity)相等,所以這會致使整個緩存使用身份 (==) 相等來比較 key,而不是使用 equals()。
Caffeine.weakValues() :使用弱引用存儲value。若是沒有其餘地方對該value有強引用,那麼該緩存就會被垃圾回收器回收。因爲垃圾回收器只依賴於身份(identity)相等,所以這會致使整個緩存使用身份 (==) 相等來比較 key,而不是使用 equals()。
Caffeine.softValues() :使用軟引用存儲value。當內存滿了事後,軟引用的對象以將使用最近最少使用(least-recently-used ) 的方式進行垃圾回收。因爲使用軟引用是須要等到內存滿了才進行回收,因此咱們一般建議給緩存配置一個使用內存的最大值。 softValues() 將使用身份相等(identity) (==) 而不是equals() 來比較值。
Caffeine.weakValues()和Caffeine.softValues()不能夠一塊兒使用。
3. 移除事件監聽
Cache<String, Object> cache = Caffeine.newBuilder() .removalListener((String key, Object value, RemovalCause cause) -> System.out.printf("Key %s was removed (%s)%n", key, cause)) .build();
4. 寫入外部存儲
CacheWriter 方法能夠將緩存中全部的數據寫入到第三方。 LoadingCache<String, Object> cache2 = Caffeine.newBuilder() .writer(new CacheWriter<String, Object>() { @Override public void write(String key, Object value) { // 寫入到外部存儲 } @Override public void delete(String key, Object value, RemovalCause cause) { // 刪除外部存儲 } }) .build(key -> function(key)); 若是你有多級緩存的狀況下,這個方法仍是很實用。 注意:CacheWriter不能與弱鍵或AsyncLoadingCache一塊兒使用。
5. 統計
與Guava Cache的統計同樣。 Cache<String, Object> cache = Caffeine.newBuilder() .maximumSize(10_000) .recordStats() .build(); 經過使用Caffeine.recordStats(), 能夠轉化成一個統計的集合. 經過 Cache.stats() 返回一個CacheStats。CacheStats提供如下統計方法: hitRate(): 返回緩存命中率 evictionCount(): 緩存回收數量 averageLoadPenalty(): 加載新值的平均時間
實例分享
SpringBoot 1.x版本中的默認本地cache是Guava Cache。在2.x(Spring Boot 2.0(spring 5) )版本中已經用Caffine Cache取代了Guava Cache。畢竟有了更優的緩存淘汰策略。
下面咱們來講在SpringBoot2.x版本中如何使用cache。
Caffeine經常使用配置說明:
initialCapacity=[integer]: 初始的緩存空間大小 maximumSize=[long]: 緩存的最大條數 maximumWeight=[long]: 緩存的最大權重 expireAfterAccess=[duration]: 最後一次寫入或訪問後通過固定時間過時 expireAfterWrite=[duration]: 最後一次寫入後通過固定時間過時 refreshAfterWrite=[duration]: 建立緩存或者最近一次更新緩存後通過固定的時間間隔,刷新緩存 weakKeys: 打開key的弱引用 weakValues:打開value的弱引用 softValues:打開value的軟引用 recordStats:開發統計功能 注意: expireAfterWrite和expireAfterAccess同時存在時,以expireAfterWrite爲準。 maximumSize和maximumWeight不能夠同時使用 weakValues和softValues不能夠同時使用
須要說明的是,使用配置文件的方式來進行緩存項配置,通常狀況能知足使用需求,可是靈活性不是很高,若是咱們有不少緩存項的狀況下寫起來會致使配置文件很長。因此通常狀況下你也能夠選擇使用bean的方式來初始化Cache實例。
SpringBoot 有倆種使用 Caffeine 做爲緩存的方式:
- 直接引入 Caffeine 依賴,而後使用 Caffeine 方法實現緩存。
- 引入 Caffeine 和 Spring Cache 依賴,使用 SpringCache 註解方法實現緩存。
1. 直接引入 Caffeine 依賴
Pom
<!-- caffeine cache --> <dependency> <groupId>com.github.ben-manes.caffeine</groupId> <artifactId>caffeine</artifactId> <version>2.5.5</version> </dependency>
Conf
package com.spring.master.spring.caffeine.conf; import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.concurrent.TimeUnit; /** * @author Huan Lee * @version 1.0 * @date 2020-09-30 15:54 * @describtion 業精於勤,荒於嬉;行成於思,毀於隨。 */ @Configuration public class CaffeineCacheConfig { @Bean public Cache<String, Object> caffeineCache() { return Caffeine.newBuilder() // 設置最後一次寫入或訪問後通過固定時間過時 .expireAfterWrite(60, TimeUnit.SECONDS) // 初始的緩存空間大小 .initialCapacity(100) // 緩存的最大條數 .maximumSize(1000) .build(); } }
Bean
package com.spring.master.spring.caffeine.bean; import lombok.Data; import lombok.ToString; /** * @author Huan Lee * @version 1.0 * @date 2020-09-30 15:56 * @describtion 業精於勤,荒於嬉;行成於思,毀於隨。 */ @Data @ToString public class UserInfo { private Integer id; private String name; }
Service
package com.spring.master.spring.caffeine.service; import com.spring.master.spring.caffeine.bean.UserInfo; /** * @author Huan Lee * @version 1.0 * @date 2020-09-30 15:57 * @describtion 業精於勤,荒於嬉;行成於思,毀於隨。 */ public interface UserInfoService { /** * 增長用戶信息 * * @param userInfo 用戶信息 */ void addUserInfo(UserInfo userInfo); /** * 獲取用戶信息 * * @param id 用戶ID * @return 用戶信息 */ UserInfo getByName(Integer id); /** * 刪除用戶信息 * * @param id 用戶ID */ void deleteById(Integer id); }
Impl
package com.spring.master.spring.caffeine.impl; import com.github.benmanes.caffeine.cache.Cache; import com.spring.master.spring.caffeine.bean.UserInfo; import com.spring.master.spring.caffeine.service.UserInfoService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.HashMap; /** * @author Huan Lee * @version 1.0 * @date 2020-09-30 15:57 * @describtion 業精於勤,荒於嬉;行成於思,毀於隨。 */ @Service @Slf4j public class UserInfoServiceImpl implements UserInfoService { /** * 模擬數據庫存儲數據 */ private HashMap<Integer, UserInfo> userInfoMap = new HashMap<>(); @Autowired Cache<String, Object> caffeineCache; @Override public void addUserInfo(UserInfo userInfo) { userInfoMap.put(userInfo.getId(), userInfo); // 加入緩存 caffeineCache.put(String.valueOf(userInfo.getId()),userInfo); } @Override public UserInfo getByName(Integer id) { // 先從緩存讀取 caffeineCache.getIfPresent(id); UserInfo userInfo = (UserInfo) caffeineCache.asMap().get(String.valueOf(id)); if (userInfo != null){ System.out.println("Hello, 我來自Caffeine Cache"); return userInfo; } // 若是緩存中不存在,則從庫中查找 userInfo = userInfoMap.get(id); System.out.println("Hello, 我來自DataBase"); // 若是用戶信息不爲空,則加入緩存 if (userInfo != null){ caffeineCache.put(String.valueOf(userInfo.getId()),userInfo); } return userInfo; } @Override public void deleteById(Integer id) { userInfoMap.remove(id); // 從緩存中刪除 caffeineCache.asMap().remove(String.valueOf(id)); } }
Controller
package com.spring.master.spring.caffeine.controller; import com.spring.master.spring.caffeine.bean.UserInfo; import com.spring.master.spring.caffeine.service.UserInfoService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.*; /** * @author Huan Lee * @version 1.0 * @date 2020-09-30 15:59 * @describtion 業精於勤,荒於嬉;行成於思,毀於隨。 */ @RequestMapping(value = "/caffeine") @RestController public class CaffeineCacheController { @Autowired private UserInfoService userInfoService; @GetMapping("/userInfo/{id}") public Object getUserInfo(@PathVariable Integer id) { UserInfo userInfo = userInfoService.getByName(id); if (userInfo == null) { return "沒有該用戶"; } return userInfo; } @RequestMapping(value = "/userInfo") public Object createUserInfo() { UserInfo userInfo = new UserInfo(); userInfo.setId(1); userInfo.setName("HLee"); userInfoService.addUserInfo(userInfo); return "SUCCESS"; } @GetMapping(value = "/delete/{id}") public Object deleteUserInfo(@PathVariable Integer id) { userInfoService.deleteById(id); return "SUCCESS"; } } 啓動服務: localhost:2000/spring-master/caffeine/userInfo localhost:2000/spring-master/caffeine/userInfo/1 localhost:2000/spring-master/caffeine/delete/1
2. 引入 Caffeine 和 Spring Cache 依賴
Pom
<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>
Conf
@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; } }
Bean
package com.spring.master.spring.caffeine.bean; import lombok.Data; import lombok.ToString; /** * @author Huan Lee * @version 1.0 * @date 2020-09-30 15:56 * @describtion 業精於勤,荒於嬉;行成於思,毀於隨。 */ @Data @ToString public class UserInfo { private Integer id; private String name; }
Service
package com.spring.master.spring.caffeine.service; import com.spring.master.spring.caffeine.bean.UserInfo; /** * @author Huan Lee * @version 1.0 * @date 2020-09-30 15:57 * @describtion 業精於勤,荒於嬉;行成於思,毀於隨。 */ public interface UserInfoService { /** * 增長用戶信息 * * @param userInfo 用戶信息 */ void addUserInfo(UserInfo userInfo); /** * 獲取用戶信息 * * @param id 用戶ID * @return 用戶信息 */ UserInfo getByName(Integer id); /** * 刪除用戶信息 * * @param id 用戶ID */ void deleteById(Integer id); }
Impl
import lombok.extern.slf4j.Slf4j; import mydlq.club.example.entity.UserInfo; import mydlq.club.example.service.UserInfoService; import org.springframework.cache.annotation.CacheConfig; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.CachePut; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import java.util.HashMap; @Slf4j @Service @CacheConfig(cacheNames = "caffeineCacheManager") public class UserInfoServiceImpl implements UserInfoService { /** * 模擬數據庫存儲數據 */ private HashMap<Integer, UserInfo> userInfoMap = new HashMap<>(); @Override @CachePut(key = "#userInfo.id") public void addUserInfo(UserInfo userInfo) { userInfoMap.put(userInfo.getId(), userInfo); } @Override @Cacheable(key = "#id") public UserInfo getByName(Integer id) { return userInfoMap.get(id); } @Override @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; } @Override @CacheEvict(key = "#id") public void deleteById(Integer id) { userInfoMap.remove(id); } }
Controller
@RestController @RequestMapping public class UserInfoController { @Autowired private UserInfoService userInfoService; @GetMapping("/userInfo/{id}") public Object getUserInfo(@PathVariable Integer id) { UserInfo userInfo = userInfoService.getByName(id); if (userInfo == null) { return "沒有該用戶"; } return userInfo; } @PostMapping("/userInfo") public Object createUserInfo(@RequestBody UserInfo userInfo) { userInfoService.addUserInfo(userInfo); return "SUCCESS"; } @PostMapping("/updateInfo") public Object updateUserInfo(@RequestBody UserInfo userInfo) { UserInfo newUserInfo = userInfoService.updateUserInfo(userInfo); if (newUserInfo == null){ return "不存在該用戶"; } return newUserInfo; } @GetMapping("/delete/{id}") public Object deleteUserInfo(@PathVariable Integer id) { userInfoService.deleteById(id); return "SUCCESS"; } }