Зачем нужна изоляция транзакций в базах данных и какие уровни изоляции существуют?

Ответ

Изоляция — это одно из свойств ACID (Atomicity, Consistency, Isolation, Durability), которое определяет, как и в какой степени изменения, сделанные в одной транзакции, видны другим параллельным транзакциям. Основная цель — предотвратить аномалии конкурентного доступа.

Проблемы, которые решает изоляция:

  • Грязное чтение (Dirty Read): Одна транзакция читает данные, которые были изменены другой транзакцией, но еще не зафиксированы (не COMMIT'нуты). Если вторая транзакция откатится (ROLLBACK), первая будет работать с неверными, "грязными" данными.
  • Неповторяемое чтение (Non-repeatable Read): В ходе одной транзакции происходит повторное чтение тех же данных, но результат оказывается разным, так как другая транзакция успела изменить и зафиксировать эти данные.
  • Фантомное чтение (Phantom Read): В ходе одной транзакции выполняется запрос с одним и тем же условием выборки, но результаты различаются, так как другая транзакция добавила или удалила строки, удовлетворяющие этому условию.

Стандартные уровни изоляции SQL:

  1. Read Uncommitted: Самый низкий уровень. Предотвращает только потерю обновлений, но допускает все три аномалии.
  2. Read Committed: Предотвращает грязное чтение. Транзакция видит только зафиксированные изменения. Это уровень по умолчанию для многих СУБД (например, PostgreSQL).
  3. Repeatable Read: Предотвращает грязное и неповторяемое чтение. Гарантирует, что если транзакция прочитала строку, эта строка не изменится до конца транзакции. Однако фантомы возможны.
  4. Serializable: Самый строгий уровень. Предотвращает все аномалии, включая фантомы. Гарантирует, что параллельные транзакции выполняются так, как если бы они выполнялись последовательно.

Пример в Go с database/sql:

// Устанавливаем высокий уровень изоляции для важной операции
tx, err := db.BeginTx(ctx, &sql.TxOptions{
    Isolation: sql.LevelSerializable,
})
if err != nil {
    log.Fatal(err)
}
// defer Rollback откатит транзакцию, если Commit() не будет вызван
// (например, из-за паники или ошибки)
defer tx.Rollback()

// ... Операции внутри транзакции ...
_, err = tx.ExecContext(ctx, "UPDATE accounts SET balance = balance - 100 WHERE id = 1")
if err != nil {
    // Rollback будет вызван через defer
    log.Fatal(err)
}

// Фиксируем изменения
err = tx.Commit()
if err != nil {
    log.Fatal(err)
}