Обеспечьте строгую согласованность, охватывающую несколько агрегатов

Учтите следующие бизнес-требования:

У нас есть игроки, которые могут играть в игры. Игрок может играть только в одну игру за раз. Для игры нужны два игрока.

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

Мы хотим соответствовать правилу, согласно которому одна транзакция включает единую совокупность. Кроме того, возможная согласованность не должна приводить к принятию игр, которые должны быть впоследствии отменены (даже если на короткий период времени) из-за проблем с параллелизмом. Таким образом, конечная согласованность не совсем подходит.

Как нам нужно определять агрегаты и их границы, чтобы обеспечить соблюдение этих бизнес-правил?

Я задумал два подхода:

1. Рукопожатие на основе событий

Совокупный Player, агрегированный Game.

Когда игра запрашивается, она отправляет GameRequested-событие. Players подписываются на это событие и отвечают соответствующим событием, либо GamePlayerAccepted, либо GamePlayerRejected. Только если оба Players приняли, Game запускается (GameStarted).

Плюсы:

  • Агрегат Player отвечает за управление своей собственной доступностью, которая соответствует модели предметной области.

Минусы:

  • Ответственность за запуск Game разбросана по нескольким агрегатам (это похоже на «фальшивую» согласованность событий)
  • Значительные накладные расходы на связь
  • Необходимые меры согласованности, например освобождение Players, если что-то пошло не так

2. Коллекция-агрегат

Агрегат Player, агрегат GamesManager (с набором объектов-значений ActiveGamePlayers), агрегат Game.

GameManager запрашивается, чтобы начать новый Game с двумя заданными Player. GameManager может гарантировать, что Player будет воспроизводиться только один раз, поскольку это единый агрегат.

Плюсы:

  • Нет событий, обеспечивающих согласованность, таких как GamePlayerAccepted, GamePlayerRejected и т. Д.

Минусы:

  • Модель предметной области кажется неясной
  • Сменилась ответственность Player за управление доступностью
  • Мы должны гарантировать, что создается только один экземпляр GameManager, и ввести доменные механизмы, которые позволят клиенту не беспокоиться о посредническом агрегате.
  • Независимые Game-старты мешают друг другу, потому что GameManager-агрегат блокирует себя
  • Необходимость оптимизации производительности, поскольку GameManager-aggregate собирает всех активных игроков, которых будут десятки миллионов.

Похоже, что ни один из этих подходов не подходит для решения проблемы. Я не знаю, как установить границы, чтобы обеспечить как строгую согласованность и ясность модели, так и производительность.


person Luca Nate Mahler    schedule 07.06.2017    source источник
comment
Вы можете пересмотреть свое решение, чтобы быть очень догматичным в отношении границ транзакции. Иногда вы нарушаете правила. Если догмы мешают вам продемонстрировать ценность - это плохой знак.   -  person Alexey Zimarev    schedule 08.06.2017
comment
@AlexeyZimarev Что бы вы посоветовали?   -  person Luca Nate Mahler    schedule 08.06.2017
comment
У меня нет прямого предложения, так как я мало знаю о вашем домене. Есть много способов исправить это. Некоторые используют кеш приложения предварительной обработки, который не позволяет вам отправлять потенциально неудачные команды, только один из вариантов. Допускается наличие некоторых ограничений вне модели предметной области. Или вы можете использовать транзакцию. Или вы можете использовать маршрутную накладку с компенсирующими действиями.   -  person Alexey Zimarev    schedule 08.06.2017


Ответы (1)


Я бы пошел с рукопожатием на основе событий, и вот как я бы реализовал:

Насколько я понимаю, вам понадобится Game процесс, реализованный как Saga. Вам также нужно будет определить агрегат Player, команду RequestGame, событие GameRequested, событие GameAccepted, событие GameRejected, команду MarkGameAsAccepted, команду MarkGameAsRejected, событие GameStarted и событие GameFailed.

Итак, когда Player A хотят сыграть в игру с Player B, Player A получает команду RequestGame. Если этот игрок играет во что-то еще, возникает PlayerAlreadyPlaysAGame исключение, в противном случае оно вызывает событие GameRequested и обновляет его внутреннее состояние как playing.

Сага Game перехватывает событие GameRequested и отправляет команду RequestGame агрегату Player B (это агрегат Player с ID равным A). Потом:

  • Если Player B играет в другую игру (он знает это, запрашивая свое внутреннее playing состояние), то он вызывает событие GameRejected; сага Game перехватывает это событие и отправляет MarkGameAsRejected команду Player A; затем Player A вызывает событие GameFailed и обновляет его внутреннее состояние как not_playing.

  • Если Player B не играет в другую игру, он вызывает событие GameAccepted; сага Game перехватывает это событие и отправляет команду MarkGameAsAccepted в Player A агрегат; Player A затем генерирует событие GameStarted и обновляет его внутреннее состояние как playing.

Чтобы понять это, вы должны попытаться смоделировать сценарий использования, как если бы компьютеров не существовало, а игроками были бы люди, которые общаются по печатной почте.

Это решение масштабируемое, и я понимаю, что это необходимо.

Другое решение не подходит для миллионов игроков.

Третье решение - использовать совокупность активных игроков в таблице SQL или совокупность NoSQL без использования тактического шаблона "Агрегат". Для обеспечения согласованности при установке пары игроков в качестве активных вы можете использовать блокировку optimistick или транзакции, если они поддерживаются (низкая масштабируемость), или двухфазные фиксации (что-то уродливое).

person Constantin Galbenu    schedule 07.06.2017
comment
Спасибо за ответ. Вы правы, я также придумал, какие мероприятия мне понадобятся. Но вопрос скорее в том, следует ли мне вообще использовать подход событий или разработать другую концепцию для обеспечения согласованности, например, совокупность активных игр. Подход событий кажется слишком сложным и разрозненным, а подход совокупного агрегирования кажется затрудняющим понимание модели и, возможно, неэффективным. Итак, мой вопрос касается концепции, а не деталей реализации. - person Luca Nate Mahler; 07.06.2017
comment
Вам нужно принимать во внимание производительность. С моим ответом нет необходимости в централизованном сборнике всех игроков, поэтому он масштабируемый. Архитектура коллекции не масштабируется: вы упомянули миллионы игроков. - person Constantin Galbenu; 07.06.2017
comment
Да, именно по этой причине мне не нравится метод сбора; но были бы способы избежать загрузки всех активных игр сразу, а вместо этого выборочно проверять наличие соответствующих активных игр, извлеченных игроками - но да, мы как бы возвращаемся к проблемам с производительностью, которые должна была решить конечная согласованность. И меня беспокоит тот факт, что игрок не выражает свою собственную доступность и поэтому привязан к более высокому контексту (возможно, к самой системе), в котором размещен единственный экземпляр GameManager (или ActiveGames, или любое другое собственное имя). - person Luca Nate Mahler; 07.06.2017
comment
Разве не было бы более естественным, если бы Player AR не отвечал за возникновение GameRequested события. Мне кажется, что было бы лучше, если бы команда запускала Game диспетчер процессов, а не событие из Player? - person plalx; 13.06.2017
comment
Саги не выделяют событий. - person Constantin Galbenu; 13.06.2017
comment
@ConstantinGALBENU Спасибо. Теперь мне интересно, как в целом решаются временные отношения с помощью DDD. Должно быть много случаев, когда несколько компонентов (агрегатов) должны составлять единицу на некоторое время, а затем снова разрешаться. Это сложное рукопожатие события не подходит для такого рода отношений обхода. Вы знаете, есть ли лучшая практика? - person Luca Nate Mahler; 15.06.2017
comment
Вы говорите, что бывают случаи, когда два или более агрегата должны сохраняться в одной транзакции? - person Constantin Galbenu; 15.06.2017
comment
@ConstantinGALBENU Вроде. Но это указывало бы на неправильные совокупные границы. Рассмотрим простое переходное отношение 1: 1, которое подразумевает, что нельзя назначать другие компоненты уже назначенному компоненту. Как бы вы решить эту проблему, не имея двух транзакций? - person Luca Nate Mahler; 15.06.2017
comment
Так же, как и в реальном мире; допустим, мужчина хочет обручиться с женщиной, которая находится в отдаленном городке. Мужчина объявляет себя помолвленным, затем он идет в другой город и просит женщину нанять его. Если бы она уже обручилась с другим мужчиной, наш мужчина освободился бы. В противном случае они оба станут помолвлены друг с другом. - person Constantin Galbenu; 15.06.2017
comment
@ConstantinGALBENU Хорошо, я думаю, это просто необходимое зло с соображениями компромисса. - person Luca Nate Mahler; 15.06.2017
comment
Как всегда. В любом случае, DDD хорош, потому что код / ​​архитектура выглядит как реальный мир / бизнес. - person Constantin Galbenu; 15.06.2017