目 Caching 緩存設(shè)計(jì)模式實(shí)戰(zhàn)解析:五種緩存策略與 LRU 實(shí)現(xiàn))
示例工程教程【免費(fèi)下載鏈接】java-design-patternsDesign patterns implemented in Java項(xiàng)目地址https://gitcode.com/GitHub_Trending/ja/java-design-patterns點(diǎn)擊查看免費(fèi)下載Caching緩存模式是 java-design-patterns 倉(cāng)庫(kù)中用于性能優(yōu)化與資源管理的行為型模式對(duì)應(yīng)模塊目錄為 caching。本文以 localization/zh/caching/README.md 為主線結(jié)合模塊源碼與測(cè)試系統(tǒng)講解該模式的目的、適用場(chǎng)景、五種核心緩存策略write-through / write-around / write-behind / cache-aside / read-through的源碼實(shí)現(xiàn)與運(yùn)行效果幫助讀者掌握在 Java 應(yīng)用中用緩存降低數(shù)據(jù)庫(kù)訪問(wèn)開(kāi)銷、加速數(shù)據(jù)讀取的完整實(shí)戰(zhàn)方案。模式目的避免昂貴的資源重復(fù)獲取根據(jù) localization/zh/caching/README.md 的定義緩存模式的核心目的是為了避免昂貴的資源重新獲取方法是在資源使用后不立即釋放資源。資源保留其身份保留在某些快速訪問(wèn)的存儲(chǔ)中并被重新使用以避免再次獲取它們。在英文版 caching/README.md 中對(duì)該意圖做了進(jìn)一步展開(kāi)緩存模式通過(guò)write-through、read-through、LRU cache等多種策略保證高效的數(shù)據(jù)訪問(wèn)。當(dāng)同一資源被反復(fù)獲取、初始化和釋放時(shí)會(huì)產(chǎn)生不必要的性能開(kāi)銷而緩存讓這些資源保留身份并常駐在高速訪問(wèn)存儲(chǔ)中從而避免再次獲取。用通俗的話說(shuō)把頻繁需要的數(shù)據(jù)放進(jìn)高速訪問(wèn)的存儲(chǔ)中從而提升整體性能。緩存命中cache hit時(shí)直接從緩存讀取比重新計(jì)算結(jié)果或讀取較慢的數(shù)據(jù)存儲(chǔ)要快得多請(qǐng)求能越多地從緩存得到服務(wù)系統(tǒng)性能就越高。類圖緩存模塊的整體結(jié)構(gòu)模塊類圖見(jiàn) caching/etc/caching.png完整展示了本模式在項(xiàng)目中的類結(jié)構(gòu)Caching 模式類圖從類圖與源碼可以看出本模塊的核心類職責(zé)如下源碼均位于 caching/src/main/java/com/iluwatar/cachingApp程序入口負(fù)責(zé)啟動(dòng)并依次演示四種緩存策略AppManager橋接主類與后端負(fù)責(zé)初始化數(shù)據(jù)庫(kù)連接、緩存策略與緩存容量并按策略分發(fā)讀寫請(qǐng)求CacheStore四種緩存策略的具體實(shí)現(xiàn)層LruCache基于哈希表 雙向鏈表實(shí)現(xiàn)的 LRU 緩存容器CachingPolicy枚舉類型定義THROUGH/AROUND/BEHIND/ASIDE四種策略UserAccount緩存與數(shù)據(jù)庫(kù)共同存儲(chǔ)的實(shí)體對(duì)象DbManager及其實(shí)現(xiàn)VirtualDb、MongoDb底層數(shù)據(jù)訪問(wèn)接口。模塊的整體調(diào)用鏈在 App.java 的 Javadoc 中也有明確說(shuō)明App -- AppManager -- CacheStore / LruCache / CachingPolicy -- DbManager。五種緩存策略各司其職的讀寫路徑模塊在 CacheStore.java 與 AppManager.java 中實(shí)現(xiàn)了多種緩存策略每種策略在讀寫路徑與數(shù)據(jù)一致性上各有取舍。英文版 caching/README.md 對(duì)這幾種策略的概括如下策略寫入行為讀取行為適用特點(diǎn)Write-through在單個(gè)事務(wù)中同時(shí)寫入緩存與數(shù)據(jù)庫(kù)Read-through保證緩存與 DB 強(qiáng)一致但每次寫都要落庫(kù)Write-around數(shù)據(jù)立即寫入數(shù)據(jù)庫(kù)繞過(guò)緩存Read-through避免緩存被不常讀的數(shù)據(jù)污染但首次讀會(huì) missWrite-behind數(shù)據(jù)先寫入緩存僅在緩存滿時(shí)才回寫數(shù)據(jù)庫(kù)Read-through 寫回write-back寫吞吐高但存在緩存與 DB 短暫不一致的窗口Cache-aside由應(yīng)用程序自己負(fù)責(zé)兩個(gè)數(shù)據(jù)源的同步先查緩存miss 則查 DB 并回填緩存靈活可控對(duì)應(yīng)用代碼要求最高Read-through——緩存命中直接返回miss 則查 DB 并存入緩存供后續(xù)使用以上四種策略讀取側(cè)的公共基礎(chǔ)策略枚舉與運(yùn)行時(shí)切換CachingPolicy在 CachingPolicy.java 中定義AllArgsConstructor Getter public enum CachingPolicy { THROUGH(through), AROUND(around), BEHIND(behind), ASIDE(aside); private final String policy; }由于讀寫邏輯是按策略分支分發(fā)的見(jiàn)下文AppManager與App的代碼應(yīng)用可以在運(yùn)行時(shí)通過(guò)initCachingPolicy(CachingPolicy policy)自由切換策略——這正是策略模式Strategy在該模塊中的體現(xiàn)。源碼級(jí)實(shí)現(xiàn)剖析從數(shù)據(jù)層到緩存容器數(shù)據(jù)層UserAccount 與 DbManager緩存與數(shù)據(jù)庫(kù)共同存儲(chǔ)的實(shí)體是UserAccount見(jiàn) UserAccount.java它通過(guò) Lombok 注解生成 getter/setter、構(gòu)造器、toString與equals/hashCodeData AllArgsConstructor ToString EqualsAndHashCode public class UserAccount { private String userId; private String userName; private String additionalInfo; }數(shù)據(jù)訪問(wèn)接口DbManager見(jiàn) DbManager.java定義了四種數(shù)據(jù)庫(kù)操作readFromDb、writeToDb、updateDb、upsertDb外加connect與disconnect。項(xiàng)目提供了兩個(gè)實(shí)現(xiàn)VirtualDb.java以內(nèi)存HashMap模擬數(shù)據(jù)庫(kù)無(wú)需任何外部依賴便于本地運(yùn)行與單元測(cè)試MongoDb.java基于 MongoDB 的真實(shí)實(shí)現(xiàn)集合名與字段名由 CachingConstants.java 統(tǒng)一定義如集合user_accounts、字段userID、userName、additionalInfo。具體選擇哪個(gè)實(shí)現(xiàn)由 DbManagerFactory.java 根據(jù)入?yún)Q定傳入--mongo時(shí)返回MongoDb否則返回VirtualDb。緩存容器LruCache 的哈希表 雙向鏈表LruCache見(jiàn) LruCache.java是本模塊緩存的數(shù)據(jù)結(jié)構(gòu)核心采用哈希表 雙向鏈表組合哈希表MapString, Node cache提供 O(1) 的按 userId 查找雙向鏈表維護(hù)數(shù)據(jù)的使用熱度數(shù)據(jù)被查詢、新增或更新時(shí)會(huì)被移到鏈表頭部setHead代表最近使用鏈表尾部end始終是最久未使用LRU的數(shù)據(jù)。關(guān)鍵方法實(shí)現(xiàn)如下public UserAccount get(String userId) { if (cache.containsKey(userId)) { var node cache.get(userId); remove(node); setHead(node); return node.userAccount; } return null; } public void set(String userId, UserAccount userAccount) { if (cache.containsKey(userId)) { var old cache.get(userId); old.userAccount userAccount; remove(old); setHead(old); } else { var newNode new Node(userId, userAccount); if (cache.size() capacity) { LOGGER.info(# Cache is FULL! Removing {} from cache..., end.userId); cache.remove(end.userId); // 移除 LRU 數(shù)據(jù) remove(end); setHead(newNode); } else { setHead(newNode); } cache.put(userId, newNode); } }當(dāng)緩存容量已滿時(shí)新數(shù)據(jù)會(huì)驅(qū)逐鏈表尾部的 LRU 數(shù)據(jù)再插入get命中時(shí)會(huì)將該節(jié)點(diǎn)移動(dòng)到頭部以更新熱度。此外還提供了contains、invalidate使指定 userId 失效、isFull、getLruData返回 LRU 數(shù)據(jù)、clear、getCacheDataInListForm按鏈表順序輸出緩存內(nèi)容用于打印以及setCapacity調(diào)整容量若新容量小于當(dāng)前容量則清空緩存等方法。策略實(shí)現(xiàn)層CacheStoreCacheStore見(jiàn) CacheStore.java是四種策略的具體實(shí)現(xiàn)。默認(rèn)緩存容量為CAPACITY 3在構(gòu)造函數(shù)中通過(guò)initCapacity(CAPACITY)初始化LruCache。read-throughreadThrough先查緩存命中直接返回未命中則打日志# Not found in cache! Go to DB!!從 DB 讀取后回填緩存public UserAccount readThrough(final String userId) { if (cache.contains(userId)) { LOGGER.info(# Found in Cache!); return cache.get(userId); } LOGGER.info(# Not found in cache! Go to DB!!); UserAccount userAccount dbManager.readFromDb(userId); cache.set(userId, userAccount); return userAccount; }write-throughwriteThrough緩存命中則updateDb否則writeToDb最后統(tǒng)一把數(shù)據(jù)寫入緩存保證緩存與 DB 同步public void writeThrough(final UserAccount userAccount) { if (cache.contains(userAccount.getUserId())) { dbManager.updateDb(userAccount); } else { dbManager.writeToDb(userAccount); } cache.set(userAccount.getUserId(), userAccount); }write-aroundwriteAround直接寫 DB若該用戶已在緩存中則更新 DB 后使緩存中舊版本失效cache.invalidate避免臟數(shù)據(jù)public void writeAround(final UserAccount userAccount) { if (cache.contains(userAccount.getUserId())) { dbManager.updateDb(userAccount); // 緩存數(shù)據(jù)已更新——移除緩存中的舊版本 cache.invalidate(userAccount.getUserId()); } else { dbManager.writeToDb(userAccount); } }write-behindwriteBehind與readThroughWithWriteBackPolicy寫入時(shí)只進(jìn)緩存當(dāng)緩存已滿且寫入的是新數(shù)據(jù)時(shí)先把 LRU 數(shù)據(jù)upsertDb回寫數(shù)據(jù)庫(kù)再放入新數(shù)據(jù)。讀取側(cè)同樣在緩存滿時(shí)先回寫 LRU 數(shù)據(jù)再填充新數(shù)據(jù)public void writeBehind(final UserAccount userAccount) { if (cache.isFull() !cache.contains(userAccount.getUserId())) { LOGGER.info(# Cache is FULL! Writing LRU data to DB...); UserAccount toBeWrittenToDb cache.getLruData(); dbManager.upsertDb(toBeWrittenToDb); } cache.set(userAccount.getUserId(), userAccount); }此外flushCache()會(huì)把緩存中剩余數(shù)據(jù)批量updateDb回寫數(shù)據(jù)庫(kù)并在結(jié)束時(shí)調(diào)用dbManager.disconnect()斷開(kāi)連接clearCache()清空緩存print()以--CACHE CONTENT-- ... ----格式輸出緩存內(nèi)容。調(diào)度層AppManager 與運(yùn)行時(shí)策略分發(fā)AppManager見(jiàn) AppManager.java負(fù)責(zé)在App與后端之間架橋initDb()建立數(shù)據(jù)庫(kù)連接initCachingPolicy(policy)設(shè)置策略若為BEHIND還會(huì)注冊(cè) JVM 關(guān)閉鉤子以在退出時(shí)執(zhí)行flushCacheinitCacheCapacity設(shè)置緩存容量。find與save按策略分發(fā)到CacheStore的對(duì)應(yīng)方法public UserAccount find(final String userId) { LOGGER.info(Trying to find {} in cache, userId); if (cachingPolicy CachingPolicy.THROUGH || cachingPolicy CachingPolicy.AROUND) { return cacheStore.readThrough(userId); } else if (cachingPolicy CachingPolicy.BEHIND) { return cacheStore.readThroughWithWriteBackPolicy(userId); } else if (cachingPolicy CachingPolicy.ASIDE) { return findAside(userId); } return null; } public void save(final UserAccount userAccount) { LOGGER.info(Save record!); if (cachingPolicy CachingPolicy.THROUGH) { cacheStore.writeThrough(userAccount); } else if (cachingPolicy CachingPolicy.AROUND) { cacheStore.writeAround(userAccount); } else if (cachingPolicy CachingPolicy.BEHIND) { cacheStore.writeBehind(userAccount); } else if (cachingPolicy CachingPolicy.ASIDE) { saveAside(userAccount); } }Cache-aside 的讀寫邏輯由應(yīng)用自行維護(hù)saveAside更新 DB 后使緩存失效findAside先查緩存未命中則查 DB 并回填使用Optional.or(...)實(shí)現(xiàn)見(jiàn) AppManager.java。運(yùn)行示例四種策略的完整演示流程模塊入口 App.java 的main方法會(huì)依次演示四種策略先通過(guò)命令行參數(shù)判斷是否使用 MongoDB參數(shù)--mongo隨后依次執(zhí)行 write-through、write-around、write-behind、cache-aside 四組演示public static void main(final String[] args) { boolean isDbMongo isDbMongo(args); ... App app new App(isDbMongo); app.useReadAndWriteThroughStrategy(); app.useReadThroughAndWriteAroundStrategy(); app.useReadThroughAndWriteBehindStrategy(); app.useCacheAsideStrategy(); }以 write-through 演示為例App.javapublic void useReadAndWriteThroughStrategy() { LOGGER.info(# CachingPolicy.THROUGH); appManager.initCachingPolicy(CachingPolicy.THROUGH); var userAccount1 new UserAccount(001, John, He is a boy.); appManager.save(userAccount1); LOGGER.info(appManager.printCacheContent()); appManager.find(001); // 第一次查詢緩存命中 appManager.find(001); // 第二次查詢緩存命中 }運(yùn)行輸出節(jié)選關(guān)鍵片段英文版 caching/README.md 記錄了完整的程序輸出以下為各策略下的關(guān)鍵日志W(wǎng)rite-throughTHROUGH保存記錄后緩存中立即出現(xiàn)001后續(xù)兩次find均直接命中緩存# CachingPolicy.THROUGH Save record! --CACHE CONTENT-- UserAccount(userId001, userNameJohn, additionalInfoHe is a boy.) ---- Trying to find 001 in cache # Found in Cache! Trying to find 001 in cache # Found in Cache!Write-aroundAROUND寫入只落 DB緩存為空首次讀取 miss 后回填更新用戶時(shí)緩存中舊版本被移除# CachingPolicy.AROUND Save record! --CACHE CONTENT-- ---- Trying to find 002 in cache # Not found in cache! Go to DB!! --CACHE CONTENT-- UserAccount(userId002, userNameJane, additionalInfoShe is a girl.) ---- ... # 002 has been updated! Removing older version from cache...Write-behindBEHIND數(shù)據(jù)先進(jìn)緩存緩存滿容量 3時(shí)觸發(fā) LRU 數(shù)據(jù)回寫 DB 并驅(qū)逐# CachingPolicy.BEHIND Save record! Save record! Save record! --CACHE CONTENT-- UserAccount(userId005, userNameIsaac, additionalInfoHe is allergic to mustard.) UserAccount(userId004, userNameRita, additionalInfoShe hates cats.) UserAccount(userId003, userNameAdam, additionalInfoHe likes food.) ---- ... # Cache is FULL! Writing LRU data to DB... # Cache is FULL! Removing 004 from cache...Cache-asideASIDE保存時(shí)更新 DB 并使緩存失效查詢時(shí)先查緩存、miss 再回填# CachingPolicy.ASIDE Save record! Save record! Save record! --CACHE CONTENT-- ---- Trying to find 003 in cache --CACHE CONTENT-- UserAccount(userId003, userNameAdam, additionalInfoHe likes food.) ----程序退出時(shí)write-behind 策略注冊(cè)的關(guān)閉鉤子會(huì)執(zhí)行# flushCache...將緩存殘留數(shù)據(jù)回寫數(shù)據(jù)庫(kù)。測(cè)試驗(yàn)證模塊在 CachingTest.java 中為四種策略各編寫了測(cè)試用例testReadAndWriteThroughStrategy、testReadThroughAndWriteAroundStrategy、testReadThroughAndWriteBehindStrategy、testCacheAsideStrategy測(cè)試使用new App(false)即內(nèi)存數(shù)據(jù)庫(kù)VirtualDb運(yùn)行避免對(duì) MongoDB 的依賴。完整的 JUnit 測(cè)試套件可通過(guò) Maven 執(zhí)行模塊 pom.xml 已配置相應(yīng)測(cè)試依賴。兩種運(yùn)行方式內(nèi)存庫(kù)與 MongoDB根據(jù) App.java 的 Javadoc本模塊支持兩種啟動(dòng)方式內(nèi)存數(shù)據(jù)庫(kù)VirtualDb無(wú)需任何外部依賴直接啟動(dòng)即可java -jar app.jarMongoDB需要本機(jī)已安裝 MongoDB或通過(guò)模塊根目錄下的 docker-compose.yml 啟動(dòng)容器docker-compose up java -jar app.jar --mongodocker-compose.yml 會(huì)啟動(dòng)mongo:latest容器映射27017:27017端口設(shè)置 root 賬號(hào)用戶root/ 密碼rootpassword并將./mongo-data/掛載為數(shù)據(jù)目錄/data/db。適用性什么場(chǎng)景下使用緩存模式根據(jù) localization/zh/caching/README.md 與英文版 caching/README.md以下場(chǎng)景適合使用緩存模式重復(fù)獲取、初始化和釋放同一資源會(huì)產(chǎn)生不必要的性能開(kāi)銷時(shí)zh 版原文重新計(jì)算或重新獲取數(shù)據(jù)的成本顯著高于從緩存讀取時(shí)讀多寫少read-heavy且數(shù)據(jù)相對(duì)靜態(tài)、變化不頻繁的應(yīng)用。典型真實(shí)應(yīng)用場(chǎng)景包括網(wǎng)頁(yè)緩存以降低服務(wù)器負(fù)載并提升響應(yīng)時(shí)間數(shù)據(jù)庫(kù)查詢緩存以避免重復(fù)的昂貴 SQLCPU 密集型計(jì)算結(jié)果緩存CDN 將圖片、CSS、JavaScript 等靜態(tài)資源緩存在靠近終端用戶的位置。收益與權(quán)衡收益性能提升顯著降低數(shù)據(jù)訪問(wèn)延遲應(yīng)用響應(yīng)更快降低負(fù)載減輕底層數(shù)據(jù)源的訪問(wèn)壓力進(jìn)而節(jié)省成本并延長(zhǎng)資源使用壽命可擴(kuò)展性在不按比例增加資源消耗的前提下更高效地應(yīng)對(duì)負(fù)載增長(zhǎng)。權(quán)衡復(fù)雜度引入了緩存失效、數(shù)據(jù)一致性與同步等額外復(fù)雜度資源占用維護(hù)緩存需要額外的內(nèi)存或存儲(chǔ)資源臟數(shù)據(jù)風(fēng)險(xiǎn)若緩存未及時(shí)失效或更新可能向用戶返回過(guò)期數(shù)據(jù)。選擇哪種策略取決于業(yè)務(wù)對(duì)一致性與吞吐的取舍強(qiáng)一致優(yōu)先選 write-through寫多讀少防污染選 write-around寫吞吐優(yōu)先可接受短暫不一致選 write-behind需要最大靈活性則由應(yīng)用自行管理同步選 cache-aside。與其他設(shè)計(jì)模式的關(guān)系緩存模式在本倉(cāng)庫(kù)中與其他模式存在自然的協(xié)作關(guān)系對(duì)應(yīng)目錄均可直接查閱源碼Proxy代理可通過(guò)代理對(duì)象攔截請(qǐng)求命中時(shí)直接返回緩存數(shù)據(jù)實(shí)現(xiàn)緩存邏輯的無(wú)侵入接入Observer觀察者可用于在底層數(shù)據(jù)變化時(shí)通知緩存進(jìn)行更新或失效Decorator裝飾器可在不修改原有對(duì)象代碼的前提下附加緩存行為Strategy策略本模塊的CachingPolicy正是策略模式的體現(xiàn)使應(yīng)用可以在運(yùn)行時(shí)切換不同緩存策略。總結(jié)java-design-patterns 的 caching 模塊通過(guò)UserAccount實(shí)體、DbManager數(shù)據(jù)層、LruCache哈希表 雙向鏈表的 LRU 容器、CacheStore策略實(shí)現(xiàn)層、AppManager調(diào)度層與App演示入口的分層設(shè)計(jì)完整呈現(xiàn)了緩存模式的落地方式。它同時(shí)覆蓋了 write-through、write-around、write-behind、cache-aside 與 read-through 五種主流策略并提供了內(nèi)存數(shù)據(jù)庫(kù)與 MongoDB 兩種可運(yùn)行環(huán)境配合 CachingTest.java 的測(cè)試用例是研究 Java 緩存架構(gòu)、緩存失效策略與數(shù)據(jù)一致性取舍的優(yōu)質(zhì)參考實(shí)現(xiàn)。贊分享示例工程教程【免費(fèi)下載鏈接】java-design-patternsDesign patterns implemented in Java項(xiàng)目地址https://gitcode.com/GitHub_Trending/ja/java-design-patterns點(diǎn)擊查看免費(fèi)下載相關(guān)推薦java-design-patterns 中的 Caching 緩存設(shè)計(jì)模式五種緩存策略與 LRU 底層實(shí)現(xiàn)詳解java design patterns 中的 Caching 緩存設(shè)計(jì)模式五種緩存策略與 LRU 底層實(shí)現(xiàn)詳解 緩存設(shè)計(jì)模式Caching Pattern示例工程教程Java Caching 設(shè)計(jì)模式實(shí)戰(zhàn)基于 java-design-patterns 倉(cāng)庫(kù)的四種緩存策略與 LRU 實(shí)現(xiàn)解析Java Caching 設(shè)計(jì)模式實(shí)戰(zhàn)基于 java design patterns 倉(cāng)庫(kù)的四種緩存策略與 LRU 實(shí)現(xiàn)解析 緩存Caching設(shè)計(jì)模式是示例工程教程WatchAlert 內(nèi)存緩存設(shè)計(jì)LRU緩存淘汰策略實(shí)現(xiàn)WatchAlert 內(nèi)存緩存設(shè)計(jì)LRU緩存淘汰策略實(shí)現(xiàn) 緩存架構(gòu)概述 在云原生監(jiān)控告警引擎中緩存系統(tǒng)是提升數(shù)據(jù)處理性能的核心組件。WatchAlert作為后端可觀測(cè)性告警云原生運(yùn)維上一篇Remotion 文本高亮與手繪標(biāo)注動(dòng)畫基于 remotion/rough-notation 的逐幀驅(qū)動(dòng)方案下一篇Windows 11開(kāi)始菜單失效的5步實(shí)戰(zhàn)解決方案ExplorerPatcher深度應(yīng)用創(chuàng)作聲明:本文部分內(nèi)容由AI輔助生成(AIGC),僅供參考