The Optimistic Concurrency Control in Distributed Systems
Jether Rodrigues
Senior Software Engineer | Java | Kotlin | Spring | Web3 | Solidity | Smart Contracts | Blockchain | Chainlink Advocate
Concurrency control is a critical aspect of distributed systems, ensuring that simultaneous operations on shared resources do not lead to inconsistent states. Among the various concurrency control mechanisms, Optimistic Concurrency Control (OCC) has emerged as a significant approach, particularly suited for distributed environments with a low likelihood of transaction conflicts. This article delves into the concept of OCC, its relationship with database ACID properties, provides a real-world example, and illustrates its implementation using Spring Boot.
Understanding Optimistic Concurrency Control
Optimistic Concurrency Control operates on the assumption that most transactions do not conflict, making it a lightweight and efficient choice for distributed systems. Unlike Pessimistic Concurrency Control, which locks resources to prevent conflicts, OCC allows transactions to execute without restrictions. Conflicts are detected only at the commit stage.
The OCC workflow typically involves three phases:
OCC’s approach aligns with the ACID (Atomicity, Consistency, Isolation, Durability) properties of databases as follows:
OCC is particularly effective in systems with:
Real-World Example
E-commerce Inventory Management
Consider an e-commerce platform where multiple users can purchase the same product simultaneously. Each purchase involves:
Using OCC ensures that stock updates are consistent without relying on database locks, which can degrade performance under high traffic.
Implementing OCC with Spring Boot
Below is an implementation of OCC in a Spring Boot application for the e-commerce example:
Base Entity
import jakarta.persistence.*;
import java.time.LocalDateTime;
import io.azam.ulidj.ULID;
@MappedSuperclass
public abstract class BaseEntity {
@Id
private String id;
@Version
private int version;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
@PrePersist
public void prePersist() {
this.createdAt = LocalDateTime.now();
this.updatedAt = this.createdAt;
this.applyId();
}
@PreUpdate
public void preUpdate() {
this.updatedAt = LocalDateTime.now();
}
private void applyId() {
if (this.id == null) {
this.id = ULID.random();
}
}
// Getters and setters omitted for brevity
}
Entity Classes
Product Entity
import jakarta.persistence.Entity;
@Entity
public class Product extends BaseEntity {
private String name;
private int stock;
// Getters and setters omitted for brevity
}
领英推荐
PurchaseTransaction Entity
import jakarta.persistence.*;
@Entity
public class PurchaseTransaction extends BaseEntity {
@ManyToOne
private Product product;
private int quantity;
@Enumerated(EnumType.STRING)
private TransactionStatus status;
// Getters and setters omitted for brevity
}
public enum TransactionStatus {
CONFIRMED,
PENDING,
REFUSED,
CANCELLED
}
Repository
Product Repository
import org.springframework.data.jpa.repository.JpaRepository;
public interface ProductRepository extends JpaRepository<Product, String> {
}
PurchaseTransaction Repository
import org.springframework.data.jpa.repository.JpaRepository;
public interface PurchaseTransactionRepository extends JpaRepository<PurchaseTransaction, String> {
}
Custom Exceptions
public class ProductNotFoundException extends RuntimeException {
public ProductNotFoundException(String message) {
super(message);
}
}
public class InsufficientStockException extends RuntimeException {
public InsufficientStockException(String message) {
super(message);
}
}
Service
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class ProductService {
@Autowired
private ProductRepository productRepository;
@Autowired
private PurchaseTransactionRepository transactionRepository;
@Transactional
public PurchaseResponseDTO purchaseProduct(PurchaseRequestDTO purchaseRequest) {
Product product = productRepository.findById(purchaseRequest.getProductId())
.orElseThrow(() -> new ProductNotFoundException("Product not found"));
if (product.getStock() < purchaseRequest.getQuantity()) {
throw new InsufficientStockException("Insufficient stock");
}
product.setStock(product.getStock() - purchaseRequest.getQuantity());
productRepository.save(product);
PurchaseTransaction transaction = new PurchaseTransaction();
transaction.setProduct(product);
transaction.setQuantity(purchaseRequest.getQuantity());
transaction.setStatus(TransactionStatus.CONFIRMED);
transactionRepository.save(transaction);
return new PurchaseResponseDTO(transaction.getId(), "Purchase successful");
}
}
Controller
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/products")
public class ProductController {
@Autowired
private ProductService productService;
@PostMapping("/purchase")
public ResponseEntity<PurchaseResponseDTO> purchaseProduct(@RequestBody PurchaseRequestDTO purchaseRequest) {
PurchaseResponseDTO response = productService.purchaseProduct(purchaseRequest);
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}
}
class PurchaseRequestDTO {
private String productId;
private int quantity;
// Getters and setters omitted for brevity
}
class PurchaseResponseDTO {
private String transactionId;
private String message;
public PurchaseResponseDTO(String transactionId, String message) {
this.transactionId = transactionId;
this.message = message;
}
// Getters and setters omitted for brevity
}
Global Exception Handling
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ProductNotFoundException.class)
public ResponseEntity<ApiErrorResponse> handleProductNotFoundException(ProductNotFoundException ex) {
return new ResponseEntity<>(new ApiErrorResponse(HttpStatus.NOT_FOUND.value(), ex.getMessage()), HttpStatus.NOT_FOUND);
}
@ExceptionHandler(InsufficientStockException.class)
public ResponseEntity<ApiErrorResponse> handleInsufficientStockException(InsufficientStockException ex) {
return new ResponseEntity<>(new ApiErrorResponse(HttpStatus.BAD_REQUEST.value(), ex.getMessage()), HttpStatus.BAD_REQUEST);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiErrorResponse> handleGeneralException(Exception ex) {
return new ResponseEntity<>(new ApiErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR.value(), "An unexpected error occurred: " + ex.getMessage()), HttpStatus.INTERNAL_SERVER_ERROR);
}
}
class ApiErrorResponse {
private int statusCode;
private String message;
public ApiErrorResponse(int statusCode, String message) {
this.statusCode = statusCode;
this.message = message;
}
// Getters and setters omitted for brevity
}
Conflict Resolution
If two users attempt to update the same product simultaneously, the @Version field ensures that one transaction fails validation and is retried. Spring Data JPA handles this automatically by throwing an OptimisticLockException.
Advantages of OCC
Limitations of OCC
Conclusion
Optimistic Concurrency Control provides a robust mechanism for managing concurrency in distributed systems, especially in scenarios with low contention. By leveraging OCC in Spring Boot, developers can build scalable and performant applications that ensure data consistency even under concurrent access. As demonstrated in the e-commerce example, OCC’s combination of simplicity and efficiency makes it an invaluable tool for modern distributed architectures.
Senior Software Engineer | Guidewire | Gosu | Java | Node.js | React.js | Javascript | TypeScript | AWS
2 个月Awesome!