Как стать автором
Обновить

Погружение в CQRS

Время на прочтение 9 мин
Количество просмотров 7.1K
Автор оригинала: Udi Dahan

Эта статья является конспектом материала Clarified CQRS.

Прежде чем начать разбираться с CQRS, нужно понять две основные движущие силы, стоящие за ним: сотрудничество и устаревание.

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

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

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

Рис.1. – Модель CQRS
Рис.1. – Модель CQRS

Компоненты на рисунке с названием AC являются автономными системами. Позже будет описано, что делает их автономными во время обсуждения команд (Commands – CQRS). Но для начала давайте разберемся с запросами (Queries – CQRS)

Запросы (Queries)

Если данные, которые собираемся показывать пользователям, все равно устаревшие, нужно ли идти в основную БД и получать их из нее? Зачем преобразовывать эти структуры в доменные объекты, если нам нужны только данные, а не поведение?

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

Как насчет того, чтобы создать дополнительное хранилище данных, данные которого могут быть немного не синхронизированы с основной БД – имеется в виду, что данные, которые показываются пользователям, все равно устарели, так почему бы не отразить их в самом хранилище данных?

Какая же будет структура такого хранилища данных? Как на счет по одной таблице для каждого представления? Данные формируются с помощью одного запроса SELECT * FROM MyViewTable и передаются пользователю на экран. Это было бы максимально просто. Можно при необходимости это обернуть тонким фасадом. В итоге данные для представления уже будут готовы и не нужно преобразовывать их во что-то другое (например, в доменные объекты).

Хранилище данных для запросов

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

Масштабирование запросов

Если в итоге запросы выполняются из отдельного хранилища данных, а не из главной БД, можно легко добавить больше экземпляров этих хранилищ. Тот же механизм, который обновляет один экземпляр, может использоваться для многих экземпляров.

Это дает дешевое горизонтальное масштабирование для запросов. Кроме того, поскольку выполняется меньше преобразований, задержка на запрос также снижается. Простой код – это быстрый код.

Модификация данных

Поскольку наши пользователи принимают решение на основе устаревших данных, нам нужно быть осторожными с тем, какие вещи мы пропускаем.

Допустим, у нас есть представитель службы поддержки клиентов, который разговаривает по телефону с клиентом. Этот пользователь смотрит на данные клиента на экране и хочет их изменить (адрес, ФИО и др.). Однако пользователь не знает, что после отображения данных на экране произошло некое событие. Из отдела выставления счетов пришло уведомление, что этот же клиент не оплачивает свои счета – они просроченные. На данном этапе пользователь отправляет данные для изменения. Стоит ли принимать эти изменения?

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

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

Чтобы предоставить правильный уровень детализации данных и избежать конфликтов используются команды (CQRS).

Команды (Commands)

Ключевым элементом CQRS является переосмысление дизайна пользовательского интерфейса, чтобы избежать конфликтов конкурентного доступа к данным. Использование пользовательского интерфейса, подобного Excel (как я понял, автор оригинала имел в виду сущности с большим количеством полей – денормализованные данные), для изменения данных не подходит.

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

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

Команды и валидация

При обдумывании того, что может привести к сбою команды, одна из тем, которая возникает - это валидация. Валидация отличается от бизнес-правил тем, что она устанавливает контекстно-независимый факт о команде. Либо команда действительна, либо нет. С другой стороны, бизнес-правила зависят от контекста.

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

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

Переосмысление UIs и команды с точки зрения валидации

Клиент может использовать хранилище запросов (Queries) для проверки команд. Например, перед отправкой команды, мы можем проверить, существует ли название улицы в хранилище запросов.

В этот момент мы можем переосмыслить UI и сделать автоматическое заполнение текстового поля для названия улицы, таким образом, гарантируя, что название улицы, которое мы передадим в команде, будет действительным. Но почему бы не пойти дальше, например, указывать ID улицы вместо ее названия. Пусть команда представляет улицу не в виде строки, а в виде ID (int, guid или др.).

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

Причины сбоя валидных команд и что с этим делать

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

В приведенном выше примере отклонение происходит только потому, что событие биллинга пришло первым. Но «первый» может быть за миллисекунду до команды. Что, если пользователь нажал кнопку на миллисекунду раньше? Должно ли это на самом деле изменить бизнес-результат?

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

Если мы решили не возвращать ошибки клиенту (который уже отправил нам валидную команду), возможно, все, что нужно сделать на клиенте при отправке команды, это уведомить пользователя: «Спасибо, вы получите подтверждение по электронной почте в ближайшее время». Нам даже не нужен виджет пользовательского интерфейса, показывающий ожидающие команды.

Команды и автономность

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

Кроме того, не нужно обращаться к хранилищу запросов для обработки команды. Состояние, которое затрагивает команда, должно управляться автономным компонентом – это часть смысла автономности.

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

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

Автономные компоненты

Хотя на рисунке 1 мы видим, что все команды передаются в один и тот же AC, мы могли бы обработать каждую команду другим AC, у каждой из которых своя очередь. Это дало бы нам представление о том, какая очередь является самой большой, что позволит легко увидеть, какая часть системы является узким местом. И можно будет добавить дополнительные узлы обработки очередей, чтобы масштабировать такие части системы, которые работают медленно.

Уровень обслуживания (service layer)

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

Смотря на уровень обслуживания с точки зрения команд мы видим, что объекты обрабатываются различными командами. Каждая команда не зависит от другой, так почему мы должны позволять объектам, которые их обрабатывают, зависеть друг от друга? Зависимости – это вещи, которых следует избегать, если для них нет веской причины.

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

Где доменная модель?

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

Еще одна вещь, которую следует понять – это то, что доменная модель не использует запросы (CQRS). Итак, вопрос в том, зачем нужно иметь так много связей между сущностями в доменной модели?

Действительно ли нам нужна коллекция заказов для сущности «клиент»? В какой команде нам нужно перемещаться по этой коллекции? Нужно ли в самом деле для команды отношение «один ко многим»?

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

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

Персистентность для обработки команд

Учитывая, что БД, используемая для обработки команд, не используется для запросов и что большинство команд содержат идентификаторы строк, на которые они будут влиять, действительно ли нужен столбец для каждого отдельного свойства объекта домена? Что, если просто сериализовать доменную сущность и поместить ее в один столбец, а другой столбец будет содержать идентификатор? Это звучит очень похоже на хранилище key-value. В таком случае действительно ли нужно объектно-реляционное преобразование данных? Также можно выделить дополнительные свойства для каждого фрагмента данных, которое требует обеспечения уникальности.

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

Синхронизация хранилища запросов

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

MakeCustomerPerferredCommand → CustomerHasBeenMadePerferredEvent

Публикация события выполняется транзакционно вместе с обработкой команды и изменениями в БД. Таким образом, любой сбой фиксации приведет к тому, что событие не будет отправлено.

Автономный компонент, который обрабатывает эти события и обновляет хранилище данных запросов, довольно прост. Он преобразует данные события в данные модели представления.

Ограниченный контекст

Хотя CQRS затрагивает многие части архитектуры ПО, он все еще не находится на вершине пищевой цепочки. Если используется CQRS, то в ограниченном контексте или бизнес-компоненте (SOA), который является частью предметной области. На события, публикуемые одним бизнес-компонентом, подписываются другие бизнес-компоненты, каждый из которых обновляет свои хранилища запросов и команд по мере необходимости.

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

Вывод

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

Невозможно по-настоящему оценить преимущества CQRS, не рассматривая пользовательский интерфейс. Принимая во внимание проверку на стороне клиента, структуры команд могут быть скорректированы. Обдумывание порядка, в котором обрабатываются команды и события, может привести к использованию нотификаций, которые сделают возврат ошибок ненужным.

Хотя вследствии применения CQRS в проекте приведет к более удобному обслуживанию и повышению производительности, эта простота и масштабируемость требует понимание подробных бизнес-требований.

Теги:
Хабы:
+7
Комментарии 0
Комментарии Комментировать

Публикации

Истории

Работа

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн
PG Bootcamp 2024
Дата 16 апреля
Время 09:30 – 21:00
Место
Минск Онлайн
EvaConf 2024
Дата 16 апреля
Время 11:00 – 16:00
Место
Москва Онлайн