Проблема «двойного письма»


Отказ от ответственности: Эта статья основана на реальных фактах 😬.

В этой статье я познакомлю вас с проблемой «двойной записи» и последствиями этой проблемы в реальной системе, над которой я работал. Я не рассчитываю дать вам окончательный ответ, как справиться с этой проблемой, но надеюсь, что это введение заставит вас понять проблему, задуматься о ней, узнать, как ее выявить и избежать.

Проблема

Давайте начнем с описания нашей проблемы. У нас была система, которая делала следующее:

  1. Пользователь создает новый отзыв
  2. Служба reviewService создает обзор в хранилище как ожидающий.
  3. Служба reviewService вызывает службу approvalService, отправляя сообщение в систему модерации, чтобы одобрить или не одобрить обзор. Этот процесс происходит асинхронно.
  4. Система отвечает на запрос с идентификатором ожидающего рассмотрения, чтобы пользователь мог позже проверить его статус, был ли он одобрен или отклонен.

В коде наш вариант использования будет выглядеть следующим образом:


package com.hugomarques;

public class ReviewController {

    private final ReviewRepository reviewsRepository;
    private final ApprovalWorkflow approvalService;

    ReviewController(ReviewRepository repository) {
        this.repository = repository;
    }

    @PostMapping("/reviews")
    @Transactional
    Review newReview(Review newReview) {
        var pendingReview = reviewsRepository.save(newReview);
        approvalService.send(pendingReview);
        return pendingReview;
    }
} 
Войдите в полноэкранный режим Выход из полноэкранного режима

Много лет назад, в середине 2009-2010 годов, этот код, вероятно, был бы развернут на сервере JBoss, хранилище было бы MySQL или PostgresSQL, а служба approvalService находилась бы за очередью JMS. И почему эти детали важны? Эти технологии вместе обеспечивали транзакционный контроль, т.е. если отправка сообщения в очередь не удалась, транзакция будет отменена в базе данных.

Сегодня эта же система (по разным причинам) может быть реализована без JBoss в качестве сервера приложений, хранилище может использовать AWS DynamoDB, а мессенджер — AWS SQS. К какой проблеме это приводит? У нас больше нет транзакционного контроля. Если наше сообщение не будет отправлено, у DynamoDB не будет способа отката 😱.

Правдивая история

Одна из команд, с которой я работал, столкнулась с этой проблемой некоторое время назад. В один прекрасный день команда начала получать сообщения о том, что некоторые обзоры блокируются в статусе dependent.

Я был дежурным инженером и решил провести анализ. Я проверил, что каждый раз, когда обзор ожидает рассмотрения, наш код выдает исключение "Failed to send message to queue".

Я проанализировал код и увидел нечто похожее на пример в этой статье. Что еще хуже, код, отправивший сообщение, находился внутри блока try/catch, блок catch регистрировал ошибку и больше ничего не делал. Другими словами, операция была успешно возвращена пользователю!

Если вы не поняли суть проблемы, то вместо идеального потока, который мы имеем в примере, этот поток ошибок выглядел следующим образом:

  1. Пользователь создает новый отзыв
  2. Служба reviewService создает обзор в хранилище как ожидающий.
  3. Служба reviewService вызывает службу approvalService, отправляя сообщение в систему модерации, чтобы одобрить или не одобрить обзор. Этот процесс происходит асинхронно.
  4. Сообщение по какой-либо причине не удалось отправить.
  5. Служба reviewService регистрирует сообщение "Не удалось отправить сообщение в очередь".
  6. Служба reviewService возвращает пользователю отложенный отзыв.
  7. Поскольку отзыв не был отправлен в поток утверждения, он будет ожидать рассмотрения вечно!

"Наивное" решение

После того как мы обнаружили проблему, мы решили применить самое простое решение: обработать ошибку непосредственно в коде, перехватив ошибку/исключение и выполнив вызов отката к службе 1. Наш новый поток выглядит следующим образом:

Давайте посмотрим на реализацию:


package com.hugomarques;

public class ReviewController {

    private final ReviewRepository repository;
    private final ApprovalWorkflow approvalWorkflowService;

    ReviewController(ReviewRepository repository) {
        this.repository = repository;
    }

    @PostMapping("/reviews")
    Review newReview(Review newReview) {  
        var pendingReview = repository.save(newReview);
        try {
           var approvedReview 
 = approvalWorkflowService.start(pendingReview);
           return approvedReview;
        catch (Exception e) {
           repository.delete(pendingReview);
        }
    }
} 
Войдите в полноэкранный режим Выход из полноэкранного режима

Обратите внимание, что я назвал это решение наивным. Почему? Что произойдет, если у вашей службы возникнет проблема с ошибкой справедливого отката?

Обратите внимание на диаграмме выше, когда мы пытаемся откатиться назад, вызывая repository.delete DynamoDB может вернуть ошибку 400. Следовательно, при таком сценарии наше рассмотрение останется незавершенным.

Несмотря на наивность решения, оно позволило сократить количество несоответствий более чем на 90%. Будучи дешевым и простым решением, этого было достаточно, чтобы "остановить кровотечение".

Для более надежного решения команде пришлось бы перепроектировать некоторые части системы и использовать некоторые паттерны микросервисов, такие как SAGAs и/или "Transactional Outbox", но это уже сюжет для следующих глав.

Заключение

Подведение итогов того, что мы узнали на сегодняшний день:

  1. Остерегайтесь последовательных вызовов распределенных систем без транзакционного контекста. Это рецепт проблем с двойной записью и несогласованности данных.
  2. Иногда простое решение - это все, что вам нужно, в зависимости от ваших масштабов.
  3. Хотя в данном случае речь идет не об этом, вы теперь знаете, что существуют и другие шаблоны для работы с ошибками такого типа.

Надеюсь, вам понравилось. Если вам понравилось, обязательно следите за моими советами на twitter @hugaomarques.

Особая благодарность @rponte и @zanfranceschi за рецензию на статью и идеи.

Хотите узнать больше?

В твиттере продолжается интересная дискуссия со ссылками на другие решения и стандарты, в которых рассматривается эта проблема.

Уго Маркес
@hugaomarques
Еще одна статья прямо из печи. Я сбился со счета, сколько раз я рефакторил это 😅.

#sistemasdistribuidos #initiante

dev.to/hugaomarques/o...

20:57 - 06 мая 2022 г.

Оцените статью
Procodings.ru
Добавить комментарий