0

0

Spring Data JPA悲观锁与PostgreSQL事务隔离级别深度解析

花韻仙語

花韻仙語

发布时间:2025-11-21 20:30:05

|

183人浏览过

|

来源于php中文网

原创

Spring Data JPA悲观锁与PostgreSQL事务隔离级别深度解析

本文深入探讨了spring data jpa中悲观锁(pessimistic_write)与postgresql数据库事务隔离级别(特别是serializable)的复杂交互。文章解释了为何在serializable隔离级别下,悲观锁可能无法按预期阻塞并发更新,反而会触发序列化失败异常。教程强调,在使用悲观锁时,通常应避免将事务隔离级别设置为serializable,以确保锁的阻塞机制能够正常生效,从而实现预期的并发控制行为。

1. 理解Spring Data JPA中的悲观锁

在数据库并发控制中,悲观锁是一种“先发制人”的策略,它假定并发冲突很可能发生,因此在数据被读取时就对其进行锁定,阻止其他事务同时修改。在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 {

    @Lock(LockModeType.PESSIMISTIC_WRITE)
    Optional 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);
    // }
}

2. PostgreSQL的SERIALIZABLE事务隔离级别

事务隔离级别定义了并发事务之间相互影响的程度。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隔离级别本质上是一种乐观并发控制策略,它允许事务并行执行,并在提交时检查冲突,如果存在冲突则回滚。

3. 悲观锁与SERIALIZABLE隔离级别的冲突

原问题中遇到的错误正是由于悲观锁(悲观并发控制)与PostgreSQL的SERIALIZABLE隔离级别(乐观并发控制)的机制冲突所致。

当一个事务同时使用了@Lock(LockModeType.PESSIMISTIC_WRITE)(期望阻塞)和@Transactional(isolation = Isolation.SERIALIZABLE)(期望序列化且可能回滚)时,PostgreSQL的SERIALIZABLE隔离级别会优先其自身的冲突检测机制。即使SELECT ... FOR UPDATE已经尝试获取了行级锁,如果PostgreSQL的事务管理器检测到当前事务的执行顺序可能无法与某个并发的SERIALIZABLE事务保持严格的串行化,它就会抛出序列化失败的异常,而不是等待悲观锁的释放。

先见AI
先见AI

数据为基,先见未见

下载

换句话说,SERIALIZABLE隔离级别在PostgreSQL中并不保证“等待直到锁可用”,而是保证“如果不能串行化就报错”。因此,在这种组合下,预期的阻塞行为被SERIALIZABLE的错误回滚行为所取代。

4. 正确使用悲观锁的实践

为了让悲观锁能够按预期工作(即阻塞并发事务而非抛出异常),通常不应将事务隔离级别设置为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 {

    // 示例:如果通过Spring Data JPA的查询方法加锁
    @Lock(LockModeType.PESSIMISTIC_WRITE)
    Optional 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语句将能够在数据库层面正常发挥作用,当多个并发事务尝试获取同一行的写锁时,后续的事务将进入等待状态,直到前一个事务释放锁,从而实现预期的阻塞行为。

5. 注意事项与总结

  1. 并发控制策略的选择:

    • 悲观锁适用于高竞争、数据一致性要求极高且冲突发生概率较大的场景,它通过阻塞来避免冲突。
    • 乐观锁(例如通过版本号或时间戳)适用于低竞争、冲突发生概率较低的场景,它允许并发执行,并在提交时通过版本检查来解决冲突(通常是重试)。
    • SERIALIZABLE隔离级别在PostgreSQL中更偏向于乐观控制,它通过回滚来解决冲突,以确保全局的串行化效果。
  2. PostgreSQL的特性: PostgreSQL的SERIALIZABLE隔离级别是一个强大的工具,但它的行为与一些开发者对“锁”的直观理解可能有所不同。它侧重于检测并阻止任何可能导致非串行化执行的并发操作,而不是简单地等待资源释放。

  3. 权衡与选择: 在设计并发系统时,理解数据库和ORM框架的并发控制机制至关重要。错误地组合不同的并发控制策略可能会导致非预期的行为,例如本例中的阻塞失效和异常抛出。应根据具体的业务需求、并发模式和数据库特性,选择最合适的并发控制策略。如果需要明确的行级阻塞,应使用悲观锁,并避免使用PostgreSQL的SERIALIZABLE隔离级别。如果业务能够容忍事务回滚并重试,并且需要最高级别的数据一致性保证,那么SERIALIZABLE隔离级别可能是一个合适的选择,但需要做好异常处理和重试机制。

相关专题

更多
数据分析工具有哪些
数据分析工具有哪些

数据分析工具有Excel、SQL、Python、R、Tableau、Power BI、SAS、SPSS和MATLAB等。详细介绍:1、Excel,具有强大的计算和数据处理功能;2、SQL,可以进行数据查询、过滤、排序、聚合等操作;3、Python,拥有丰富的数据分析库;4、R,拥有丰富的统计分析库和图形库;5、Tableau,提供了直观易用的用户界面等等。

677

2023.10.12

SQL中distinct的用法
SQL中distinct的用法

SQL中distinct的语法是“SELECT DISTINCT column1, column2,...,FROM table_name;”。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

320

2023.10.27

SQL中months_between使用方法
SQL中months_between使用方法

在SQL中,MONTHS_BETWEEN 是一个常见的函数,用于计算两个日期之间的月份差。想了解更多SQL的相关内容,可以阅读本专题下面的文章。

346

2024.02.23

SQL出现5120错误解决方法
SQL出现5120错误解决方法

SQL Server错误5120是由于没有足够的权限来访问或操作指定的数据库或文件引起的。想了解更多sql错误的相关内容,可以阅读本专题下面的文章。

1095

2024.03.06

sql procedure语法错误解决方法
sql procedure语法错误解决方法

sql procedure语法错误解决办法:1、仔细检查错误消息;2、检查语法规则;3、检查括号和引号;4、检查变量和参数;5、检查关键字和函数;6、逐步调试;7、参考文档和示例。想了解更多语法错误的相关内容,可以阅读本专题下面的文章。

357

2024.03.06

oracle数据库运行sql方法
oracle数据库运行sql方法

运行sql步骤包括:打开sql plus工具并连接到数据库。在提示符下输入sql语句。按enter键运行该语句。查看结果,错误消息或退出sql plus。想了解更多oracle数据库的相关内容,可以阅读本专题下面的文章。

675

2024.04.07

sql中where的含义
sql中where的含义

sql中where子句用于从表中过滤数据,它基于指定条件选择特定的行。想了解更多where的相关内容,可以阅读本专题下面的文章。

572

2024.04.29

sql中删除表的语句是什么
sql中删除表的语句是什么

sql中用于删除表的语句是drop table。语法为drop table table_name;该语句将永久删除指定表的表和数据。想了解更多sql的相关内容,可以阅读本专题下面的文章。

414

2024.04.29

Golang gRPC 服务开发与Protobuf实战
Golang gRPC 服务开发与Protobuf实战

本专题系统讲解 Golang 在 gRPC 服务开发中的完整实践,涵盖 Protobuf 定义与代码生成、gRPC 服务端与客户端实现、流式 RPC(Unary/Server/Client/Bidirectional)、错误处理、拦截器、中间件以及与 HTTP/REST 的对接方案。通过实际案例,帮助学习者掌握 使用 Go 构建高性能、强类型、可扩展的 RPC 服务体系,适用于微服务与内部系统通信场景。

0

2026.01.15

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
MySQL 教程
MySQL 教程

共48课时 | 1.8万人学习

MySQL 初学入门(mosh老师)
MySQL 初学入门(mosh老师)

共3课时 | 0.3万人学习

简单聊聊mysql8与网络通信
简单聊聊mysql8与网络通信

共1课时 | 793人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号