
本文深入探讨了spring data jpa中悲观锁(pessimistic_write)与postgresql数据库事务隔离级别(特别是serializable)的复杂交互。文章解释了为何在serializable隔离级别下,悲观锁可能无法按预期阻塞并发更新,反而会触发序列化失败异常。教程强调,在使用悲观锁时,通常应避免将事务隔离级别设置为serializable,以确保锁的阻塞机制能够正常生效,从而实现预期的并发控制行为。
在数据库并发控制中,悲观锁是一种“先发制人”的策略,它假定并发冲突很可能发生,因此在数据被读取时就对其进行锁定,阻止其他事务同时修改。在Spring Data JPA中,可以通过@Lock(LockModeType.PESSIMISTIC_WRITE)注解来实现悲观写锁。
当一个方法(通常是查询方法或自定义的更新方法)被@Lock(LockModeType.PESSIMISTIC_WRITE)注解时,Spring Data JPA会指示底层ORM框架(如Hibernate)在执行查询时,向数据库发送一个带有FOR UPDATE子句的SELECT语句。例如:
SELECT * FROM five_entity WHERE id = ? FOR UPDATE;
这条SQL语句的作用是,在当前事务中锁定被查询的数据行。这意味着,在当前事务提交或回滚之前,其他试图修改或获取相同行写锁的事务将会被阻塞,直到当前事务释放锁。这种机制旨在确保对关键数据的独占访问,防止脏读、不可重复读和幻读等问题,尤其适用于需要严格顺序执行的并发操作,例如序列号生成或库存扣减。
在原问题提供的代码中,@Lock(LockModeType.PESSIMISTIC_WRITE)被应用到了addToSequence方法上,尽管该方法内部直接使用了entityManager.find,但其意图是希望在操作FiveEntity时获得悲观锁。为了确保悲观锁的有效性,通常建议在Spring Data JPA的Repository接口中定义一个查询方法并添加@Lock注解,或者在entityManager.find时显式指定LockModeType。
// 示例:Spring Data JPA Repository中更规范的悲观锁用法
@Repository
public interface FiveEntityRepository extends JpaRepository<FiveEntity, Integer> {
@Lock(LockModeType.PESSIMISTIC_WRITE)
Optional<FiveEntity> findById(Integer id); // 通过Spring Data JPA生成的查询方法加锁
// 如果是自定义方法,确保内部操作能正确利用到锁
// 例如,可以传递LockModeType到entityManager.find
// @Override
// public void addToSequence(Integer id) {
// FiveEntity fiveEntity = entityManager.find(FiveEntity.class, id, LockModeType.PESSIMISTIC_WRITE);
// fiveEntity.setSequence(fiveEntity.getSequence() + 1);
// entityManager.merge(fiveEntity);
// }
}事务隔离级别定义了并发事务之间相互影响的程度。SQL标准定义了四种隔离级别:READ UNCOMMITTED、READ COMMITTED、REPEATABLE READ和SERIALIZABLE。其中,SERIALIZABLE是最高的隔离级别,旨在确保事务的执行如同串行执行一样,完全避免所有并发问题。
在PostgreSQL中,SERIALIZABLE隔离级别是通过“快照隔离”和“序列化错误检测”机制实现的。当事务被设置为SERIALIZABLE时,PostgreSQL会为该事务创建一个数据库的快照。所有在该事务中读取的数据都将基于这个快照。如果一个事务在提交时发现其所做的修改或读取的数据,在其他并发SERIALIZABLE事务中发生了冲突,导致无法保证串行化执行的顺序,PostgreSQL将不会等待,而是立即抛出一个ERROR: could not serialize access due to concurrent update的异常,并强制当前事务回滚。
这种机制的优点是能够提供极高的数据一致性保证,但缺点是可能会增加事务回滚的频率,尤其是在高并发写操作的场景下。PostgreSQL的SERIALIZABLE隔离级别本质上是一种乐观并发控制策略,它允许事务并行执行,并在提交时检查冲突,如果存在冲突则回滚。
原问题中遇到的错误正是由于悲观锁(悲观并发控制)与PostgreSQL的SERIALIZABLE隔离级别(乐观并发控制)的机制冲突所致。
当一个事务同时使用了@Lock(LockModeType.PESSIMISTIC_WRITE)(期望阻塞)和@Transactional(isolation = Isolation.SERIALIZABLE)(期望序列化且可能回滚)时,PostgreSQL的SERIALIZABLE隔离级别会优先其自身的冲突检测机制。即使SELECT ... FOR UPDATE已经尝试获取了行级锁,如果PostgreSQL的事务管理器检测到当前事务的执行顺序可能无法与某个并发的SERIALIZABLE事务保持严格的串行化,它就会抛出序列化失败的异常,而不是等待悲观锁的释放。
换句话说,SERIALIZABLE隔离级别在PostgreSQL中并不保证“等待直到锁可用”,而是保证“如果不能串行化就报错”。因此,在这种组合下,预期的阻塞行为被SERIALIZABLE的错误回滚行为所取代。
为了让悲观锁能够按预期工作(即阻塞并发事务而非抛出异常),通常不应将事务隔离级别设置为SERIALIZABLE。悲观锁本身就是一种强大的并发控制机制,它在大多数情况下能够与数据库的默认隔离级别(如PostgreSQL的READ COMMITTED或MySQL的REPEATABLE READ)良好协作,实现行级阻塞。
修正方案:
移除服务层@Transactional注解中的isolation = Isolation.SERIALIZABLE设置。让事务使用数据库的默认隔离级别,或显式设置为一个允许阻塞的级别(如READ_COMMITTED)。
// 修正后的Service层代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.persistence.EntityManager;
import javax.persistence.LockModeType;
@Service
public class FiveEntityService {
@Autowired
private FiveEntityRepository fiveEntityRepository; // 假设这是Spring Data JPA Repository
@Autowired
private EntityManager entityManager; // 用于演示直接使用EntityManager加锁
// 修正后的Service方法:移除 isolation = Isolation.SERIALIZABLE
@Transactional
public void processWithPessimisticLock(Integer id) {
// 假设fiveEntityRepository中有一个findByIdAndLock方法
// fiveEntityRepository.findByIdAndLock(id)
// 或者直接使用EntityManager显式加锁
FiveEntity fiveEntity = entityManager.find(FiveEntity.class, id, LockModeType.PESSIMISTIC_WRITE);
if (fiveEntity != null) {
fiveEntity.setSequence(fiveEntity.getSequence() + 1);
entityManager.merge(fiveEntity); // merge操作会将更新同步到数据库
// log.debug("sequence: {}", fiveEntity.getSequence()); // 可以在此处添加日志
} else {
// 处理未找到实体的情况
}
}
}
// 修正后的Repository层(保持不变,但需要确保@Lock注解生效或通过EntityManager显式加锁)
// 如果addToSequence是自定义实现,需要确保内部find操作使用了PESSIMISTIC_WRITE
// 如果是Spring Data JPA自动生成的查询,@Lock注解会生效
@Repository
public interface FiveEntityRepository extends JpaRepository<FiveEntity, Integer> {
// 示例:如果通过Spring Data JPA的查询方法加锁
@Lock(LockModeType.PESSIMISTIC_WRITE)
Optional<FiveEntity> findById(Integer id);
// 原问题中的addToSequence方法如果这样实现,则需要确保entityManager.find时指定LockModeType
// @Lock(LockModeType.PESSIMISTIC_WRITE) // 此注解在自定义方法上可能不会直接影响entityManager.find
// @Override
// public void addToSequence(Integer id) {
// // 确保这里显式加锁
// FiveEntity fiveEntity = entityManager.find(FiveEntity.class, id, LockModeType.PESSIMISTIC_WRITE);
// fiveEntity.setSequence(fiveEntity.getSequence()+1);
// entityManager.merge(fiveEntity);
// }
}通过移除Isolation.SERIALIZABLE,悲观锁的SELECT ... FOR UPDATE语句将能够在数据库层面正常发挥作用,当多个并发事务尝试获取同一行的写锁时,后续的事务将进入等待状态,直到前一个事务释放锁,从而实现预期的阻塞行为。
并发控制策略的选择:
PostgreSQL的特性: PostgreSQL的SERIALIZABLE隔离级别是一个强大的工具,但它的行为与一些开发者对“锁”的直观理解可能有所不同。它侧重于检测并阻止任何可能导致非串行化执行的并发操作,而不是简单地等待资源释放。
权衡与选择: 在设计并发系统时,理解数据库和ORM框架的并发控制机制至关重要。错误地组合不同的并发控制策略可能会导致非预期的行为,例如本例中的阻塞失效和异常抛出。应根据具体的业务需求、并发模式和数据库特性,选择最合适的并发控制策略。如果需要明确的行级阻塞,应使用悲观锁,并避免使用PostgreSQL的SERIALIZABLE隔离级别。如果业务能够容忍事务回滚并重试,并且需要最高级别的数据一致性保证,那么SERIALIZABLE隔离级别可能是一个合适的选择,但需要做好异常处理和重试机制。
以上就是Spring Data JPA悲观锁与PostgreSQL事务隔离级别深度解析的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号