8 вопросов
Транзакция объединяет несколько операций в одну логическую единицу: либо все выполняются, либо ни одна. Обеспечивает консистентность при сбоях и конкурентном доступе. В 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()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})Dirty read - чтение незакоммиченных данных другой транзакции. Non-repeatable read - два чтения одной строки в одной транзакции дают разные значения. Phantom read - два чтения по условию возвращают разный набор строк. Read Uncommitted допускает dirty; Read Committed - non-repeatable; Repeatable Read в Postgres устраняет phantom за счет snapshot; Serializable устраняет все. В 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
}Пессимистичная: блокируем строки при чтении (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 }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Журнал упреждающей записи: изменения сначала записываются в лог на диск, затем в сами данные. При сбое восстановление по WAL - повторное применение закоммиченных изменений. Обеспечивает durability. В Postgres WAL используется и для репликации (streaming). В Go приложение не управляет WAL напрямую; настройки (fsync, checkpoint) - на стороне БД.
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")