Как должна вести себя система при ошибках сохранения данных в базе?
Что проверяют
Различаете ли вы временные и постоянные ошибки и умеете ли оставить данные согласованными. Для Java-разработчика это почти всегда разговор про @Transactional в Spring. Следом спрашивают, что будет, если после записи в базу нужно отправить сообщение в Kafka.
Как отвечать
Разделите ошибки на группы:
- временные: обрыв соединения, deadlock, таймаут блокировки. Spring оборачивает их в наследников
TransientDataAccessException, напримерCannotAcquireLockException. Их повторяют с паузой и лимитом попыток (Spring Retry, Resilience4j), каждую попытку в новой транзакции; - постоянные: нарушение уникальности или внешнего ключа, слишком длинное значение. Это
DataIntegrityViolationExceptionи его наследникDuplicateKeyException. Повтор бесполезен, клиенту уходит 409 или 422 с понятным текстом; - конфликт версий:
OptimisticLockingFailureException. Её повторяют, перечитав данные, или сообщают пользователю, что запись уже изменили.
Транзакция откатывается целиком. По умолчанию @Transactional откатывает на RuntimeException и Error, а на проверяемых исключениях коммитит, если не указать rollbackFor.
Стек и текст SQL наружу не отдаются: @RestControllerAdvice превращает исключение в ответ, а полная ошибка с correlation id идёт в лог. Письма и события отправляйте после коммита через @TransactionalEventListener(phase = AFTER_COMMIT) или transactional outbox, иначе уйдёт событие о записи, которой нет.
Где спотыкаются
Ловят исключение внутри @Transactional метода и глотают его. Если оно прошло через вложенный транзакционный вызов, например метод репозитория Spring Data, транзакция уже помечена rollback-only, и на коммите прилетает UnexpectedRollbackException.
Оборачивают repository.save() в try-catch и ждут там нарушения уникальности. В JPA запись часто уходит в базу только при flush, обычно на коммите (вставка с IDENTITY ключом исключение), и ошибка вылетает за пределами блока. Нужен saveAndFlush() или обработка уровнем выше.
Повторяют всё подряд, включая нарушение ограничений, и без идемпотентности. Повтор после таймаута, когда первая попытка на самом деле прошла, создаёт дубль.