Перейти к содержанию

Dual write problem

Проблема двойной записи возникает, когда вы хотите изменить данные в двух системах. Например, записать что-то в БД и отправить сообщение в Kafka. Или отправить POST запрос по HTTP и сохранить файл на диск. В этих случаях вы не можете обеспечить транзакционность записи, если одна из операций вернет ошибку, то система может оказаться в не консистентном состоянии.

Решением данной проблемы может стать использования Transactional outbox паттерна или идемпотентного API.

Примеры

  1. Сначала делаем commit транзакции в БД, потом пытаемся отправить сообщение в Kafka. Система ломается в случае ошибки при записи в Kafka или в случае падения сервиса сразу после commit’а. После перезапуска сервис увидит, что в БД запись уже сделана и никак не сможет узнать, что отправка сообщения в Kafka не удалась.
    500
    Решение: использовать Transactional outbox паттерн.
    Иногда, в случае если у нас запись идет в БД + Kafka, можно использовать прием с чтением своих же сообщений из очереди. То есть, приложение, обрабатывая запрос, не делает никаких вставок в БД, а только отправляет сообщение в очередь с результатом обработки запроса. Далее из очереди это сообщение считывает не только конечный получатель, но и само приложение его отправившее. И уже после этого делает в БД вставку.

  2. Делаем отправку сообщения в Kafka внутри БД транзакции. Проблемы возникают, если нам не удается сделать commit и транзакцию нужно откатить, а сообщение в Kafka уже отправлено и его назад не вернуть.
    500
    Решение: такое же как в примере 1.

  3. Приложение A делает HTTP запрос на изменение данных в приложение B. Приложение B успешно меняет данные, делает commit транзакции, но по какой-то причине не может вернуть ответ со статусом 200 OK, например из-за неполадок сети, обрыва соединения по таймауту или падения приложения A. При этом приложение A, хоть и видит, что запрос завершен с ошибкой, но не может быть уверено, что изменения не были сделаны в БД приложения B.
    500
    Решение: API, предоставляемый приложением B, должен быть идемпотентным. Например, добавляем в API уникальное поле request_id с номером запроса. Сохраняем его в БД в поле с уникальным индексом в одной транзакции с остальной обработкой. В случае нарушения уникальности индекса приложение B понимает, что этот запрос уже был обработан и возвращает в качестве ответа HTTP код 2xx.