Каким образом база данных защищает от проблем с состоянием гонки (Data Race)?

«Каким образом база данных защищает от проблем с состоянием гонки (Data Race)?» — вопрос из категории Базы данных, который задают на 25% собеседований C# Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

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

1. Транзакции и ACID: Гарантируют, что группа операций выполнится как единое целое.

  • Изоляция (Isolation) — ключевое свойство для борьбы с гонками. Уровни изоляции определяют, насколько транзакции "видят" изменения друг друга.

2. Уровни изоляции (от слабых к сильным):

  • Read Uncommitted: Минимум защиты. Возможны "грязные" чтения.
  • Read Committed (стандарт в PostgreSQL, SQL Server): Гарантирует, что читаются только зафиксированные данные. Защищает от "грязного" чтения, но возможны "неповторяющиеся чтения" (значение строки изменилось между двумя чтениями в одной транзакции).
  • Repeatable Read: Гарантирует, что строки, прочитанные в транзакции, не изменятся другими транзакциями. Защищает от "неповторяющегося чтения", но возможны "фантомные" чтения (появление новых строк).
  • Serializable (максимальная защита): Полная изоляция. Гарантирует, что результат параллельного выполнения транзакций идентичен их последовательному выполнению. Полностью предотвращает состояние гонки, но ценой производительности.

3. Механизмы блокировок:

  • Пессимистичные блокировки: СУБД явно блокирует строки/таблицы для чтения (LOCK IN SHARE MODE) или записи (FOR UPDATE), предотвращая доступ других транзакций.
    -- Пример в SQL
    BEGIN;
    SELECT * FROM accounts WHERE id = 1 FOR UPDATE; -- Блокировка на запись
    UPDATE accounts SET balance = balance - 100 WHERE id = 1;
    COMMIT;
  • Оптимистичный контроль параллелизма (Optimistic Concurrency Control - OCC): Не блокирует данные при чтении. При обновлении проверяет, не изменилась ли версия записи (через timestamp или rowversion). Если изменилась — транзакция откатывается.
    // Пример в EF Core с полем ConcurrencyToken
    var product = await db.Products.FindAsync(id);
    product.Quantity--;
    try {
        await db.SaveChangesAsync(); // EF автоматически проверит версию в WHERE
    } catch (DbUpdateConcurrencyException) {
        // Обработка конфликта: перезагрузить данные и повторить логику
    }

4. Многовариантное управление параллелизмом (MVCC): Используется в PostgreSQL, Oracle, MySQL (InnoDB). СУБД хранит несколько версий строки. Каждая транзакция "видит" снимок данных (snapshot) на момент своего начала. Это позволяет избежать блокировок при чтении, повышая производительность, сохраняя согласованность.

Практический вывод: Для защиты от гонки при операциях типа "списать со счёта" нужно использовать либо высокий уровень изоляции (Serializable), либо пессимистичную блокировку (SELECT ... FOR UPDATE), либо оптимистичный контроль с повторением операции в случае конфликта.