1. 概述
在企业级应用开发中,正确处理数据库的并发访问至关重要。我们必须能够高效且无错误地管理多个并发事务,同时确保在并发读写过程中数据的一致性。
为了解决这个问题,Java Persistence API(JPA)提供了乐观锁(Optimistic Locking)机制。它允许多个事务同时读取同一数据,只有在提交更新时才检查冲突,从而避免相互干扰。
✅ 适用场景:读多写少、数据竞争不激烈的系统
❌ 不适合:高频并发写入、强一致要求的金融交易系统(需结合其他机制)
2. 乐观锁的核心原理
使用乐观锁的关键是:实体类中必须包含一个带有 @Version
注解的字段。
其工作流程如下:
- 事务 A 读取某条记录时,同时读取其
version
值(比如为 1) - 事务 B 也读取同一条记录,
version
同样是 1 - 事务 A 先提交更新,JPA 自动将
version
更新为 2,并成功提交 - 事务 B 提交时,发现数据库中的
version
已经是 2,而自己持有的还是 1 → 冲突! - 此时 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 规范建议:新项目使用 OPTIMISTIC
和 OPTIMISTIC_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
字段,乐观锁即生效
✅ 灵活控制:可通过 find
、query
、lock
等多种方式显式指定锁模式
✅ 异常处理:冲突时抛出 OptimisticLockException
,需捕获并重试
与悲观锁相比,乐观锁不占用数据库锁资源,更适合高并发读场景,但需合理设计重试逻辑以应对写冲突。
本文示例代码已托管至 GitHub:https://github.com/tech-tutorial-jpa/optimistic-locking