二級緩存介紹
- 二級緩存分為本地緩存和遠端緩存,也可稱為記憶體緩存和網絡緩存
- 常見的流行緩存架構
- 本地緩存:Caffeine,Guava Cache
- 遠端緩存:Redis,MemCache
- 二級緩存的通路流程
- 二級緩存的優勢與問題
- 優勢:二級緩存優先使用本地緩存,通路資料非常快,有效減少和遠端緩存之間的資料交換,節約網絡開銷
- 問題:分布式環境下本地緩存存在一緻性問題,本地緩存變更後需要通知其他節點重新整理本地緩存,這對一緻性要求高的場景可能不能很好的适應
SpringBoot Cache 元件
- SpringBoot Cache 元件提供了一套緩存管理的接口以及聲明式使用的緩存的注解
- 引入 SpringBoot Cache
- xml複制代碼
- <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency>
- 如何內建第三方緩存架構到 Cache 元件
- 實作 Cache 接口,适配第三方緩存架構的操作,實作 CacheManager 接口,提供緩存管理器的 Bean
- SpringBoot Cache 預設提供了 Caffeine、ehcache 等常見緩存架構的管理器,引入相關依賴後即可使用
- 引入 Caffeine
- xml複制代碼
- <dependency> <groupId>com.github.ben-manes.caffeine</groupId> <artifactId>caffeine</artifactId> </dependency>
- SpringBoot Redis 提供了 Redis 緩存的實作及管理器
- 引入 Redis 緩存、RedisTemplate
- xml複制代碼
- <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
- SpringBoot Cache 聲明式緩存注解
- @Cacheable:執行方法前,先從緩存中擷取,沒有擷取到才執行方法,并将其結果更新到緩存
- @CachePut:執行方法後,将其結果更新到緩存
- @CacheEvict:執行方法後,清除緩存
- @Caching:組合前三個注解
- @Cacheable 注解的常用屬性:
- vbnet複制代碼
- cacheNames/value:緩存名稱 key:緩存資料的 key,預設使用方法參數值,支援 SpEL keyGenerator:指定 key 的生成器,和 key 屬性二選一 cacheManager:指定使用的緩存管理器。 condition:在方法執行開始前檢查,在符合 condition 時,進行緩存操作 unless:在方法執行完成後檢查,在符合 unless 時,不進行緩存操作 sync:是否使用同步模式,同步模式下,多個線程同時未命中一個 key 的資料,将阻塞競争執行方法
- SpEL 支援的表達式
本地緩存 Caffeine
Caffeine 介紹
- Caffeine 是繼 Guava Cache 之後,在 SpringBoot 2.x 中預設內建的緩存架構
- Caffeine 使用了 Window TinyLFU 淘汰政策,緩存命中率極佳,被稱為現代高性能緩存庫之王
- 建立一個 Caffeine Cache
- java複制代碼
- Cache<String, Object> cache = Caffeine.newBuilder().build();
Caffeine 記憶體淘汰政策
- FIFO:先進先出,命中率低
- LRU:最近最久未使用,不能應對冷門突發流量,會導緻熱點資料被淘汰
- LFU:最近最少使用,需要維護使用頻率,占用記憶體空間,
- W-TinyLFU:LFU 的變種,綜合了 LRU LFU 的長處,高命中率,低記憶體占用
Caffeine 緩存失效政策
- 基于容量大小
- 根據最大容量
- java複制代碼
- Cache<String, Object> cache = Caffeine.newBuilder() .maximumSize(10000) .build();
- 根據權重
- java複制代碼
- Cache<String, Object> cache = Caffeine.newBuilder() .maximumWeight(10000) .weigher((Weigher<String, Object>) (s, o) -> { // 根據不同對象計算權重 return 0; }) .build();
- 基于引用類型
- 基于弱引用,當不存在強引用時淘汰
- java複制代碼
- Cache<String, Object> cache = Caffeine.newBuilder() .weakKeys() .weakValues() .build();
- 基于軟引用,當不存在強引用且記憶體不足時淘汰
- java複制代碼
- Cache<String, Object> cache = Caffeine.newBuilder() .softValues() .build();
- 基于過期時間
- expireAfterWrite,寫入後一定時間後過期
- java複制代碼
- Cache<String, Object> cache = Caffeine.newBuilder() .expireAfterWrite(5, TimeUnit.SECONDS) .build();
- expireAfterAccess(long, TimeUnit),通路後一定時間後過期,一直通路則一直不過期
- expireAfter(Expiry),自定義時間的計算方式
Caffeine 線程池
- Caffeine 預設使用 ForkJoinPool.commonPool()
- Caffeine 線程池可通過 executor 方法設定
Caffeine 名額統計
- Caffeine 通過配置 recordStats 方法開啟名額統計,通過緩存的 stats 方法擷取資訊
- Caffeine 名額統計的内容有:命中率,加載資料耗時,緩存數量相關等
Caffeine Cache 的種類
- 普通 Cache
- java複制代碼
- Cache<String, Object> cache = Caffeine.newBuilder() .expireAfterWrite(5, TimeUnit.SECONDS) .build(); // 存入 cache.put("key1", "123"); // 取出 Object key1Obj = cache.getIfPresent("key1"); // 清除 cache.invalidate("key1"); // 清除全部 cache.invalidateAll();
- 異步 Cache
- 響應結果通過 CompletableFuture 包裝,利用線程池異步執行
- java複制代碼
- AsyncCache<String, Object> asyncCache = Caffeine.newBuilder() .expireAfterWrite(5, TimeUnit.SECONDS) .buildAsync(); // 存入 asyncCache.put("key1", CompletableFuture.supplyAsync(() -> "123")); // 取出 CompletableFuture<Object> key1Future = asyncCache.getIfPresent("key1"); try { Object key1Obj = key1Future.get(); } catch (InterruptedException | ExecutionException e) { // } // 清除 asyncCache.synchronous().invalidate("key1"); // 清除全部 asyncCache.synchronous().invalidateAll();
- Loading Cache
- 和普通緩存使用方式一緻
- 在緩存未命中時,自動加載資料到緩存,需要設定加載資料的回調,比如從資料庫查詢資料
- java複制代碼
- LoadingCache<String, Object> cache = Caffeine.newBuilder() .expireAfterWrite(5, TimeUnit.SECONDS) .build(key -> { // 擷取業務資料 return "Data From DB"; });
- 異步 Loading Cache
- 和異步緩存使用方式一緻
- 在緩存未命中時,自動加載資料到緩存,與 Loading Cache 不同的是,加載資料是異步的
- java複制代碼
- // 使用 AsyncCache 的線程池異步加載 AsyncLoadingCache<String, Object> asyncCache0 = Caffeine.newBuilder() .expireAfterWrite(5, TimeUnit.SECONDS) .buildAsync(key -> { // 擷取業務資料 return "Data From DB"; }); // 指定加載使用的線程池 AsyncLoadingCache<String, Object> asyncCache1 = Caffeine.newBuilder() .expireAfterWrite(5, TimeUnit.SECONDS) .buildAsync((key, executor) -> CompletableFuture.supplyAsync(() -> { // 異步擷取業務資料 return "Data From DB"; }, otherExecutor));
- 注意:AsyncLoadingCache 不支援弱引用和軟引用相關淘汰政策
Caffeine 自動重新整理機制
- Caffeine 可通過 refreshAfterWrite 設定定時重新整理
- 必須是指定了 CacheLoader 的緩存,即 LoadingCache 和 AsyncLoadingCache
- java複制代碼
- LoadingCache<String, Object> cache = Caffeine.newBuilder() .expireAfterWrite(5, TimeUnit.SECONDS) .refreshAfterWrite(3, TimeUnit.SECONDS) .build(key -> { // 擷取業務資料 return "Data From DB"; });
- refreshAfterWrite 是一種定時重新整理,key 過期時并不一定會立即重新整理
實作二級緩存
配置類 DLCacheProperties
java複制代碼@Data
@ConfigurationProperties(prefix = "uni-boot.cache.dl")
public class DLCacheProperties {
/**
* 是否存儲 null 值
*/
private boolean allowNullValues = true;
/**
* 過期時間,為 0 表示不過期,預設 30 分鐘
* 機關:毫秒
*/
private long defaultExpiration = 30 * 60 * 1000;
/**
* 針對 cacheName 設定過期時間,為 0 表示不過期
* 機關:毫秒
*/
private Map<String, Long> cacheExpirationMap;
/**
* 本地緩存 caffeine 配置
*/
private LocalConfig local = new LocalConfig();
/**
* 遠端緩存 redis 配置
*/
private RemoteConfig remote = new RemoteConfig();
@Data
public static class LocalConfig {
/**
* 初始化大小,為 0 表示預設
*/
private int initialCapacity;
/**
* 最大緩存個數,為 0 表示預設
* 預設最多 5 萬條
*/
private long maximumSize = 10000L;
}
@Data
public static class RemoteConfig {
/**
* Redis pub/sub 緩存重新整理通知主題
*/
private String syncTopic = "cache:dl:refresh:topic";
}
}
緩存實作 DLCache
本地緩存基于 Caffeine,遠端緩存使用 Redis
實作 SpringBoot Cache 的抽象類,AbstractValueAdaptingCache
java複制代碼@Slf4j
@Getter
public class DLCache extends AbstractValueAdaptingCache {
private final String name;
private final long expiration;
private final DLCacheProperties cacheProperties;
private final Cache<String, Object> caffeineCache;
private final RedisTemplate<String, Object> redisTemplate;
public DLCache(String name, long expiration, DLCacheProperties cacheProperties,
Cache<String, Object> caffeineCache, RedisTemplate<String, Object> redisTemplate) {
super(cacheProperties.isAllowNullValues());
this.name = name;
this.expiration = expiration;
this.cacheProperties = cacheProperties;
this.caffeineCache = caffeineCache;
this.redisTemplate = redisTemplate;
}
@Override
public String getName() {
return name;
}
@Override
public Object getNativeCache() {
return this;
}
@Override
protected Object lookup(Object key) {
String redisKey = getRedisKey(key);
Object val;
val = caffeineCache.getIfPresent(key);
// val 是 toStoreValue 包裝過的值,為 null 則 key 不存在
// 因為存儲的 null 值被包裝成了 DLCacheNullVal.INSTANCE
if (ObjectUtil.isNotNull(val)) {
log.debug("DLCache local get cache, key:{}, value:{}", key, val);
return val;
}
val = redisTemplate.opsForValue().get(redisKey);
if (ObjectUtil.isNotNull(val)) {
log.debug("DLCache remote get cache, key:{}, value:{}", key, val);
caffeineCache.put(key.toString(), val);
return val;
}
return val;
}
@SuppressWarnings("unchecked")
@Override
public <T> T get(Object key, Callable<T> valueLoader) {
T val;
val = (T) lookup(key);
if (ObjectUtil.isNotNull(val)) {
return val;
}
// 雙檢鎖
synchronized (key.toString().intern()) {
val = (T) lookup(key);
if (ObjectUtil.isNotNull(val)) {
return val;
}
try {
// 攔截的業務方法
val = valueLoader.call();
// 加入緩存
put(key, val);
} catch (Exception e) {
throw new DLCacheException("DLCache valueLoader fail", e);
}
return val;
}
}
@Override
public void put(Object key, Object value) {
putRemote(key, value);
sendSyncMsg(key);
putLocal(key, value);
}
@Override
public void evict(Object key) {
// 先清理 redis 再清理 caffeine
clearRemote(key);
sendSyncMsg(key);
clearLocal(key);
}
@Override
public void clear() {
// 先清理 redis 再清理 caffeine
clearRemote(null);
sendSyncMsg(null);
clearLocal(null);
}
private void sendSyncMsg(Object key) {
String syncTopic = cacheProperties.getRemote().getSyncTopic();
DLCacheRefreshMsg refreshMsg = new DLCacheRefreshMsg(name, key);
// 加入 SELF_MSG_MAP 防止自身節點重複處理
DLCacheRefreshListener.SELF_MSG_MAP.add(refreshMsg);
redisTemplate.convertAndSend(syncTopic, refreshMsg);
}
private void putLocal(Object key, Object value) {
// toStoreValue 包裝 null 值
caffeineCache.put(key.toString(), toStoreValue(value));
}
private void putRemote(Object key, Object value) {
if (expiration > 0) {
// toStoreValue 包裝 null 值
redisTemplate.opsForValue().set(getRedisKey(key), toStoreValue(value), expiration, TimeUnit.MILLISECONDS);
return;
}
redisTemplate.opsForValue().set(getRedisKey(key), toStoreValue(value));
}
public void clearRemote(Object key) {
if (ObjectUtil.isNull(key)) {
Set<String> keys = redisTemplate.keys(getRedisKey("*"));
if (ObjectUtil.isNotEmpty(keys)) {
keys.forEach(redisTemplate::delete);
}
return;
}
redisTemplate.delete(getRedisKey(key));
}
public void clearLocal(Object key) {
if (ObjectUtil.isNull(key)) {
caffeineCache.invalidateAll();
return;
}
caffeineCache.invalidate(key);
}
/**
* 檢查是否允許緩存 null
*
* @param value 緩存值
* @return 不為空則 true,為空但允許則 false,否則異常
*/
private boolean checkValNotNull(Object value) {
if (ObjectUtil.isNotNull(value)) {
return true;
}
if (isAllowNullValues() && ObjectUtil.isNull(value)) {
return false;
}
// val 不能為空,但傳了空
throw new DLCacheException("Check null val is not allowed");
}
@Override
protected Object fromStoreValue(Object storeValue) {
if (isAllowNullValues() && DLCacheNullVal.INSTANCE.equals(storeValue)) {
return null;
}
return storeValue;
}
@Override
protected Object toStoreValue(Object userValue) {
if (!checkValNotNull(userValue)) {
return DLCacheNullVal.INSTANCE;
}
return userValue;
}
/**
* 擷取 redis 完整 key
*/
private String getRedisKey(Object key) {
// 雙冒号,與 spring cache 預設一緻
return this.name.concat("::").concat(key.toString());
}
/**
* 在緩存時代替 null 值,以區分是 key 不存在還是 val 為 null
*/
@Data
public static class DLCacheNullVal {
public static final DLCacheNullVal INSTANCE = new DLCacheNullVal();
private String desc = "nullVal";
}
}
注意:需要區分緩存 get 到 null 值和 key 不存在,是以使用了 DLCacheNullVal 來代替 null 值
緩存管理器 DLCacheManager
緩存管理器
實作 SpringBoot Cache 的 CacheManager 接口
java複制代碼@Slf4j
@RequiredArgsConstructor
public class DLCacheManager implements CacheManager {
private final ConcurrentHashMap<String, DLCache> cacheMap = new ConcurrentHashMap<>();
private final DLCacheProperties cacheProperties;
private final RedisTemplate<String, Object> redisTemplate;
@Override
public DLCache getCache(String name) {
return cacheMap.computeIfAbsent(name, (o) -> {
DLCache dlCache = buildCache(o);
log.debug("Create DLCache instance, name:{}", o);
return dlCache;
});
}
private DLCache buildCache(String name) {
Caffeine<Object, Object> caffeine = Caffeine.newBuilder();
// 設定過期時間 expireAfterWrite
long expiration = 0;
// 擷取針對 cache name 設定的過期時間
Map<String, Long> cacheExpirationMap = cacheProperties.getCacheExpirationMap();
if (ObjectUtil.isNotEmpty(cacheExpirationMap) && cacheExpirationMap.get(name) > 0) {
expiration = cacheExpirationMap.get(name);
} else if (cacheProperties.getDefaultExpiration() > 0) {
expiration = cacheProperties.getDefaultExpiration();
}
if (expiration > 0) {
caffeine.expireAfterWrite(expiration, TimeUnit.MILLISECONDS);
}
// 設定參數
LocalConfig localConfig = cacheProperties.getLocal();
if (ObjectUtil.isNotNull(localConfig.getInitialCapacity()) && localConfig.getInitialCapacity() > 0) {
caffeine.initialCapacity(localConfig.getInitialCapacity());
}
if (ObjectUtil.isNotNull(localConfig.getMaximumSize()) && localConfig.getMaximumSize() > 0) {
caffeine.maximumSize(localConfig.getMaximumSize());
}
return new DLCache(name, expiration, cacheProperties, caffeine.build(), redisTemplate);
}
@Override
public Collection<String> getCacheNames() {
return Collections.unmodifiableSet(cacheMap.keySet());
}
}
緩存重新整理監聽器
緩存消息
java複制代碼@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class DLCacheRefreshMsg {
private String cacheName;
private Object key;
}
緩存重新整理消息監聽
java複制代碼@Slf4j
@RequiredArgsConstructor
@Component
public class DLCacheRefreshListener implements MessageListener, InitializingBean {
public static final ConcurrentHashSet<DLCacheRefreshMsg> SELF_MSG_MAP = new ConcurrentHashSet<>();
private final DLCacheManager dlCacheManager;
private final DLCacheProperties cacheProperties;
private final RedisMessageListenerContainer listenerContainer;
@Override
public void onMessage(Message message, byte[] pattern) {
// 序列化出重新整理消息
DLCacheRefreshMsg refreshMsg = (DLCacheRefreshMsg) RedisUtil.getTemplate().getValueSerializer().deserialize(message.getBody());
if (ObjectUtil.isNull(refreshMsg)) {
return;
}
// 判斷是不是自身節點發出
if (SELF_MSG_MAP.contains(refreshMsg)) {
SELF_MSG_MAP.remove(refreshMsg);
return;
}
log.debug("DLCache refresh local, cache name:{}, key:{}", refreshMsg.getCacheName(), refreshMsg.getKey());
// 清理本地緩存
dlCacheManager.getCache(refreshMsg.getCacheName()).clearLocal(refreshMsg.getKey());
}
@Override
public void afterPropertiesSet() {
// 注冊到 RedisMessageListenerContainer
listenerContainer.addMessageListener(this, new ChannelTopic(cacheProperties.getRemote().getSyncTopic()));
}
}
使用二級緩存
注入 DLCacheManager
java複制代碼@Bean(name = "dlCacheManager")
public DLCacheManager dlCacheManager(DLCacheProperties cacheProperties, RedisTemplate<String, Object> redisTemplate) {
return new DLCacheManager(cacheProperties, redisTemplate);
}
使用 @Cacheable 配合 DLCacheManager
java複制代碼@ApiOperation("測試 @Cacheable")
@Cacheable(cacheNames = "test", key = "'dl'", cacheManager = "dlCacheManager")
@PostMapping("test_cacheable")
public String testCacheable() {
log.info("testCacheable 執行");
return "Cacheable";
}
@ApiOperation("測試 @Cacheable null 值")
@Cacheable(cacheNames = "test", key = "'dl'", cacheManager = "dlCacheManager")
@PostMapping("test_cacheable_null")
public String testCacheableNull() {
log.info("testCacheableNull 執行");
return null;
}
@ApiOperation("測試 @CachePut")
@CachePut(cacheNames = "test", key = "'dl'", cacheManager = "dlCacheManager")
@PostMapping("test_put")
public String testPut() {
return "Put";
}
@ApiOperation("測試 @CacheEvict")
@CacheEvict(cacheNames = "test", key = "'dl'", cacheManager = "dlCacheManager")
@PostMapping("test_evict")
public String testEvict() {
return "Evict";
}