SpringBoot?Cache?二級緩存的使用
二級緩存介紹
二級緩存分為本地緩存和遠(yuǎn)程緩存,也可稱為內(nèi)存緩存和網(wǎng)絡(luò)緩存
常見的流行緩存框架
- 本地緩存:Caffeine,Guava Cache
- 遠(yuǎn)程緩存:Redis,MemCache
二級緩存的訪問流程
二級緩存的優(yōu)勢與問題
- 優(yōu)勢:二級緩存優(yōu)先使用本地緩存,訪問數(shù)據(jù)非常快,有效減少和遠(yuǎn)程緩存之間的數(shù)據(jù)交換,節(jié)約網(wǎng)絡(luò)開銷
- 問題:分布式環(huán)境下本地緩存存在一致性問題,本地緩存變更后需要通知其他節(jié)點(diǎn)刷新本地緩存,這對一致性要求高的場景可能不能很好的適應(yīng)
SpringBoot Cache 組件
SpringBoot Cache 組件提供了一套緩存管理的接口以及聲明式使用的緩存的注解
引入 SpringBoot Cache
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency>
如何集成第三方緩存框架到 Cache 組件
實現(xiàn) Cache 接口,適配第三方緩存框架的操作,實現(xiàn) CacheManager 接口,提供緩存管理器的 Bean
SpringBoot Cache 默認(rèn)提供了 Caffeine、ehcache 等常見緩存框架的管理器,引入相關(guān)依賴后即可使用
引入 Caffeine
<dependency> <groupId>com.github.ben-manes.caffeine</groupId> <artifactId>caffeine</artifactId> </dependency>
SpringBoot Redis 提供了 Redis 緩存的實現(xiàn)及管理器
引入 Redis 緩存、RedisTemplate
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
SpringBoot Cache 聲明式緩存注解
@Cacheable:執(zhí)行方法前,先從緩存中獲取,沒有獲取到才執(zhí)行方法,并將其結(jié)果更新到緩存
@CachePut:執(zhí)行方法后,將其結(jié)果更新到緩存
@CacheEvict:執(zhí)行方法后,清除緩存
@Caching:組合前三個注解
@Cacheable 注解的常用屬性:
- cacheNames/value:緩存名稱
- key:緩存數(shù)據(jù)的 key,默認(rèn)使用方法參數(shù)值,支持 SpEL
- keyGenerator:指定 key 的生成器,和 key 屬性二選一
- cacheManager:指定使用的緩存管理器。
- condition:在方法執(zhí)行開始前檢查,在符合 condition 時,進(jìn)行緩存操作
- unless:在方法執(zhí)行完成后檢查,在符合 unless 時,不進(jìn)行緩存操作
- sync:是否使用同步模式,同步模式下,多個線程同時未命中一個 key 的數(shù)據(jù),將阻塞競爭執(zhí)行方法
SpEL 支持的表達(dá)式
本地緩存 Caffeine
Caffeine 介紹
Caffeine 是繼 Guava Cache 之后,在 SpringBoot 2.x 中默認(rèn)集成的緩存框架
Caffeine 使用了 Window TinyLFU 淘汰策略,緩存命中率極佳,被稱為現(xiàn)代高性能緩存庫之王
創(chuàng)建一個 Caffeine Cache
Cache<String, Object> cache = Caffeine.newBuilder().build();
Caffeine 內(nèi)存淘汰策略
- FIFO:先進(jìn)先出,命中率低
- LRU:最近最久未使用,不能應(yīng)對冷門突發(fā)流量,會導(dǎo)致熱點(diǎn)數(shù)據(jù)被淘汰
- LFU:最近最少使用,需要維護(hù)使用頻率,占用內(nèi)存空間,
- W-TinyLFU:LFU 的變種,綜合了 LRU LFU 的長處,高命中率,低內(nèi)存占用
Caffeine 緩存失效策略
1.基于容量大小
根據(jù)最大容量
Cache<String, Object> cache = Caffeine.newBuilder() .maximumSize(10000) .build();
根據(jù)權(quán)重
Cache<String, Object> cache = Caffeine.newBuilder() .maximumWeight(10000) .weigher((Weigher<String, Object>) (s, o) -> { // 根據(jù)不同對象計算權(quán)重 return 0; }) .build();
2.基于引用類型
基于弱引用,當(dāng)不存在強(qiáng)引用時淘汰
Cache<String, Object> cache = Caffeine.newBuilder() .weakKeys() .weakValues() .build();
基于軟引用,當(dāng)不存在強(qiáng)引用且內(nèi)存不足時淘汰
Cache<String, Object> cache = Caffeine.newBuilder() .softValues() .build();
3.基于過期時間
expireAfterWrite,寫入后一定時間后過期
Cache<String, Object> cache = Caffeine.newBuilder() .expireAfterWrite(5, TimeUnit.SECONDS) .build();
expireAfterAccess(long, TimeUnit),訪問后一定時間后過期,一直訪問則一直不過期
expireAfter(Expiry),自定義時間的計算方式
Caffeine 線程池
- Caffeine 默認(rèn)使用 ForkJoinPool.commonPool()
- Caffeine 線程池可通過 executor 方法設(shè)置
Caffeine 指標(biāo)統(tǒng)計
- Caffeine 通過配置 recordStats 方法開啟指標(biāo)統(tǒng)計,通過緩存的 stats 方法獲取信息
- Caffeine 指標(biāo)統(tǒng)計的內(nèi)容有:命中率,加載數(shù)據(jù)耗時,緩存數(shù)量相關(guān)等Caffeine Cache 的種類
1.普通 Cache
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();
2.異步 Cache
響應(yīng)結(jié)果通過 CompletableFuture 包裝,利用線程池異步執(zhí)行
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();
3.Loading Cache
和普通緩存使用方式一致
在緩存未命中時,自動加載數(shù)據(jù)到緩存,需要設(shè)置加載數(shù)據(jù)的回調(diào),比如從數(shù)據(jù)庫查詢數(shù)據(jù)
LoadingCache<String, Object> cache = Caffeine.newBuilder() .expireAfterWrite(5, TimeUnit.SECONDS) .build(key -> { // 獲取業(yè)務(wù)數(shù)據(jù) return "Data From DB"; });
4.異步 Loading Cache
和異步緩存使用方式一致
在緩存未命中時,自動加載數(shù)據(jù)到緩存,與 Loading Cache 不同的是,加載數(shù)據(jù)是異步的
// 使用 AsyncCache 的線程池異步加載 AsyncLoadingCache<String, Object> asyncCache0 = Caffeine.newBuilder() .expireAfterWrite(5, TimeUnit.SECONDS) .buildAsync(key -> { // 獲取業(yè)務(wù)數(shù)據(jù) return "Data From DB"; }); // 指定加載使用的線程池 AsyncLoadingCache<String, Object> asyncCache1 = Caffeine.newBuilder() .expireAfterWrite(5, TimeUnit.SECONDS) .buildAsync((key, executor) -> CompletableFuture.supplyAsync(() -> { // 異步獲取業(yè)務(wù)數(shù)據(jù) return "Data From DB"; }, otherExecutor));
注意:AsyncLoadingCache 不支持弱引用和軟引用相關(guān)淘汰策略
Caffeine 自動刷新機(jī)制
Caffeine 可通過 refreshAfterWrite 設(shè)置定時刷新
必須是指定了 CacheLoader 的緩存,即 LoadingCache 和 AsyncLoadingCache
LoadingCache<String, Object> cache = Caffeine.newBuilder() .expireAfterWrite(5, TimeUnit.SECONDS) .refreshAfterWrite(3, TimeUnit.SECONDS) .build(key -> { // 獲取業(yè)務(wù)數(shù)據(jù) return "Data From DB"; });
refreshAfterWrite 是一種定時刷新,key 過期時并不一定會立即刷新
實現(xiàn)二級緩存
配置類 DLCacheProperties
@Data @ConfigurationProperties(prefix = "uni-boot.cache.dl") public class DLCacheProperties { /** * 是否存儲 null 值 */ private boolean allowNullValues = true; /** * 過期時間,為 0 表示不過期,默認(rèn) 30 分鐘 * 單位:毫秒 */ private long defaultExpiration = 30 * 60 * 1000; /** * 針對 cacheName 設(shè)置過期時間,為 0 表示不過期 * 單位:毫秒 */ private Map<String, Long> cacheExpirationMap; /** * 本地緩存 caffeine 配置 */ private LocalConfig local = new LocalConfig(); /** * 遠(yuǎn)程緩存 redis 配置 */ private RemoteConfig remote = new RemoteConfig(); @Data public static class LocalConfig { /** * 初始化大小,為 0 表示默認(rèn) */ private int initialCapacity; /** * 最大緩存?zhèn)€數(shù),為 0 表示默認(rèn) * 默認(rèn)最多 5 萬條 */ private long maximumSize = 10000L; } @Data public static class RemoteConfig { /** * Redis pub/sub 緩存刷新通知主題 */ private String syncTopic = "cache:dl:refresh:topic"; } }
緩存實現(xiàn) DLCache
本地緩存基于 Caffeine,遠(yuǎn)程緩存使用 Redis
實現(xiàn) SpringBoot Cache 的抽象類,AbstractValueAdaptingCache
@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 { // 攔截的業(yè)務(wù)方法 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 防止自身節(jié)點(diǎn)重復(fù)處理 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 默認(rèn)一致 return this.name.concat("::").concat(key.toString()); } /** * 在緩存時代替 null 值,以區(qū)分是 key 不存在還是 val 為 null */ @Data public static class DLCacheNullVal { public static final DLCacheNullVal INSTANCE = new DLCacheNullVal(); private String desc = "nullVal"; } }
注意:需要區(qū)分緩存 get 到 null 值和 key 不存在,因此使用了 DLCacheNullVal 來代替 null 值
緩存管理器 DLCacheManager
緩存管理器
實現(xiàn) SpringBoot Cache 的 CacheManager 接口
@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(); // 設(shè)置過期時間 expireAfterWrite long expiration = 0; // 獲取針對 cache name 設(shè)置的過期時間 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); } // 設(shè)置參數(shù) 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()); } }
緩存刷新監(jiān)聽器
緩存消息
@Data @AllArgsConstructor @NoArgsConstructor @Builder public class DLCacheRefreshMsg { private String cacheName; private Object key; }
緩存刷新消息監(jiān)聽
@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; } // 判斷是不是自身節(jié)點(diǎn)發(fā)出 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
@Bean(name = "dlCacheManager") public DLCacheManager dlCacheManager(DLCacheProperties cacheProperties, RedisTemplate<String, Object> redisTemplate) { return new DLCacheManager(cacheProperties, redisTemplate); }
使用 @Cacheable 配合 DLCacheManager
@ApiOperation("測試 @Cacheable") @Cacheable(cacheNames = "test", key = "'dl'", cacheManager = "dlCacheManager") @PostMapping("test_cacheable") public String testCacheable() { log.info("testCacheable 執(zhí)行"); return "Cacheable"; } @ApiOperation("測試 @Cacheable null 值") @Cacheable(cacheNames = "test", key = "'dl'", cacheManager = "dlCacheManager") @PostMapping("test_cacheable_null") public String testCacheableNull() { log.info("testCacheableNull 執(zhí)行"); 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"; }
到此這篇關(guān)于SpringBoot Cache 二級緩存的使用的文章就介紹到這了,更多相關(guān)SpringBoot Cache 二級緩存內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- springboot整合單機(jī)緩存ehcache的實現(xiàn)
- SpringBoot中整合Ehcache實現(xiàn)熱點(diǎn)數(shù)據(jù)緩存的詳細(xì)過程
- Spring中的@Cacheable緩存注解詳解
- 詳解如何使用SpringBoot的緩存@Cacheable
- Spring中緩存注解@Cache的使用詳解
- SpringCache緩存處理詳解
- 詳解Springboot @Cacheable 注解(指定緩存位置)
- Spring Cache @Cacheable 緩存在部分Service中不生效的解決辦法
- Springboot使用@Cacheable注解實現(xiàn)數(shù)據(jù)緩存
- SpringBoot使用Spring?Cache高效處理緩存數(shù)據(jù)
相關(guān)文章
Springboot如何使用mybatis實現(xiàn)攔截SQL分頁
這篇文章主要介紹了Springboot使用mybatis實現(xiàn)攔截SQL分頁,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-06-06SpringBoot+Thymeleaf靜態(tài)資源的映射規(guī)則說明
這篇文章主要介紹了SpringBoot+Thymeleaf靜態(tài)資源的映射規(guī)則說明,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-11-11java用靜態(tài)工廠代替構(gòu)造函數(shù)使用方法和優(yōu)缺點(diǎn)
這篇文章主要介紹了java用靜態(tài)工廠代替構(gòu)造函數(shù)使用方法和優(yōu)缺點(diǎn),需要的朋友可以參考下2014-02-02SpringBoot Application注解原理及代碼詳解
這篇文章主要介紹了SpringBoot Application注解原理及代碼詳解,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-06-06redis scan命令導(dǎo)致redis連接耗盡,線程上鎖的解決
這篇文章主要介紹了redis scan命令導(dǎo)致redis連接耗盡,線程上鎖的解決,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-11-11