The Optimistic Concurrency Control in Distributed Systems

The Optimistic Concurrency Control in Distributed Systems

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:

  1. Read Phase: The transaction reads the necessary data and performs computations without modifying the actual database state.
  2. Validation Phase: Before committing, the transaction checks whether the data it read has been modified by other transactions during its execution.
  3. Write Phase: If the validation succeeds, the transaction applies its changes. Otherwise, it aborts and may retry.

OCC’s approach aligns with the ACID (Atomicity, Consistency, Isolation, Durability) properties of databases as follows:

  • Atomicity: OCC ensures that a transaction either completes fully or not at all.
  • Consistency: Validation ensures that transactions maintain database consistency.
  • Isolation: Conflicts are detected during validation, ensuring that no partial updates from other transactions affect the current transaction.
  • Durability: Once committed, changes are persistent, even in the event of a system failure.

OCC is particularly effective in systems with:

  • High read-to-write ratios.
  • Minimal contention for shared resources.
  • Scenarios where lock contention overhead in Pessimistic Concurrency Control would be significant.


Real-World Example

E-commerce Inventory Management

Consider an e-commerce platform where multiple users can purchase the same product simultaneously. Each purchase involves:

  • Reading the current stock level.
  • Reducing the stock level by the number of items purchased.
  • Writing the updated stock level back to the database.

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

  • Scalability: Reduced contention makes OCC highly scalable.
  • Non-blocking: Transactions proceed without waiting for locks.
  • Simplicity: Eliminates complex locking mechanisms.

Limitations of OCC

  • High Retry Overhead: Frequent conflicts can lead to multiple retries, affecting performance.
  • Validation Cost: Validation may be computationally expensive for complex transactions.


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.


Bruno Moura

Senior Software Engineer | Guidewire | Gosu | Java | Node.js | React.js | Javascript | TypeScript | AWS

2 个月

Awesome!

要查看或添加评论,请登录

Jether Rodrigues的更多文章

社区洞察

其他会员也浏览了