緩存使用紀要

一、本地緩存:Caffeine

1、簡介

Caffeine是一種高性能、高命中率、內存占用低的本地緩存庫,簡單來說它是 Guava Cache 的優化加強版,是當下最流行、最佳(最優)緩存框架。

Spring5 即將放棄掉 Guava Cache 作為緩存機制,而改用 Caffeine 作為新的本地 Cache 的組件,這對于 Caffeine 來說是一個很大的肯定。為什么 Spring 會這樣做呢?其實在 Caffeine 的Benchmarks[3]里給出了極具說服力的數據,對于讀和寫的場景,和其他幾個緩存工具進行了比較,Caffeine 的性能都表現很突出。

https://zhuanlan.zhihu.com/p/610410926

從上圖可以看出Caffine性能遠超其他本地緩存框架,所以本地緩存用它準沒錯~~
Caffeine其性能突出表現得益于采用了W-TinyLFU(LUR和LFU的優點結合)開源的緩存技術,緩存性能接近理論最優,同時借鑒了 Guava Cache 大部分的概念(諸如核心概念Cache、LoadingCache、CacheLoader、CacheBuilder等等,幾乎可以保證開發人員從Guava Cache 到Caffeine的無縫切換,Caffeine可謂是站在巨人肩膀上呱呱落地的,并且做到了精益求精。

2、用法

Caffeine借鑒了 Guava Cache 大部分的概念(諸如核心概念Cache、LoadingCache、CacheLoader、CacheBuilder)等等,所以數據加載方式都是一個套路

1)緩存類型

Caffeine提供了多種緩存類型:
從同步、異步的角度來說有

#	1、同步加載的緩存:Cache
Cache<Object, Object> cache = Caffeine.newBuilder().maximumSize(10).expireAfterWrite(1, TimeUnit.SECONDS).build();cache.put("1","張三");
System.out.println(cache.getIfPresent("1"));#	2、異步加載的緩存:AsnyncCache
AsyncCache<String, User> asyncCache = Caffeine.newBuilder().maximumSize(100).expireAfterWrite(5, TimeUnit.MINUTES).buildAsync();// 手動提交異步加載任務
CompletableFuture<User> future = asyncCache.get("user1", key -> userDao.getUserAsync(key));
future.thenAccept(user -> System.out.println("異步加載完成:" + user));

從手動加載、自動加載的角度來說有

#	1、手動加載的:Cache、AsnyncCache#	2、自動加載的:LoadingCache、LoadingAsnyncCache
LoadingCache<String, String> dictionaryCache = Caffeine.newBuilder().maximumSize(500).expireAfterWrite(60 * 12, TimeUnit.MINUTES).removalListener(new DictionaryRemovalListener()).recordStats().build(new DictionaryLoader());
-- 或者 --
AsyncLoadingCache<String, String> asyncLoadingCache = Caffeine.newBuilder().expireAfterWrite(1, TimeUnit.SECONDS).maximumSize(10).buildAsync(key -> {Thread.sleep(1000);return new Date().toString();});//異步緩存返回的是CompletableFuture
CompletableFuture<String> future = asyncLoadingCache.get("1");
future.thenAccept(System.out::println);

2)常用的緩存屬性

緩存初始容量

initialCapacity :整數,表示能存儲多少個緩存對象。
為什么要設置初始容量呢?
因為如果提前能預估緩存的使用大小,那么可以設置緩存的初始容量,以免緩存不斷地進行擴容,致使效率不高。

最大容量

maximumSize :最大容量,如果緩存中的數據量超過這個數值,Caffeine 會有一個異步線程來專門負責清除緩存,按照指定的清除策略來清除掉多余的緩存
注意,清除多余緩存不會立即執行,需要時間;比如最大容量為3,當添加第4個緩存后立即獲取緩存數量可能發現是4,因為此時Caffeine的異步線程還沒來得及根據策略清除多余的緩存,等待一段時間再查就變成3了。

最大權重(比較少用)

maximumWeight :最大權重,可以為存入緩存的每個元素都設置一個權重值,當緩存中所有元素的權重值超過最大權重時,就會觸發異步清除。

static class Student{Integer age;String name;
}Caffeine<String, Student> caffeine = Caffeine.newBuilder().maximumWeight(30).weigher((String key, Student value)-> value.getAge()).build();cache.put("one", new Student(12, "one"));
cache.put("two", new Student(18, "two"));
cache.put("three", new Student(1, "three"));
TimeUnit.SECONDS.sleep(10);
System.out.println(cache.estimatedSize());  // 2
System.out.println(cache.getIfPresent("two")); // null#	如上示例,以緩存對象的age為權重值,并設置最大權重為30
#	當放入緩存對象的age之和超過30時,會執行異步清除(需要時間)
#	應該是依次清除占比最大的,直到低于最大權重值
過期策略

expireAfterAccess: 根據最后一次訪問時間和當前時間間隔,超過指定值觸發清除,注意:這里訪問包括讀取和寫入

expireAfterWrite: 某個數據在多久沒有被更新后,就過期。

refreshAfterWrite: 寫操作完成后多久才將數據刷新進緩存中,即通過LoadingCache.refresh(K)進行異步刷新, 如果想覆蓋默認的刷新行為, 可以實現CacheLoader.reload(K, V)方法

上面是基于時間實現過期清除的,Cadfeine也提供基于軟/弱引用實現過期,但是比較復雜、我沒搞懂

清除、更新監聽

removalListener: 當緩存中的數據發送更新,或者被清除時,就會觸發監聽器:
removalListener 方法的參數是一個 RemovalListener 對象,但是可以函數式傳參,當數據被更新或者清除時,會給監聽器提供三個內容,(鍵,值,原因)分別對應代碼中的三個參數,(鍵,值)都是更新前,清除前的舊值, 這樣可以了解到清除的詳細了

清除的原因有 5 個,存儲在枚舉類 RemovalCause 中:
EXPLICIT : 表示顯式地調用刪除操作,直接將某個數據刪除。
REPLACED:表示某個數據被更新。
EXPIRED:表示因為生命周期結束(過期時間到了),而被清除。
SIZE:表示因為緩存空間大小受限,總權重受限,而被清除。
COLLECTED : 英文意思“冷靜”,這個不明白。

public class CaffeineCacheRemovalListener implements RemovalListener<Object, Object> {@Overridepublic void onRemoval(@Nullable Object k, @Nullable Object v, @NonNull RemovalCause cause) {log.info("[移除緩存] key:{} reason:{}", k, cause.name());// 超出最大緩存if (cause == RemovalCause.SIZE) {
?}// 超出過期時間if (cause == RemovalCause.EXPIRED) {// do something}// 顯式移除if (cause == RemovalCause.EXPLICIT) {// do something}// 舊數據被更新if (cause == RemovalCause.REPLACED) {// do something}}
}
緩存狀態與統計

默認情況下,緩存的狀態會用一個 CacheStats 對象記錄下來,通過訪問 CacheStats 對象就可以知道當前緩存的各種狀態指標,指標如下所示:
totalLoadTime :總共加載時間。
loadFailureRate :加載失敗率,= 總共加載失敗次數 / 總共加載次數
averageLoadPenalty :平均加載時間,單位-納秒
evictionCount :被淘汰出緩存的數據總個數
evictionWeight :被淘汰出緩存的那些數據的總權重
hitCount :命中緩存的次數
hitRate :命中緩存率
loadCount :加載次數
loadFailureCount :加載失敗次數
loadSuccessCount :加載成功次數
missCount :未命中次數
missRate :未命中率
requestCount :用戶請求查詢總次數

3、實例

思路:
1)如果是mvc架構,就直接定義一個共用的util或者配置類加載緩存即可;
2)如果是ddd設計模式,建議直接在基礎層新建一個配置類,配置類加載的時候初始化;
然后在repo調用這個緩存配置類對外暴露的查詢方法獲取緩存;
不同領域的app層的ervice調用repo獲取緩存、使用。

@Component
@Slf4j
public class DictionaryCache {//	定義默認值,用于兜底private static final Map<String, String> defaultDictionaryMap = new HashMap<>(10);static {defaultDictionaryMap.put(CommonConstants.Dictionary.KEY_ELECTRICITY_ACTP, CommonConstants.Dictionary.VAL_ELECTRICITY_ACTP);defaultDictionaryMap.put(CommonConstants.Dictionary.KEY_NO_ACTP_MONIT_TIME, CommonConstants.Dictionary.VAL_NO_ACTP_MONIT_TIME);}//	定義緩存LoadingCache<String, String> dictionaryCache;//	注入查詢實例,例如dao、delegate等@Resourceprivate SystemDelegate systemDelegate;//	初始化@PostConstructpublic void init() {dictionaryCache = Caffeine.newBuilder().maximumSize(500)	//	容量.expireAfterWrite(12 * 60, TimeUnit.MINUTES)	//	過期時間.removalListener(new DictionaryRemovalListener())	//	清楚or更新監聽.recordStats()	//	統計.build(new DictionaryLoader());}//	加載class DictionaryLoader implements CacheLoader<String, String> {@Overridepublic String load(@NotNull String dicCode) {try {DictionaryQuery query = new DictionaryQuery();query.setDicParentCode("dic_type");List<SysDictionary> dictionaryList = systemDelegate.getDictionaryList(query);if(dictionaryList.isEmpty()) {//  查詢字典失敗處理log.error("dic list is empty, use default val");return StringUtils.isBlank(defaultDictionaryMap.get(dicCode)) ? null : defaultDictionaryMap.get(dicCode);}List<SysDictionary> resultList = dictionaryList.stream().filter(obj -> dicCode.equals(obj.getDicCode())).collect(Collectors.toList());if(resultList.isEmpty()) {//  字典不存在處理log.error("dicCode does not match value, use default val");return StringUtils.isBlank(defaultDictionaryMap.get(dicCode)) ? null : defaultDictionaryMap.get(dicCode);}return resultList.get(0).getDicExtValue1();} catch (Exception e) {log.error("load system dictionary from systemDelegate error, dicCode:{}, e:{}", dicCode, e.getMessage());return StringUtils.isBlank(defaultDictionaryMap.get(dicCode)) ? null : defaultDictionaryMap.get(dicCode);}}}//	清除or更新監聽實現class DictionaryRemovalListener implements RemovalListener<Object, Object> {@Overridepublic void onRemoval(@Nullable Object key, @Nullable Object val, @NonNull RemovalCause cause) {//	可以打印要更新的緩存key,更新的原因log.info("DictionaryCache remove cache, key:{}, reason:{}", key, cause.name());//	可以順帶打印緩存的各種狀態統計指標log.info("DictionaryCache hitCount:{}, hitRate:{}, missCount:{}, missRate:{}, loadCount:{}, loadSuccessCount:{}, totalLoadTime:{}",dictionaryCache.stats().hitCount(),dictionaryCache.stats().hitRate(),dictionaryCache.stats().missCount(),dictionaryCache.stats().missRate(),dictionaryCache.stats().loadCount(),dictionaryCache.stats().loadSuccessCount(),dictionaryCache.stats().totalLoadTime());}}//	對外暴露一個查詢緩存的方法public String getDicValByDicCode(String dicCode) {return dictionaryCache.get(dicCode);}
}

4、補充:Caffeine高性能實現

判斷一個緩存的好壞最核心的指標就是命中率,影響緩存命中率有很多因素,包括業務場景、淘汰策略、清理策略、緩存容量等等。如果作為本地緩存, 它的性能的情況,資源的占用也都是一個很重要的指標。下面

我們來看看 Caffeine 在這幾個方面是怎么著手的,如何做優化的。

W-TinyLFU 整體設計

上面說到淘汰策略是影響緩存命中率的因素之一,一般比較簡單的緩存就會直接用到 LFU(Least Frequently Used,即最不經常使用) 或者LRU(Least Recently Used,即最近最少使用) ,而 Caffeine 就是使用了 W-TinyLFU 算法。

W-TinyLFU 看名字就能大概猜出來,它是 LFU 的變種,也是一種緩存淘汰算法。那為什么要使用 W-TinyLFU 呢?

LRU 和 LFU 的缺點

LRU 實現簡單,在一般情況下能夠表現出很好的命中率,是一個“性價比”很高的算法,平時也很常用。雖然 LRU 對突發性的稀疏流量(sparse bursts)表現很好,但同時也會產生緩存污染,舉例來說,如果偶然性的要對全量數據進行遍歷,那么“歷史訪問記錄”就會被刷走,造成污染。
如果數據的分布在一段時間內是固定的話,那么 LFU 可以達到最高的命中率。但是 LFU 有兩個缺點,第一,它需要給每個記錄項維護頻率信息,每次訪問都需要更新,這是個巨大的開銷;第二,對突發性的稀疏流量無力,因為前期經常訪問的記錄已經占用了緩存,偶然的流量不太可能會被保留下來,而且過去的一些大量被訪問的記錄在將來也不一定會使用上,這樣就一直把“坑”占著了。
無論 LRU 還是 LFU 都有其各自的缺點,不過,現在已經有很多針對其缺點而改良、優化出來的變種算法。

TinyLFU

TinyLFU 就是其中一個優化算法,它是專門為了解決 LFU 上述提到的兩個問題而被設計出來的。

解決第一個問題是采用了 Count–Min Sketch 算法。

解決第二個問題是讓記錄盡量保持相對的“新鮮”(Freshness Mechanism),并且當有新的記錄插入時,可以讓它跟老的記錄進行“PK”,輸者就會被淘汰,這樣一些老的、不再需要的記錄就會被剔除。

二、本地緩存:Guava

1、簡介

2、用法

3、實例

三、分布式緩存:Redis

1、簡介

2、用法

3、實例

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/899637.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/899637.shtml
英文地址,請注明出處:http://en.pswp.cn/news/899637.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

2025年3月29日筆記

問題&#xff1a;創建一個長度為99的整數數組&#xff0c;輸出數組的每個位置數字是幾&#xff1f; 解題思路&#xff1a; 1.因為題中沒有明確要求需要輸入,所以所有類型的答案都需要寫出 解法1&#xff1a; #include<iostream> #include<bits/stdc.h> using n…

hadoop相關面試題以及答案

什么是Hadoop&#xff1f;它的主要組件是什么&#xff1f; Hadoop是一個開源的分布式計算框架&#xff0c;用于處理大規模數據的存儲和計算。其主要組件包括Hadoop Distributed File System&#xff08;HDFS&#xff09;和MapReduce。 解釋HDFS的工作原理。 HDFS采用主從架構&…

微信小程序:數據拼接方法

1. 使用 concat() 方法拼接數組 // 在原有數組基礎上拼接新數組 Page({data: {originalArray: [1, 2, 3]},appendData() {const newData [4, 5, 6];const combinedArray this.data.originalArray.concat(newData);this.setData({originalArray: combinedArray});} }) 2. 使…

Python之貪心算法

Python實現貪心算法(Greedy Algorithm) 概念 貪心算法是一種在每一步選擇中都采取當前狀態下最優的選擇&#xff0c;從而希望導致結果是全局最優的算法策略。 基本特點 局部最優選擇&#xff1a;每一步都做出當前看起來最佳的選擇不可回退&#xff1a;一旦做出選擇&#xf…

【 <二> 丹方改良:Spring 時代的 JavaWeb】之 Spring Boot 中的 AOP:實現日志記錄與性能監控

<前文回顧> 點擊此處查看 合集 https://blog.csdn.net/foyodesigner/category_12907601.html?fromshareblogcolumn&sharetypeblogcolumn&sharerId12907601&sharereferPC&sharesourceFoyoDesigner&sharefromfrom_link <今日更新> 一、開篇整…

TCP/IP協議簇

文章目錄 應用層http/httpsDNS補充 傳輸層TCP1. 序列號與確認機制2. 超時重傳3. 流量控制&#xff08;滑動窗口機制&#xff09;4. 擁塞控制5. 錯誤檢測與校驗6. 連接管理總結 網絡層ARP**ARP 的核心功能**ARP 的工作流程1. ARP 請求&#xff08;Broadcast&#xff09;2. ARP 緩…

SpringBoot分布式項目訂單管理實戰:Mybatis最佳實踐全解

一、架構設計與技術選型 典型分布式訂單系統架構&#xff1a; [網關層] → [訂單服務] ←→ [分布式緩存]↑ ↓ [用戶服務] [支付服務]↓ ↓ [MySQL集群] ← [分庫分表中間件]技術棧組合&#xff1a; Spring Boot 3.xMybatis-Plus 3.5.xShardingSpher…

微服務架構中的精妙設計:環境和工程搭建

一.前期準備 1.1開發環境安裝 Oracle從JDK9開始每半年發布?個新版本, 新版本發布后, ?版本就不再進?維護. 但是會有?個?期維護的版本. ?前?期維護的版本有: JDK8, JDK11, JDK17, JDK21 在 JDK版本的選擇上&#xff0c;盡量選擇?期維護的版本. 為什么選擇JDK17? S…

Maven 構建配置文件詳解

Maven 構建配置文件詳解 引言 Maven 是一個強大的項目管理和構建自動化工具,廣泛應用于 Java 開發領域。在 Maven 項目中,配置文件扮演著至關重要的角色。本文將詳細介紹 Maven 構建配置文件的相關知識,包括配置文件的作用、結構、配置方法等,幫助讀者更好地理解和應用 M…

【YOLO系列】基于YOLOv8的無人機野生動物檢測

基于YOLOv8的無人機野生動物檢測 1.前言 在野生動物保護、生態研究和環境監測領域&#xff0c;及時、準確地檢測和識別野生動物對于保護生物多樣性、預防人類與野生動物的沖突以及制定科學的保護策略至關重要。傳統的野生動物監測方法通常依賴于地面巡邏、固定攝像頭或無線傳…

Hive UDF開發實戰:構建高性能JSON生成器

目錄 一、背景與需求場景 二、開發環境準備 2.1 基礎工具棧 2.2 Maven依賴配置 三、核心代碼實現

分布式特性對比

以下是關于 分片(Sharding)、一致性哈希、兩階段提交(2PC)、Paxos、Raft協議、數據局部性 的對比分析與關聯性總結,涵蓋核心機制、適用場景及相互關系: 一、概念對比與關聯 概念核心目標關鍵特性典型應用場景與其它技術的關聯分片(Sharding)數據水平拆分按規則(哈希、…

歷史分鐘高頻數據

外盤期貨高頻分鐘歷史回測行情數據下載 鏈接: https://pan.baidu.com/s/1RUbAMxfiSyBlXfrwT_0n2w?pwdhgya 提取碼: hgya通過美國期貨高頻交易所歷史行情可以看到很多細節比如品種之一&#xff1a;FGBX_1min (1)在2024-02-29 11:14:00關鍵交易時刻&#xff0c;一筆大規模訂單突…

final+模版設計模式的理解

模板設計模式在 Java 里是一種行為設計模式&#xff0c;它在抽象類里定義算法的骨架&#xff0c;把部分步驟的具體實現延遲到子類。如此一來&#xff0c;子類可以在不改變算法結構的基礎上&#xff0c;重新定義算法中的特定步驟。 模式組成 抽象類&#xff08;Abstract Class…

JAVA接口調用限速器

目錄 1、并發限速 2、串行限速 需求&#xff1a;批量調用第三方ERP接口&#xff0c;對方接口限流時&#xff0c;減緩調用速率。 1、并發限速 Slf4j RestController public class ApiCallTask {//第三方接口Resourceprivate ErpService erpService;//異步線程池Resourcepriv…

STM32 CAN控制器硬件資源與用法

1、硬件結構圖 以STM32F4為例&#xff0c;他有2個can控制器&#xff0c;分別為 CAN1 CAN2。 每個CAN控制器&#xff0c;都有3個發送郵箱、2個接收fifo&#xff0c;每個接收fifo又由3個接收郵箱組成。也即每個CAN控制器都有9個郵箱&#xff0c;其中3個供發送用&#xff0c;3個…

【C++ 繼承】—— 青花分水、和而不同,繼承中的“明明德”與“止于至善”

歡迎來到ZyyOvO的博客?&#xff0c;一個關于探索技術的角落&#xff0c;記錄學習的點滴&#x1f4d6;&#xff0c;分享實用的技巧&#x1f6e0;?&#xff0c;偶爾還有一些奇思妙想&#x1f4a1; 本文由ZyyOvO原創??&#xff0c;感謝支持??&#xff01;請尊重原創&#x1…

Qt warning LNK4042: 對象被多次指定;已忽略多余的指定

一、常規原因&#xff1a; pro或pri 文件中源文件被多次包含 解決&#xff1a;刪除變量 SOURCES 和 HEADERS 中重復條目 二、誤用 對于某些pri庫可以使用如下代碼簡寫包含 INCLUDEPATH $$PWDHEADERS $$PWD/*.hSOURCES $$PWD/*.cpp但是假如該目錄下只有頭文件&#xff0c;沒…

Visual Studio Code 無法打開源文件解決方法

&#x1f308; 個人主頁&#xff1a;Zfox_ &#x1f525; 系列專欄&#xff1a;Linux &#x1f525; 系列專欄&#xff1a;C從入門到精通 目錄 一&#xff1a;&#x1f525; 突發狀況 二&#xff1a;&#x1f525; 共勉 一&#xff1a;&#x1f525; 突發狀況 &#x1f42c;…

js文字兩端對齊

目錄 一、問題 二、原因及解決方法 三、總結 一、問題 1.text-align: justify; 不就可以了嗎&#xff1f;但是實際測試無效 二、原因及解決方法 1.原因&#xff1a;text-align只對非最后一行文字有效。只有一行文字時&#xff0c;text-align無效&#xff0c;要用text-alig…