Реализация ограничений на основе набора в CQRS - PullRequest
7 голосов
/ 05 декабря 2010

Я все еще борюсь с основными (и решаемыми) проблемами, связанными с архитектурой стиля CQRS:

Как мы реализуем бизнес-правила, основанные на наборе совокупных корней?

Возьмем, к примеру, заявку на бронирование. Это может позволить вам забронировать билеты на концерт, места для фильма или столик в ресторане. Во всех случаях на продажу будет выставлено ограниченное количество предметов.

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

На стороне запроса мы можем масштабироваться, и резервирование ставится в очередь для асинхронной обработки автономным компонентом. Сначала, когда мы извлекаем команды резервирования из очереди, мы принимаем их, но в определенный момент нам придется начинать , отклоняя остальные .

Как мы узнаем, когда достигнем предела?

Для каждой команды резервирования нам нужно будет запросить какое-то хранилище, чтобы выяснить, сможем ли мы удовлетворить запрос. Это означает, что нам нужно знать, сколько бронирований мы уже получили в то время.

Однако, если хранилище доменов является хранилищем нереляционных данных, таким как, например, Windows Azure Table Storage, мы не очень хорошо можем сделать SELECT COUNT(*) FROM ...

Один из вариантов - сохранить отдельный Aggregate Root , который просто отслеживает текущий счет, например:

  • АР: Бронирование (кто? Сколько?)
  • AR: Событие / Временной интервал / Дата (общее количество)

Второй Aggregate Root был бы денормализованным агрегированием первого, но когда базовое хранилище данных не поддерживает транзакции, тогда весьма вероятно, что они могут быть не синхронизированы в сценариях большого объема (что мы и делаем пытаемся обратиться в первую очередь).

Одним из возможных решений является сериализация обработки команд резервирования так, что обрабатывается только по одной за раз, но это идет вразрез с нашими целями масштабируемости (и избыточности).

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

Как мы справляемся с такими сценариями?

Ответы [ 4 ]

3 голосов
/ 23 декабря 2010

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

На самом деле это сводится к этой проблеме: если вам нужно обновить несколько ресурсов, как вы обеспечиваете согласованность при сбое второй операции записи?

Давайте представим, что мы хотим записывать обновления для ресурса A и ресурса B в последовательности.

  1. Ресурс A успешно обновлен
  2. Попытка обновить ресурс B не удалась

Невозможно откат первой операции записи влицо исключения, так что мы можем сделать?Поймать и подавить исключительную ситуацию для выполнения компенсирующего действия в отношении Ресурса А не является приемлемым вариантом.Во-первых, это сложно реализовать, но, во-вторых, это небезопасно: что произойдет, если первое исключение произошло из-за сбоя сетевого подключения?В этом сценарии мы также не можем написать компенсирующее действие против ресурса А.

Ключ лежит в явном идемпотентность .Хотя очереди Windows Azure не гарантируют семантику ровно один раз , они гарантируют семантику хотя бы один раз .Это означает, что перед лицом периодически возникающих исключений сообщение будет позже воспроизведено .

В предыдущем сценарии это происходит тогда:

  1. ResourceПопытка обновлена.Тем не менее, воспроизведение обнаружено, поэтому состояние A не влияет.Однако операция «запись» завершается успешно.
  2. Ресурс B. успешно обновляется.

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

2 голосов
/ 05 декабря 2010

Интересный вопрос, и с этим вы прибиваете одну из болевых точек в CQRS.

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

Однако - это не полностью отвечает на ваш вопрос.

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

Теперь. Можем ли мы сделать это возвращение менее болезненным? Конечно. Вставив ключ в наш распределенный кеш с процентом или количеством товаров на складе и уменьшив этот счетчик, когда товар продается. Таким образом, мы могли бы предупредить пользователя до того, как будет отправлен запрос на резервирование, скажем, если останется только 10% от первоначального количества товаров, что клиент не сможет получить товар, о котором идет речь. Если счетчик равен нулю, мы просто отказались бы принимать больше запросов на резервирование.

Моя точка зрения:

1) сообщите пользователю, что он делает запрос, и в этом может быть отказано 2) информировать пользователя о том, что шансы на успех при получении данного предмета низкие

Не совсем точный ответ на ваш вопрос, но именно так я бы поступил при таком сценарии при работе с CQRS.

1 голос
/ 06 декабря 2010

Давайте посмотрим на бизнес-перспективу (я имею дело с похожими вещами - бронирую встречи на свободных слотах) ...

Первое, что поражает меня в вашем анализе, это отсутствие понятия обронируемый билет / место / стол.Это резервируемые ресурсы.

В случае транзакции вы можете использовать некоторую форму уникальности, чтобы избежать двойного бронирования для одного и того же билета / места / стола (подробнее на http://seabites.wordpress.com/2010/11/11/consistent-indexes-constraints). Этот сценарий требует синхронной (но все еще одновременной) обработки команды.

В случае отсутствия транзакций вы можете задним числом отслеживать поток событий и компенсировать команду. Вы даже можете дать конечному пользователю опытожидание подтверждения бронирования, пока система не будет точно знать - т.е. после анализа потока событий - что команда выполнена и была или не была компенсирована (что сводится к «было ли бронирование сделано? да или нет?»). Другими словамикомпенсация может быть частью цикла подтверждения.

Давайте сделаем еще один шаг назад ...

Когда биллинг также задействован (например, продажа билетов онлайн), я думаю, что весь этот сценарий превращается всага в любом случае (резервный билет + билет билета). Даже без биллинга, вы быve сага (резервный стол + подтверждение бронирования), чтобы сделать опыт заслуживающим доверия.Таким образом, даже если вы только увеличиваете только один аспект бронирования билета / стола / места (то есть он все еще доступен), «длительная» транзакция не завершена, пока я не заплатил за нее или пока не подтвердил ее,Компенсация произойдет в любом случае, снова высвободив билет, когда я прерву транзакцию по любой причине.Интересной частью становится то, как бизнес хочет с этим справиться: возможно, какой-то другой клиент завершил бы транзакцию, если бы мы дали ему / ей тот же билет.В этом случае возврат может стать более интересным при двойном бронировании билета / места / стола - даже предлагая скидку на следующее / подобное мероприятие, чтобы компенсировать неудобства.Ответ лежит в бизнес-модели, а не в технической модели.

1 голос
/ 05 декабря 2010

eTag обеспечивает оптимистичный параллелизм, который вы можете использовать вместо транзакционной блокировки, чтобы обновить документ и безопасно обрабатывать потенциальные условия гонки.См. Примечания здесь http://msdn.microsoft.com/en-us/library/dd179427.aspx для получения дополнительной информации.

История может выглядеть примерно так: пользователь A создает событие E с максимальным количеством билетов 2, eTag равен 123. Из-за высокого спроса 3 пользователя пытаютсякупить билеты почти в то же время.Пользователь B создает запрос на резервирование B. Пользователь C создает запрос на резервирование C. Пользователь D создает запрос на резервирование D.

Система S получает запрос на резервирование B, считывает событие с помощью eTag 123 и изменяет событие, чтобы иметь 1 оставшегосятикет, S отправляет обновление, включающее eTag 123, который соответствует оригинальному eTag, поэтому обновление завершается успешно.ETag теперь 456. Запрос на резервирование одобрен, и пользователь уведомлен, что был успешным.

Другая система S2 получает запрос на резервирование C в то же время, когда система S обрабатывала запрос B, поэтому она также считывает событие событие с eTag 123, изменяет его на 1 оставшийся билет и пытается обновить документ.Однако на этот раз eTag 123 не совпадает, поэтому обновление завершается с ошибкой.Система S2 пытается повторить операцию, перечитав документ, который теперь имеет eTag 456 и счетчик 1, поэтому он уменьшает его до 0 и повторно отправляет с eTag 456.

К сожалению для пользователя C, система S запустиласьобрабатывает запрос пользователя D сразу после пользователя B, а также читает документ с помощью eTag 456, но, поскольку система S работает быстрее, чем система S2, она смогла обновить событие с помощью eTag 456 до системы S2, поэтому пользователь D также успешно зарезервировал свой билет.eTag теперь 789

Таким образом, система S2 снова дает сбой, дает еще одну попытку, но в этот раз, когда она читает событие с eTag 789, она видит, что билетов нет, и, таким образом, отклоняет запрос бронирования пользователя C.

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

...