1. 概述

在实际开发中,我们经常需要从数据库读取数据并进行后续处理。这时候,如果多个事务同时操作同一数据,就容易出现并发问题。为了防止别人“插一脚”,我们可以提前把数据锁住。

解决这类并发控制问题,通常有两种手段:
✅ 设置事务隔离级别
✅ 对需要的数据显式加锁

事务隔离级别是作用于整个数据库连接的,一旦连接建立,隔离级别就固定了,影响该连接下的所有操作。这种方式粒度太粗,不够灵活。

悲观锁(Pessimistic Locking) 则更精细化。它利用数据库底层的锁机制,对特定数据行加锁,确保在事务提交前,其他事务无法修改或删除这些数据。

简单来说,悲观锁的核心思想是:“我先锁住,再操作,防止别人改”。

数据库层面,常见的锁有两种:

  • 共享锁(Shared Lock):允许多个事务同时读,但不允许写。
  • 排他锁(Exclusive Lock):只有持有锁的事务能读写,其他事务完全被阻塞。

在 JPA 中,我们通过 SELECT ... FOR UPDATE 这类语句来获取排他锁,从而实现悲观锁控制。

2. 锁模式(Lock Modes)

JPA 规范定义了三种悲观锁模式,均位于 LockModeType 枚举中。这些锁会一直持有,直到事务提交或回滚。

锁模式 说明
PESSIMISTIC_READ 获取共享锁,防止数据被更新或删除
PESSIMISTIC_WRITE 获取排他锁,防止数据被读、更新或删除
PESSIMISTIC_FORCE_INCREMENT 类似 PESSIMISTIC_WRITE,额外会强制递增版本号(@Version)

⚠️ 注意:同一时间只能持有一种锁,尝试获取第二种会抛出 PersistenceException

2.1 PESSIMISTIC_READ

当你只需要读取数据,且要避免脏读时,可以使用 PESSIMISTIC_READ。它加的是共享锁,意味着你可以读,但不能改。

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

❌ 限制:你无法在这个事务中更新或删除该数据。

⚠️ 踩坑提醒:某些数据库(如 MySQL InnoDB)不支持真正的共享锁,此时 JPA 会自动升级为 PESSIMISTIC_WRITE,相当于直接上排他锁,务必留意。

2.2 PESSIMISTIC_WRITE

当你需要修改数据时,必须使用 PESSIMISTIC_WRITE。它会加排他锁,阻止其他事务读、写、删。

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

✅ 理论上,其他事务连读都不行。

⚠️ 但注意:像 PostgreSQL 这类支持 MVCC(多版本并发控制)的数据库,读操作可能不会被阻塞,因为它们读的是旧版本快照。这点容易让人误解“锁没生效”,其实是数据库机制使然。

2.3 PESSIMISTIC_FORCE_INCREMENT

这个锁专为带版本控制的实体设计——也就是实体上有 @Version 注解的。

它的行为和 PESSIMISTIC_WRITE 基本一致,但额外会立即递增版本号,即使你还没修改数据。

@Version
private Integer version;

// 使用
entityManager.find(Student.class, studentId, LockModeType.PESSIMISTIC_FORCE_INCREMENT);

✅ 适用场景:你想提前“占坑”,防止别人在这期间修改数据,哪怕你还没开始改。

⚠️ 注意:对于没有 @Version 的实体,是否支持该锁由持久化提供者(如 Hibernate)决定。不支持会直接抛 PersistenceException

2.4 异常类型

使用悲观锁时,可能遇到以下异常,必须心里有数:

  • PessimisticLockException:获取锁失败,事务级回滚。
  • LockTimeoutException:获取锁超时,语句级回滚(事务不一定回滚)。
  • PersistenceException:持久化异常,NoResultExceptionNonUniqueResultException 等少数外,其余都会标记事务为回滚状态

📌 小贴士:LockTimeoutException 不会导致事务回滚,你可以捕获它并重试,这是实现“重试机制”的基础。

3. 悲观锁的使用方式

JPA 提供了多种方式设置悲观锁,灵活应对不同场景。

3.1 find 方法

最简单直接的方式,直接在 find 时指定锁模式。

Student student = entityManager.find(
    Student.class, 
    studentId, 
    LockModeType.PESSIMISTIC_WRITE
);

3.2 Query 查询

通过 JPQL 查询,并调用 setLockMode 设置锁。

Query query = entityManager.createQuery(
    "SELECT s FROM Student s WHERE s.id = :studentId"
);
query.setParameter("studentId", studentId);
query.setLockMode(LockModeType.PESSIMISTIC_WRITE);
List<Student> students = query.getResultList();

3.3 显式加锁(Explicit Locking)

先查出实体,再手动加锁。适用于“先查后锁”的场景。

Student student = entityManager.find(Student.class, studentId);
entityManager.lock(student, LockModeType.PESSIMISTIC_WRITE);

3.4 refresh 时加锁

refresh 会从数据库重新加载实体状态,此时也可以加锁。

Student student = entityManager.find(Student.class, studentId);
entityManager.refresh(student, LockModeType.PESSIMISTIC_FORCE_INCREMENT);

3.5 命名查询(NamedQuery)

@NamedQuery 注解中直接指定锁模式。

@NamedQuery(
    name = "lockStudent",
    query = "SELECT s FROM Student s WHERE s.id = :studentId",
    lockMode = LockModeType.PESSIMISTIC_READ
)

使用时:

Query query = entityManager.createNamedQuery("lockStudent");
query.setParameter("studentId", studentId);
query.setLockMode(LockModeType.PESSIMISTIC_READ);
query.getResultList();

4. 锁的作用范围(Lock Scope)

锁的范围决定了是否连带锁定关联实体。JPA 提供 PessimisticLockScope 枚举来控制。

  • PessimisticLockScope.NORMAL(默认)
  • PessimisticLockScope.EXTENDED

设置方式:通过 properties 参数传入。

Map<String, Object> properties = new HashMap<>();
properties.put("jakarta.persistence.lock.scope", PessimisticLockScope.EXTENDED);

entityManager.find(
    Customer.class, 
    1L, 
    LockModeType.PESSIMISTIC_WRITE, 
    properties
);

4.1 PessimisticLockScope.NORMAL

默认行为,只锁定目标实体本身。

对于继承关系(如 JOINED 策略),会连带锁定父类表。

示例实体:

@Entity
@Inheritance(strategy = InheritanceType.JOINED)
public class Person {
    @Id private Long id;
    private String name;
    private String lastName;
    // getters/setters
}

@Entity
public class Employee extends Person {
    private BigDecimal salary;
    // getters/setters
}

执行加锁查询:

entityManager.find(Employee.class, 1L, LockModeType.PESSIMISTIC_WRITE);

生成的 SQL 类似:

SELECT t0.ID, t0.DTYPE, t0.LASTNAME, t0.NAME, t1.ID, t1.SALARY 
FROM PERSON t0, EMPLOYEE t1 
WHERE ((t0.ID = ?) AND ((t1.ID = t0.ID) AND (t0.DTYPE = ?))) FOR UPDATE

✅ 锁定了 PERSONEMPLOYEE 两张表中对应行。

4.2 PessimisticLockScope.EXTENDED

NORMAL 基础上,还能锁定关联的集合或实体,比如 @ElementCollection@OneToMany 配合 @JoinTable 的情况。

示例:

@Entity
public class Customer {
    @Id private Long customerId;
    private String name;
    private String lastName;
    
    @ElementCollection
    @CollectionTable(name = "customer_address")
    private List<Address> addressList;
    // getters/setters
}

@Embeddable
public class Address {
    private String country;
    private String city;
    // getters/setters
}

使用 EXTENDED 范围加锁:

Map<String, Object> properties = new HashMap<>();
properties.put("jakarta.persistence.lock.scope", PessimisticLockScope.EXTENDED);

entityManager.find(Customer.class, 1L, LockModeType.PESSIMISTIC_WRITE, properties);

生成的 SQL 会包含两条 FOR UPDATE

SELECT CUSTOMERID, LASTNAME, NAME 
FROM CUSTOMER WHERE (CUSTOMERID = ?) FOR UPDATE

SELECT CITY, COUNTRY, Customer_CUSTOMERID 
FROM customer_address 
WHERE (Customer_CUSTOMERID = ?) FOR UPDATE

✅ 主表和关联表都被锁住,防止别人修改地址列表。

⚠️ 注意:不是所有持久化框架都支持 EXTENDED 范围。Hibernate 支持,但某些旧版本或其它实现可能不支持,使用前务必验证。

5. 设置锁超时(Lock Timeout)

等锁不能无限等下去,我们可以通过设置超时时间来避免长时间阻塞。

使用 jakarta.persistence.lock.timeout 属性,单位是毫秒。

Map<String, Object> properties = new HashMap<>();
properties.put("jakarta.persistence.lock.timeout", 1000L); // 等待1秒

Student student = entityManager.find(
    Student.class, 
    1L, 
    LockModeType.PESSIMISTIC_READ, 
    properties
);
  • ✅ 超时后抛出 LockTimeoutException,你可以捕获并重试或降级处理。
  • ✅ 设置为 0 表示“不等待”,立即失败。
  • ❌ 警告:某些数据库驱动(如部分 Oracle JDBC 版本)不支持该属性,需通过数据库层面设置(如 SET LOCK_TIMEOUT)。

6. 总结

当事务隔离级别不足以应对复杂并发场景时,悲观锁是强有力的补充工具。它让你能精确控制数据访问,避免竞态条件。

核心要点回顾:

  • ✅ 三种锁模式:READWRITEFORCE_INCREMENT,按需选择。
  • ✅ 多种使用方式:findquerylock()refreshNamedQuery
  • ✅ 锁范围:NORMAL vs EXTENDED,注意关联实体是否需要锁定。
  • ✅ 锁超时:避免无限等待,提升系统响应性。
  • ⚠️ 异常处理:PessimisticLockExceptionLockTimeoutException 要区别对待。
  • ⚠️ 数据库差异:MVCC、锁机制、驱动支持程度都可能影响行为,不要假设“所有数据库都一样”。

📌 最后提醒:悲观锁虽然简单粗暴有效,但过度使用会导致性能下降和死锁风险。在高并发场景下,可结合乐观锁(@Version)或分布式锁综合设计,找到最优解。


原始标题:Pessimistic Locking in JPA | Baeldung

» 下一篇: Dagger 2 入门指南