Недавно я участвовал в обсуждении с коллегой извечной проблемы обработки дубликатов при бронировании. Я был удивлен, обнаружив, что даже по прошествии стольких лет не существует хорошей коллекции всех различных способов справиться с этим.
Итак, вот я рассказываю вам о проблеме и различных способах ее решения:
Проблема
Это распространенная проблема, которая может проявляться одним из следующих способов:
- Один и тот же пользователь несколько раз нажимает кнопку «Забронировать».
- Несколько пользователей пытаются одновременно забронировать одно и то же место/комнату/слот.
- Как Airbnb, BookMyShow и MakeMyTrip обрабатывают одновременные запросы на бронирование
Чтобы правильно понять проблему, давайте предположим следующее:
- Допустим, у нас есть таблица с именем 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.
Теперь между двумя вышеперечисленными мы бы предположили, что все проблемы будут решены, но здесь начинается сложность.
Что происходит в распределенной системе
Блокировка в распределенной среде — это больше, чем просто мьютекс в многопоточных приложениях. Это более изощренно и сложно, потому что теперь эта блокировка может быть получена разными узлами в системе, и любой из них может выйти из строя. Это многократно увеличивает сложность, поскольку нам нужно, чтобы остальная часть нашей системы по-прежнему работала безупречно, даже если один или несколько узлов вышли из строя.
Распределенные блокировки
Распределенная блокировка — это метод, используемый для координации доступа к общим ресурсам между несколькими процессами в распределенной системе. Основная цель состоит в том, чтобы гарантировать, что только один процесс одновременно может получить доступ к определенному ресурсу, предотвращая условия гонки, повреждение данных или несогласованность.
Для реализации мы можем использовать некоторые решения, такие как:
- Redis использует библиотеки, реализующие алгоритмы блокировки, такие как ShedLock и Redisson. Использование этого не рекомендуется, как описано в (https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html)
- Hazelcast предлагает систему блокировки на основе своей подсистемы CP. (https://hazelcast.com/blog/long-live-distributed-locks/)
- Zookeeper, я подробно расскажу об этом ниже.
Реализация распределенной блокировки с помощью 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, который помогает поддерживать согласованность и координировать доступ к общим ресурсам в распределенной системе. Этот механизм позволяет процессам устанавливать и снимать блокировки, гарантируя, что только один процесс имеет доступ к определенному ресурсу в каждый момент времени.