Skip to content

Instantly share code, notes, and snippets.

@susimsek
Last active June 14, 2024 14:53
Show Gist options
  • Save susimsek/f6dcdb01701107530bdff7873124632e to your computer and use it in GitHub Desktop.
Save susimsek/f6dcdb01701107530bdff7873124632e to your computer and use it in GitHub Desktop.
Spring Data JPA Optimistic Lock
server:
port: 8080
spring:
datasource:
url: jdbc:h2:mem:accountdb;MODE=PostgreSQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE;
username: datademo
password: root
hikari:
maximum-pool-size: 30
minimum-idle: 1
pool-name: Hikari
auto-commit: false
h2:
console:
enabled: true
jpa:
hibernate:
ddl-auto: none
show-sql: true
package io.github.susimsek.springdatademo.config;
import java.time.Clock;
import java.time.Instant;
import java.util.Optional;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.auditing.DateTimeProvider;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@Configuration(proxyBeanMethods = false)
@EnableJpaRepositories(basePackages = "io.github.susimsek.springdatademo.repository")
@EnableJpaAuditing(
dateTimeProviderRef = "dateTimeProvider")
@EnableTransactionManagement
@Slf4j
public class DatabaseConfig {
@Bean
public Clock clock() {
return Clock.systemDefaultZone();
}
@Bean
public DateTimeProvider dateTimeProvider(Clock clock) {
return () -> Optional.of(Instant.now(clock));
}
}
package io.github.susimsek.springdatademo.entity;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Version;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Entity
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private Double price;
@Version
private int version;
}
package io.github.susimsek.springdatademo.repository;
import io.github.susimsek.springdatademo.entity.Product;
import jakarta.persistence.LockModeType;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Lock;
import org.springframework.lang.NonNull;
public interface ProductRepository extends JpaRepository<Product, Long> {
@Lock(LockModeType.OPTIMISTIC)
@NonNull
Optional<Product> findById(@NonNull Long id);
}
package io.github.susimsek.springdatademo.service;
import io.github.susimsek.springdatademo.entity.Product;
import io.github.susimsek.springdatademo.repository.ProductRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.orm.ObjectOptimisticLockingFailureException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Slf4j
@RequiredArgsConstructor
public class ProductService {
private final ProductRepository productRepository;
@Transactional
public void updateProductPrice(Long productId, Double newPrice) {
int attempt = 0;
boolean success = false;
while (attempt < 3 && !success) {
try {
attempt++;
log.info("Attempt {}: Updating product price to {}", attempt, newPrice);
Product product = productRepository.findById(productId)
.orElseThrow(() -> new RuntimeException("Product not found"));
// Perform the update
product.setPrice(newPrice);
productRepository.save(product); // Optimistic locking is handled by Spring Data JPA
success = true;
log.info("Successfully updated product price to {}", newPrice);
} catch (ObjectOptimisticLockingFailureException e) {
log.warn("Optimistic lock exception on attempt {}: {}", attempt, e.getMessage());
// If max attempts reached, log and exit without throwing exception
if (attempt == 3) {
log.error("Failed to update product after retries: {}", productId);
return; // Exit method without throwing exception
}
// Wait before retrying
try {
retryDelay();
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
log.warn("Thread interrupted during retry delay", ex);
}
}
}
}
private void retryDelay() throws InterruptedException {
Thread.sleep(100); // Retry delay
}
}
package io.github.susimsek.springdatademo.service;
import static org.assertj.core.api.Assertions.assertThat;
import io.github.susimsek.springdatademo.entity.Product;
import io.github.susimsek.springdatademo.repository.ProductRepository;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.parallel.Execution;
import org.junit.jupiter.api.parallel.ExecutionMode;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.context.annotation.Import;
import org.springframework.orm.ObjectOptimisticLockingFailureException;
@DataJpaTest
@Import(ProductService.class)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@Execution(ExecutionMode.CONCURRENT)
class ProductServiceTest {
@Autowired
private ProductRepository productRepository;
@Autowired
private ProductService productService;
@PersistenceContext
private EntityManager entityManager;
@BeforeEach
void setUp() {
Product product = new Product();
product.setName("Test Product");
product.setPrice(50.0);
productRepository.save(product);
}
@Test
@DisplayName("Test concurrent updates with optimistic locking exception")
void testOptimisticLockingException() throws InterruptedException {
Long productId = productRepository.findAll().get(0).getId();
Double newPrice1 = 100.0;
Double newPrice2 = 150.0;
AtomicReference<Exception> exception1 = new AtomicReference<>();
AtomicReference<Exception> exception2 = new AtomicReference<>();
Runnable task1 = () -> {
try {
productService.updateProductPrice(productId, newPrice1);
} catch (Exception e) {
exception1.set(e);
}
};
Runnable task2 = () -> {
try {
productService.updateProductPrice(productId, newPrice2);
} catch (Exception e) {
exception2.set(e);
}
};
ExecutorService executorService = Executors.newFixedThreadPool(2);
executorService.submit(task1);
executorService.submit(task2);
executorService.shutdown();
executorService.awaitTermination(10, TimeUnit.SECONDS);
// Clear the EntityManager to avoid returning cached entities
entityManager.clear();
// Fetch the updated product
Product updatedProduct = productRepository.findById(productId).orElseThrow();
System.out.println("Updated price: " + updatedProduct.getPrice());
assertThat(updatedProduct.getPrice()).isIn(newPrice1, newPrice2);
// Verify that at least one thread encountered an optimistic lock exception
if (exception1.get() != null) {
assertThat(exception1.get()).isInstanceOf(ObjectOptimisticLockingFailureException.class);
} else if (exception2.get() != null) {
assertThat(exception2.get()).isInstanceOf(ObjectOptimisticLockingFailureException.class);
} else {
throw new AssertionError("Expected at least one thread to encounter an ObjectOptimisticLockingFailureException");
}
}
@Test
@DisplayName("Test successful update without contention")
void testSuccessfulUpdate() {
Long productId = productRepository.findAll().get(0).getId();
Double newPrice = 200.0;
productService.updateProductPrice(productId, newPrice);
// Clear the EntityManager to avoid returning cached entities
// Fetch the updated product
Product updatedProduct = productRepository.findById(productId).orElseThrow();
System.out.println("Updated price: " + updatedProduct.getPrice());
assertThat(updatedProduct.getPrice()).isEqualTo(newPrice);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment