Что такое `SELECT FOR UPDATE` и в каких случаях его стоит использовать? Приведите пример на Go.

Ответ

SELECT ... FOR UPDATE — это SQL-конструкция, которая позволяет заблокировать строки, возвращаемые запросом, до завершения текущей транзакции. Это необходимо для предотвращения состояния гонки (race condition) в операциях типа «прочитать-изменить-записать» (read-modify-write).

Основной сценарий использования: когда вам нужно прочитать значение из строки, выполнить на его основе какие-то вычисления, а затем обновить эту же строку, гарантируя, что никто другой не изменил её между вашим чтением и записью.

Пример на Go: обновление баланса пользователя.

// db - это ваш *sql.DB
tx, err := db.Begin()
if err != nil {
    log.Fatal(err)
}
// defer tx.Rollback() гарантирует откат транзакции, если что-то пойдет не так
defer tx.Rollback()

var balance int
// Блокируем строку с id=123 до конца транзакции
err = tx.QueryRow("SELECT balance FROM accounts WHERE id = $1 FOR UPDATE", 123).Scan(&balance)
if err != nil {
    // Если строка уже заблокирована другой транзакцией, здесь может быть ошибка или ожидание
    log.Fatal(err)
}

// Выполняем бизнес-логику
newBalance := balance + 100

// Обновляем данные в той же транзакции
_, err = tx.Exec("UPDATE accounts SET balance = $1 WHERE id = $2", newBalance, 123)
if err != nil {
    log.Fatal(err)
}

// Если все успешно, фиксируем изменения
err = tx.Commit()
if err != nil {
    log.Fatal(err)
}

Ключевые моменты и риски:

  • Область действия: Блокировка действует только в рамках транзакции и снимается после COMMIT или ROLLBACK.
  • Взаимоблокировки (Deadlocks): Неправильное использование может привести к дедлокам, когда две транзакции ждут друг друга, заблокировав нужные каждой ресурсы. Важно, чтобы транзакции блокировали ресурсы в одном и том же порядке.
  • Производительность: Блокировки снижают параллелизм и могут стать узким местом в системе. Используйте их только там, где это действительно необходимо.
  • Вариации: Некоторые СУБД, например PostgreSQL, поддерживают опции NOWAIT (немедленно вернуть ошибку, если строка заблокирована) и SKIP LOCKED (пропустить заблокированные строки).