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
:持久化异常,除NoResultException
、NonUniqueResultException
等少数外,其余都会标记事务为回滚状态。
📌 小贴士: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
✅ 锁定了 PERSON
和 EMPLOYEE
两张表中对应行。
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. 总结
当事务隔离级别不足以应对复杂并发场景时,悲观锁是强有力的补充工具。它让你能精确控制数据访问,避免竞态条件。
核心要点回顾:
- ✅ 三种锁模式:
READ
、WRITE
、FORCE_INCREMENT
,按需选择。 - ✅ 多种使用方式:
find
、query
、lock()
、refresh
、NamedQuery
。 - ✅ 锁范围:
NORMAL
vsEXTENDED
,注意关联实体是否需要锁定。 - ✅ 锁超时:避免无限等待,提升系统响应性。
- ⚠️ 异常处理:
PessimisticLockException
和LockTimeoutException
要区别对待。 - ⚠️ 数据库差异:MVCC、锁机制、驱动支持程度都可能影响行为,不要假设“所有数据库都一样”。
📌 最后提醒:悲观锁虽然简单粗暴有效,但过度使用会导致性能下降和死锁风险。在高并发场景下,可结合乐观锁(@Version
)或分布式锁综合设计,找到最优解。