Для чего используется SELECT FOR UPDATE?
Что проверяют
Понимаете ли вы пессимистическую блокировку и умеете ли правильно применить её из Go. Следом спрашивают про SKIP LOCKED, дедлоки и сравнение с оптимистической блокировкой по версии.
Как отвечать
SELECT ... FOR UPDATE читает строки и блокирует их до конца транзакции. Нужен он в сценарии «прочитал, проверил в коде, записал»: списать деньги, если хватает баланса, занять последний свободный слот. Без блокировки две транзакции прочитают одно значение и обе запишут своё, это потерянное обновление.
Поведение в PostgreSQL:
- кого блокирует: другие
UPDATE,DELETEиSELECT ... FOR UPDATE/FOR SHAREна тех же строках ждут. ОбычныйSELECTне ждёт, MVCC отдаёт ему последнюю зафиксированную версию; - варианты:
FOR NO KEY UPDATEслабее и не мешает вставкам дочерних строк с внешним ключом,NOWAITсразу возвращает ошибку,SKIP LOCKEDпропускает занятые строки. Последний даёт очередь задач прямо в таблице: воркеры берут разные задачи без конфликтов; - в Go: блокировка живёт только внутри транзакции.
tx, err := db.BeginTx(ctx, nil), затем все запросы черезtx.QueryRowContext, в концеtx.Commit(), аdefer tx.Rollback()страхует от ошибок. В pgx то же черезpgx.BeginFunc.
Если проверка укладывается в одно выражение, блокировка не нужна: UPDATE accounts SET balance = balance - $1 WHERE id = $2 AND balance >= $1 атомарен сам по себе.
Где спотыкаются
Выполняют FOR UPDATE через db.QueryRow, а не через tx. Запрос уходит в другое соединение пула в режиме автокоммита, и блокировка снимается сразу после него.
Считают, что FOR UPDATE запрещает другим читать строку. Читать можно, ждут только конкурирующие записи и блокирующие чтения.
Блокируют строки в разном порядке в разных местах кода и ловят дедлок. Помогает единый порядок, например ORDER BY id в запросе с FOR UPDATE, и lock_timeout, чтобы транзакция не висела бесконечно.