[Перевод] Распределенное управление конкурентностью

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

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

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

Что такое транзакция?


Это единый набор логически связанных операций. Основные типы:

  • Read. Операция чтения. Читает значение с Базы Данных (БД) и сохраняет в буфере памяти.

  • Write. Операция записи. Записывает значение из буфера в БД.

Проблемный сценарий

Бронирование места в кино/самолёте/отеле многими пользователями в одно и тоже время. Пути возникновения:

  1. Один пользователь кликнул «забронировать» много раз

  2. Множество пользователей забронировали тоже место/комнату/слот одновременно.

Рассмотрим обобщенную табличку »Booking» для лучшего понимания.

d10075a38deacec85c2be8eeef0d639a.png

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

Решения

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

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

Множественное бронирование может произойти, если сервис всего-лишь проверяет возможность брони. Тогда N параллельных запросов проверив одновременно осуществят бронь. В таком случае могут помочь Database Lockings — оптимистичная и пессимистичная блокировки.

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

Это наиболее простой путь конкурентного изменения данных с гарантией их консистентности. Алгоритм выполнения:

  1. Прочитать данные с версией/меткой времени

  2. Изменить данные и версию

  3. Обновить данные, если изначальная версия не изменилась. Иначе повторить алгоритм.

Это отличное решение для небольших нагрузок. Может быть реализовано с помощью аннотаций в Spring Data JPA.

Недостатки

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

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

Эксклюзивная блокировка для единоличного использования. Используется, когда целостность данных крайне важна. Требует осторожности в применение из-за возможных deadlock’ов.

Наподобие мьютекса в многопоточном программирование реализует принцип ожидания, если ресурс занят.

Такую блокировку реализуют РСУБД 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
    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();
}

Захват блокировки:

30f2cdca9133fc85aeb1f056d406f75c.png

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

227363bdc41f6c0b0e5851a068ef3f3d.png

Такая реализация позволяет сохранить консистентность данных и сериализовать доступ до общего ресурса в распределенной системе.

---

Дополнительно:

Базы Данных: телеграмм пост по сравнению clickhouse VS tarantool

Systemd Design: мой канал по подготовке к System Design Interview

© Habrahabr.ru