---
title: 'Базы данных и распределенные транзакции: изоляция, блокировки и паттерны консенсуса'
description: 'Подробное руководство по транзакциям в базах данных, гонкам данных, оптимистичным и пессимистичным блокировкам, распределенным блокировкам в Redis, 2PC, оркестрации и хореографии Saga, а также паттерну Transactional Outbox с брокерами сообщений.'
faq:
    - { question: 'В чем разница между оптимистической и пессимистической блокировками?', answer: 'Пессимистическая блокировка предотвращает конфликты параллелизма, блокируя строки базы данных (с помощью SELECT FOR UPDATE), что приостанавливает другие транзакции до освобождения блокировки. Оптимистическая блокировка предполагает, что конфликты редки; она разрешает параллельное чтение и запись, но проверяет колонку версии или временной метки при обновлении. Если за это время строка была изменена другой транзакцией, обновление завершается ошибкой, и приложение должно повторить попытку.' }
    - { question: 'Почему двухфазный коммит (2PC) редко используется в современных микросервисах?', answer: 'Двухфазный коммит — это синхронный блокирующий протокол. Он требует, чтобы все участвующие сервисы удерживали блокировки ресурсов на протяжении обеих фаз. Если один из сервисов замедляется или отключается, все остальные остаются заблокированными, что создает единую точку отказа и сильно ограничивает масштабируемость и доступность системы.' }
    - { question: "Как паттерн Transactional Outbox гарантирует доставку сообщений 'хотя бы один раз' (at-least-once)?", answer: "Вместо отправки сообщения брокеру напрямую во время транзакции БД (что может дать сбой, если брокер недоступен), сервис записывает бизнес-данные и тело сообщения в специальную таблицу 'outbox' в рамках той же локальной транзакции. Отдельный фоновый процесс асинхронно опрашивает эту таблицу, отправляет сообщения брокеру и отмечает их как обработанные, гарантируя, что ни одно сообщение не будет потеряно." }
published: '2026-06-14'
---
# Базы данных и распределенные транзакции: изоляция, блокировки и паттерны консенсуса

Представьте пользователя, пытающегося забронировать последнее свободное место на авиарейс. Два запроса приходят на серверы приложений в одну и ту же миллисекунду. Если система не спроектирована с учетом параллелизма, оба запроса прочитают статус места как «свободно», спишут оплату и выпустят два билета на одно и то же кресло. Эта классическая проблема двойного бронирования — кошмар как для разработчиков, так и для бизнеса. В монолитной системе с единой базой данных транзакции SQL легко предотвращают это. Но в распределенной системе, где сервис бронирования, платежный шлюз и система уведомлений являются изолированными микросервисами, простой транзакции базы данных уже недостаточно.

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

## Содержание
* [Локальные транзакции БД: ACID и уровни изоляции](#acid-isolation)
* [Состояние гонки: пессимистические и оптимистические блокировки](#locking-strategies)
* [Распределенные блокировки: решения на базе Redis](#redis-locks)
* [Распределенные транзакции: крах двухфазного коммита (2PC)](#distributed-2pc)
* [Согласованность в конечном счете: Saga и Transactional Outbox](#saga-outbox)
* [Практический пример кода на PHP и Laravel](#code-demo)
* [Ограничения и компромиссы](#limitations)
* [Практические выводы](#takeaways)

---

<a id="acid-isolation"></a>
## Локальные транзакции БД: ACID и уровни изоляции

В основе целостности данных лежит модель ACID: атомарность (Atomicity), согласованность (Consistency), изолированность (Isolation) и долговечность (Durability). Если атомарность («все или ничего») и долговечность (гарантия записи на диск) интуитивно понятны, то изолированность — это область постоянного компромисса между производительностью и корректностью.

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

1. **Read Uncommitted (Чтение незафиксированных данных)**: Низший уровень. Транзакция может читать изменения, сделанные другой транзакцией, еще до того, как они будут зафиксированы (Dirty Reads — грязное чтение).
2. **Read Committed (Чтение зафиксированных данных)**: Предотвращает грязное чтение. Запрос видит только те данные, которые были зафиксированы на момент начала запроса. Однако повторный запрос внутри той же транзакции может вернуть измененные данные (Non-repeatable Reads — неповторяемое чтение).
3. **Repeatable Read (Повторяемость чтения)**: Предотвращает неповторяемое чтение. Данные, прочитанные в транзакции, остаются неизменными. Тем не менее, другие транзакции могут добавлять новые строки, из-за чего повторный запрос вернет новые записи (Phantom Reads — фантомное чтение).
4. **Serializable (Сериализуемость)**: Высший уровень. Транзакции выполняются последовательно, полностью исключая все аномалии параллелизма ценой критического падения производительности.

Большинство реляционных СУБД (таких как PostgreSQL и MySQL) по умолчанию используют **Read Committed** или **Repeatable Read**. Чтобы гарантировать полную надежность без ущерба для скорости всей системы, разработчикам приходится управлять блокировками вручную на уровне отдельных запросов.

---

<a id="locking-strategies"></a>
## Состояние гонки: пессимистические и оптимистические блокировки

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

### Пессимистическая блокировка (Pessimistic Locking)
* **Тезис**: Предполагаем, что конфликты параллельного доступа неизбежны, и блокируем ресурс заранее.
* **Почему это важно**: Это гарантирует отсутствие гонки данных непосредственно силами СУБД.
* **Пример**: Запрос `SELECT ... FOR UPDATE` блокирует выбранные строки. Любая другая транзакция, пытающаяся прочесть их с помощью `FOR UPDATE` или изменить, будет ждать, пока первая транзакция не выполнит `COMMIT` или `ROLLBACK`.
* **Последствия**: Высокая надежность, но низкая пропускная способность. Если транзакция затягивается (например, ожидает ответа от стороннего API), другие потоки быстро исчерпают пул соединений СУБД, что приведет к отказу приложения.

### Оптимистическая блокировка (Optimistic Locking)
* **Тезис**: Предполагаем, что конфликты происходят редко, позволяя свободное чтение и проверяя конкурентные изменения при записи.
* **Почему это важно**: Исключает удержание блокировок в БД, максимизируя скорость чтения и масштабируемость.
* **Пример**: В таблицу добавляется колонка `version` (целое число) или `updated_at` (время изменения). SQL-запрос на обновление проверяет исходную версию:
  ```sql
  UPDATE inventory SET quantity = quantity - 1, version = version + 1 
  WHERE id = 1 AND version = 3;
  ```
* **Последствия**: Если другая транзакция успела обновить строку раньше, версия изменилась на 4, запрос обновит 0 строк. Приложение фиксирует этот факт и решает: повторить операцию или вернуть ошибку. Однако при высокой частоте одновременных записей постоянные повторы транзакций снижают общую производительность.

---

<a id="redis-locks"></a>
## Распределенные блокировки: решения на базе Redis

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

* **Тезис**: Используем быстрое хранилище в оперативной памяти (например, Redis) для управления статусами блокировок вне СУБД.
* **Почему это важно**: Redis работает в один поток и выполняет команды атомарно, обеспечивая субмиллисекундный отклик и освобождая ресурсы основной базы данных.
* **Пример**: Процесс запрашивает блокировку с помощью атомарной команды `SET lock_key unique_token NX PX 5000` (установить ключ, если его нет, на 5000 мс). Освобождение блокировки происходит через скрипт Lua, который удаляет ключ только в том случае, если токен совпадает с сохраненным.
* **Последствия**: Высокая масштабируемость. Однако в распределенном кластере Redis (master-replica) сбой мастера до репликации ключа может привести к выдаче дублирующей блокировки. Для защиты от этого применяется алгоритм Redlock, запрашивающий блокировку у большинства независимых нод Redis.

---

<a id="distributed-2pc"></a>
## Распределенные транзакции: крах двухфазного коммита (2PC)

При переходе на микросервисы данные разносятся по изолированным БД (Database-per-Service). Бизнес-логика покупки теперь затрагивает сервис пользователей, сервис склада и сервис оплаты.

Историческим решением для распределенных транзакций являлся **Двухфазный коммит (2PC)**:
1. **Фаза подготовки (Prepare)**: Координатор запрашивает у всех сервисов готовность выполнить операцию. Сервисы локально блокируют ресурсы и отвечают «да».
2. **Фаза фиксации (Commit)**: Если все ответили «да», координатор дает команду на фиксацию изменений. Если хотя бы один ответил «нет», координатор дает команду на откат (Rollback).

Несмотря на простоту концепции, 2PC является **синхронным блокирующим протоколом**. Если в фазе фиксации происходит сбой сети или один из сервисов уходит в офлайн, ресурсы остальных сервисов остаются заблокированными на неопределенный срок. Это противоречит требованиям доступности (теорема CAP). Современные системы выбирают согласованность в конечном счете (eventual consistency).

---

<a id="saga-outbox"></a>
## Согласованность в конечном счете: Saga и Transactional Outbox

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

### Паттерн Saga
Saga — это цепочка локальных транзакций. Каждый микросервис выполняет свою транзакцию в своей БД и публикует событие. Другие сервисы слушают событие и запускают свои шаги. Если на каком-то шаге происходит сбой, Saga запускает цепочку **компенсирующих транзакций** в обратном порядке для отмены изменений.

* **Хореография (Choreography)**: Сервисы обмениваются событиями напрямую без единой точки контроля. Архитектура гибкая, но сложна в отладке при разрастании цепочек.
* **Оркестрация (Orchestration)**: Выделяется отдельный сервис-оркестратор, который пошагово вызывает микросервисы и управляет логикой компенсации. Проще контролировать, но оркестратор становится критической точкой системы.

### Паттерн Transactional Outbox
Распространенная ошибка — отправлять сообщение в брокер (например, RabbitMQ) прямо внутри транзакции базы данных. Если брокер недоступен, транзакция откатится. Если брокер примет сообщение, но транзакция БД упадет при коммите, возникнет фантомное событие.

Решение дает **Transactional Outbox**:
1. Бизнес-данные и событие (в таблицу `outbox`) записываются в локальную БД в рамках одной и той же транзакции.
2. Отдельный фоновый процесс (Message Relay) опрашивает таблицу `outbox`, отправляет события в брокер и отмечает их как отправленные.
3. Этот подход гарантирует доставку **«хотя бы один раз»**. Получатель сообщения обязательно должен быть **идемпотентным** (повторная обработка того же события не должна приводить к дублированию данных).

---

<a id="code-demo"></a>
## Практический пример кода на PHP и Laravel

Вот примеры реализации описанных подходов в Laravel.

### 1. Транзакция БД с пессимистической блокировкой
```php
// app/Services/BookingService.php
namespace App\Services;

use Illuminate\Support\Facades\DB;
use App\Models\FlightSeat;

class BookingService
{
    public function bookSeat(int $seatId, int $userId): bool
    {
        return DB::transaction(function () use ($seatId, $userId) {
            // Блокируем строку. Другие параллельные запросы будут ожидать здесь.
            $seat = FlightSeat::where('id', $seatId)
                ->lockForUpdate()
                ->first();

            if (!$seat || $seat->is_booked) {
                return false;
            }

            $seat->update([
                'is_booked' => true,
                'user_id' => $userId,
            ]);

            return true;
        });
    }
}
```

### 2. Распределенная блокировка в Redis
```php
// app/Services/InventoryService.php
namespace App\Services;

use Illuminate\Support\Facades\Cache;

class InventoryService
{
    public function decreaseStock(int $productId, int $quantity): bool
    {
        $lockKey = "lock:product:{$productId}";
        
        // Запрашиваем блокировку на 10 сек, ждем освобождения до 3 сек.
        $lock = Cache::lock($lockKey, 10);

        if ($lock->get()) {
            try {
                // Быстрая работа с данными без блокировки таблиц БД
                // ... логика обновления остатков
                return true;
            } finally {
                $lock->release();
            }
        }

        return false;
    }
}
```

### 3. Запись события в Transactional Outbox
```php
// app/Services/OrderService.php
namespace App\Services;

use Illuminate\Support\Facades\DB;
use App\Models\Order;
use App\Models\OutboxMessage;

class OrderService
{
    public function createOrder(array $data): Order
    {
        return DB::transaction(function () use ($data) {
            $order = Order::create($data);

            // Сохраняем событие в той же транзакции, где создается заказ
            OutboxMessage::create([
                'event_type' => 'order.created',
                'payload' => json_encode([
                    'order_id' => $order->id,
                    'user_id' => $order->user_id,
                    'total' => $order->total_amount,
                ]),
                'processed' => false,
            ]);

            return $order;
        });
    }
}
```

---

<a id="limitations"></a>
## Ограничения и компромиссы

* **Оптимистические блокировки**: При крайне высокой частоте одновременных записей в одну сущность количество ошибок и повторных попыток возрастает, нагружая процессор приложения.
* **Блокировки Redis**: Безопасность блокировки жестко завязана на время жизни (TTL). Если процесс «зависнет» (например, из-за долгой сборки мусора в JVM/PHP), блокировка снимется раньше времени, и другой воркер нарушит изоляцию.
* **Saga**: Компенсирующие транзакции не могут физически «откатить» внешние действия (отправку Email, списание денег банком). Поэтому бизнес-логику приходится проектировать через промежуточные состояния (холдирование средств, черновики писем).

---

<a id="takeaways"></a>
## Практические выводы

1. Используйте **оптимистические блокировки** для операций с высокой долей чтения и редкими конфликтами (редактирование контента, личные кабинеты).
2. Используйте **пессимистические блокировки** (`SELECT ... FOR UPDATE`) для критических финансовых операций с умеренным параллелизмом, где конфликты должны разрешаться внутри СУБД.
3. Применяйте **блокировки Redis** для защиты от дублирования фоновых задач и тяжелых бизнес-операций на уровне приложения.
4. В микросервисах забудьте о синхронных распределенных транзакциях (2PC). Стройте процессы на базе **Saga** для координации шагов и **Transactional Outbox** для гарантированной асинхронной доставки событий.