mirror of
https://github.com/tiennm99/java-design-patterns.git
synced 2026-05-14 08:58:26 +00:00
@@ -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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user