如何在JPA(Hibernate)中使用悲观锁

一般来说,给某记录上悲观写锁,需开启事务并执行SQL:SELECT * FROM t WHERE t.id = 123 FOR UPDATE

网上检索文档,Hibernate使用悲观锁

// find the entity
Department department = repo.findById(123);
// Explicit Locking
entityManager.lock(department, LockModeType.PESSIMISTIC_WRITE);

我对这段代码有疑问,第一步从数据库已经检索出来了(但是没加FOR UPDATE),第二步执行加锁会不会有问题?没加FOR UPDATE它是怎么上锁的呢?

猜想:第二步中lock方法是不是对entity又执行了一次SELECT FOR UPDATE语句?如果是,会不会导致前后检索出的entity数据不一致

1 个赞

我猜想,这个lockEntityManager层面的锁,并不是数据库层面的。目的是解决并发环境下对同一个实体对象的读写不一致问题。

猜想,没实践。你看看日志,当执行lock 方法的时候,是否发起了SQL检索呢?

测试环境:
PostgreSQL + JDK17 + Springboot 2.7.1
测试代码:


// findById 执行SQL为: SELECT xxx FROM t WHERE id = ?
repository.findById(9L).ifPresent(log -> {
    // 这里休眠10s方便测试并发。休眠期间,另一个请求修改了这条记录
    Thread.sleep(10000); 
    // lock方法执行了SQL:  select id from t where id =? for update,注意是加了 for update
    // lock结果为:抛出 PessimisticEntityLockException 异常,加锁失败!
    entityManager.lock(log, LockModeType.PESSIMISTIC_WRITE);
    log.setResultMsg(RandomUtil.randomString(4));
    repository.save(log);
});

结论:
第二步中lock 方法对entity 执行了一次SELECT FOR UPDATE 语句进行上锁。且由于 PostgreSQL REPEATABLE_READ实现不一样,进行 FOR UPDATE 时事务快照id与最新不一致,会导致上锁失败

注意!

  • 本人测试使用数据库为 PostgreSQL 而不是 MySQL
  • 测试时事务级别为 REPEATABLE_READ,如果你使用 READ_COMMITED 级别会出现丢失更新问题
  • MySQL 与 PostgreSQL 在 REPEATABLE_READ 级别事务中有不同表现(PostgreSQL 不会出现幻读、丢失更新,但是MySQL会。也就是说,上面同样的测试代码,MySQL 会导致丢失更新,PostgreSQL 不会)
1 个赞