SpringBoot @Transactional未生效FOR UPDATE锁,请求未阻塞问题排查
DELETE加行锁后GET未阻塞的原因分析
核心原因:PostgreSQL默认隔离级别的读行为
PostgreSQL默认采用Read Committed事务隔离级别,在这个级别下,普通SELECT语句(JPAfindById执行的就是这类SQL)会读取已提交数据的最新快照,不会被行级写锁阻塞。
你的DELETE流程中,虽然通过SELECT * FROM item WHERE id = :id FOR UPDATE拿到了行的排他锁,但GET接口的findById是无锁的普通查询——在Read Committed级别下,它会直接返回该行当前已提交的最新数据(也就是删除操作提交前的原始数据),完全不会等待锁释放。这就是为什么GET能立即执行的根本原因。
其他可能的排查点
- 事务边界是否完整:确认DELETE的
@Transactional注解是否覆盖了从findByIdWithLock查询、等待10秒到执行删除的全流程。如果事务在等待前就提前提交,锁会被自动释放,GET自然不会被阻塞。 - JPA一级缓存影响:如果GET请求和之前的操作处于同一个Session/事务内,JPA会直接从一级缓存返回已加载的Item数据,不会发送SQL到数据库,也就不会触发锁检查。但这种情况仅局限于同一会话的重复调用。
验证与解决思路
- 先验证锁本身是否生效:在DELETE执行的10秒窗口内,用数据库客户端手动执行
SELECT * FROM item WHERE id = [目标ID] FOR UPDATE,如果这个查询被阻塞,说明行锁是正常生效的,问题确实出在普通SELECT的无锁特性上。 - 让GET被阻塞的可行方案:
- 给GET的查询加共享锁:将
findById替换为带锁的查询,比如用@Lock(LockModeType.PESSIMISTIC_READ)(对应PostgreSQL的FOR SHARE),或者原生SELECT ... FOR SHARE。共享锁会和DELETE持有的排他锁冲突,从而让GET请求被阻塞直到锁释放。 - 调整事务隔离级别:将应用默认隔离级别提升到
Repeatable Read或Serializable。不过要注意,Repeatable Read下普通SELECT还是会读取事务启动时的快照,不会被阻塞;只有Serializable级别下,读操作会隐式加锁,才会被写锁阻塞,但这会带来一定的性能开销,需根据业务场景权衡。
- 给GET的查询加共享锁:将
内容的提问来源于stack exchange,提问作者0xREDACTED
相关产品推荐
相关产品推荐

