☰
java-design-patterns 项目 Caching 缓存设计模式实战解析:五种缓存策略与 LRU 实现
2026/10/3 2:08:50 网站建设 项目流程
  • 示例工程
  • 教程

【免费下载链接】java-design-patterns

Design 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/caching):

  • App:程序入口,负责启动并依次演示四种缓存策略;
  • 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避免缓存被不常读的数据污染,但首次读会 miss
Write-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/hashCode:

@Data @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)是本模块缓存的数据结构核心,采用哈希表 + 双向链表组合:

  • 哈希表Map<String, 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(调整容量,若新容量小于当前容量则清空缓存)等方法。

策略实现层:CacheStore

CacheStore(见 CacheStore.java)是四种策略的具体实现。默认缓存容量为CAPACITY = 3,在构造函数中通过initCapacity(CAPACITY)初始化LruCache。

read-through(readThrough):先查缓存,命中直接返回;未命中则打日志"# 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-through(writeThrough):缓存命中则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-around(writeAround):直接写 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-behind(writeBehind与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 关闭钩子以在退出时执行flushCache),initCacheCapacity设置缓存容量。

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.java):

public 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-through(THROUGH):保存记录后缓存中立即出现001,后续两次find均直接命中缓存:

# CachingPolicy.THROUGH Save record! --CACHE CONTENT-- UserAccount(userId=001, userName=John, additionalInfo=He is a boy.) ---- Trying to find 001 in cache # Found in Cache! Trying to find 001 in cache # Found in Cache!

Write-around(AROUND):写入只落 DB,缓存为空;首次读取 miss 后回填,更新用户时缓存中旧版本被移除:

# CachingPolicy.AROUND Save record! --CACHE CONTENT-- ---- Trying to find 002 in cache # Not found in cache! Go to DB!! --CACHE CONTENT-- UserAccount(userId=002, userName=Jane, additionalInfo=She is a girl.) ---- ... # 002 has been updated! Removing older version from cache...

Write-behind(BEHIND):数据先进缓存;缓存满(容量 3)时触发 LRU 数据回写 DB 并驱逐:

# CachingPolicy.BEHIND Save record! Save record! Save record! --CACHE CONTENT-- UserAccount(userId=005, userName=Isaac, additionalInfo=He is allergic to mustard.) UserAccount(userId=004, userName=Rita, additionalInfo=She hates cats.) UserAccount(userId=003, userName=Adam, additionalInfo=He likes food.) ---- ... # Cache is FULL! Writing LRU data to DB... # Cache is FULL! Removing 004 from cache...

Cache-aside(ASIDE):保存时更新 DB 并使缓存失效;查询时先查缓存、miss 再回填:

# CachingPolicy.ASIDE Save record! Save record! Save record! --CACHE CONTENT-- ---- Trying to find 003 in cache --CACHE CONTENT-- UserAccount(userId=003, userName=Adam, additionalInfo=He likes food.) ----

程序退出时,write-behind 策略注册的关闭钩子会执行# flushCache...,将缓存残留数据回写数据库。

测试验证

模块在 CachingTest.java 中为四种策略各编写了测试用例(testReadAndWriteThroughStrategy、testReadThroughAndWriteAroundStrategy、testReadThroughAndWriteBehindStrategy、testCacheAsideStrategy),测试使用new App(false)即内存数据库(VirtualDb)运行,避免对 MongoDB 的依赖。完整的 JUnit 测试套件可通过 Maven 执行(模块 pom.xml 已配置相应测试依赖)。

两种运行方式:内存库与 MongoDB

根据 App.java 的 Javadoc,本模块支持两种启动方式:

  1. 内存数据库(VirtualDb):无需任何外部依赖,直接启动即可:
    java -jar app.jar
  2. MongoDB:需要本机已安装 MongoDB,或通过模块根目录下的 docker-compose.yml 启动容器:
    docker-compose up java -jar app.jar --mongo

docker-compose.yml 会启动mongo:latest容器,映射27017:27017端口,设置 root 账号(用户root/ 密码rootpassword),并将./mongo-data/挂载为数据目录(/data/db)。

适用性:什么场景下使用缓存模式

根据 localization/zh/caching/README.md 与英文版 caching/README.md,以下场景适合使用缓存模式:

  • 重复获取、初始化和释放同一资源会产生不必要的性能开销时(zh 版原文);
  • 重新计算或重新获取数据的成本显著高于从缓存读取时;
  • 读多写少(read-heavy)且数据相对静态、变化不频繁的应用。

典型真实应用场景包括:网页缓存以降低服务器负载并提升响应时间;数据库查询缓存以避免重复的昂贵 SQL;CPU 密集型计算结果缓存;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-patterns

Design patterns implemented in Java

项目地址:https://gitcode.com/GitHub_Trending/ja/java-design-patterns
点击查看免费下载

相关推荐

上一篇:Remotion 文本高亮与手绘标注动画:基于 @remotion/rough-notation 的逐帧驱动方案
下一篇:Windows 11开始菜单失效的5步实战解决方案:ExplorerPatcher深度应用

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询