> Как избежать гонок и рейсов в базе данных (Go)

Уровень: middle · Роль: backend · Язык: Go · Категория: Технические вопросы

Компании: Wildberries

Стек: Go

> Пример ответа

Гонки (race conditions) и рейсы (deadlocks) в базах данных - частые проблемы при конкурентном доступе. В Go с типичным стеком (PostgreSQL, MySQL) основные методы предотвращения:

  1. Транзакции с правильным уровнем изоляции
    Используйте SERIALIZABLE для критичных операций (например, бронирование билетов). Это гарантирует, что параллельные транзакции не приведут к несогласованности. Пример на Go с database/sql:

    GO
    tx, err := db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelSerializable})
    // выполнение запросов
    tx.Commit()
  2. Оптимистичные блокировки (optimistic locking)
    Добавьте поле version в таблицу. При обновлении проверяйте, что версия не изменилась:

    SQL
    UPDATE items SET value = $1, version = version + 1 WHERE id = $2 AND version = $3

    Если RowsAffected == 0 - конфликт, повторите операцию.

  3. Пессимистичные блокировки (SELECT FOR UPDATE)
    Для предотвращения гонок при чтении-записи используйте блокировку строки:

    GO
    tx.ExecContext(ctx, "SELECT id FROM accounts WHERE id = $1 FOR UPDATE")
    // затем обновление
  4. Избегание deadlock’ов

    • Всегда блокируйте ресурсы в одном и том же порядке (например, по возрастанию ID).
    • Используйте таймауты для транзакций (SET lock_timeout = '5s' в PostgreSQL).
    • В Go задавайте контекст с дедлайном: ctx, cancel := context.WithTimeout(ctx, 3*time.Second).
  5. Правильная обработка ошибок
    При получении 40001 (serialization failure) или 40P01 (deadlock detected) - повторяйте транзакцию с экспоненциальной задержкой.

Пример комплексного подхода для Go:

GO
func updateBalance(ctx context.Context, db *sql.DB, userID int, amount int) error {
for retries := 0; retries < 3; retries++ {
tx, err := db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelSerializable})
if err != nil { return err }
// Пессимистичная блокировка
_, err = tx.ExecContext(ctx, "SELECT balance FROM users WHERE id = $1 FOR UPDATE", userID)
if err != nil { tx.Rollback(); continue }
_, err = tx.ExecContext(ctx, "UPDATE users SET balance = balance + $1 WHERE id = $2", amount, userID)
if err != nil {
tx.Rollback()
if isDeadlockOrSerialization(err) { continue }
return err
}
return tx.Commit()
}
return fmt.Errorf("failed after retries")
}

Ключевое - тестировать под нагрузкой (например, с pgbench или go test -race) и выбирать стратегию исходя из частоты конфликтов: для редких - оптимистичные блокировки, для частых - пессимистичные.

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью