Loader loads a data from persistContext no matter what query is used, only the key is matter.
So a lock doesn’t work for an entity which is loaded already by a range query.
Is this mean that developers should think what data are loaded in persistence context before locking? or use optimistic lock when a lock is needed in the middle of transaction.
In this case, actually the lock query is sent to DB and application wait for lock release. However the data is not hydrated from the JDBC resultSet. It comes from persistence context and hard to debug why the lock doesn’t work. It would be good to load data from persistence context without query fetch at least. Then developers know that the data comes from the context.
Upgrading the lock doesn’t help when the locked entity is already loaded in the Persistence Context in a specific case likes checking previous status.
//Transaction start
List policies = policyRepository.findByStatus(CREATED);
Policy policy = policyRepository.findOneWithLock(one of policies found);
if (policy.getStatus() != PolicyStatus.CREATED) {
throw new RuntimeException();
}
policy.setStatus(PolicyStatus.SENDING);
policyRepository.saveAndFlush(policy);
//do some works which only one thread is allowed
Because Hibernate doesn’t hydrated the entity from the ResultSet.
In this case I think setting a new transaction on the locked query or using optimistic lock would be better choice.
I read the article you shared. It helps me a lot to understand how Hibernate works.
Thanks.
This is not a bug. I mean that developers should think what data are loaded in the Persistence Context when using a lock in order to allow a single thread to run.
Below code run multiple times because Policy with CREATED returned always in race conditions.
//do some works which only one thread is allowed
ThreadA - the status of policy from findOneWithLock is CREATED from the Persistence Context.
List policies = policyRepository.findByStatus(CREATED);
Policy policy = policyRepository.findOneWithLock(one of policies found);
if (policy.getStatus() != PolicyStatus.CREATED) {
throw new RuntimeException();
}
policy.setStatus(PolicyStatus.SENDING);
policyRepository.saveAndFlush(policy);
//do some works which only one thread is allowed -- run 1 times
ThreadB - the status of policy from findOneWithLock is CREATED from the Persistence Context.
List policies = policyRepository.findByStatus(CREATED);
Policy policy = policyRepository.findOneWithLock(one of policies found);
if (policy.getStatus() != PolicyStatus.CREATED) {
throw new RuntimeException();
}
policy.setStatus(PolicyStatus.SENDING);
policyRepository.saveAndFlush(policy);
//do some works which only one thread is allowed -- run 2 times
Nope, there’s no bug. It’s just that you’re not using the right Hibernate locking method but the Spring Data method called findOneWithLock which is supposed to fetch an entity.
Most likely, the Spring Data findOneWithLock calls this EntityManager method:
I switch the second query to Lock method.
When I run two Threads A,B, the later thread(B) wait and come to the if statement after the end of first thread(A) transaction. However the Policy in thread(B) status is still PolicyStatus.CREATED.
Does entityManager.lock refresh the entity?
List policies = policyRepository.findByStatus(CREATED);
entityManager.lock(one of polices, LockModeType.PESSIMISTIC_WRITE);
if (policy.getStatus() != PolicyStatus.CREATED) {
throw new RuntimeException();
}
policy.setStatus(PolicyStatus.SENDING);
policyRepository.saveAndFlush(policy);
//do some works which only one thread is allowed -- run 2 times
When I run two Threads A,B, the later thread(B) wait and come to the if statement after the end of first
Yes, that exactly how database locking works. For more details, check out this article.
However the Policy in thread(B) status is still PolicyStatus.CREATED.
Yes, that’s how MVCC works. Until you commit a Transaction, all other transactions don’t observe any state change unless you are using 2PL, in which case reads will block. For more details, check out this article.
Does entityManager.lock refresh the entity?
No. I does not. Lock, as its name implies, is about locking a database record, either optimistically or pessimistically. Refresh is a totally different concept.
This Hibernate tutorial contains many articles about Concurrency Control. You should read them to better understand the concepts.
This presentation is also worth watching if you prefer videos instead.
Mysql
A(read committed) | B(read committed)
start transaction | start transaction
select * from policy where status='CREATED' |
policy#1,2,3 <------- | select * from policy where status='CREATED'
| ------> policy#1,2,3
select * from policy where id = 1 for update |
| select * from policy where id = 1 for update
| ------ waiting
policy#1(CREATED) <------- |
update policy set status='SENDING' where id = 1 |
|
commit | ------ end of waiting
| ------> policy#1(SENDING)
| if (policy.getStatus() != PolicyStatus.CREATED) { throw new RuntimeException(); }
When I tested above in mysql, connection B get a Policy with SENDING status because A committed.
So I though Hibernate uses the result of SELECT FOR UPDATE in B connection. However, it doesn’t use it because as you said Hibernate is application level repeatable reads. Ok. I understand. Then I should clear the loaded entity and next time I should think what entity is loaded. This is my understanding.
Lock method is also the same manner. When lock method is called, SELECT FOR UPDATE query is sent, but the result of SELECT FOR UPDATE is not used because Hibernate is application level repeatable reads. Ok.
All comes from the wrong idea that Hibernate works following DB isolation level, in my case read committed.