Обеспечить строгую согласованность, охватывающую несколько агрегатов
Рассмотрим следующие бизнес-требования:
У нас есть игроки, которые могут играть в игры. Игрок может играть только в одну игру одновременно. Для игры нужны два игрока.
Система будет содержать миллионы игроков, а игры займут около двух минут. Проблемы параллелизма, вероятно, возникнут.
Мы хотим соблюдать правило, согласно которому одна транзакция включает в себя один агрегат. Кроме того, возможная согласованность не должна приводить к принятию игр, которые впоследствии должны быть отменены (даже в течение короткого периода времени) из-за проблем с параллелизмом. Таким образом, возможная последовательность не совсем уместна.
Как нам нужно определить агрегаты и их границы для обеспечения соблюдения этих бизнес-правил?
Я задумал два подхода:
1. Событийное рукопожатие
заполнитель Player
, совокупность Game
,
Когда игра запрашивается, она выдвигает GameRequested
-событие. Player
s подписаться на это событие и ответить соответствующим событием, либо GamePlayerAccepted
или же GamePlayerRejected
, Только если оба Player
s приняли, Game
начинается (GameStarted
).
Плюсы:
- Совокупность
Player
отвечает за управление собственной доступностью, которая соответствует модели домена
Минусы:
- Ответственность за запуск
Game
разбросано по нескольким агрегатам (это выглядит как "поддельная"-eventual-консистенция) - Много коммуникационных накладных расходов
- Необходимы меры согласованности, например, освобождение
Player
Если что-то пошло не так
2. Коллекция-агрегат
заполнитель Player
, совокупность GamesManager
(с коллекцией объектов-значений ActiveGamePlayers
), совокупность Game
,
GameManager
предлагается начать новый Game
с двумя данными Player
s. GameManager
способен гарантировать, что Player
играет только один раз за раз, так как это один агрегат.
Плюсы:
- Нет событий, обеспечивающих согласованность, таких как
GamePlayerAccepted
,GamePlayerRejected
и так далее
Минусы:
- Модель предметной области кажется неясной
- Ответственность
Player
управлять доступностью сдвинули - Мы должны обеспечить, чтобы только один экземпляр
GameManager
создается и внедряет доменные механизмы, которые позволяют клиенту не беспокоиться о промежуточном агрегате - независимый
Game
-старты мешают друг другу, потому чтоGameManager
Агрегат блокирует себя - Необходимость оптимизации производительности, так как
GameManager
-агрегат собирает всех активных игроков игры, которые будут десятки миллионов
Кажется, что ни один из этих подходов не подходит для решения проблемы. Я не знаю, как установить границы, чтобы обеспечить как строгую согласованность и ясность модели, так и производительность.
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, без использования тактического шаблона Aggregate. Для большей ясности, когда вы устанавливаете пару игроков в качестве активных, вы можете использовать оптимистическую блокировку или транзакции, где это поддерживается (низкий уровень масштабирования) или двухфазные фиксации (что-то уродливое).