
Управление конкурентным доступом является очень важной концепцией в Системе Управления Базами Данных. Оно гарантирует, что одновременное выполнение запросов несколькими процессами или пользователями оставит данные в согласованном состоянии. Особое место занимает доступ к Базе Данных в распределенной системе с множеством конкурирующих за ресурс узлов.
Что такое транзакция?
Это единый набор логически связанных операций. Основные типы:
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
