news 2026/10/2 17:12:50

Identity Map 模式在 Java 中的实现:保证对象只加载一次的内存缓存方案

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Identity Map 模式在 Java 中的实现:保证对象只加载一次的内存缓存方案
  • 示例工程
  • 教程

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

Design patterns implemented in Java

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

导读

Identity Map(身份映射表)是面向数据库访问场景的关键设计模式,核心思想是保证每个业务对象在整个会话内只从数据库加载一次,之后的所有引用都从内存中的映射表复用同一实例。本仓库 java-design-patterns 的 identity-map 模块以"按身份证号查询人员档案"为演示场景,完整实现了该模式。阅读本文后,你将掌握 Identity Map 的模式意图、类结构、缓存命中与未命中的查找流程、异常处理策略,以及如何通过单元测试验证缓存行为的正确性,并理解它与 Data Mapper、Unit of Work 等企业级持久化模式的关系。

Identity Map 模式类图

Identity Map 查找流程时序图

模式意图:为什么需要 Identity Map

Identity Map 模式的目的可以用一句话概括:通过把每个已加载的对象保存在一张 Map 中,确保每个对象只被加载一次;当再次引用该对象时,直接从 Map 中查找并返回(见 localization/es/identity-map/README.md)。

在典型的数据库应用中,反复查询同一条记录会产生三类问题:

  1. 重复加载:同一份数据在内存中被实例化成多个对象,白白浪费内存;
  2. 引用不一致:应用不同模块各自持有"同一行数据"的不同对象,一处修改另一处不可见,破坏引用完整性;
  3. 无谓的 IO:相同数据的重复查询增加了数据库调用次数,拖慢性能。

Wikipedia 对该模式的定义精准地概括了它的定位:

在 DBMS 的设计中,identity map 模式是一种数据库访问设计模式,用于通过提供特定上下文的内存缓存,避免从数据库中重复检索同一对象数据,从而提升性能。

用更朴素的语言来说:构建一张身份映射表,把最近在数据库中查过的记录存进去;下次再查同一条记录时,直接从映射表取,不再访问数据库。

现实世界的类比

设想你正在组织一场会议,签到台的工作人员第一次为与会者登记时,会把信息录入系统;当同一位与会者再次来到签到台,系统直接调出他的资料,无需重新提交。Identity Map 正是如此:每个对象只加载一次,并在整个应用生命周期内被反复复用。

模块结构:五个核心类的分工

在深入代码前,先了解本模块的完整文件布局(源码位于 identity-map/src/main/java/com/iluwatar/identitymap):

类角色职责
Person.java实体业务记录,personNationalId作为唯一标识
IdentityMap.java缓存以HashMap<Integer, Person>维护"已加载对象"
PersonFinder.java门面/查找器对外提供查找入口,协调缓存与数据库
PersonDbSimulator.java数据库接口定义find/insert/update/delete契约
PersonDbSimulatorImplementation.java模拟数据库用内存List模拟数据表及 CRUD 操作
IdNotFoundException.java异常记录不存在时控制流程的运行时异常

类图(identity-map/etc/IdentityMap.png)清晰展示了PersonFinder同时持有IdentityMap与数据库对象的引用关系:它是连接缓存层与数据层的枢纽。

核心源码解读:实体、缓存与查找器

实体 Person:唯一标识决定"同一性"

Person.java 是一个不可变风格的实体类,其关键设计在于用 Lombok 注解精确定义"对象同一性":

@EqualsAndHashCode(onlyExplicitlyIncluded = true) @Getter @Setter @AllArgsConstructor public final class Person implements Serializable { @Serial private static final long serialVersionUID = 1L; @EqualsAndHashCode.Include private int personNationalId; private String name; private long phoneNum; @Override public String toString() { return "Person ID is : " + personNationalId + " ; Person Name is : " + name + " ; Phone Number is :" + phoneNum; } }

@EqualsAndHashCode(onlyExplicitlyIncluded = true)配合@EqualsAndHashCode.Include意味着:两个 Person 是否相等,只取决于personNationalId,与姓名、电话无关。这从语义上保证了"同一身份证号 = 同一人",也正是 Identity Map 判定缓存命中的理论基础——Map 的 key 就是personNationalId,任何两个同 ID 的 Person 实例都被视为同一逻辑对象。

缓存 IdentityMap:HashMap 的薄封装

IdentityMap.java 本质是对HashMap<Integer, Person>的封装,提供三个方法:

@Slf4j @Getter public class IdentityMap { private Map<Integer, Person> personMap = new HashMap<>(); public void addPerson(Person person) { if (!personMap.containsKey(person.getPersonNationalId())) { personMap.put(person.getPersonNationalId(), person); } else { // 防止 addPerson 更新已有记录,仅用于测试场景 LOGGER.info("Key already in Map"); } } public Person getPerson(int id) { Person person = personMap.get(id); if (person == null) { LOGGER.info("ID not in Map."); } return person; } public int size() { if (personMap == null) { return 0; } return personMap.size(); } }

值得注意的细节是addPerson的防重复策略:当 key 已存在时只打印日志、不覆盖原记录。源码注释明确说明这是"确保addPerson不会更新已有记录"——缓存层只负责记录"首次加载",后续数据变更应由显式的更新流程处理,避免缓存被静默污染。

查找器 PersonFinder:缓存优先的核心流程

PersonFinder.java 是模式逻辑的集中体现,其类注释概括了整体行为:"先检查对象是否已缓存在 Identity Map 中并直接返回;否则访问数据库、将记录存入 Identity Map 后再返回,后续查找将命中缓存,使查询更快。"

@Slf4j @Getter @Setter public class PersonFinder { private static final long serialVersionUID = 1L; // 访问 Identity Map private IdentityMap identityMap = new IdentityMap(); private PersonDbSimulatorImplementation db = new PersonDbSimulatorImplementation(); public Person getPerson(int key) { // 先在 Identity Map 中查找 Person person = this.identityMap.getPerson(key); if (person != null) { LOGGER.info("Person found in the Map"); return person; } else { // 缓存未命中,去数据库查找 person = this.db.find(key); if (person != null) { this.identityMap.addPerson(person); LOGGER.info("Person found in DB."); return person; } LOGGER.info("Person with this ID does not exist."); return null; } } }

这段代码完整实现了"缓存优先"的查找算法,可用三步概括:

  1. 查缓存:调用identityMap.getPerson(key),命中则直接返回(日志:Person found in the Map);
  2. 查数据库:未命中则调用db.find(key),找到后先addPerson写入缓存再返回(日志:Person found in DB.);
  3. 兜底处理:两处均未找到,打印提示并返回null。

从调用链来看,getPerson的返回对象永远是同一实例——第二次查询虽然从 Map 拿到的是同一个Person引用,却省掉了一次数据库访问,这正是模式降低 IO、维持引用一致性的机理。

模拟数据库:真实的 CRUD 语义

为了让演示不依赖外部数据库,本模块用 PersonDbSimulatorImplementation.java 以内存List<Person>模拟数据表,personNationalId充当主键。接口 PersonDbSimulator.java 定义了四个操作:

  • find(int personNationalId):按主键流式过滤查找,不存在时抛出IdNotFoundException;
  • insert(Person person):主键已存在则提示Record already exists.,否则追加到列表;
  • update(Person person):用同 ID 的新 Person 覆盖姓名与电话(源码中通过elem.get().setName(...)实现),不存在则抛异常;
  • delete(int id):删除指定主键记录,不存在则抛异常。

异常类 IdNotFoundException.java 继承RuntimeException,其注释说明它是"在 Person ID 不存在时控制流程"的工具——即用异常而非空返回值标记"查无此人",使调用方(含测试)能够通过assertThrows明确断言失败场景。

完整可运行示例:从入库到缓存命中

App.java 是模块入口(mainClass已在 identity-map/pom.xml 中声明为com.iluwatar.identitymap.App),演示了完整的使用链路:

public static void main(String[] args) { // 构造 5 条虚拟人员记录 Person person1 = new Person(1, "John", 27304159); Person person2 = new Person(2, "Thomas", 42273631); Person person3 = new Person(3, "Arthur", 27489171); Person person4 = new Person(4, "Finn", 20499078); Person person5 = new Person(5, "Michael", 40599078); // 初始化数据库并入库 PersonDbSimulatorImplementation db = new PersonDbSimulatorImplementation(); db.insert(person1); db.insert(person2); db.insert(person3); db.insert(person4); db.insert(person5); // 初始化查找器并绑定数据库 PersonFinder finder = new PersonFinder(); finder.setDb(db); // 前三次查找:缓存未命中,从数据库加载 LOGGER.info(finder.getPerson(2).toString()); LOGGER.info(finder.getPerson(4).toString()); LOGGER.info(finder.getPerson(5).toString()); // 第四次查找:缓存命中,不再访问数据库 LOGGER.info(finder.getPerson(2).toString()); }

运行后控制台输出(日志格式为 SLF4J/Logback 标准输出):

... INFO com.iluwatar.identitymap.IdentityMap -- ID not in Map. ... INFO com.iluwatar.identitymap.PersonDbSimulatorImplementation -- Person ID is : 2 ; Person Name is : Thomas ; Phone Number is :42273631 ... INFO com.iluwatar.identitymap.PersonFinder -- Person found in DB. ... INFO com.iluwatar.identitymap.App -- Person ID is : 2 ; Person Name is : Thomas ; Phone Number is :42273631 ... INFO com.iluwatar.identitymap.IdentityMap -- ID not in Map. ... INFO com.iluwatar.identitymap.PersonDbSimulatorImplementation -- Person ID is : 4 ; Person Name is : Finn ; Phone Number is :20499078 ... INFO com.iluwatar.identitymap.PersonFinder -- Person found in DB. ... INFO com.iluwatar.identitymap.App -- Person ID is : 4 ; Person Name is : Finn ; Phone Number is :20499078 ... INFO com.iluwatar.identitymap.IdentityMap -- ID not in Map. ... INFO com.iluwatar.identitymap.PersonDbSimulatorImplementation -- Person ID is : 5 ; Person Name is : Michael ; Phone Number is :40599078 ... INFO com.iluwatar.identitymap.PersonFinder -- Person found in DB. ... INFO com.iluwatar.identitymap.App -- Person ID is : 5 ; Person Name is : Michael ; Phone Number is :40599078 ... INFO com.iluwatar.identitymap.IdentityMap -- Person ID is : 2 ; Person Name is : Thomas ; Phone Number is :42273631 ... INFO com.iluwatar.identitymap.PersonFinder -- Person found in the Map ... INFO com.iluwatar.identitymap.App -- Person ID is : 2 ; Person Name is : Thomas ; Phone Number is :42273631

逐行对比可以发现关键差异:查询 ID 为 2 的 Thomas 时,第一次经过IdentityMap -- ID not in Map.→PersonDbSimulatorImplementation(数据库)→Person found in DB.三跳;第二次则直接从IdentityMap返回对象并打印Person found in the Map,全程不触碰数据库。这直观验证了缓存命中的行为:相同 ID 的记录,数据库只被访问一次。

如何运行

在仓库根目录下,以 Maven 包装器执行即可编译、测试并运行该模块:

# 运行 identity-map 模块的 App 主类 ./mvnw -pl identity-map exec:java -Dexec.mainClass="com.iluwatar.identitymap.App" # 或仅运行该模块的测试 ./mvnw -pl identity-map test

模块依赖关系(见 identity-map/pom.xml)非常精简:仅依赖slf4j-api、logback-classic(日志)与junit-jupiter-engine(测试),无任何第三方 ORM 框架,便于聚焦模式本身。

测试验证:缓存行为的自动化保障

模块提供了五组 JUnit 5 测试,其中与模式语义最相关的三组值得研读:

1. 命中缓存路径 —— PersonFinderTest.java

personFoundInIdMap测试完整复现了"首次入库、二次命中"的断言链:

// 确保 key=3 此刻不在缓存中 Assertions.assertFalse(personFinder.getIdentityMap().getPersonMap().containsKey(3)); // 首次查找:从数据库加载 Assertions.assertEquals(person3, personFinder.getPerson(3), "Finder returns incorrect record."); // 查找后确认该记录已被写入缓存 Assertions.assertTrue(personFinder.getIdentityMap().getPersonMap().containsKey(3)); // 再次查找:这次应从缓存命中,结果一致 Assertions.assertEquals(person3, personFinder.getPerson(3), "Finder returns incorrect record.");

2. 缓存防重复 —— IdentityMapTest.java

addToMap测试构造了两个personNationalId同为 11 的 Person(电话号码不同),连续调用addPerson后断言:

// 5 次添加、4 个不同 ID → 缓存大小必须为 4(不允许重复) Assertions.assertEquals(4, idMap.size(), "Size of the map is incorrect"); // 且 addPerson 不得用后添加的同 ID 记录覆盖原记录(电话号码保持 27304159) Assertions.assertEquals(27304159, idMap.getPerson(11).getPhoneNum(), "Incorrect return value for phone number");

这从测试层面印证了addPerson的"只写一次、不覆盖"语义,正是保证缓存数据稳定的关键约束。

3. 异常路径 —— 记录不存在

PersonDbSimulatorImplementationTest.java 对find、update、delete三种操作均断言了IdNotFoundException的抛出;PersonFinderTest.java 中的personNotFoundInDB则验证了查找器对"查无此人"的异常传导。

适用场景:何时使用 Identity Map

从模式定义和源码实现可以总结出以下适用条件(原文档 "Aplicabilidad" 部分的完整表述):

  • 同一会话/事务内对同一数据存在多次访问:每次读取记录前先检查 Identity Map,若已检索过就直接返回内存中的现有引用,而不是新建对象——这是模式的首要价值,即维护引用完整性(referential integrity);
  • 希望减少数据库调用次数:作为缓存的副作用,Identity Map 降低了检索对象的数据库访问次数,带来直接的性能提升。

从更广的行业视角看,Identity Map 在以下场景中尤其常见:

  • ORM(对象关系映射)框架:Hibernate 等 Java ORM 框架在持久化上下文中正是借助 Identity Map 保证"同一持久化上下文内同一实体只有一个实例",从而支持脏检查与变更追踪;
  • 企业级应用:在跨多个业务流程共享数据状态的场景中,Identity Map 帮助维持数据状态的一致性。

优点与权衡

优点:

  • 减少内存占用:每个对象在内存中只保留一份副本;
  • 防止更新不一致:应用各处引用同一实例,一处修改处处可见;
  • 提升性能:避免对相同数据的重复数据库读取。

权衡:

  • 增加复杂度:对象管理与持久化逻辑变得更复杂,需要额外维护缓存生命周期;
  • 潜在脏数据风险:若缓存管理不当(尤其在并发环境中),可能读到过期数据。因此实践中通常需要与失效策略、事务边界配合使用。

与相关模式的关系

Identity Map 常与以下模式协同工作(两个模块均在本仓库中):

  • Data Mapper(见>
  • 示例工程
  • 教程

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

Design patterns implemented in Java

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

相关推荐

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/2 17:12:14

记录:在Ubuntu22.04上安装UppASD和Spirit的完整配置流程

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/2 17:11:26

上海企业员工班车租赁深度测评:指标与核验

上海企业员工班车租赁常被当成一次比价采购&#xff0c;真正的风险却不在报价单上。线路临时调整、司机更换、车辆年检到期、旺季调不到备用车&#xff0c;这些问题往往在合同签订后才暴露。所谓深度测评&#xff0c;不是给服务商打分排名&#xff0c;而是把采购指标变成可验证…

作者头像 李华
网站建设 2026/10/2 17:11:23

具身智能最性感的生意,藏在“边角料”里?

作者&#xff1a;Evin编辑&#xff1a;刘致呈审核&#xff1a;徐徐出品&#xff1a;互联网江湖最近&#xff0c;国内具身智能赛道迎来了一场意外风波。先是上市公司梅卡曼德的CEO邵天兰&#xff0c;在朋友圈公开炮轰当前行业盛行的一种“攒局型”具身智能创业——套路就是靠各种…

作者头像 李华
网站建设 2026/10/2 17:08:55

AI应用开发平台工程化底座:Agent编排与多供应商接入实战

1. 为什么我们需要重新审视 AI 应用开发平台过去一年&#xff0c;我接触了不下二十个团队在搞 AI 应用落地&#xff0c;从几个人小团队到大厂创新部门都有。一个非常普遍的困境是&#xff1a;Demo 跑通只要两天&#xff0c;但真要上线一个能稳定服务几百上千用户的 AI 应用&…

作者头像 李华