Spring Data JPA(Hibernate)悲观锁实现及相关疑问求助
Hey there! Let's tackle your pessimistic locking questions for the account transfer scenario, and first fix up the code to make it work correctly (your current approach of adding @Lock to the save method won't achieve the desired locking behavior).
Fixed Code Implementation
First, update your AccountRepository to add a locked query method—because pessimistic locking needs to be applied when you fetch the entities, not when you save them:
interface AccountRepository extends JpaRepository<AccountEntity, Integer> { // Apply pessimistic write lock when fetching the account by ID @Lock(LockModeType.PESSIMISTIC_WRITE) Optional<AccountEntity> findById(Integer id); }
Then adjust your service layer to use this locked query instead of getOne (since getOne returns a lazy proxy and doesn't trigger a database query immediately):
@Service class PaymentServiceImpl implements PaymentService { @Autowired private AccountRepository accountRepository; @Autowired private PaymentRepository paymentRepository; @Transactional @Override public PaymentEntity create(PaymentEntity payment) { // Fetch both accounts with pessimistic write locks AccountEntity from = accountRepository.findById(payment.getAccountFrom().getId()) .orElseThrow(() -> new RuntimeException("Source account not found")); AccountEntity to = accountRepository.findById(payment.getAccountTo().getId()) .orElseThrow(() -> new RuntimeException("Target account not found")); Long newFromBalance = from.getBalance() - payment.getAmount(); if (newFromBalance < 0) throw new RuntimeException("Insufficient balance"); from.setBalance(newFromBalance); to.setBalance(to.getBalance() + payment.getAmount()); PaymentEntity result = paymentRepository.save(payment); // Saving the accounts will update them in the same locked transaction accountRepository.save(from); accountRepository.save(to); return result; } }
Answering Your Questions
Let's go through each of your questions one by one:
1. Using database locks instead of Java application locks is correct?
Absolutely correct! Java application-level locks (like synchronized blocks or ReentrantLock) only work within a single JVM instance. Since your app runs multiple instances sharing the same database, database-level locks are the only way to ensure cross-instance consistency. PostgreSQL will apply row-level write locks to the account rows you fetch with PESSIMISTIC_WRITE, which all instances respect.
2. @Lock can only be applied to repository methods, not service methods?
That's right. The @Lock annotation is specific to Spring Data JPA repository query methods—it tells Hibernate to append locking syntax (like SELECT ... FOR UPDATE in PostgreSQL) to the generated SQL query. Service layer methods don't directly interact with database query generation, so the annotation won't have any effect there.
3. @Lock will lock all entities queried in the transaction?
Nope, that's a common misconception. The @Lock annotation is scoped to individual query methods. Only the entities returned by the annotated query method will be locked. In your scenario, you need to call the locked findById method for both accounts to lock both rows—if you used a regular (unlocked) query for one account, that row wouldn't be locked.
4. Locked entities are automatically unlocked when the transaction commits or rolls back?
100% correct. Pessimistic locks are tied to the database transaction. When your Spring-managed transaction commits or rolls back, the underlying database transaction ends, and PostgreSQL automatically releases all row locks held by that transaction. No manual unlock is needed.
5. Other transfer requests targeting locked accounts will wait for the lock to be released?
Mostly correct, with a small detail to note:
- By default, PostgreSQL will make competing write requests wait for the lock to be released (the wait time is controlled by the
lock_timeoutdatabase parameter, which defaults to infinite). If the wait times out, PostgreSQL will throw a lock timeout exception that your app should handle. - For read requests, under PostgreSQL's default
READ COMMITTEDisolation level, reads won't be blocked by write locks—they'll read the last committed snapshot of the row. If you need reads to block too, you'd need a higher isolation level or a different lock mode, but that's not necessary for your transfer scenario.
Learning Resources
To dive deeper into these topics, you can refer to:
- Spring Data JPA's official documentation: Look for the "Pessimistic Locking" section in the Spring Data JPA reference guide, which covers how to use
@Lockwith repository methods. - Hibernate 5 ORM documentation: Check the "Locking" chapter to understand how Hibernate interacts with database locks.
- PostgreSQL official documentation: The "Explicit Locking" section explains row-level locks,
SELECT ... FOR UPDATEbehavior, and transaction isolation levels in detail.
内容的提问来源于stack exchange,提问作者Bohdan Petrenko

