1. 概述

在企业级应用开发中,正确处理数据库的并发访问至关重要。我们必须能够高效且无错误地管理多个并发事务,同时确保在并发读写过程中数据的一致性。

为了解决这个问题,Java Persistence API(JPA)提供了乐观锁(Optimistic Locking)机制。它允许多个事务同时读取同一数据,只有在提交更新时才检查冲突,从而避免相互干扰。

✅ 适用场景:读多写少、数据竞争不激烈的系统
❌ 不适合:高频并发写入、强一致要求的金融交易系统(需结合其他机制)


2. 乐观锁的核心原理

使用乐观锁的关键是:实体类中必须包含一个带有 @Version 注解的字段

其工作流程如下:

  1. 事务 A 读取某条记录时,同时读取其 version 值(比如为 1)
  2. 事务 B 也读取同一条记录,version 同样是 1
  3. 事务 A 先提交更新,JPA 自动将 version 更新为 2,并成功提交
  4. 事务 B 提交时,发现数据库中的 version 已经是 2,而自己持有的还是 1 → 冲突!
  5. 此时 JPA 抛出 OptimisticLockException,事务 B 回滚

⚠️ 注意:version 字段的增减由持久化框架(如 Hibernate)自动完成,禁止手动修改,否则会破坏一致性。


3. 乐观锁 vs 悲观锁

JPA 同时支持两种并发控制机制:

特性 乐观锁(Optimistic Locking) 悲观锁(Pessimistic Locking)
加锁时机 提交时检查冲突 读取时即加锁
实现方式 @Version 字段比对 数据库行锁(如 SELECT FOR UPDATE
性能表现 高并发读性能好,冲突少时效率高 存在锁等待,可能引发死锁
适用场景 Web 应用、读多写少 强一致性、短事务

📌 简单粗暴理解:

  • 乐观锁:先干活,提交时“对账”,不对就重来
  • 悲观锁:先占坑,别人别想动

悲观锁详细内容可参考我们之前的专题:JPA 中的悲观锁


4. Version 字段详解

@Version 注解标记的字段是开启乐观锁的前提。示例如下:

@Entity
public class Student {

    @Id
    private Long id;

    private String name;

    private String lastName;

    @Version
    private Integer version;

    // getters and setters
}

✅ 必须遵守的规则

  • 每个实体类只能有一个 @Version 字段
  • 若实体映射多张表,该字段必须位于主表
  • 支持的字段类型包括:
    • int / Integer
    • long / Long
    • short / Short
    • java.sql.Timestamp

⚠️ 注意事项

  • version 值由 JPA 提供者(如 Hibernate)自动维护,不要手动 set 或 increment
  • 虽然部分持久化框架支持无 @Version 字段的乐观锁(基于脏字段比对),但强烈建议显式声明,避免行为不一致
  • 如果尝试对无 @Version 字段的实体加乐观锁,且框架不支持 → 抛出 PersistenceException

5. 锁模式(Lock Modes)

JPA 定义了两种乐观锁模式(含别名),均在 LockModeType 枚举中:

模式 别名 行为
OPTIMISTIC READ 检查版本,防止脏读和不可重复读
OPTIMISTIC_FORCE_INCREMENT WRITE 同上,且强制递增 version 值

📌 JPA 规范建议:新项目使用 OPTIMISTICOPTIMISTIC_FORCE_INCREMENT,而非 READ/WRITE(虽然后者仍可用)

5.1 OPTIMISTIC(READ)

  • 保证事务不会提交到已被其他事务修改或删除的数据上
  • 防止以下情况:
    • 其他事务已修改但未提交(脏写)
    • 其他事务已成功修改(丢失更新)

5.2 OPTIMISTIC_FORCE_INCREMENT(WRITE)

  • OPTIMISTIC 基础上,一定会递增 version 字段
  • 递增时机由实现决定:可能立即执行,也可能延迟到 flush 或 commit 时
  • 框架允许在请求 OPTIMISTIC 时也提供 OPTIMISTIC_FORCE_INCREMENT 的行为(兼容性考虑)

6. 乐观锁的使用方式

对于带 @Version 的实体,乐观锁默认生效。但你也可以显式指定锁模式,以下是常见用法:

6.1 使用 find 方法

entityManager.find(Student.class, studentId, LockModeType.OPTIMISTIC);

6.2 使用 Query 设置锁

Query query = entityManager.createQuery("from Student where id = :id");
query.setParameter("id", studentId);
query.setLockMode(LockModeType.OPTIMISTIC_FORCE_INCREMENT);
query.getResultList();

6.3 显式调用 lock 方法

Student student = entityManager.find(Student.class, id);
entityManager.lock(student, LockModeType.OPTIMISTIC);

6.4 使用 refresh 方法

Student student = entityManager.find(Student.class, id);
entityManager.refresh(student, LockModeType.READ); // READ 等价于 OPTIMISTIC

6.5 通过 @NamedQuery 配置

@NamedQuery(
    name = "optimisticLock",
    query = "SELECT s FROM Student s WHERE s.id LIKE :id",
    lockMode = LockModeType.WRITE // WRITE 等价于 OPTIMISTIC_FORCE_INCREMENT
)

7. 处理 OptimisticLockException

当发生版本冲突时,JPA 会抛出 OptimisticLockException并且当前事务会被自动标记为 rollback-only

关键点:

  • 异常中可能包含冲突的实体引用(getEntity()),但不保证一定有
  • 框架实现可能因性能或实现原因不填充该字段

推荐处理策略

try {
    // 执行更新逻辑
    entityManager.merge(student);
    entityManager.flush();
} catch (OptimisticLockException e) {
    // 最佳实践:在新事务中重试
    // 1. 重新获取最新数据
    Student latest = entityManager.find(Student.class, student.getId());
    // 2. 合并业务逻辑(如重新设置需要更新的字段)
    latest.setName(student.getName());
    // 3. 重新提交
    entityManager.merge(latest);
}

📌 建议结合重试机制(如 Spring Retry)实现自动重试,避免用户感知冲突。


8. 总结

本文系统介绍了 JPA 中的乐观锁机制:

核心机制:通过 @Version 字段检测并发修改,避免数据覆盖
默认开启:只要实体有 @Version 字段,乐观锁即生效
灵活控制:可通过 findquerylock 等多种方式显式指定锁模式
异常处理:冲突时抛出 OptimisticLockException,需捕获并重试

与悲观锁相比,乐观锁不占用数据库锁资源,更适合高并发读场景,但需合理设计重试逻辑以应对写冲突。

本文示例代码已托管至 GitHub:https://github.com/tech-tutorial-jpa/optimistic-locking


原始标题:Optimistic Locking in JPA

» 下一篇: Spring 5 教程