ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

java-design-patterns 项目 Caching 缓存设计模式实战解析:五种缓存策略与 LRU 实现

java-design-patterns 项目 Caching 缓存设计模式实战解析:五种缓存策略与 LRU 实现 示例工程教程【免费下载链接】java-design-patternsDesign patterns implemented in Java项目地址https://gitcode.com/GitHub_Trending/ja/java-design-patterns点击查看免费下载Caching缓存模式是 java-design-patterns 仓库中用于性能优化与资源管理的行为型模式对应模块目录为 caching。本文以 localization/zh/caching/README.md 为主线结合模块源码与测试系统讲解该模式的目的、适用场景、五种核心缓存策略write-through / write-around / write-behind / cache-aside / read-through的源码实现与运行效果帮助读者掌握在 Java 应用中用缓存降低数据库访问开销、加速数据读取的完整实战方案。模式目的避免昂贵的资源重复获取根据 localization/zh/caching/README.md 的定义缓存模式的核心目的是为了避免昂贵的资源重新获取方法是在资源使用后不立即释放资源。资源保留其身份保留在某些快速访问的存储中并被重新使用以避免再次获取它们。在英文版 caching/README.md 中对该意图做了进一步展开缓存模式通过write-through、read-through、LRU cache等多种策略保证高效的数据访问。当同一资源被反复获取、初始化和释放时会产生不必要的性能开销而缓存让这些资源保留身份并常驻在高速访问存储中从而避免再次获取。用通俗的话说把频繁需要的数据放进高速访问的存储中从而提升整体性能。缓存命中cache hit时直接从缓存读取比重新计算结果或读取较慢的数据存储要快得多请求能越多地从缓存得到服务系统性能就越高。类图缓存模块的整体结构模块类图见 caching/etc/caching.png完整展示了本模式在项目中的类结构Caching 模式类图从类图与源码可以看出本模块的核心类职责如下源码均位于 caching/src/main/java/com/iluwatar/cachingApp程序入口负责启动并依次演示四种缓存策略AppManager桥接主类与后端负责初始化数据库连接、缓存策略与缓存容量并按策略分发读写请求CacheStore四种缓存策略的具体实现层LruCache基于哈希表 双向链表实现的 LRU 缓存容器CachingPolicy枚举类型定义THROUGH/AROUND/BEHIND/ASIDE四种策略UserAccount缓存与数据库共同存储的实体对象DbManager及其实现VirtualDb、MongoDb底层数据访问接口。模块的整体调用链在 App.java 的 Javadoc 中也有明确说明App -- AppManager -- CacheStore / LruCache / CachingPolicy -- DbManager。五种缓存策略各司其职的读写路径模块在 CacheStore.java 与 AppManager.java 中实现了多种缓存策略每种策略在读写路径与数据一致性上各有取舍。英文版 caching/README.md 对这几种策略的概括如下策略写入行为读取行为适用特点Write-through在单个事务中同时写入缓存与数据库Read-through保证缓存与 DB 强一致但每次写都要落库Write-around数据立即写入数据库绕过缓存Read-through避免缓存被不常读的数据污染但首次读会 missWrite-behind数据先写入缓存仅在缓存满时才回写数据库Read-through 写回write-back写吞吐高但存在缓存与 DB 短暂不一致的窗口Cache-aside由应用程序自己负责两个数据源的同步先查缓存miss 则查 DB 并回填缓存灵活可控对应用代码要求最高Read-through——缓存命中直接返回miss 则查 DB 并存入缓存供后续使用以上四种策略读取侧的公共基础策略枚举与运行时切换CachingPolicy在 CachingPolicy.java 中定义AllArgsConstructor Getter public enum CachingPolicy { THROUGH(through), AROUND(around), BEHIND(behind), ASIDE(aside); private final String policy; }由于读写逻辑是按策略分支分发的见下文AppManager与App的代码应用可以在运行时通过initCachingPolicy(CachingPolicy policy)自由切换策略——这正是策略模式Strategy在该模块中的体现。源码级实现剖析从数据层到缓存容器数据层UserAccount 与 DbManager缓存与数据库共同存储的实体是UserAccount见 UserAccount.java它通过 Lombok 注解生成 getter/setter、构造器、toString与equals/hashCodeData AllArgsConstructor ToString EqualsAndHashCode public class UserAccount { private String userId; private String userName; private String additionalInfo; }数据访问接口DbManager见 DbManager.java定义了四种数据库操作readFromDb、writeToDb、updateDb、upsertDb外加connect与disconnect。项目提供了两个实现VirtualDb.java以内存HashMap模拟数据库无需任何外部依赖便于本地运行与单元测试MongoDb.java基于 MongoDB 的真实实现集合名与字段名由 CachingConstants.java 统一定义如集合user_accounts、字段userID、userName、additionalInfo。具体选择哪个实现由 DbManagerFactory.java 根据入参决定传入--mongo时返回MongoDb否则返回VirtualDb。缓存容器LruCache 的哈希表 双向链表LruCache见 LruCache.java是本模块缓存的数据结构核心采用哈希表 双向链表组合哈希表MapString, Node cache提供 O(1) 的按 userId 查找双向链表维护数据的使用热度数据被查询、新增或更新时会被移到链表头部setHead代表最近使用链表尾部end始终是最久未使用LRU的数据。关键方法实现如下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 数据 remove(end); setHead(newNode); } else { setHead(newNode); } cache.put(userId, newNode); } }当缓存容量已满时新数据会驱逐链表尾部的 LRU 数据再插入get命中时会将该节点移动到头部以更新热度。此外还提供了contains、invalidate使指定 userId 失效、isFull、getLruData返回 LRU 数据、clear、getCacheDataInListForm按链表顺序输出缓存内容用于打印以及setCapacity调整容量若新容量小于当前容量则清空缓存等方法。策略实现层CacheStoreCacheStore见 CacheStore.java是四种策略的具体实现。默认缓存容量为CAPACITY 3在构造函数中通过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最后统一把数据写入缓存保证缓存与 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避免脏数据public void writeAround(final UserAccount userAccount) { if (cache.contains(userAccount.getUserId())) { dbManager.updateDb(userAccount); // 缓存数据已更新——移除缓存中的旧版本 cache.invalidate(userAccount.getUserId()); } else { dbManager.writeToDb(userAccount); } }write-behindwriteBehind与readThroughWithWriteBackPolicy写入时只进缓存当缓存已满且写入的是新数据时先把 LRU 数据upsertDb回写数据库再放入新数据。读取侧同样在缓存满时先回写 LRU 数据再填充新数据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()会把缓存中剩余数据批量updateDb回写数据库并在结束时调用dbManager.disconnect()断开连接clearCache()清空缓存print()以--CACHE CONTENT-- ... ----格式输出缓存内容。调度层AppManager 与运行时策略分发AppManager见 AppManager.java负责在App与后端之间架桥initDb()建立数据库连接initCachingPolicy(policy)设置策略若为BEHIND还会注册 JVM 关闭钩子以在退出时执行flushCacheinitCacheCapacity设置缓存容量。find与save按策略分发到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(...)实现见 AppManager.java。运行示例四种策略的完整演示流程模块入口 App.java 的main方法会依次演示四种策略先通过命令行参数判断是否使用 MongoDB参数--mongo随后依次执行 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); // 第二次查询缓存命中 }运行输出节选关键片段英文版 caching/README.md 记录了完整的程序输出以下为各策略下的关键日志Write-throughTHROUGH保存记录后缓存中立即出现001后续两次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数据先进缓存缓存满容量 3时触发 LRU 数据回写 DB 并驱逐# 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 策略注册的关闭钩子会执行# flushCache...将缓存残留数据回写数据库。测试验证模块在 CachingTest.java 中为四种策略各编写了测试用例testReadAndWriteThroughStrategy、testReadThroughAndWriteAroundStrategy、testReadThroughAndWriteBehindStrategy、testCacheAsideStrategy测试使用new App(false)即内存数据库VirtualDb运行避免对 MongoDB 的依赖。完整的 JUnit 测试套件可通过 Maven 执行模块 pom.xml 已配置相应测试依赖。两种运行方式内存库与 MongoDB根据 App.java 的 Javadoc本模块支持两种启动方式内存数据库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/挂载为数据目录/data/db。适用性什么场景下使用缓存模式根据 localization/zh/caching/README.md 与英文版 caching/README.md以下场景适合使用缓存模式重复获取、初始化和释放同一资源会产生不必要的性能开销时zh 版原文重新计算或重新获取数据的成本显著高于从缓存读取时读多写少read-heavy且数据相对静态、变化不频繁的应用。典型真实应用场景包括网页缓存以降低服务器负载并提升响应时间数据库查询缓存以避免重复的昂贵 SQLCPU 密集型计算结果缓存CDN 将图片、CSS、JavaScript 等静态资源缓存在靠近终端用户的位置。收益与权衡收益性能提升显著降低数据访问延迟应用响应更快降低负载减轻底层数据源的访问压力进而节省成本并延长资源使用寿命可扩展性在不按比例增加资源消耗的前提下更高效地应对负载增长。权衡复杂度引入了缓存失效、数据一致性与同步等额外复杂度资源占用维护缓存需要额外的内存或存储资源脏数据风险若缓存未及时失效或更新可能向用户返回过期数据。选择哪种策略取决于业务对一致性与吞吐的取舍强一致优先选 write-through写多读少防污染选 write-around写吞吐优先可接受短暂不一致选 write-behind需要最大灵活性则由应用自行管理同步选 cache-aside。与其他设计模式的关系缓存模式在本仓库中与其他模式存在自然的协作关系对应目录均可直接查阅源码Proxy代理可通过代理对象拦截请求命中时直接返回缓存数据实现缓存逻辑的无侵入接入Observer观察者可用于在底层数据变化时通知缓存进行更新或失效Decorator装饰器可在不修改原有对象代码的前提下附加缓存行为Strategy策略本模块的CachingPolicy正是策略模式的体现使应用可以在运行时切换不同缓存策略。总结java-design-patterns 的 caching 模块通过UserAccount实体、DbManager数据层、LruCache哈希表 双向链表的 LRU 容器、CacheStore策略实现层、AppManager调度层与App演示入口的分层设计完整呈现了缓存模式的落地方式。它同时覆盖了 write-through、write-around、write-behind、cache-aside 与 read-through 五种主流策略并提供了内存数据库与 MongoDB 两种可运行环境配合 CachingTest.java 的测试用例是研究 Java 缓存架构、缓存失效策略与数据一致性取舍的优质参考实现。赞分享示例工程教程【免费下载链接】java-design-patternsDesign patterns implemented in Java项目地址https://gitcode.com/GitHub_Trending/ja/java-design-patterns点击查看免费下载相关推荐java-design-patterns 中的 Caching 缓存设计模式五种缓存策略与 LRU 底层实现详解java design patterns 中的 Caching 缓存设计模式五种缓存策略与 LRU 底层实现详解 缓存设计模式Caching Pattern示例工程教程Java Caching 设计模式实战基于 java-design-patterns 仓库的四种缓存策略与 LRU 实现解析Java Caching 设计模式实战基于 java design patterns 仓库的四种缓存策略与 LRU 实现解析 缓存Caching设计模式是示例工程教程WatchAlert 内存缓存设计LRU缓存淘汰策略实现WatchAlert 内存缓存设计LRU缓存淘汰策略实现 缓存架构概述 在云原生监控告警引擎中缓存系统是提升数据处理性能的核心组件。WatchAlert作为后端可观测性告警云原生运维上一篇Remotion 文本高亮与手绘标注动画基于 remotion/rough-notation 的逐帧驱动方案下一篇Windows 11开始菜单失效的5步实战解决方案ExplorerPatcher深度应用创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表