Go: Транзакции

8 вопросов

1 Зачем нужны транзакции? Как использовать в Go?

Транзакция объединяет несколько операций в одну логическую единицу: либо все выполняются, либо ни одна. Обеспечивает консистентность при сбоях и конкурентном доступе. В Go: tx, err := db.BeginTx(ctx, nil), затем Exec/Query/QueryRow, в конце tx.Commit() или tx.Rollback(). Обязательно вызывать Rollback при ошибке (через defer).

tx, _ := db.BeginTx(ctx, nil)
defer tx.Rollback()
_, err = tx.ExecContext(ctx, "INSERT ...")
if err != nil { return err }
_, err = tx.ExecContext(ctx, "UPDATE ...")
if err != nil { return err }
return tx.Commit()
Открыть отдельно →
2 Уровни изоляции транзакций. Настройка в Go.

Read Uncommitted, Read Committed, Repeatable Read, Serializable. В Go уровень задают при начале транзакции: db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelSerializable}). По умолчанию в Postgres - Read Committed. Более высокий уровень - меньше аномалий, но больше блокировок и откатов. Для большинства сценариев достаточно уровня по умолчанию.

tx, err := db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelRepeatableRead})
Открыть отдельно →
3 Аномалии: dirty read, non-repeatable read, phantom read.

Dirty read - чтение незакоммиченных данных другой транзакции. Non-repeatable read - два чтения одной строки в одной транзакции дают разные значения. Phantom read - два чтения по условию возвращают разный набор строк. Read Uncommitted допускает dirty; Read Committed - non-repeatable; Repeatable Read в Postgres устраняет phantom за счет snapshot; Serializable устраняет все. В Go выбирают уровень под требования приложения.

Открыть отдельно →
4 Что такое deadlock? Как обрабатывать в Go?

Deadlock - взаимная блокировка: транзакция A ждет ресурс B, B ждет ресурс A. СУБД обнаруживает и откатывает одну из транзакций (жертва). В Go при ошибке "deadlock detected" делают retry с экспоненциальной задержкой. Избегают deadlock: фиксированный порядок блокировки (например, по id), короткие транзакции, минимум блокируемых строк.

for i := 0; i < maxRetries; i++ {
    err := doTx(ctx, db)
    if err == nil { return nil }
    if isDeadlock(err) { time.Sleep(backoff(i)); continue }
    return err
}
Открыть отдельно →
5 Оптимистичная и пессимистичная блокировка в Go.

Пессимистичная: блокируем строки при чтении (SELECT FOR UPDATE), другие ждут. В Go: tx.QueryRowContext(ctx, "SELECT * FROM orders WHERE id = $1 FOR UPDATE", id). Оптимистичная: читаем без блокировки, при обновлении проверяем версию/значения (version column или WHERE с старыми значениями); при конфликте - retry. В Go реализуют через UPDATE ... WHERE id = $1 AND version = $2 и проверку RowsAffected.

result, _ := tx.ExecContext(ctx, "UPDATE orders SET total = $1, version = version+1 WHERE id = $2 AND version = $3", total, id, version)
if n, _ := result.RowsAffected(); n == 0 { return ErrConflict }
Открыть отдельно →
6 Что такое SELECT FOR UPDATE? Когда применять в Go?

SELECT FOR UPDATE блокирует выбранные строки до конца транзакции. Другая транзакция с FOR UPDATE или изменением этих строк будет ждать. Используют для пессимистичной блокировки при "прочитал-изменил-записал". FOR UPDATE SKIP LOCKED - пропускать заблокированные строки (очереди задач). В Go выполняют в той же транзакции перед UPDATE.

tx.QueryRowContext(ctx, "SELECT * FROM orders WHERE id = $1 FOR UPDATE", id)
// затем UPDATE в той же tx
Открыть отдельно →
7 Что такое WAL (Write-Ahead Log)?

Журнал упреждающей записи: изменения сначала записываются в лог на диск, затем в сами данные. При сбое восстановление по WAL - повторное применение закоммиченных изменений. Обеспечивает durability. В Postgres WAL используется и для репликации (streaming). В Go приложение не управляет WAL напрямую; настройки (fsync, checkpoint) - на стороне БД.

Открыть отдельно →
8 Что такое SAVEPOINT? Как использовать в Go?

SAVEPOINT - точка сохранения внутри транзакции. Можно откатиться к ней без отмены всей транзакции. В Go: tx.ExecContext(ctx, "SAVEPOINT sp1"), при ошибке tx.ExecContext(ctx, "ROLLBACK TO SAVEPOINT sp1") и продолжить. Удобно для вложенной логики: откатить часть операций, остальное оставить. Не все драйверы поддерживают одинаково; в database/sql выполняют сырым Exec.

tx.ExecContext(ctx, "SAVEPOINT before_update")
// ...
tx.ExecContext(ctx, "ROLLBACK TO SAVEPOINT before_update")
Открыть отдельно →
🧠Квиз 🏆Лидеры 🎯Собесед. 📖Вопросы 📚База зн.