A pretty common web-application problem is concurrent transaction handling. If you have a form to view and edit an entity, you need to consider if multiple user is going to be editing at the same time, and how to handle them
There are two popular approaches of solving concurrency problem: pessimistic locking, and optimistic locking. Pessimistic locking means access to shared resources is explicitly restricted until the transaction finishes. This is commonly achieved using two-phase locking algorithm found in most database products.
Consider one bank account owned jointly by one lovely couple Jim and Sue. Jim walk into an ATM wanting to see how much money he got, then withdraw $10 (represented by T1). About the same time Sue also want to withdraw $20 from another ATM. (It is assumed both T1 and T2 uses READ COMITTED isolation level which is the default level of many dbms, and the first select acquires exclusive write, eg: select-for-update in Oracle):
Time | Jim (T1) | Sue (T2) |
---|---|---|
t=1 | select balance from account where account_id = 1 | |
t=2 | select balance from account where account_id = 1 | |
t=3 | * ATM waits for Jim to input how much he would like to withdraw * | * blocked waiting T1 to commit * |
t=4 | update account set balance = balance – 10 where account_id = 1 | * blocked waiting T1 to commit * |
t=5 | commit | |
t=6 | * ATM waits for Sue to input how much she would like to withdraw * | |
t=7 | update account set balance = balance – 20 where account_id = 1 |
At t=2 T2 will be blocked waiting until T1 commits. This is because T2 tries to read accound id 1 which had been changed by T1 but not yet comitted. Only by t=5 T2 is able to progress again. This might not be ideal, especially if Jim takes a while to think how much money he would like to withdraw, Sue might not be very happy thinking why her ATM machine just seem to hung.
With optimistic locking, as the terminology says, we’re being an optimist. We just simply assume a best case scenario where no other transaction is going to modify the object except us. However if another transaction happen to execute on the same time, only by then we do something about it.
Row versioning is a common strategy to achieve optimistic locking. With row versioning, each row is given a version number which gets incremented everytime it’s updated. Each transaction that performs read-then-update operation remembers the version of the row it’s reading, and perform a check on the update that it’s indeed still the same version.
The same example would look like following with row versioning optimistic locking:
Time | Jim (T1) | Sue (T2) |
---|---|---|
t=1 | select balance, version from account where account_id = 1 (retrieved version = 0) | |
t=2 | select balance, version from account where account_id = 1 (retrieved version = 0) | |
t=3 | * ATM waits for Jim to input how much he would like to withdraw * | * ATM waits for Sue to input how much she would like to withdraw * |
t=4 | update account set balance = balance – 10, version = version + 1 where account_id = 1 and version = 0 (update count = 1, version updated to 1) | |
t=5 | update account set balance = balance – 20, version = version + 1 where account_id = 1 and version = 0 (this will wait until T1 commits) | |
t=6 | commit | |
t=7 | Update count of the update at t=5 is 0 beacuse T1 got in first. ATM could inform Sue that the account balance has changed since she checked it, and she should get the latest balance and try again. |
Note that the most important thing here is Sue (T2) doesn’t have to wait Jim (T1) for his input — which can potentially be a long time.
JPA provide automatic support of row versioning via the annotation. When you have entity with annotated field or property, optimistic locking will be enabled automatically.
Following is example of a simplified bank account JPA entity with version field:
[sourcecode language=”java”]
public class Account {
@Id private long id;
private String name;
private double balance;
long version;
// Getters and setters omitted for brevity
}
[/sourcecode]
Having a annotated field allow JPA to detect if the entity is stale when it’s about to synchronize the change back to database. An OptimisticLockException will be thrown if update to a stale entity is made:
[sourcecode language=”java”]
public void withdraw (long accountId, double amount) {
em.getTransaction().begin();
// Retrieve the account row from the database
Account account = em.find(Account.class, accountId);
// Check the account has enough money. Let’s assume another transaction had updated the account at this point, and our account entity object is now stale
Assert.isTrue(amount
// Withdraw the amount from the account
account.setBalance(account.getBalance() – amount);
// OptimisticLockException will be thrown here because we’re trying to update stale object
em.getTransaction().commit();
}
[/sourcecode]