Что такое `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 (пропустить заблокированные строки).

Ответ 18+ 🔞

Смотри, вот эта штука SELECT ... FOR UPDATE — это как взять на кассе последний эклер и крикнуть: «ЭТО МОЁ, СУКИ, НЕ ТРОГАТЬ!», пока не расплатишься. По-умному — это блокировка строк в SQL, чтобы никто другой не мог их трогать, пока твоя транзакция не закончится.

Зачем это, блядь, нужно? Ну представь: два процесса одновременно пытаются снять бабки с одного счёта. Оба читают, что там 1000 рублей. Оба вычитают 500. Оба пишут 500. И в итоге на счету 500, а не ноль, как должно было быть. Пиздец, деньги испарились. Чтобы такого не было, ты сначала блокируешь запись, читаешь, меняешь, пишешь — и только потом отпускаешь. Всё по-честному.

Вот тебе живой пример на Go, как это может выглядеть:

// db - это твой *sql.DB
tx, err := db.Begin()
if err != nil {
    log.Fatal(err)
}
// На всякий случай подготовим откат, если всё пойдёт по пизде
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 (просто пропустить заблокированные строки и работать с тем, что свободно). Очень удобно для всяких очередей заданий.

Короче, инструмент мощный, но если использовать его как слон в посудной лавке — сами знаете, что будет. Пиздец и дедлоки.