Как реализовать выдачу книги одному читателю с учетом уникальности и предотвращения конфликтов
Что проверяют
Понимаете ли вы, что сервис обычно работает в нескольких репликах, и мьютекс в памяти одного процесса уникальность не гарантирует. Её должна обеспечивать база. Следом спрашивают, что получит второй из двух одновременных запросов и как сделать повтор после таймаута безопасным.
Как отвечать
Начните с того, где живёт истина, и перечислите способы:
- ограничение в базе: таблица
loansсbook_id,reader_id,issued_at,returned_atи частичный уникальный индекс в PostgreSQL:CREATE UNIQUE INDEX ON loans (book_id) WHERE returned_at IS NULL. Вторая одновременная вставка упадёт с кодом23505; - обработка в Go: с драйвером
pgxошибка разбирается черезerrors.Asв*pgconn.PgError, код23505превращается в доменнуюErrBookAlreadyIssued, а наружу уходит HTTP 409; - условный UPDATE:
UPDATE books SET issued_to = $1 WHERE id = $2 AND issued_to IS NULLи проверка, чтоRowsAffected()равен единице. Проверка и запись происходят атомарно, одним запросом; - пессимистичная блокировка:
SELECT ... FOR UPDATEв транзакции, когда перед выдачей нужны дополнительные проверки, например лимит книг на читателя; - оптимистичная блокировка: колонка
versionи условиеWHERE version = $n, при нуле обновлённых строк конфликт; - идемпотентность: если тот же читатель повторяет запрос, вернуть успех, а не конфликт. Помогает ключ идемпотентности от клиента.
sync.Mutex уместен, только если на собеседовании прямо сказали «без базы, всё в памяти». Тогда лучше блокировать конкретную книгу, а не всю библиотеку, и заметить вслух, что при рестарте состояние пропадёт.
Где спотыкаются
Решают глобальным мьютексом и не замечают, что реплик три. Каждая держит свой замок, и книга спокойно уходит двум читателям.
Делают «проверить, потом записать» двумя запросами. На уровне изоляции Read Committed, который в PostgreSQL стоит по умолчанию, обе транзакции увидят книгу свободной и обе запишут выдачу.
Распознают конфликт по тексту ошибки или отдают клиенту 500. Текст меняется между версиями и локалями, а стабилен только код SQLSTATE.