Какие методы кроме изменения уровня изоляции можно использовать для предотвращения проблем с параллельными транзакциями
Что проверяют
Знаете ли вы механизмы конкурентного доступа на уровне самих запросов, а не только таблицу уровней изоляции. Типичный сценарий: два запроса одновременно списывают деньги с одного баланса. Следом спрашивают, что изменится, когда сервис запущен в нескольких репликах.
Как отвечать
Перечислите приёмы от самого дешёвого:
- атомарный запрос: вместо «прочитать, посчитать в Go, записать» одна команда
UPDATE accounts SET balance = balance - $1 WHERE id = $2 AND balance >= $1. Проверка и изменение идут в одной строке под её блокировкой; - ограничения базы:
UNIQUE,CHECK (balance >= 0),INSERT ... ON CONFLICT. База держит инвариант, даже если код ошибся; - пессимистичная блокировка:
SELECT ... FOR UPDATEвнутри транзакции держит строку доCOMMIT.NOWAITсразу возвращает ошибку,SKIP LOCKEDпозволяет воркерам разбирать очередь без столкновений; - оптимистичная блокировка: колонка
versionиUPDATE ... WHERE id = $1 AND version = $2. ЕслиRowsAffected()вернул 0, кто-то успел раньше, операцию повторяют; - advisory locks:
pg_advisory_xact_lock(key)в PostgreSQL для бизнес-сущности, у которой нет одной строки; - порядок захвата: блокировать строки всегда в одном порядке, например по возрастанию
id, чтобы не ловить взаимоблокировки.
В Go вокруг этого: db.BeginTx(ctx, opts), сразу defer tx.Rollback(), таймаут через context.WithTimeout и lock_timeout на стороне базы. Отдельно назовите повтор транзакции при ошибках сериализации и дедлока, коды 40001 и 40P01, с ограниченным числом попыток.
Где спотыкаются
Защищают данные в базе через sync.Mutex. Он работает только внутри одного процесса, и со второй репликой гонка возвращается.
Делают SELECT ... FOR UPDATE вне транзакции или через db, а не tx. В режиме автокоммита блокировка снимается сразу после запроса.
Реализуют оптимистичную блокировку, но не проверяют число изменённых строк. Конфликт тихо проглатывается, и чужое обновление теряется.