Позвольте мне описать случай, когда полезны агрегатные версии:
В нашей reSove Framework агрегатная версия используется для оптимистичного управления параллелизмом.
Я объясню это на примере.Допустим, InventoryItem
агрегат принимает команды AddItems
и OrderItems
.AddItems
увеличивает количество товаров на складе, OrderItems
- уменьшает.Предположим, у вас есть InventoryItem
агрегат # 123 с одним событием - ITEMS_ADDED
с количеством 5. Агрегат # 123 сообщает, что на складе 5 товаров.
Итак, ваш пользовательский интерфейс показывает пользователям, что их 5товары на складеПользователь А решает заказать 3 предмета, пользователь Б - 4 предмета.Обе команды OrederItems
выдают почти одновременно, скажем, пользователь А сначала на пару миллисекунд.
Теперь, если у вас есть один экземпляр агрегата # 123 в памяти, в одном потоке у вас нет проблем - первая команда от пользователя A будет успешной, событие будет применено, состояние скажет количестворавно 2, поэтому вторая команда от пользователя B не будет выполнена.
В распределенной или безсерверной системе, где команды от A и B будут находиться в отдельных процессах, обе команды будут успешными и приведут агрегат в неправильное состояние, если мы не используем какой-либо контроль параллелизма.Есть несколько способов сделать это - пессимистическая блокировка, очередь команд, агрегатное хранилище или оптимистическая блокировка.
Оптимистическая блокировка представляется наиболее простым и практичным решением:
Мы говорим, что когда-либо агрегат имеет версию - число событий в своем потоке.Таким образом, наш агрегат # 123 имеет версию 1.
Когда агрегат генерирует событие, эти данные события имеют агрегатную версию.В нашем случае ITEMS_ORDERED
события от пользователей A и B будут иметь агрегированную версию событий 2. Очевидно, что у агрегированных событий должны быть версии, чтобы последовательно увеличиваться.Поэтому нам нужно просто установить ограничение базы данных, чтобы кортеж {aggregateId, aggregateVersion}
был уникальным при записи в хранилище событий.
Давайте посмотрим, как наш пример будет работать в распределенной системе с оптимистичным управлением параллелизмом:
- Пользователь A выдает команду
OrderItem
для агрегата # 123 - Агрегат # 123 восстанавливается из событий
{version 1, quantity 5}
- Пользователь B выдает команду
OrderItem
дляaggregate # 123 - Еще один экземпляр Aggregate # 123 восстанавливается из событий (версия 1, количество 5)
- Экземпляр агрегата для пользователя A выполняет команду, успешно, событие
ITEMS_ORDERED {aggregateId 123, version 2}
isзаписывается в хранилище событий. Экземпляр агрегата для пользователя B выполняет команду, успешно, событие ITEMS_ORDERED {aggregateId 123, version 2}
пытается записать ее в хранилище событий и fais с исключением параллелизма.
В таком обработчике исключений команда для пользователя B просто повторяет всю процедуру - тогда Агрегат # 123 будет в состоянии {version 2, quantity 2}
, и команда будет выполнена правильно.
Надеюсь, это прояснит ситуацию, когда используются агрегатные версии.