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