Skip to content

Commit 8b11e76

Browse files
authored
feature: Add optimistic offline lock (iluwatar#1306) (iluwatar#2551)
1 parent 70692f6 commit 8b11e76

File tree

9 files changed

+399
-0
lines changed

9 files changed

+399
-0
lines changed

Diff for: optimistic-offline-lock/README.md

+147
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
---
2+
title: Optimistic Offline Lock
3+
category: Concurrency
4+
language: en
5+
tag:
6+
- Data access
7+
---
8+
9+
## Intent
10+
11+
Provide an ability to avoid concurrent changes of one record in relational databases.
12+
13+
## Explanation
14+
15+
Each transaction during object modifying checks equation of object's version before start of transaction
16+
and before commit itself.
17+
18+
**Real world example**
19+
> Since people love money, the best (and most common) example is banking system:
20+
> imagine you have 100$ on your e-wallet and two people are trying to send you 50$ both at a time.
21+
> Without locking, your system will start **two different thread**, each of whose will read your current balance
22+
> and just add 50$. The last thread won't re-read balance and will just rewrite it.
23+
> So, instead 200$ you will have only 150$.
24+
25+
**In plain words**
26+
> Each transaction during object modifying will save object's last version and check it before saving.
27+
> If it differs, the transaction will be rolled back.
28+
29+
**Wikipedia says**
30+
> Optimistic concurrency control (OCC), also known as optimistic locking,
31+
> is a concurrency control method applied to transactional systems such as
32+
> relational database management systems and software transactional memory.
33+
34+
**Programmatic Example**
35+
Let's simulate the case from *real world example*. Imagine we have next entity:
36+
37+
```java
38+
import lombok.AllArgsConstructor;
39+
import lombok.Data;
40+
import lombok.NoArgsConstructor;
41+
42+
/**
43+
* Bank card entity.
44+
*/
45+
@Data
46+
@NoArgsConstructor
47+
@AllArgsConstructor
48+
public class Card {
49+
50+
/**
51+
* Primary key.
52+
*/
53+
private long id;
54+
55+
/**
56+
* Foreign key points to card's owner.
57+
*/
58+
private long personId;
59+
60+
/**
61+
* Sum of money.
62+
*/
63+
private float sum;
64+
65+
/**
66+
* Current version of object;
67+
*/
68+
private int version;
69+
}
70+
```
71+
72+
Then the correct modifying will be like this:
73+
74+
```java
75+
76+
import lombok.RequiredArgsConstructor;
77+
78+
/**
79+
* Service to update {@link Card} entity.
80+
*/
81+
@RequiredArgsConstructor
82+
public class CardUpdateService implements UpdateService<Card> {
83+
84+
private final JpaRepository<Card> cardJpaRepository;
85+
86+
@Override
87+
@Transactional(rollbackFor = ApplicationException.class) //will roll back transaction in case ApplicationException
88+
public Card doUpdate(Card card, long cardId) {
89+
float additionalSum = card.getSum();
90+
Card cardToUpdate = cardJpaRepository.findById(cardId);
91+
int initialVersion = cardToUpdate.getVersion();
92+
float resultSum = cardToUpdate.getSum() + additionalSum;
93+
cardToUpdate.setSum(resultSum);
94+
//Maybe more complex business-logic e.g. HTTP-requests and so on
95+
96+
if (initialVersion != cardJpaRepository.getEntityVersionById(cardId)) {
97+
String exMessage = String.format("Entity with id %s were updated in another transaction", cardId);
98+
throw new ApplicationException(exMessage);
99+
}
100+
101+
cardJpaRepository.update(cardToUpdate);
102+
return cardToUpdate;
103+
}
104+
}
105+
```
106+
107+
## Applicability
108+
109+
Since optimistic locking can cause degradation of system's efficiency and reliability due to
110+
many retries/rollbacks, it's important to use it safely. They are useful in case when transactions are not so long
111+
and does not distributed among many microservices, when you need to reduce network/database overhead.
112+
113+
Important to note that you should not choose this approach in case when modifying one object
114+
in different threads is common situation.
115+
116+
## Tutorials
117+
118+
- [Offline Concurrency Control](https://www.baeldung.com/cs/offline-concurrency-control)
119+
- [Optimistic lock in JPA](https://www.baeldung.com/jpa-optimistic-locking)
120+
121+
## Known uses
122+
123+
- [Hibernate ORM](https://docs.jboss.org/hibernate/orm/4.3/devguide/en-US/html/ch05.html)
124+
125+
## Consequences
126+
127+
**Advantages**:
128+
129+
- Reduces network/database overhead
130+
- Let to avoid database deadlock
131+
- Improve the performance and scalability of the application
132+
133+
**Disadvantages**:
134+
135+
- Increases complexity of the application
136+
- Requires mechanism of versioning
137+
- Requires rollback/retry mechanisms
138+
139+
## Related patterns
140+
141+
- [Pessimistic Offline Lock](https://martinfowler.com/eaaCatalog/pessimisticOfflineLock.html)
142+
143+
## Credits
144+
145+
- [Source (Martin Fowler)](https://martinfowler.com/eaaCatalog/optimisticOfflineLock.html)
146+
- [Advantages and disadvantages](https://www.linkedin.com/advice/0/what-benefits-drawbacks-using-optimistic)
147+
- [Comparison of optimistic and pessimistic locks](https://www.linkedin.com/advice/0/what-advantages-disadvantages-using-optimistic)

Diff for: optimistic-offline-lock/pom.xml

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!--
3+
4+
This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
5+
6+
The MIT License
7+
Copyright © 2014-2022 Ilkka Seppälä
8+
9+
Permission is hereby granted, free of charge, to any person obtaining a copy
10+
of this software and associated documentation files (the "Software"), to deal
11+
in the Software without restriction, including without limitation the rights
12+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13+
copies of the Software, and to permit persons to whom the Software is
14+
furnished to do so, subject to the following conditions:
15+
16+
The above copyright notice and this permission notice shall be included in
17+
all copies or substantial portions of the Software.
18+
19+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25+
THE SOFTWARE.
26+
27+
-->
28+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
29+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
30+
<modelVersion>4.0.0</modelVersion>
31+
32+
<parent>
33+
<groupId>com.iluwatar</groupId>
34+
<artifactId>java-design-patterns</artifactId>
35+
<version>1.26.0-SNAPSHOT</version>
36+
</parent>
37+
38+
<artifactId>optimistic-offline-lock</artifactId>
39+
40+
<dependencies>
41+
<dependency>
42+
<groupId>org.junit.jupiter</groupId>
43+
<artifactId>junit-jupiter-engine</artifactId>
44+
<scope>test</scope>
45+
</dependency>
46+
<dependency>
47+
<groupId>org.mockito</groupId>
48+
<artifactId>mockito-core</artifactId>
49+
<scope>test</scope>
50+
</dependency>
51+
</dependencies>
52+
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.iluwatar.api;
2+
3+
/**
4+
* Service for entity update.
5+
*
6+
* @param <T> target entity
7+
*/
8+
public interface UpdateService<T> {
9+
10+
/**
11+
* Update entity.
12+
*
13+
* @param obj entity to update
14+
* @param id primary key
15+
* @return modified entity
16+
*/
17+
T doUpdate(T obj, long id);
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.iluwatar.exception;
2+
3+
/**
4+
* Exception happens in application during business-logic execution.
5+
*/
6+
public class ApplicationException extends RuntimeException {
7+
8+
/**
9+
* Inherited constructor with exception message.
10+
*
11+
* @param message exception message
12+
*/
13+
public ApplicationException(String message) {
14+
super(message);
15+
}
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package com.iluwatar.model;
2+
3+
4+
import lombok.AllArgsConstructor;
5+
import lombok.Builder;
6+
import lombok.Data;
7+
import lombok.NoArgsConstructor;
8+
9+
/**
10+
* Bank card entity.
11+
*/
12+
@Data
13+
@Builder
14+
@NoArgsConstructor
15+
@AllArgsConstructor
16+
public class Card {
17+
18+
/**
19+
* Primary key.
20+
*/
21+
private long id;
22+
23+
/**
24+
* Foreign key points to card's owner.
25+
*/
26+
private long personId;
27+
28+
/**
29+
* Sum of money.
30+
*/
31+
private float sum;
32+
33+
/**
34+
* Current version of object.
35+
*/
36+
private int version;
37+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package com.iluwatar.repository;
2+
3+
/**
4+
* Imitation of Spring's JpaRepository.
5+
*
6+
* @param <T> target database entity
7+
*/
8+
public interface JpaRepository<T> {
9+
10+
/**
11+
* Get object by it's PK.
12+
*
13+
* @param id primary key
14+
* @return {@link T}
15+
*/
16+
T findById(long id);
17+
18+
/**
19+
* Get current object version.
20+
*
21+
* @param id primary key
22+
* @return object's version
23+
*/
24+
int getEntityVersionById(long id);
25+
26+
/**
27+
* Update object.
28+
*
29+
* @param obj entity to update
30+
* @return number of modified records
31+
*/
32+
int update(T obj);
33+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package com.iluwatar.service;
2+
3+
import com.iluwatar.api.UpdateService;
4+
import com.iluwatar.exception.ApplicationException;
5+
import com.iluwatar.model.Card;
6+
import com.iluwatar.repository.JpaRepository;
7+
import lombok.RequiredArgsConstructor;
8+
9+
/**
10+
* Service to update {@link Card} entity.
11+
*/
12+
@RequiredArgsConstructor
13+
public class CardUpdateService implements UpdateService<Card> {
14+
15+
private final JpaRepository<Card> cardJpaRepository;
16+
17+
@Override
18+
public Card doUpdate(Card obj, long id) {
19+
float additionalSum = obj.getSum();
20+
Card cardToUpdate = cardJpaRepository.findById(id);
21+
int initialVersion = cardToUpdate.getVersion();
22+
float resultSum = cardToUpdate.getSum() + additionalSum;
23+
cardToUpdate.setSum(resultSum);
24+
//Maybe more complex business-logic e.g. HTTP-requests and so on
25+
26+
if (initialVersion != cardJpaRepository.getEntityVersionById(id)) {
27+
String exMessage =
28+
String.format("Entity with id %s were updated in another transaction", id);
29+
throw new ApplicationException(exMessage);
30+
}
31+
32+
cardJpaRepository.update(cardToUpdate);
33+
return cardToUpdate;
34+
}
35+
}

0 commit comments

Comments
 (0)