Как обеспечить идемпотентность в операциях с базой данных
Что проверяют
Отличаете ли вы идемпотентность от атомарности и понимаете ли, откуда берутся повторы: ретраи клиента, повторная доставка из брокера, повтор после обрыва на коммите. Следом обычно спрашивают, что будет, если два одинаковых запроса придут одновременно.
Как отвечать
Определение короткое: повторное выполнение операции оставляет систему в том же состоянии, что и первое. Дальше инструменты на стеке .NET:
- ключ идемпотентности: клиент передаёт заголовок
Idempotency-Key, сервер пишет ключ в таблицу с уникальным ограничением в той же транзакции, что и бизнес-изменение. Повторный запрос ловит нарушение уникальности и получает сохранённый ответ; - уникальные ограничения в базе: защиту от дублей обеспечивает индекс, а не проверка в C#. На
DbUpdateExceptionс кодом нарушения уникальности (2627 или 2601 в SQL Server, 23505 в PostgreSQL) операция считается уже выполненной; - upsert:
INSERT ... ON CONFLICT DO NOTHINGв PostgreSQL,MERGEсHOLDLOCKв SQL Server, иначе при конкуренцииMERGEсам создаёт дубли; - абсолютные и условные изменения:
UPDATE Orders SET Status = 'Paid' WHERE Id = @id AND Status = 'Pending'повторять безопасно, аBalance = Balance + 100нет. В EF Core такое пишется черезExecuteUpdateAsync; - оптимистичная блокировка:
rowversionчерез атрибут[Timestamp]илиIsConcurrencyToken(), конфликт приходит какDbUpdateConcurrencyException; - Inbox для сообщений: потребитель хранит id обработанных сообщений, потому что брокеры гарантируют доставку «хотя бы один раз».
Где спотыкаются
Отвечают «оберну в транзакцию». Транзакция даёт атомарность, но повторный вызов просто выполнит вторую транзакцию.
Делают «проверил, что записи нет, потом вставил» без уникального ограничения. Два параллельных запроса оба проходят проверку и оба вставляют.
Включают EnableRetryOnFailure и забывают, что при обрыве связи на коммите EF Core не знает, прошёл ли он, и может повторить уже применённую операцию.