拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

java-design-patterns 中的 Version Number(版本号)模式:用乐观锁解决并发更新冲突

java-design-patterns 中的 Version Number(版本号)模式:用乐观锁解决并发更新冲突
  • 示例工程
  • 教程

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

Design patterns implemented in Java

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

本篇技术指南围绕 java-design-patterns 项目中的 Version Number(版本号)模式展开,讲解如何通过为实体维护递增版本号,在多个客户端同时更新同一数据时检测并阻止相互覆盖。你将掌握该模式的实体建模、仓库层并发控制实现、冲突处理流程,并结合仓库源码与测试用例理解其在真实数据库场景(如 JPA/Hibernate 乐观锁)中的落地方式。

名字 / 分类

  • 模式名:Version Number(版本号)
  • 别名:实体版本控制(Entity Versioning)、乐观锁(Optimistic Locking)
  • 分类:Concurrency(并发)、Data access(数据访问)、Microservices(微服务)适用

目的

解决多个客户端尝试同时更新同一实体时的并发冲突。其核心思想是:不再用悲观锁阻塞其他事务,而是为每条数据维护一个版本号,在提交更新时校验版本号是否仍为最新,从而以"先提交、后校验"的方式检测冲突。

解释

现实世界的例子

爱丽丝(Alice)和鲍勃(Bob)正在管理书,该书存储在数据库中。两位操作者正在同时进行更改,我们需要某种机制来防止他们相互覆盖。

在仓库的 App.java 中,这个场景被完整复现:Alice 和 Bob 同时从仓库取出同一本书,Alice 先修改标题并保存成功,随后 Bob 基于过期副本修改作者,保存时因版本号不匹配而失败。

通俗地说

版本号模式可防止对同一实体进行并发更新。

维基百科说

乐观并发控制假设多个事务可以频繁完成而不会互相干扰。在运行时,事务使用数据资源而不获取这些资源的锁。在提交之前,每个事务都将验证没有其他事务修改了已读取的数据。如果检查发现有冲突的修改,则提交的事务将回滚并可以重新启动。

这正是版本号模式的理论基础:不加锁、提交前校验、冲突则回滚重试。

实体建模:带版本号的 Book

仓库中的Book是已版本化的实体,它有一个复制构造函数,这是整个模式的关键前提——客户端与仓库持有的是不同的对象副本:

@Getter @Setter public class Book { private long id; private String title = ""; private String author = ""; private long version = 0; // version number public Book() {} /** 复制构造函数:用于在 BookRepository 中复制 book 表示 */ public Book(Book book) { this.id = book.id; this.title = book.title; this.author = book.author; this.version = book.version; } }

完整源码见 Book.java。可以看到:

  • version字段是long类型,默认值为0,随每次成功更新递增;
  • 复制构造函数把id、title、author、version全部拷贝到新对象,保证客户端拿到的"副本"与仓库内的"原件"互不影响;
  • 该实体使用 Lombok 的@Getter/@Setter生成访问器,用于读取与修改标题、作者和版本号。

仓库层实现并发控制:BookRepository

BookRepository模拟了一个简化数据库,负责在更新时执行版本校验。仓库源码见 BookRepository.java,实现细节如下:

public class BookRepository { private final ConcurrentHashMap<Long, Book> collection = new ConcurrentHashMap<>(); private final Object lock = new Object(); /** 新增书籍(以副本形式保存) */ public void add(Book book) throws BookDuplicateException { if (collection.containsKey(book.getId())) { throw new BookDuplicateException("Duplicated book with id: " + book.getId()); } // 保存副本而非引用 collection.put(book.getId(), new Book(book)); } /** 仅当客户端持有最新版本时才允许更新 */ public void update(Book book) throws BookNotFoundException, VersionMismatchException { if (!collection.containsKey(book.getId())) { throw new BookNotFoundException("Not found book with id: " + book.getId()); } // synchronized 块确保"比较版本 + 更新版本"是原子操作 synchronized (lock) { var latestBook = collection.get(book.getId()); if (book.getVersion() != latestBook.getVersion()) { throw new VersionMismatchException( "Tried to update stale version " + book.getVersion() + " while actual version is " + latestBook.getVersion()); } // 更新版本号(同时同步客户端表示),版本号 +1 book.setVersion(book.getVersion() + 1); // 保存书籍副本到仓库 collection.put(book.getId(), new Book(book)); } } /** 返回书籍副本给客户端 */ public Book get(long bookId) throws BookNotFoundException { if (!collection.containsKey(bookId)) { throw new BookNotFoundException("Not found book with id: " + bookId); } return new Book(collection.get(bookId)); } }

源码级要点:

  1. 按值存取(copy semantics):get和add/update都通过复制构造函数保存/返回副本,而非共享引用。这与真实数据库"客户端操作的是缓存行副本"的语义一致,也正是并发冲突能够发生的根源——两个客户端拿到的都是同一版本(如 version=0)的不同副本。
  2. 校验逻辑:update先把客户端传入的book.getVersion()与仓库内最新latestBook.getVersion()比较,不等即抛VersionMismatchException,仓库中的书保持原样不被覆盖。
  3. 原子性保证:虽然底层集合用了ConcurrentHashMap,但"比较版本 → 递增版本 → 写回"三步必须整体原子执行,因此实现中引入了synchronized (lock)同步块,确保多线程下只有一个线程能通过版本校验并完成更新。
  4. 异常体系:模式配套三个自定义异常,见 BookDuplicateException.java、BookNotFoundException.java 与 VersionMismatchException.java,分别对应重复添加、操作不存在的书籍、提交过期版本三种失败场景。

实战演示:Alice 与 Bob 的并发更新

App.java 是模式的完整运行示例:

var bookId = 1; var bookRepository = new BookRepository(); var book = new Book(); book.setId(bookId); bookRepository.add(book); // 添加一本空标题、空作者的书籍 LOGGER.info("An empty book with version {} was added to repository", book.getVersion()); // Alice 和 Bob 同时取走了这本书 final var aliceBook = bookRepository.get(bookId); final var bobBook = bookRepository.get(bookId); aliceBook.setTitle("Kama Sutra"); // Alice 更新了书籍标题 bookRepository.update(aliceBook); // 并成功保存到数据库 LOGGER.info("Alice updates the book with new version {}", aliceBook.getVersion()); // 此时 Bob 手里的书是过期版本:空标题、version = 0 // 而数据库中的实际书籍已有标题且 version = 1 bobBook.setAuthor("Vatsyayana Mallanaga"); // Bob 更新作者 try { LOGGER.info("Bob tries to update the book with his version {}", bobBook.getVersion()); bookRepository.update(bobBook); // Bob 尝试保存到数据库 } catch (VersionMismatchException e) { // Bob 更新失败,仓库中的书保持原样 LOGGER.info("Exception: {}", e.getMessage()); // Bob 应当重新读取仓库中的最新版本,再次修改并保存 }

程序输出:

14:51:04.119 [main] INFO com.iluwatar.versionnumber.App -- An empty book with version 0 was added to repository 14:51:04.122 [main] INFO com.iluwatar.versionnumber.App -- Alice updates the book with new version 1 14:51:04.122 [main] INFO com.iluwatar.versionnumber.App -- Bob tries to update the book with his version 0 14:51:04.123 [main] INFO com.iluwatar.versionnumber.App -- Exception: Tried to update stale version 0 while actual version is 1

关键输出解读:

  • Alice 提交成功,版本号从0递增为1,仓库中保存的是带标题的新副本;
  • Bob 提交时携带版本0,与仓库最新版本1不匹配,抛出VersionMismatchException,仓库数据未被覆盖;
  • 正确恢复流程是:Bob 重新get(bookId)拉取最新版本,在最新副本上重做修改,再update提交。

模式运行流程

下图展示了版本号模式的完整执行流程:读取带版本号的记录 → 在内存中修改 → 尝试更新 → 查询当前版本 → 版本匹配则更新并递增版本号,不匹配则拒绝更新并报告版本冲突。

类图

类图反映了本模块的核心设计:Book封装带version字段的业务数据并提供复制构造函数;BookRepository以Map<Long, Book>存储数据,提供add、get、update三个操作,其中update承担版本校验;App作为演示入口调用上述流程。类图对应的 PlantUML 源文件见 version-number.urm.puml。

适用性

将版本号模式用于:

  • 解决对数据的并发写访问:多个客户端/服务同时更新同一行数据时,防止后提交者静默覆盖先提交者的修改;
  • 强的数据一致性:对一致性要求高的业务(如订单、账户、文档编辑)需要保证每次写入都基于最新状态。

同时适合以下场景:

  • 需要处理分布式系统中的并发数据修改;
  • 系统对数据一致性与完整性有硬性要求;
  • 应用所依赖的数据库支持版本号或行版本(row versioning)特性,可低成本集成。

已知用途

版本号/乐观并发控制是工业界的成熟实践,以下知名系统均采用类似机制:

  • Hibernate(Java Persistence API):通过实体@Version属性实现乐观锁,提交时校验版本并抛出乐观锁异常;
  • Microsoft SQL Server 与 Oracle:提供基于版本的行级并发控制能力;
  • Apache CouchDB等 NoSQL 数据库:通过修订版本号(revision)进行冲突检测与解决;
  • Elasticsearch:索引文档的版本控制,冲突时返回版本冲突错误;
  • Apache Solr:部分文档更新时的版本校验机制。

模式意义

版本号模式允许实现并发控制,通常通过乐观离线锁(Optimistic Offline Lock)模式来完成。它属于 Martin Fowler《企业应用架构模式》中并发控制家族的一员,与悲观离线锁(Pessimistic Offline Lock)形成对比:

  • 乐观(本模式):不加锁,用版本号在提交时检测冲突,冲突概率低、读多写少时吞吐更高;
  • 悲观:更新前先加锁阻止他人修改,冲突频繁或写密集场景下更直接,但会带来锁等待与死锁风险。

优势与权衡

优势(Benefits)

  • 提高数据一致性与完整性,杜绝"丢失更新"(lost update)问题;
  • 相比悲观锁,读多写少场景下无锁等待,并发吞吐更优;
  • 冲突检测机制简单明确,异常信息可直接定位是哪一方基于过期版本提交。

权衡(Trade-offs)

  • 需要额外的版本校验与冲突处理逻辑(如异常捕获、重试);
  • 数据库表结构与应用逻辑复杂度增加(需维护版本字段);
  • 版本校验与冲突解决会带来一定性能开销,冲突频繁时提交失败率上升,需要配合重试策略。

测试验证

仓库为模式实现提供了 JUnit 5 单元测试,见 BookRepositoryTest.java,核心断言包括:

  • testDefaultVersionRemainsZeroAfterAdd:新增书籍后版本号保持为0;
  • testAliceAndBobHaveDifferentVersionsAfterAliceUpdate:Alice 提交后其副本版本为1,Bob 副本仍为0,仓库内实际书籍与 Alice 版本一致;
  • testShouldThrowVersionMismatchExceptionOnStaleUpdate:Bob 基于过期版本提交时抛出VersionMismatchException,且仓库数据(版本1、Alice 的标题)未被破坏。

此外 AppTest.java 验证了主程序可无异常完整运行。

如何运行示例

在仓库根目录(java-design-patterns)下,可单独构建并运行 version-number 模块(需要本机具备 JDK 与 Maven 环境):

# 编译并运行模块主类(pom.xml 中已配置 mainClass 为 com.iluwatar.versionnumber.App) ./mvnw -pl version-number compile java -cp version-number/target/classes com.iluwatar.versionnumber.App

模块的依赖与构建配置见 version-number/pom.xml,其中父工程为 java-design-patterns 聚合项目,maven-assembly-plugin已指定 App.java 作为主类入口;运行后即可在控制台观察到上文的四行日志输出,直观验证乐观锁的冲突拦截效果。

  • 示例工程
  • 教程

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

Design patterns implemented in Java

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

相关推荐

上一篇:终极缠论分析指南:3分钟掌握ChanlunX自动化技术分析
下一篇:当AI遇见医学影像:FastMRI如何用深度学习加速磁共振扫描

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

返回列表