Пример использования FOR UPDATE в PostgreSQL
Что проверяют
Зачем вообще блокировать строку и как это выглядит в Go: запрос обязан идти внутри явной транзакции, а блокировка держится до её конца. Дальше почти всегда спрашивают про очередь задач и про взаимоблокировки.
Как отвечать
select ... for update блокирует выбранные строки: другая транзакция, которая захочет их изменить или взять ту же блокировку, будет ждать коммита. Нужно это, когда решение принимается между чтением и записью: проверить остаток и списать, выдать следующий номер, забрать задание из очереди.
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()
var balance int64
err = tx.QueryRowContext(ctx,
`select balance from accounts where id = $1 for update`, id).Scan(&balance)
if err != nil {
return err
}
if balance < amount {
return ErrInsufficientFunds
}
if _, err = tx.ExecContext(ctx,
`update accounts set balance = balance - $1 where id = $2`, amount, id); err != nil {
return err
}
return tx.Commit()Назовите варианты: nowait отдаёт ошибку вместо ожидания, skip locked даёт честную очередь, где каждый воркер берёт свободные строки. И оговорку: если операция выражается одним запросом с условием на остаток, блокировка не нужна, достаточно проверить число затронутых строк.
Где спотыкаются
Пишут for update через db.QueryRow, без транзакции. Неявная транзакция закрывается сразу, блокировка снимается вместе с ней, защиты нет, а тест в один поток этого не показывает.
Берут строки в разном порядке в разных местах кода и получают взаимоблокировку, одну из транзакций база снимает сама. Лечится фиксированным порядком (order by id) и повтором операции по коду ошибки.
Держат блокировку во время похода во внешний сервис или забывают завершить транзакцию на ветке с ошибкой. Строки заперты до конца транзакции, соединения пула кончаются, и встаёт всё приложение. Внешние вызовы выносятся за транзакцию, defer tx.Rollback() ставится сразу после открытия.