Могут ли возникнуть проблемы с повторяющимися пользователями и параллельными запросами при добавлении пользователей
Что проверяют
Видите ли вы гонку «проверили, потом вставили» и знаете ли, что закрывает её только база. Интервьюер ждёт, что вы не остановитесь на existsByEmail в сервисе. Следом обычно спрашивают, что будет при нескольких экземплярах приложения и помогут ли synchronized или @Transactional.
Как отвечать
Проблема есть. Два запроса с одним email приходят почти одновременно, оба выполняют existsByEmail, оба получают false, оба вставляют строку. Причины обыденные: двойной клик, повтор после таймаута, ретрай на балансировщике.
Каркас ответа:
- уникальное ограничение в базе:
UNIQUEна email, лучше на нормализованное значение, например индекс поlower(email)в PostgreSQL. Это единственная защита, которая работает при любом числе экземпляров; - обработка конфликта: Spring переводит нарушение ограничения в
DataIntegrityViolationException, его ловят и отдают409 Conflictс понятным сообщением, а не 500; - момент ошибки: Hibernate откладывает
INSERTдо flush, поэтому исключение может вылететь при коммите, за пределами вашегоtry. Чтобы поймать его на месте, используютsaveAndFlush; - вариант без исключений:
INSERT ... ON CONFLICT DO NOTHINGв PostgreSQL через нативный запрос и проверка числа вставленных строк; - идемпотентность: для повторов с клиента заголовок
Idempotency-Keyи сохранённый результат первого запроса, тогда ретрай получает тот же ответ, а не ошибку.
Предварительную проверку в коде можно оставить ради понятного сообщения в обычном случае, но не как защиту.
Где спотыкаются
Предлагают synchronized на методе регистрации. Он работает только внутри одной JVM, а при двух экземплярах гонка остаётся. К тому же транзакция может закоммититься уже после выхода из блока.
Надеются на @Transactional. При изоляции READ COMMITTED по умолчанию транзакции не видят незакоммиченные строки друг друга и спокойно вставляют дубль. SERIALIZABLE в PostgreSQL поймает конфликт, но тогда нужны повторы транзакций.
Путают задачу с оптимистической блокировкой. @Version защищает существующую строку от потерянного обновления, а у новой строки сравнивать версию не с чем.