
Управление конкурентным доступом является очень важной концепцией в Системе Управления Базами Данных. Оно гарантирует, что одновременное выполнение запросов несколькими процессами или пользовате��ями оставит данные в согласованном состоянии. Особое место занимает доступ к Базе Данных в распределенной системе с множеством конкурирующих за ресурс узлов.
Что такое транзакция?
Это единый набор логически связанных операций. Основные типы:
Read. Операция чтения. Читает значение с Базы Данных(БД) и сохраняет в буфере памяти.
Write. Операция записи. Записывает значение из буфера в БД.
Проблемный сценарий
Бронирование места в кино/самолёте/отеле многими пользователями в одно и тоже время. Пути возникновения:
Один пользователь кликнул "забронировать" много раз
Множество пользователей забронировали тоже место/комнату/слот одновременно.
Рассмотрим обобщенную табличку "Booking" для лучшего понимания.

1ую проблему можно легко решить(к примеру, введением ключа идемпотентности). Рассмотрим более детально вторую.
Решения

Множественное бронирование может произойти, если сервис всего-лишь проверяет возможность брони. Тогда N параллельных запросов проверив одновременно осуществят бронь. В таком случае могут помочь Database Lockings - оптимистичная и пессимистичная блокировки.
Оптимистичная блокировка
Это наиболее простой путь конкурентного изменения данных с гарантией их консистентности. Алгоритм выполнения:
Прочитать данные с версией/меткой времени
Изменить данные и версию
Обновить данные, если изначальная версия не изменилась. Иначе повторить алгоритм.
Это отличное решение для небольших нагрузок. Может быть реализовано с помощью аннотаций в Spring Data JPA.
Недостатки
В случае большого количества запросов происходит частое соперничество за данные. Как следствие, частые отмены и повторы транзакций. Это повышает нагрузку на систему, увеличивает задержки на выполнение. Такой вид блокировки больше подходит для сценариев не частого со��ерничества.
Пессимистичная блокировка
Эксклюзивная блокировка для единоличного использования. Используется, когда целостность данных крайне важна. Требует осторожности в применение из-за возможных взаимных блокировок.
Наподобие мьютекса в многопоточном программирование реализует принцип ожидания, если ресурс занят.
Такую блокировку реализуют РСУБД Postgres, MYSQL, Oracle. Также ORM Spring Data JPA.
Казалось бы, все проблемы с конкурентным доступом решены. Но что будет в распределенных системах?
Распределенная блокировка
Блокировка в распределенных системах нечно большее, чем мьютекс. Блокировать могут несколько нод, который могут выходить из строя. В то же время, необходимо сохранять работоспособность все системы.
Распределенная блокировка - механизм для скоординированного доступа к совместным ресурсам для всех участников распределенной системы. Главная цель такой блокировки - обеспечить доступ к ресурсу лишь одному сервису/запросу в данный момент времени. И предотвратить гонку за данными и неконсистентность данных.
Для её реализации могут быть использованы решения:
Redis, который реализует алгоритмы типа ShedLock, Redisson. Однако, они подвергаются критики
Hazelcast предоставляет систему блокировки, основанную на CP subsystem
Zookeeper, который рассмотрим далее
Реализация Распределенной Блокировки с Apache ZooKeeper
Apache ZooKeeper является распределенным координирующим сервисом, который может быть использован для реализации такой блокировки.
Пример Java реализации:
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 try { if (lock.acquire(100, TimeUnit.MILLISECONDS)) { try { // Access the shared resource // Perform your operations here } finally { lock.release(); } } } finally { lock.close(); } }
Захват блокировки:

Освобождение блокировки:

Такая реализация позволяет сохранить консистентность данных и сериализовать доступ до общего ресурса в распределенной системе.
---
Дополнительно:
Базы Данных: телеграмм пост по сравнению clickhouse VS tarantool
System Design: мой канал по подготовке к System Design Interview
