Как в Go управлять режимами и уровнями изоляции транзакций в базах данных?

Ответ

В Go управление транзакциями осуществляется через объект *sql.Tx, который создается вызовом db.BeginTx(). Для тонкой настройки поведения транзакции, включая уровень изоляции и режим "только чтение", используется структура sql.TxOptions.

Уровни изоляции транзакций (Isolation Levels)

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

В пакете database/sql определены следующие константы:

  • sql.LevelDefault: Используется уровень по умолчанию, заданный в самой базе данных.
  • sql.LevelReadUncommitted: Самый низкий уровень. Позволяет читать незафиксированные изменения ("грязное чтение").
  • sql.LevelReadCommitted: Предотвращает "грязное чтение". Транзакция видит только те данные, которые были зафиксированы до ее начала. Это стандартный выбор для многих систем.
  • sql.LevelRepeatableRead: Гарантирует, что при повторном чтении в рамках одной транзакции данные будут теми же. Однако возможны "фантомные чтения" (появление новых строк).
  • sql.LevelSerializable: Самый строгий уровень. Полностью изолирует транзакции друг от друга, предотвращая все аномалии, включая фантомные чтения. Гарантирует максимальную согласованность, но может сильно снизить производительность.

Режим «только чтение» (ReadOnly)

Поле ReadOnly: true в sql.TxOptions — это подсказка для драйвера базы данных, что транзакция не будет изменять данные. Некоторые СУБД могут использовать это для оптимизации производительности. Не все драйверы поддерживают эту опцию.

Пример использования

import (
    "context"
    "database/sql"
    "log"
)

func performTransaction(ctx context.Context, db *sql.DB) error {
    // Задаем опции: уровень изоляции Serializable и режим "только чтение"
    opts := &sql.TxOptions{
        Isolation: sql.LevelSerializable,
        ReadOnly:  true, // Устанавливаем в false для операций записи
    }

    // Начинаем транзакцию с заданными опциями
    tx, err := db.BeginTx(ctx, opts)
    if err != nil {
        return err
    }

    // defer tx.Rollback() — это страховка.
    // Если Commit() будет выполнен успешно, Rollback() вернет ошибку sql.ErrTxDone, 
    // которую можно безопасно игнорировать.
    defer tx.Rollback()

    var balance int
    // Выполняем чтение внутри транзакции
    err = tx.QueryRowContext(ctx, "SELECT balance FROM accounts WHERE id = 1").Scan(&balance)
    if err != nil {
        return err // Rollback будет вызван через defer
    }

    log.Printf("Current balance: %d", balance)

    // Если бы мы изменяли данные:
    // _, err = tx.ExecContext(ctx, "UPDATE ...")
    // if err != nil { return err }

    // Фиксируем транзакцию
    return tx.Commit()
}

Как выбрать?

  • Простые CRUD-операции: LevelReadCommitted обычно является хорошим балансом.
  • Отчеты или длительные операции чтения: LevelRepeatableRead или ReadOnly: true для консистентности данных.
  • Критические финансовые или складские операции: LevelSerializable для предотвращения любых аномалий, даже ценой производительности.