Двухфазных коммит (2PC, Two-Phase Commit)

В рамках своих недавних задач появилась необходимость спроектировать систему (а точнее доработать существующую) используя принцип двухфазного коммита. Всё началось с того, что обнаружили проблему с несогласованностью данных (data inconsistency) в том процессе, когда мастер-система посылала запрос на сохранение определенных сущностей в две другие подсистемы. И с точки зрения бизнеса эти системы были обязаны либо обе сохранить свои сущности, либо откатить изменения, если хотя бы в одной из них что-то пошло не так.

Однако, у нас это не было предусмотрено, потому что изначально мы делали распределенную систему, в которой эти объекты не должны были зависеть друг от друга - но что-то пошло не так, как это часто бывает =)

Поэтому пришлось в спешке придумывать различные варианты решения данной проблемы, пока на проде не столкнулись с последствиями. Выбор был между SAGA-паттерном, оркестрацией или хореографией (об это я где-то давно рассказывал в канале) и двухфазным коммитом. Выбрали последнее, т.к. посчитали это более надежным решением, потому что SAGA-паттерн подразумевает следующее: ➖Мастер-система отправила событие в два топика подсистем о необходимости сохранить данные; ➖Одна из подсистем ответила успехом и отправила уже свои события своим потребителям, а вторая ответила ошибкой; ➖Мастер-система это обработала и отправила компенсационное сообщение в подсистему, которая ответила успехом; ➖Подсистема обработала, удалилаоткатила изменения и отправила сообщение об откате транзакции своим потребителям. И проблема подхода в нашем случае в том, что т.к. у нас безумная нагрузка и большое количество сообщений, то между 2 и 4 пунктом может пройти достаточное количество времени и всё это время у потребителей будет некорректная информация.

Поэтому от такого подхода отказались и пришли к 2PC.

Если упрощенно, то он работает следующим образом: ➖Мастер-система отправляет сначала события в подсистемы, начиная распределенную транзакцию. Подсистемы вычитывают эти сообщения, проверяют все бизнесовые валидацииштуки, которые должны проверить, но пока не публикуют результаты своих действий, держа эти события "в уме" (а по факту в отдельных табличках, в которых эти объекты хранятся до следующего шага); ➖Начинается Фаза Голосования: мастер-система отправляет команду-опрос, спрашивая: "Ребята, готовы ли вы окончательно закоммититься под этими сообщениям? Выполните все необходимые проверкидействия, чтобы в этом убедиться"; ➖Мастер-система собирает ответы подсистем и приступает к Фазе Решения: ➖➖ Если все участники ответили "Да", то мастер-система сохраняет к себе эти ответы (на случая какого-то сбоя, чтобы потом эту транзакцию можно было восстановить) и отправляет команду commit подсистемам; ➖➖➖Подсистемы окончательно переносят данные из временных хранилищ в основные таблицы и публикуют эти события, отправляя их своим потребителям. Также отвечают мастер-системе успехом, что данные закоммичены; ➖➖Если хотя бы один участник ответил "Нет", то мастер-система сохраняет к себе эти ответы и отправляет команду rollback тем подсистемам, которые ответили "Да". ➖➖➖Подсистемы откатывают изменения, удаляют данные из временных хранилищ.

У этого подхода есть свои недостатки, потому что достаточно сильно усложняется логика и увеличивается время обработки каждой транзакции. Однако, т.к приоритет был именно на обязательном одновременном сохранении транзакции в двух подсистемах, то пришлось этим жертвовать. Вообще было очень интересно с разработчикамиархитекторами брейнштормить эту задачу, было придумано множество супер-интересных решений, которые тоже могли бы подойти, но в силу их минусов пришлось от них отказаться. Я изначально предложил именно SAGA-паттерн, но потом посчитали нагрузку, потенциальные проблемы с неконсистентными данными у потребителей и продолжили штормовать эту тему, пока не пришли к единому варианту.

Приходилось ли вам на практике использовать какие-нибудь из перечисленных выше паттернов?