Blog System Design

Mastering Distributed Locking for Seat Reservations with Java Spring Boot

Introduction:

In online ticket booking systems like BookMyShow, ensuring a smooth and reliable seat reservation experience is paramount. With multiple users vying for the same seats simultaneously, the risk of overbooking and data inconsistencies becomes a critical challenge.

This is where distributed locking comes into play, providing a robust solution to coordinate access to shared resources across multiple servers or nodes in a distributed system.

Understanding Distributed Locking:

Distributed locking is a mechanism that allows a process or thread to acquire an exclusive lock on a shared resource, preventing other processes from accessing or modifying that resource until the lock is released.


This concept is particularly crucial in distributed systems where multiple instances of an application run concurrently, accessing shared data or resources.

In the context of a seat reservation system like BookMyShow, distributed locking ensures that only one user can successfully book a particular seat or set of seats at a given time.

Without proper locking mechanisms, race conditions can occur, leading to overbooking, data inconsistencies, and a frustrating user experience.

Why Distributed Locking is Essential:

  1. Preventing Overbooking: Without a centralized locking mechanism, multiple users could attempt to book the same seat simultaneously, resulting in overbooking and potential double bookings.
  2. Scalability: Relying solely on database-level locks can become a bottleneck when multiple application servers interact with the database concurrently. Distributed locks offload this coordination, enabling seamless scalability.
  3. Session Handling: User sessions with potential seat selections need to be tracked reliably across different servers in the distributed system, ensuring a consistent user experience.

System Design and Components:

To implement distributed locking for seat reservations, we need to consider the following key components:

  1. Application Servers: These are the Java Spring Boot instances responsible for handling user requests, processing seat reservations, and interacting with the distributed lock service and the database.
  2. Distributed Lock Service: A dedicated service or middleware that manages and coordinates distributed locks across the application servers. Popular options include Redis, Apache ZooKeeper, etcd, and Hazelcast.
  3. Database: A reliable database (e.g., MySQL, PostgreSQL) to store seat inventory, reservations, and other relevant data.
  4. Load Balancer: A load balancer to distribute incoming user requests across the available application servers, ensuring high availability and scalability.

Choosing a Distributed Lock Service:

The choice of a distributed lock service depends on various factors, including performance, reliability, and integration with your existing technology stack. Let’s explore some popular options:

  1. Redis: Redis is an in-memory data store that can be used as a distributed lock service. It offers fast locking operations, making it suitable for scenarios with high concurrency. However, care must be taken to manage failover and potential data loss depending on the Redis setup.
  2. Apache ZooKeeper: ZooKeeper is a distributed coordination service designed for distributed applications, including distributed locking. It provides a highly reliable and consistent locking mechanism, making it suitable for critical locks, but it can add slight overhead compared to Redis.
  3. etcd: etcd is a highly consistent distributed key-value store similar to ZooKeeper, often used for coordinating distributed systems and implementing distributed locks.

For this blog post, we’ll focus on implementing distributed locking using Redis as the distributed lock service.

Java Spring Boot Implementation:

To illustrate the concept of distributed locking for seat reservations, let’s walk through a Java Spring Boot implementation using Redis as the distributed lock service.

Dependencies:

Java
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

We’ll need the spring-boot-starter-data-redis dependency to interact with Redis and the spring-boot-starter-web dependency for building RESTful APIs.

Configuration:

Java
@Configuration
public class RedisConfig {

    @Value("${redis.host}")
    private String redisHost;

    @Value("${redis.port}")
    private int redisPort;

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);
        return template;
    }

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        return new JedisConnectionFactory(new RedisStandaloneConfiguration(redisHost, redisPort));
    }
}

In the RedisConfig class, we configure the Redis connection details (redisHost and redisPort) and set up the RedisTemplate and RedisConnectionFactory beans for interacting with Redis.

Service Layer:

Java
@Service
public class SeatReservationService {

    private static final String LOCK_PREFIX = "seat-lock:";
    private static final int LOCK_EXPIRATION_SECONDS = 600; // 10 minutes

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    public boolean tryAcquireSeatLock(String showId, int seatNumber, String sessionId) {
        String lockKey = LOCK_PREFIX + showId + ":" + seatNumber;
        return acquireLock(lockKey, sessionId);
    }

    public void releaseSeatLock(String showId, int seatNumber, String sessionId) {
        String lockKey = LOCK_PREFIX + showId + ":" + seatNumber;
        releaseLock(lockKey, sessionId);
    }

    private boolean acquireLock(String lockKey, String sessionId) {
        Boolean lockAcquired = redisTemplate.opsForValue().setIfAbsent(lockKey, sessionId, Duration.ofSeconds(LOCK_EXPIRATION_SECONDS));
        return lockAcquired != null && lockAcquired;
    }

    private void releaseLock(String lockKey, String sessionId) {
        String lockValue = (String) redisTemplate.opsForValue().get(lockKey);
        if (sessionId.equals(lockValue)) {
            redisTemplate.delete(lockKey);
        }
    }
}

The SeatReservationService class encapsulates the logic for acquiring and releasing distributed locks using Redis. The tryAcquireSeatLock method attempts to acquire a lock for a specific seat, identified by the showId and seatNumber. The releaseSeatLock method releases the lock for the given seat.

The acquireLock method uses the RedisTemplate to set a key-value pair in Redis, where the key is the lock key (seat-lock:<showId>:<seatNumber>), and the value is the session ID. If the key doesn’t exist, it means the lock is available, and Redis will set the key with the provided value and expiration time. The method returns true if the lock is acquired successfully.

The releaseLock method checks if the current session ID matches the value stored in Redis for the lock key. If they match, it means the session holds the lock, and the method deletes the key from Redis, effectively releasing the lock.

Controller Layer:

Java
@RestController
@RequestMapping("/reservations")
public class SeatReservationController {

    @Autowired
    private SeatReservationService reservationService;

    @PostMapping("/seats")
    public ResponseEntity<String> reserveSeats(@RequestBody SeatReservationRequest request) {
        String showId = request.getShowId();
        List<Integer> seatNumbers = request.getSeatNumbers();
        String sessionId = request.getSessionId();

        List<Integer> lockedSeats = new ArrayList<>();
        List<Integer> unavailableSeats = new ArrayList<>();

        for (int seatNumber : seatNumbers) {
            if (reservationService.tryAcquireSeatLock(showId, seatNumber, sessionId)) {
                lockedSeats.add(seatNumber);
            } else {
                unavailableSeats.add(seatNumber);
            }
        }

        if (!unavailableSeats.isEmpty()) {
            // Release locks for acquired seats
            for (int seatNumber : lockedSeats) {
                reservationService.releaseSeatLock(showId, seatNumber, sessionId);
            }

            return ResponseEntity.status(HttpStatus.CONFLICT)
                    .body("Some seats are not available: " + unavailableSeats);
        }

        // Proceed with payment processing or further actions
        // ...

        return ResponseEntity.ok("Seats reserved successfully: " + lockedSeats);
    }
}

In the SeatReservationController, the reserveSeats method handles the seat reservation request. It iterates through the requested seat numbers and attempts to acquire a distributed lock for each seat using the tryAcquireSeatLock method from the SeatReservationService.

If any seat is unavailable (lock acquisition fails), the method releases all previously acquired locks and returns a CONFLICT response indicating the unavailable seat numbers. If all requested seats are available and locks are acquired successfully, the method can proceed with further actions, such as payment processing or reservation confirmation.

Note that in a production scenario, you would typically handle the payment process, commit the reservation to the database, and release the locks upon successful payment or after a predefined timeout.

Workflow:

  1. A user initiates a seat reservation request for a specific show and a list of seat numbers.
  2. The application server receives the request and iterates through the requested seat numbers.
  3. For each seat number, the application server attempts to acquire a distributed lock using the tryAcquireSeatLock method from the SeatReservationService.
  4. If the lock is acquired successfully, the seat number is added to the list of locked seats.
  5. If the lock acquisition fails, the seat number is added to the list of unavailable seats.
  6. After iterating through all requested seat numbers, the application server checks if any seats were unavailable.
  7. If there are unavailable seats, the application server releases all previously acquired locks and returns a CONFLICT response with the list of unavailable seat numbers.
  8. If all requested seats were successfully locked, the application server can proceed with further actions, such as payment processing or reservation confirmation.
  9. Upon successful payment or after a predefined timeout, the application server should release the acquired locks using the releaseSeatLock method from the SeatReservationService.

Considerations:

  1. Lock Granularity: You can choose between fine-grained locking (locking each seat individually) or coarse-grained locking (locking blocks of seats). Fine-grained locking provides higher concurrency but higher overhead, while coarse-grained locking reduces overhead but may result in lower concurrency.
  2. Error Handling: Ensure proper error handling and recovery mechanisms are in place to handle scenarios such as lock acquisition failures, processes holding locks crashing, or network partitions.
  3. Consistency: Maintain consistency between the distributed lock state and the database state to avoid phantom bookings or data inconsistencies.
  4. Monitoring and Logging: Implement monitoring and logging mechanisms to track lock acquisitions, releases, and potential issues for troubleshooting and performance analysis.
  5. Load Testing: Conduct load testing to ensure the distributed locking system can handle high concurrency and identify potential bottlenecks or performance issues.
  6. Scalability and Failover: Ensure the distributed lock service (Redis, in this case) is properly configured for scalability and failover to avoid single points of failure.
  7. Caching: Consider caching seat availability and reservation data to improve performance and reduce the load on the database and distributed lock service.

By implementing distributed locking with Java Spring Boot and a reliable distributed lock service like Redis, you can build a robust and scalable seat reservation system that ensures data consistency and prevents overbooking, even under high concurrency scenarios.

Avatar

Neelabh

About Author

As Neelabh Singh, I am a Senior Software Engineer with 6.6 years of experience, specializing in Java technologies, Microservices, AWS, Algorithms, and Data Structures. I am also a technology blogger and an active participant in several online coding communities.

You may also like

Blog Design Pattern

Understanding the Builder Design Pattern in Java | Creational Design Patterns | CodeTechSummit

Overview The Builder design pattern is a creational pattern used to construct a complex object step by step. It separates
Blog Tech Toolkit

Base64 Decode

Base64 encoding is a technique used to encode binary data into ASCII characters, making it easier to transmit data over