为何SELECT ... FOR UPDATE仅对带@Version的实体生效?(HSQLDB+Spring Data JPA)
这问题我之前也碰到过,核心原因是你用错了悲观锁的模式——PESSIMISTIC_READ的语义并不像你想的那样能阻止并发更新,咱们一步步拆解:
为什么PESSIMISTIC_READ没拦住脏写?
PESSIMISTIC_READ对应的是共享锁(S锁),它的设计目的是防止脏读:多个事务可以同时读取同一行数据,互相之间不会阻塞,但其他事务无法在当前事务未提交时修改该行(理论上)。但这里有两个关键问题:
- 共享锁允许多个事务持有:process1和process2的事务都能获取到id=1行的S锁,这是完全符合共享锁规则的。
- 锁升级的不确定性:当process2准备更新时,需要把S锁升级为排他锁(X锁)。在HSQLDB的实现中,可能并没有严格等待所有S锁释放就完成了更新(或者你的事务隔离级别+锁机制的组合导致锁提前释放),最终两个事务都执行了
UPDATE task SET ... WHERE id=?的语句——因为没有版本校验,后执行的update直接覆盖了之前的修改,就出现了日志里的脏写。
为什么加@Version就正常了?
@Version开启了乐观锁机制,这时候JPA生成的更新语句会带上版本校验条件:
UPDATE task SET data=?, state=?, version=? WHERE id=? AND version=?
process2先完成更新,把version值递增了;当process1后续执行更新时,数据库里的version已经和process1持有的实体version不匹配,就会抛出OptimisticLockingFailureException,直接阻止脏写操作。
正确的悲观锁解决方式
如果你想通过悲观锁彻底阻止并发更新同一行,应该把锁模式改成LockModeType.PESSIMISTIC_WRITE:
@Lock(LockModeType.PESSIMISTIC_WRITE) @Query("select t from Task t where t.state = 0") Page<Task> findUnprocessed(Pageable p);
PESSIMISTIC_WRITE是排他锁(X锁),process1查询到id=1的行后,会立即持有排他锁,其他事务(比如process2)无法读取或修改该行,直到process1的事务提交(休眠结束、save完成)。这时候process2会等到process1提交后,再去查询未处理的Task,自然就会拿到id=2的那条,不会再出现并发更新同一行的问题。
额外验证建议
- 开启SQL日志,看看生成的查询语句是否带了锁指令:HSQLDB中
PESSIMISTIC_READ对应SELECT ... FOR SHARE,PESSIMISTIC_WRITE对应SELECT ... FOR UPDATE,确保锁真的加上了。 - 确认事务范围:你的
process1和process2方法都加了@Transactional,要保证查询、休眠、save操作都在同一个事务里,锁才能持有到事务结束。
内容的提问来源于stack exchange,提问作者littleAlien
相关产品推荐
相关产品推荐

