Недавно я участвовал в обсуждении с коллегой извечной проблемы обработки дубликатов при бронировании. Я был удивлен, обнаружив, что даже по прошествии стольких лет не существует хорошей коллекции всех различных способов справиться с этим.

Итак, вот я рассказываю вам о проблеме и различных способах ее решения:

Проблема

Это распространенная проблема, которая может проявляться одним из следующих способов:

  1. Один и тот же пользователь несколько раз нажимает кнопку «Забронировать».
  2. Несколько пользователей пытаются одновременно забронировать одно и то же место/комнату/слот.
  3. Как Airbnb, BookMyShow и MakeMyTrip обрабатывают одновременные запросы на бронирование

Чтобы правильно понять проблему, давайте предположим следующее:

  1. Допустим, у нас есть таблица с именем booking. Я предположил упрощенный дизайн, который можно использовать для любого из упомянутых выше случаев.

2. Существует два способа двойного бронирования:

Пользователи нажимают кнопку «Книга» / API несколько раз

Несколько пользователей пытаются забронировать одно и то же место, особенно в случае с популярным фильмом/шоу и рейсами, это происходит часто

3. Первую проблему легко решить, и ее может решить как клиент, так и сервер, о чем я расскажу в другой статье. Здесь давайте подробно сосредоточимся на проблеме нескольких пользователей, пытающихся забронировать одно и то же место/комнату.

Решения

Несколько пользователей пытаются забронировать одно и то же место

В приведенном выше случае, если наш сервис просто проверяет перед новым бронированием, забронировано ли место, у нас возникнет проблема, если будет n одновременных запросов, что приведет к многократному бронированию одного и того же места.

Самый простой способ решить описанную выше проблему: Блокировка базы данных (оптимистичная и пессимистичная)

Оптимистическая блокировка

Это самый простой способ обеспечить сохранение качества данных. Оптимистическая блокировка — это стратегия, при которой вы читаете запись, записываете номер версии и проверяете, что версия не изменилась, прежде чем записывать запись обратно.

С такими фреймворками, как Spring Data JPA, это легко реализовать с помощью аннотаций. Подробнее об этом можно прочитать на https://www.baeldung.com/jpa-optimistic-locking.

Для таких систем, как Hotel Booking, где количество запросов/запросов/транзакций в секунду может быть не очень высоким, это отличный вариант.

Когда этого недостаточно

Когда есть много одновременных запросов, скажем, как в случае с бронированием рейсов или популярным фильмом, это имеет большие последствия для производительности. Оптимистическая блокировка плохо работает, если одновременно происходит много конфликтов, потому что это приводит к необходимости отказа от многих транзакций.

Если система уже работает с максимальной пропускной способностью, повторная попытка транзакции может снизить производительность. Со временем система сможет обрабатывать все транзакции по порядку, но в то же время некоторые из них могут испытывать задержки.

Пессимистическая блокировка

Пессимистическая блокировка — это блокировка записи для вашего исключительного использования до тех пор, пока вы не закончите с ней работать. Он имеет гораздо лучшую целостность, чем оптимистическая блокировка, но требует осторожности при разработке приложения, чтобы избежать взаимоблокировок.

Он основан на принципе, что если что-то потенциально может пойти не так, лучше подождать, пока ситуация снова не станет безопасной, прежде чем что-либо предпринимать (Аналогично взаимному исключению в многопоточности).

Системы РСУБД, такие как Postgres, MYSQL и ORACLE, предоставляют способы сделать это.

Даже в ORMS, таких как Spring Data JPA, есть простой способ сделать это. В этой статье подробно объясняется это
https://www.baeldung.com/java-jpa-transaction-locks#:~:text=When%20using%20Pessimistic%20Locking%2C%20, укажите% 20значение%20lock%20timeout%20.

Теперь между двумя вышеперечисленными мы бы предположили, что все проблемы будут решены, но здесь начинается сложность.

Что происходит в распределенной системе

Блокировка в распределенной среде — это больше, чем просто мьютекс в многопоточных приложениях. Это более изощренно и сложно, потому что теперь эта блокировка может быть получена разными узлами в системе, и любой из них может выйти из строя. Это многократно увеличивает сложность, поскольку нам нужно, чтобы остальная часть нашей системы по-прежнему работала безупречно, даже если один или несколько узлов вышли из строя.

Распределенные блокировки

Распределенная блокировка — это метод, используемый для координации доступа к общим ресурсам между несколькими процессами в распределенной системе. Основная цель состоит в том, чтобы гарантировать, что только один процесс одновременно может получить доступ к определенному ресурсу, предотвращая условия гонки, повреждение данных или несогласованность.

Для реализации мы можем использовать некоторые решения, такие как:

Реализация распределенной блокировки с помощью Apache ZooKeeper

Apache ZooKeeper — это служба распределенной координации, которую можно использовать для реализации распределенной блокировки. В следующем примере кода Java демонстрируется базовая распределенная блокировка с использованием ZooKeeper.

import org.apache.zookeeper.ZooKeeper;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;

import java.util.concurrent.TimeUnit;

public class DistributedLock {
    private CuratorFramework client;
    private InterProcessMutex lock;

    public DistributedLock(String zkConnectionString, String lockPath) {
        client = CuratorFrameworkFactory.newClient(zkConnectionString, new ExponentialBackoffRetry(1000, 3));
        client.start();
        lock = new InterProcessMutex(client, lockPath);
    }

    public boolean acquire(long waitTime, TimeUnit timeUnit) {
        try {
            return lock.acquire(waitTime, timeUnit);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    public void release() {
        try {
            lock.release();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void close() {
        client.close();
    }
}

Применение

public static void main(String[] args) {
    String zkConnectionString = "127.0.0.1:2181";
    String lockPath = "/my_resource_lock";

    DistributedLock lock = new DistributedLock(zkConnectionString, lockPath);

    // Acquire the lock
    if (lock.acquire(100, TimeUnit.MILLISECONDS)) {
        // Access the shared resource
        // Perform your operations here

        // Release the lock
        lock.release();
    }

    // Close the ZooKeeper connection
    lock.close();

}

Блокировка приобретения

Блокировка выпуска

Используя Apache ZooKeeper, мы реализовали распределенный механизм блокировки на Java, который помогает поддерживать согласованность и координировать доступ к общим ресурсам в распределенной системе. Этот механизм позволяет процессам устанавливать и снимать блокировки, гарантируя, что только один процесс имеет доступ к определенному ресурсу в каждый момент времени.