mirror of
https://github.com/tiennm99/java-design-patterns.git
synced 2026-10-13 08:15:38 +00:00
1 parent
70692f647f
commit
8b11e76f46
9 files changed
+399
No files matched your search
@@ -0,0 +1,147 @@
|
|||||||
|
---
|
||||||
|
title: Optimistic Offline Lock
|
||||||
|
category: Concurrency
|
||||||
|
language: en
|
||||||
|
tag:
|
||||||
|
- Data access
|
||||||
|
---
|
||||||
|
|
||||||
|
## Intent
|
||||||
|
|
||||||
|
Provide an ability to avoid concurrent changes of one record in relational databases.
|
||||||
|
|
||||||
|
## Explanation
|
||||||
|
|
||||||
|
Each transaction during object modifying checks equation of object's version before start of transaction
|
||||||
|
and before commit itself.
|
||||||
|
|
||||||
|
**Real world example**
|
||||||
|
> Since people love money, the best (and most common) example is banking system:
|
||||||
|
> imagine you have 100$ on your e-wallet and two people are trying to send you 50$ both at a time.
|
||||||
|
> Without locking, your system will start **two different thread**, each of whose will read your current balance
|
||||||
|
> and just add 50$. The last thread won't re-read balance and will just rewrite it.
|
||||||
|
> So, instead 200$ you will have only 150$.
|
||||||
|
|
||||||
|
**In plain words**
|
||||||
|
> Each transaction during object modifying will save object's last version and check it before saving.
|
||||||
|
> If it differs, the transaction will be rolled back.
|
||||||
|
|
||||||
|
**Wikipedia says**
|
||||||
|
> Optimistic concurrency control (OCC), also known as optimistic locking,
|
||||||
|
> is a concurrency control method applied to transactional systems such as
|
||||||
|
> relational database management systems and software transactional memory.
|
||||||
|
|
||||||
|
**Programmatic Example**
|
||||||
|
Let's simulate the case from *real world example*. Imagine we have next entity:
|
||||||
|
|
||||||
|
```java
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bank card entity.
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class Card {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Primary key.
|
||||||
|
*/
|
||||||
|
private long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Foreign key points to card's owner.
|
||||||
|
*/
|
||||||
|
private long personId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sum of money.
|
||||||
|
*/
|
||||||
|
private float sum;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current version of object;
|
||||||
|
*/
|
||||||
|
private int version;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then the correct modifying will be like this:
|
||||||
|
|
||||||
|
```java
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service to update {@link Card} entity.
|
||||||
|
*/
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class CardUpdateService implements UpdateService<Card> {
|
||||||
|
|
||||||
|
private final JpaRepository<Card> cardJpaRepository;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = ApplicationException.class) //will roll back transaction in case ApplicationException
|
||||||
|
public Card doUpdate(Card card, long cardId) {
|
||||||
|
float additionalSum = card.getSum();
|
||||||
|
Card cardToUpdate = cardJpaRepository.findById(cardId);
|
||||||
|
int initialVersion = cardToUpdate.getVersion();
|
||||||
|
float resultSum = cardToUpdate.getSum() + additionalSum;
|
||||||
|
cardToUpdate.setSum(resultSum);
|
||||||
|
//Maybe more complex business-logic e.g. HTTP-requests and so on
|
||||||
|
|
||||||
|
if (initialVersion != cardJpaRepository.getEntityVersionById(cardId)) {
|
||||||
|
String exMessage = String.format("Entity with id %s were updated in another transaction", cardId);
|
||||||
|
throw new ApplicationException(exMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
cardJpaRepository.update(cardToUpdate);
|
||||||
|
return cardToUpdate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Applicability
|
||||||
|
|
||||||
|
Since optimistic locking can cause degradation of system's efficiency and reliability due to
|
||||||
|
many retries/rollbacks, it's important to use it safely. They are useful in case when transactions are not so long
|
||||||
|
and does not distributed among many microservices, when you need to reduce network/database overhead.
|
||||||
|
|
||||||
|
Important to note that you should not choose this approach in case when modifying one object
|
||||||
|
in different threads is common situation.
|
||||||
|
|
||||||
|
## Tutorials
|
||||||
|
|
||||||
|
- [Offline Concurrency Control](https://www.baeldung.com/cs/offline-concurrency-control)
|
||||||
|
- [Optimistic lock in JPA](https://www.baeldung.com/jpa-optimistic-locking)
|
||||||
|
|
||||||
|
## Known uses
|
||||||
|
|
||||||
|
- [Hibernate ORM](https://docs.jboss.org/hibernate/orm/4.3/devguide/en-US/html/ch05.html)
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
**Advantages**:
|
||||||
|
|
||||||
|
- Reduces network/database overhead
|
||||||
|
- Let to avoid database deadlock
|
||||||
|
- Improve the performance and scalability of the application
|
||||||
|
|
||||||
|
**Disadvantages**:
|
||||||
|
|
||||||
|
- Increases complexity of the application
|
||||||
|
- Requires mechanism of versioning
|
||||||
|
- Requires rollback/retry mechanisms
|
||||||
|
|
||||||
|
## Related patterns
|
||||||
|
|
||||||
|
- [Pessimistic Offline Lock](https://martinfowler.com/eaaCatalog/pessimisticOfflineLock.html)
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
- [Source (Martin Fowler)](https://martinfowler.com/eaaCatalog/optimisticOfflineLock.html)
|
||||||
|
- [Advantages and disadvantages](https://www.linkedin.com/advice/0/what-benefits-drawbacks-using-optimistic)
|
||||||
|
- [Comparison of optimistic and pessimistic locks](https://www.linkedin.com/advice/0/what-advantages-disadvantages-using-optimistic)
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--
|
||||||
|
|
||||||
|
This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
|
||||||
|
|
||||||
|
The MIT License
|
||||||
|
Copyright © 2014-2022 Ilkka Seppälä
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
|
|
||||||
|
-->
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<parent>
|
||||||
|
<groupId>com.iluwatar</groupId>
|
||||||
|
<artifactId>java-design-patterns</artifactId>
|
||||||
|
<version>1.26.0-SNAPSHOT</version>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<artifactId>optimistic-offline-lock</artifactId>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
<artifactId>junit-jupiter-engine</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.mockito</groupId>
|
||||||
|
<artifactId>mockito-core</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.iluwatar.api;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service for entity update.
|
||||||
|
*
|
||||||
|
* @param <T> target entity
|
||||||
|
*/
|
||||||
|
public interface UpdateService<T> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update entity.
|
||||||
|
*
|
||||||
|
* @param obj entity to update
|
||||||
|
* @param id primary key
|
||||||
|
* @return modified entity
|
||||||
|
*/
|
||||||
|
T doUpdate(T obj, long id);
|
||||||
|
}
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
package com.iluwatar.exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception happens in application during business-logic execution.
|
||||||
|
*/
|
||||||
|
public class ApplicationException extends RuntimeException {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inherited constructor with exception message.
|
||||||
|
*
|
||||||
|
* @param message exception message
|
||||||
|
*/
|
||||||
|
public ApplicationException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package com.iluwatar.model;
|
||||||
|
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bank card entity.
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class Card {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Primary key.
|
||||||
|
*/
|
||||||
|
private long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Foreign key points to card's owner.
|
||||||
|
*/
|
||||||
|
private long personId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sum of money.
|
||||||
|
*/
|
||||||
|
private float sum;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current version of object.
|
||||||
|
*/
|
||||||
|
private int version;
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package com.iluwatar.repository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Imitation of Spring's JpaRepository.
|
||||||
|
*
|
||||||
|
* @param <T> target database entity
|
||||||
|
*/
|
||||||
|
public interface JpaRepository<T> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get object by it's PK.
|
||||||
|
*
|
||||||
|
* @param id primary key
|
||||||
|
* @return {@link T}
|
||||||
|
*/
|
||||||
|
T findById(long id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current object version.
|
||||||
|
*
|
||||||
|
* @param id primary key
|
||||||
|
* @return object's version
|
||||||
|
*/
|
||||||
|
int getEntityVersionById(long id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update object.
|
||||||
|
*
|
||||||
|
* @param obj entity to update
|
||||||
|
* @return number of modified records
|
||||||
|
*/
|
||||||
|
int update(T obj);
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package com.iluwatar.service;
|
||||||
|
|
||||||
|
import com.iluwatar.api.UpdateService;
|
||||||
|
import com.iluwatar.exception.ApplicationException;
|
||||||
|
import com.iluwatar.model.Card;
|
||||||
|
import com.iluwatar.repository.JpaRepository;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service to update {@link Card} entity.
|
||||||
|
*/
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class CardUpdateService implements UpdateService<Card> {
|
||||||
|
|
||||||
|
private final JpaRepository<Card> cardJpaRepository;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Card doUpdate(Card obj, long id) {
|
||||||
|
float additionalSum = obj.getSum();
|
||||||
|
Card cardToUpdate = cardJpaRepository.findById(id);
|
||||||
|
int initialVersion = cardToUpdate.getVersion();
|
||||||
|
float resultSum = cardToUpdate.getSum() + additionalSum;
|
||||||
|
cardToUpdate.setSum(resultSum);
|
||||||
|
//Maybe more complex business-logic e.g. HTTP-requests and so on
|
||||||
|
|
||||||
|
if (initialVersion != cardJpaRepository.getEntityVersionById(id)) {
|
||||||
|
String exMessage =
|
||||||
|
String.format("Entity with id %s were updated in another transaction", id);
|
||||||
|
throw new ApplicationException(exMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
cardJpaRepository.update(cardToUpdate);
|
||||||
|
return cardToUpdate;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package com.iluwatar;
|
||||||
|
|
||||||
|
import com.iluwatar.exception.ApplicationException;
|
||||||
|
import com.iluwatar.model.Card;
|
||||||
|
import com.iluwatar.repository.JpaRepository;
|
||||||
|
import com.iluwatar.service.CardUpdateService;
|
||||||
|
import org.junit.jupiter.api.Assertions;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.mockito.Mockito;
|
||||||
|
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
import static org.mockito.Mockito.eq;
|
||||||
|
|
||||||
|
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||||
|
public class OptimisticLockTest {
|
||||||
|
|
||||||
|
private CardUpdateService cardUpdateService;
|
||||||
|
|
||||||
|
private JpaRepository cardRepository;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void setUp() {
|
||||||
|
cardRepository = Mockito.mock(JpaRepository.class);
|
||||||
|
cardUpdateService = new CardUpdateService(cardRepository);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldNotUpdateEntityOnDifferentVersion() {
|
||||||
|
int initialVersion = 1;
|
||||||
|
long cardId = 123L;
|
||||||
|
Card card = Card.builder()
|
||||||
|
.id(cardId)
|
||||||
|
.version(initialVersion)
|
||||||
|
.sum(123f)
|
||||||
|
.build();
|
||||||
|
when(cardRepository.findById(eq(cardId))).thenReturn(card);
|
||||||
|
when(cardRepository.getEntityVersionById(Mockito.eq(cardId))).thenReturn(initialVersion + 1);
|
||||||
|
|
||||||
|
Assertions.assertThrows(ApplicationException.class,
|
||||||
|
() -> cardUpdateService.doUpdate(card, cardId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldUpdateOnSameVersion() {
|
||||||
|
int initialVersion = 1;
|
||||||
|
long cardId = 123L;
|
||||||
|
Card card = Card.builder()
|
||||||
|
.id(cardId)
|
||||||
|
.version(initialVersion)
|
||||||
|
.sum(123f)
|
||||||
|
.build();
|
||||||
|
when(cardRepository.findById(eq(cardId))).thenReturn(card);
|
||||||
|
when(cardRepository.getEntityVersionById(Mockito.eq(cardId))).thenReturn(initialVersion);
|
||||||
|
|
||||||
|
cardUpdateService.doUpdate(card, cardId);
|
||||||
|
|
||||||
|
Mockito.verify(cardRepository).update(Mockito.any());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -206,6 +206,7 @@
|
|||||||
<module>component</module>
|
<module>component</module>
|
||||||
<module>context-object</module>
|
<module>context-object</module>
|
||||||
<module>thread-local-storage</module>
|
<module>thread-local-storage</module>
|
||||||
|
<module>optimistic-offline-lock</module>
|
||||||
</modules>
|
</modules>
|
||||||
<repositories>
|
<repositories>
|
||||||
<repository>
|
<repository>
|
||||||
|
|||||||
Reference in new issue
Block a user