Как СУБД реализуют уровни изоляции транзакций и как с этим работать в Go?

Ответ

Уровни изоляции транзакций — это набор правил, которые определяют, как транзакции видят изменения, сделанные параллельно идущими транзакциями. В Go уровень изоляции задается при создании транзакции через database/sql.

Как это выглядит в Go:

// Контекст для управления временем жизни транзакции
ctx := context.Background()

// Указываем уровень изоляции в опциях
tx, err := db.BeginTx(ctx, &sql.TxOptions{
    Isolation: sql.LevelRepeatableRead, // Например, Repeatable Read
    ReadOnly:  false,
})
if err != nil {
    log.Fatal(err)
}
// ... работа с транзакцией ...
tx.Commit() // или tx.Rollback()

Стандартные уровни изоляции и проблемы, которые они решают:

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

Как это реализовано «под капотом» в СУБД:

  • MVCC (Multi-Version Concurrency Control): Основной механизм в PostgreSQL, Oracle и MySQL (InnoDB). Вместо блокировки данных при чтении, СУБД создает «снимок» (snapshot) данных на момент начала транзакции. При изменении строки создается ее новая версия, а старая сохраняется для транзакций, которые начались раньше. Это позволяет читающим транзакциям не блокировать пишущие.
  • Блокировки (Locking): Более традиционный механизм. Бывают общие (shared, для чтения) и эксклюзивные (exclusive, для записи) блокировки. Этот подход может приводить к простоям и взаимоблокировкам (deadlocks), но он проще и используется во многих системах, часто в комбинации с MVCC.