Event sourcing (источники событий, регистрация событий, генерация событий) это мощный архитектурный шаблон, при котором все изменения, вносимые в состояние приложения, сохраняются в той последовательности, в которой они происходили. Эти записи служат как источником для получения текущего состояния, так и журналом аудита того, что происходило в приложении за время его существования. Event sourcing способствует децентрализованному изменению и чтению данных. Такая архитектура хорошо масштабируется и подходит для систем, которые уже работают с обработкой событий или хотят перейти на такую архитектуру.
Что такое Event Sourcing
Эксперты предметной области обычно описывают свои системы как совокупность сущностей (entity), представляющих собой контейнеры для хранения состояния и событий (event), отображающих изменения сущностей в результате обработки входных данных в рамках различных бизнес-процессов. Часто события инициируется командами (command), исходящими от пользователей, фоновых процессов или интеграций с внешними системами.
По сути, Event sourcing фокусируется на событиях, связанных с изменениями в системе.
Многие архитектурные шаблоны рассматривают сущности как первоочередную концепцию. Эти шаблоны описывают то, как их сохранять, как к ним получать доступ и как их модифицировать. В рамках такого архитектурного стиля события часто находятся сбоку: являются последствиями изменения сущностей.
Обычно в основе подобных архитектур лежит хранилище сущностей, например, реляционная база данных или документное хранилище. Хотя в такой архитектуре могут присутствовать события, но они, по своей сути, не являются первоочередной концепцией, и могут быть отделены от сущностей, с которыми они связаны, а также скрыты за слоями бизнес-логики.
Event Sourcing переворачивает этот подход, фокусируясь на реализации событий: на том как они сохраняются и как могут быть использованы для получения состояния сущности. В данном случае в базе данных будет последовательный журнал всех событий, которые произошли за время существования системы.
Ниже показано сравнение хранилища событий (Event Store) с хранилищем сущностей (Entity Store) (подробнее будет описано далее):
Event sourcing, используя события в качестве основной архитектурной концепции, также является парадигмой моделирования предметной области, лучше отражающей представление заказчика о системе. Проектирование систем с акцентом на события и журналы событий дает следующие преимущества:
- Помогает уменьшить несоответствие импеданса и необходимость в сопоставлении концепций, позволяя технологическим командам говорить на одном языке с бизнесом при обсуждении системы.
- Поощряет разделение ответственности на команды и запросы (command/query responsibility), позволяя оптимизировать запись и чтение независимо друг от друга.
- Обеспечивает темпоральность и историю изменений, как само собой разумеющееся, позволяя отвечать на вопросы о том, как система выглядела в определенные моменты в прошлом и какие события происходили до этого момента.
Принцип работы Event Sourcing
Рассмотрим простой пример с банковским счетом. У нас будет сущность (entity), представляющая собой банковский счет (Bank Account). Для простоты сделаем только один счет без его идентификации с помощью номера счета или каким-либо другим способом. Счет будет хранить текущий остаток средств.
Для счета будут доступны две команды (command): внести деньги (deposit) и снять деньги (withdraw). В командах будет указываться сумма для внесения или снятия. Также определим бизнес-правило, которое проверяет, что команда на снятие средств может быть обработана только в том случае, если запрашиваемая сумма равна или меньше текущего остатка на счете.
При таком подходе можно выделить два события (event) Account Credited (Счет пополнен) и Account Debited (Средства списаны со счета). В этих событиях есть информация о сумме (amount), которая была внесена или снята. Здесь можно было бы упростить до одного события с положительной или отрицательной суммой, но в данном примере мы их разделим.
На диаграмме ниже показана модель данных.
Обратите внимание, что события это прошедшее время. Они указывают на то, что произошло в системе в момент их записи, и сохраняются только в том случае, если обработка команды прошла успешно. При таком подходе необходимо проявлять осторожность, чтобы не перепутать команды с событиями. Особенно если они зеркально похожи друг на друга.
Последовательность команд может выглядеть следующим образом:
1. deposit { amount: 100 } внести 100
2. withdraw { amount: 80 } снять 80
3. withdraw { amount: 50 } снять 50
Самая простая реализация Event Sourcing требует журнала событий (event log), который представляет собой просто последовательность событий. При обработке команд, приведенных выше, получится такой журнал.
Третья команда не может быть выполнена, так как запрошенная сумма превышает доступный баланс.
Для получения текущего баланса система должна обработать или сгенерировать события в порядке их возникновения. Для нашего примера это может выглядеть следующим образом:
- bank account { current balance: 0 } (starting state)
банковский счет {текущий баланс: 0 } (начальное состояние) - bank account { current balance: 100 } (processed: Account
Credited, +100)
банковский счет {текущий баланс: 100 } (обработано: Счет пополнен, +100) - bank account { current balance: 20 } (processed: Account
Debited, -80)
банковский счет {текущий баланс: 20 } (обработано: Средства списаны со счета, -80)
Текущий баланс вычисляется через обработку всех событий до текущего момента. Так как каждое событие имеет неявную метку времени, то можно вычислить состояние счета на любой момент времени, обработав все события за необходимый промежуток времени.
Это законченный (хоть и тривиальный) пример Event Sourcing. В реальной системе, скорее всего, этот пример придется расширить.
Возможно, потребуется сохранять последовательность команд, чтобы была возможность идентифицировать, как возникло событие, а также сделать отдельный журнал ошибочных событий (error event), в который записывать команды, которые не удалось выполнить, для дальнейшей обработки ошибок и ведения полной истории успешных и неуспешных команд.
Со временем, при увеличении количества команд, может потребоваться сохранять текущий баланс счета, чтобы при получении команды на снятие средств не нужно было обрабатывать полный список событий для определения возможности выполнения команды (т. е. имеется ли на счете достаточно средств). Это пример производного хранилища и, по сути, то же самое, что и хранилище сущностей.
Ниже показано, как будет выглядеть хранилище сущностей для нашего примера после обработки всех команд.
Очевидно, что по сравнению с полноценным хранилищем событий (Event Store), это очень примитивный пример. И это одна из причин, по которой многие разработчики используют только хранилище сущностей. В этом случае текущий остаток на счете доступен сразу и нет необходимости обрабатывать все исторические события.
Однако Event Sourcing не исключает хранилища сущностей. Часто хранилища сущностей присутствуют и в Event Sourcing проектах.
Конец первой части.