---
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 ms). Освобождаването става чрез 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). Ако процесът „замръзне“ (например поради дълъг Garbage Collector в JVM/PHP), блокировката се маха предсрочно и друг воркер нарушава изолацията.
* **Saga**: Компенсиращите транзакции не могат физически да „отменят“ външни действия (изпращане на имейл, таксуване на пари от банка). Затова бизнес логиката трябва да се проектира през междинни състояния (холдиране на средства, чернови).

---

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

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