#Симптом и границы применимости
Повтор одного HTTP-запроса, задания очереди или транзакции после тайм-аута может создать две одинаковые бизнес-записи. Сетевая ошибка не доказывает, что первая транзакция откатилась; нужна идемпотентная операция с уникальным ключом.
#Как отличить возможные причины
| Возможная причина | Как узнать | Что делать |
|---|---|---|
| Повтор после потери ответаСервер сохранил запись, но клиент не получил подтверждение. | Две строки с одним номером заказа | Сверить журнал с идентификатором операции |
| Проверка без уникальностиПриложение сначала делает SELECT, затем INSERT, а параллельные процессы оба не находят строку. | Дубли только при конкурентной нагрузке | Добавить бизнес-ограничение UNIQUE после очистки |
| Разные ключи повтораДля каждой повторной попытки генерируется новый идентификатор операции. | Повторы имеют разные request_id | Передавать один ключ для всей операции |
#Пошаговая диагностика и безопасное исправление
- 1. Определите, что именно дублируетсяВ панели приложения найдите два одинаковых заказа или события и выпишите пользовательский ключ, время и ID запроса из журнала. Проверьте, не являются ли записи двумя законными покупками: удаление одной без проверки может нарушить учёт.
- 2. Проверьте, что означает сообщение о сбоеСравните момент выполнения INSERT с тайм-аутом клиента и записью COMMIT в журнале приложения. Если ответ потерялся после фиксации, повтор должен вернуть ранее созданный результат, а не делать ещё одну вставку.
- 3. Изучите действующие уникальные индексыВ mysql-клиенте запросите описание индексов вашей таблицы. Если уникален только технический id, он не мешает создать два заказа с одинаковым внешним идентификатором.
- Чтение; appdb.orders — примерSQLКомандаЧтение; appdb.orders — пример. Не создавайте индексы без проверки имеющихся дублей.
SHOW INDEX FROM appdb.orders;
- 4. Найдите повторы по бизнес-ключуНа копии базы выполните группировку по idempotency_key или другому реальному бизнес-идентификатору. Не используйте GROUP BY по имени покупателя: разные покупки могут иметь одинаковое имя.
- Чтение; idempotency_key — пример поля, которое должно сохраняться при всех повторахSQLКомандаЧтение; idempotency_key — пример поля, которое должно сохраняться при всех повторах.
SELECT idempotency_key, COUNT(*) AS n FROM appdb.orders GROUP BY idempotency_key HAVING COUNT(*) > 1 LIMIT 20;
- 5. Обеспечьте устойчивый ключ операцииНа уровне HTTP/очереди создавайте уникальный ключ один раз при начале операции и передавайте его неизменным при повторном запросе. В структуре тестовой базы после устранения исторических дублей установите UNIQUE на этот ключ. Перед DDL проверьте блокировки и резервную копию.
- 6. Применяйте атомарную вставку с повторным чтениемВ тестовой среде разработчик должен использовать уникальный индекс вместе с INSERT ... ON DUPLICATE KEY UPDATE либо перехватом ошибки уникальности и запросом уже существующей строки. Обновлять сумму заказа в ветке конфликта без бизнес-логики нельзя.
- 7. Смоделируйте потерю ответа в тестеОтправьте одну операцию и дважды повторите её с тем же ключом, имитируя тайм-аут после фиксации. Ожидается один заказ с одним ID; проверьте журнал клиента, сумму и побочные действия вроде оплаты и отправки письма.
- 8. Верните изменения только после успешной проверкиПри появлении двух записей на тесте вернитесь к проверке ключа и границ транзакции; не включайте повторную отправку на рабочих платежах. Переносите исправление поэтапно с наблюдением за числом конфликтов.
#Как проверить, что проблема устранена
Повторные запросы с одним ключом возвращают одну и ту же запись, а уникальное ограничение не допускает вторую строку; побочные действия также не повторяются.
#Если исправление не помогло
Проверьте, не находится ли запись платежа или событие внешней системы вне транзакции базы: тогда одной уникальности недостаточно, потребуется согласованный механизм повторной обработки.