[{"data":1,"prerenderedAt":17},["ShallowReactive",2],{"api:\u002Fapi\u002Fconfig:ru":3,"api:\u002Fapi\u002Fnotes\u002Fdouble-booking:ru":8},{"email":4,"lang":5,"name":6,"role":7},"hello@ewsirdev.example","ru","Ewsirdev","Веб-разработчик: сайты, лендинги и сервисы на Go",{"slug":9,"lang":5,"title":10,"date":11,"summary":12,"project":13,"minutes":14,"body_md":15,"body_html":16},"double-booking","Двойная бронь: транзакция плюс частичный UNIQUE-индекс","2026-10-20","Как booking-slots защищается от двух клиентов, которые одновременно записываются на одно время, и чем проверяется эта защита.","booking-slots",1,"Классическая гонка: два клиента одновременно видят свободный слот и оба нажимают «записаться». Одной проверки «свободно ли» в коде недостаточно: между проверкой и вставкой может вклиниться второй запрос.\n\n## Первая линия: транзакция\n\nВ `Book` пересчёт свободных слотов и вставка записи выполняются в одной транзакции. Слот, которого нет среди свободных, даёт `ErrTaken` (или `ErrPast`, если время уже прошло).\n\n## Вторая линия: индекс\n\nБаза сама не даст создать дубль:\n\n```sql\nCREATE UNIQUE INDEX IF NOT EXISTS ux_bookings_active_start\n  ON bookings(date, start_min) WHERE status = 'active';\n```\n\nИндекс **частичный**: он действует только для активных записей, поэтому отменённая запись освобождает время. Если вставка упирается в индекс, код превращает ошибку в тот же `ErrTaken`, и клиент получает 409.\n\n## Чем проверено\n\nТест запускает 30 горутин на одно и то же время: успешной должна быть ровно одна запись.\n\n## Границы\n\nЭто защита от двойной брони одного ресурса: все услуги делят одно расписание, и модель с несколькими специалистами потребует другого индекса. Об этом сказано в ограничениях проекта.\n","\u003Cp>Классическая гонка: два клиента одновременно видят свободный слот и оба нажимают «записаться». Одной проверки «свободно ли» в коде недостаточно: между проверкой и вставкой может вклиниться второй запрос.\u003C\u002Fp>\n\u003Ch2>Первая линия: транзакция\u003C\u002Fh2>\n\u003Cp>В \u003Ccode>Book\u003C\u002Fcode> пересчёт свободных слотов и вставка записи выполняются в одной транзакции. Слот, которого нет среди свободных, даёт \u003Ccode>ErrTaken\u003C\u002Fcode> (или \u003Ccode>ErrPast\u003C\u002Fcode>, если время уже прошло).\u003C\u002Fp>\n\u003Ch2>Вторая линия: индекс\u003C\u002Fh2>\n\u003Cp>База сама не даст создать дубль:\u003C\u002Fp>\n\u003Cpre tabindex=\"0\">\u003Ccode class=\"lang-sql\">CREATE UNIQUE INDEX IF NOT EXISTS ux_bookings_active_start\n  ON bookings(date, start_min) WHERE status = &#39;active&#39;;\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Индекс \u003Cstrong>частичный\u003C\u002Fstrong>: он действует только для активных записей, поэтому отменённая запись освобождает время. Если вставка упирается в индекс, код превращает ошибку в тот же \u003Ccode>ErrTaken\u003C\u002Fcode>, и клиент получает 409.\u003C\u002Fp>\n\u003Ch2>Чем проверено\u003C\u002Fh2>\n\u003Cp>Тест запускает 30 горутин на одно и то же время: успешной должна быть ровно одна запись.\u003C\u002Fp>\n\u003Ch2>Границы\u003C\u002Fh2>\n\u003Cp>Это защита от двойной брони одного ресурса: все услуги делят одно расписание, и модель с несколькими специалистами потребует другого индекса. Об этом сказано в ограничениях проекта.\u003C\u002Fp>\n",1792697892638]