Event Sourcing и CQRS на примере

Автор: Ануар Нурмаканов

Проектный пример

Пример о конференции

Пару слов обо мне

Казахстан, Караганда

Software Engineer

фанат практик XP/EngX

Верю в Agile

И люблю Java

В общем всем привет

sshogunn

Как меня найти

Как Event Sourcing стал частью плана?

К вам приходит заказчик

И просит сделать все иначе

Имеем, что имеем

а точнее кучу одинаковых проектов

Реляционная структура хранения данных

Классическая MVC модель

Новый проект = новые требования

Поехали!

Agenda

  • Введение в Event Sourcing and CQRS

  • Хотелки заказчика

  • Патерны решений

  • Event Sourcing. Что такое хорошо и что такое плохо!

Галопом по Европам

Или введение в Event Sourcing и CQRS

DDD для начала

DDD идеи

Сущность/Entity

Когда очень важно иметь идентификатор

Value Object

Когда value object прекрасен как часть

Агрегат/Aggregate

группа сущностей с одним root

Агрегат/Aggregate

Domain/Домен

CQRS

Команды и запросы

Командую "сидеть"

Прошу "принести палку"

Command and Query

CQRS

сюда идем за чтением

все операции по изменению

не забываем синхронизировать

Материализованное представление на уровне БД

Не похоже ли?

Материал.
представление

Основная структура данных

Прило
жение

И что????

Какие плюсы?

Когда только чтение или только запись под большой нагрузкой

То масштабируем только там, где надо!

Независимость при разделении

Сервис для записи

Сервис для чтения

Также доступен

Разные структуры для чтения

для отчета

для графиков

структура для списка

данные по нескольким агрегатам

Event Sourcing

Череда событий

Первое событие

Второе событие

...

...

...

...

...

Последнее событие

Пример

Подать заявку

Первое ревью идеи

...

Получить финальный отзыв

Череда событий

дельта изменений

Event Sourcing

Все события в начальной очередности

дельта изменений

дельта изменений

дельта изменений

дельта изменений

Историчность изменений и последовательность

Событие как разница

или дельта изменений

Вечные как камень

События не меняются никогда после сохранения

Удалить event

Изменить event

Пример из жизни

Контракт с вашим работодателем

Пример из жизни

Домашняя бухгалтерия

Тип(имя) события

Дельта изменений

Получение состояния

Snapshot

Конференция

Состояние

События

name: "JPoint 2018",
id: "CONF-785seeuis3",
location: "Moscow",
organizator: "JUG.ru",
status: "PLANNED"
NewConferencePlanned
id: "CONF-785seeuis3",
name: "JPoint 2018",
location: "Moscow",
organizator: "JUG.ru"
name: "JPoint 2018",
id: "CONF-785seeuis3",
location: "Almaty",
organizator: "JUG.ru",
status: "PLANNED"
ConferenceNewLocation
id: "CONF-785seeuis3",
location: "Almaty"
name: "JPoint 2018",
id: "CONF-785seeuis3",
location: "Almaty",
organizator: "JUG.ru",
status: "CANCELED"
ConferenceCanceled
id: "CONF-785seeuis3"

Новая конференция

Поменяли место проведения

Отменили =(

Возобновили =)

name: "JPoint 2018",
id: "CONF-785seeuis3",
location: "Almaty",
organizator: "JUG.ru",
status: "PLANNED"
ConferencePlannedAgain
id: "CONF-785seeuis3"

EventSourcing и CQRS

Event Sourcing и CQRS

сюда идем за чтением

все операции по изменению

не забываем синхронизировать

Прекрасно сочетаются

Но вполне независимы

Реляционная модель

Или

Но вполне незавимы

Пишем и читаем с event store

События как источник данных

Хотелки заказчика

Хочу Историчность!

Хочу Быструю запись!

Хочу Быстрое чтение!

Хочу Отчеты!

Хочу Full-text search!

Историчность изменений

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

© Сервантес

"Сделай мне почти также"

Но помни, у нас больше полей!

Как бы можно было бы это сделать

  • Cделан специально для Hibernate
  • Нужен Hibernate и Entities
  • А если у меня не ORM/RDBMS?
  • Eще один Framework
  • Асинхронный
  • Последний update 6 месяцев назад
  • Изобретаем велосипед
  • Конечно, он сделан под себя
  • Время = деньги

Audit4J

Hibernate Envers/Audit

Если с Event Sourcing

событие/история

Event Sourcing

событие/история

событие/история

событие/история

событие/история

Истории

Событие как история

ID aggregateId creationDate userId revision data type
{
    id: "CONF-785seeuis3",
    name: "JPoint 2018",
    location: "Moscow",
    organizator: "JUG.ru"
}
"CONF-45rs21u"
22/12/1990
"U-412gjk"
 1
23
"AddNewConference"

Как говорится

"Все из коробки"

Историчность в Event Sourcing

С высоты птичьего полета

PostgreSQL как Event Log

JSON Type как дельта изменений

Почему бы и не объединить

  • Тип схемы: статический и динамический

  • Атомарность транзакций в рамках схемы

  • Data Constraints

Agregate и Events

Агрегат

Событие 1

Событие 2

Событие 3

Событие 4

Событие 5

внешний ключ

Транзакция

Транзакция

Events в PostgreSQL

@Getter
@Setter
@ToString
@Entity
@Table(name = "EVENTS")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorValue("base")
@DiscriminatorColumn(name = "type")
@EqualsAndHashCode(of = {"id", "revision"})
@TypeDefs({
        @TypeDef(name = "jsonb", typeClass = JsonBinaryType.class)
})
public class BaseEvent<A extends BaseAggregate> implements Event {
    @Id
    @Type(type = "pg-uuid")
    @Column(name = "EVENT_ID")
    private UUID id;
    private int revision;
    @Column(name = "AGGREGATE_ID")
    private String aggregateId;
    @Column(name = "CREATION_DATE")
    private LocalDateTime creationDate = LocalDateTime.now();
    @Type(type = "jsonb")
    private String data;
    @Column(name = "USER_BUSINESS_ID")
    private String userId;
    @Transient
    private A aggregate;
}

Дельта изменений

Внешний ключ

Где-то в мире грустит один

Aggregate в PostgreSQL

@Getter
@Setter
@Entity
@Table(name = "AGGREGATES")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorValue("base")
@DiscriminatorColumn(name = "type")
@EqualsAndHashCode(of = {"id", "revision"}, callSuper = true)
public class BaseAggregate extends AbstractFunctionalAggregate {
    @Id
    @Column(name = "BUSINESS_ID")
    private String id;
    @JsonIgnore
    private int revision;
    private String description;
    @Column(name = "CREATION_DATE")
    private LocalDateTime creationDate = LocalDateTime.now();
    @Column(name = "MODIFICATION_DATE")
    private LocalDateTime modificationDate = LocalDateTime.now();
}

ConferenceAggregate

@Getter
@Setter
@ToString
@Entity
@EqualsAndHashCode(callSuper = true)
@DiscriminatorValue("conference")
public class ConferenceAggregate extends BaseAggregate<BaseEvent> {

    @Transient //это поле состояния, будет установлено событием
    private String name;
    @Transient ///это поле состояния, будет установлено событием
    private ConferenceStatus status = ConferenceStatus.PLANNED;
    
    ...
}

Построение состояния

Агрегат

Событие 1

Событие 2

Событие 3

Событие 4

Событие 5

Получаем агрегат

Получаем события

copyTo

Построение состояния

@Getter
@Setter
@Entity
@DiscriminatorValue(СonferenceCancelEvent.EVENT_TYPE)
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
public class СonferenceCancelEvent extends BaseEvent {
    public static final String EVENT_TYPE = "conference-cancel";

    @Override
    public void copyTo(Aggregate aggregate) {
        ConferenceAggregate conference= (ConferenceAggregate) aggregate;
        conference.setStatus(ConferenceStatus.CANCELED);
    }
    ....
}

Построение состояния

@Getter
@Setter
@Entity
@DiscriminatorValue(СonferenceRenameEvent.EVENT_TYPE)
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
public class СonferenceRenameEvent extends BaseEvent {
    public static final String EVENT_TYPE = "conference-rename";

    @EventFieldData // кастомная анотация, говорит о том, что поле будет в JSON
    @Transient //это не колонка в таблице, хранится внутри JSON
    private String name;

    @Override
    public void copyTo(Aggregate aggregate) {
        ConferenceAggregate conference= (ConferenceAggregate) aggregate;
        conference.setName(name);
    }
    ....
}

В поле data
(Base Event)

deserialize

name будет заполнен

Конференция

Конференция создана

Добавлена структура

Включен доклад

Обновлен доклад

...

{
    topic: "Event Sourcing & CQRS",
    speaker: "Ануар Нурмаканов",
    type: "Введение в технологию",
    day: "первый",
    sector: "Зал 1", 
    ...
}
{
    day: "второй"
}

Уникальность ревизии

1

2

3

4

...

...

...

...

...

...

aggregate id + revision number

Хочу Историчность!

Хочу Быструю запись!

Хочу Быстрое чтение!

Хочу Отчеты!

Хочу Full-text search!

Списки, они везде

Одна таблица

Другая таблица

...

Зачастую это все ведет к N-му количеству JOIN или нескольким запросам для получения данных

В итоге что-то пытаются кэшировать

Или подготовить на уровне БД

Если с CQRS

Наша тема =)

Процесс синхронизации

Процесс синхронизации

  • SQS - Simple Queue Service
  • SNS - Simple Notification Service

Материализованное представление

Вот так

Или так

Ну или так

Модели для чтения

Конференция

Информация о докладе Информация о спикере Ревьюверы Прошлые выступления

При регистрации

Как подтвердили

С большой частотой

Спустя
время

Конференция

Новый доклад

+ 1 ревьювер

первый
feedback

Логика и алгоритмы

Хочу Историчность!

Хочу Быструю запись!

Хочу Быстрое чтение!

Хочу Отчеты!

Хочу Full-text search!

Пишем, пишем и сохраняем

Одна таблица

Другая таблица

...

Запрос

БД

Тяжелые операции

создать/сохранить

Если с Event Sourcing

Одно хранилище событий

  • отсутствие операций "Update", только Insert
  • одна таблица для хранения событий
  • практические полное отсутствие constraint и других проверок на уровне БД
  • практические полное отсутствие индексации

Рекомендуется к прочтению

Хочу Историчность!

Хочу Быструю запись!

Хочу Быстрое чтение!

Хочу Отчеты!

Хочу Full-text search!

Отчетность

И еще графиков хочу разных

Решение в лоб!

Готовим данные на уровне БД

Материал.
представление

Основная структура данных

Прило
жение

Или же подключаем BI

Слушаем дядю Фаулера

Нужно синхронизировать. Например запускать процесс каждую ночь

Если CQRS

Представления для отчетов

Таблица с подготовленными данными для отчетов

Готовим данные для выборки и отчетов заблаговременно

Денормализация

Хочу Историчность!

Хочу Быструю запись!

Хочу Быстрое чтение!

Хочу Отчеты!

Хочу Full-text search!

SELECT to_tsvector('text') 
    @@ to_tsquery('query');
  • не нужно ставить другой движок

  • не нужны дополнительные сервера

  • нет проблем с синхронизацией

  • одно единственное хранилище

  • отсутствие опыта работы и доверия

Можно совместить

Забудьте об этом, если вы с AWS

Если CQRS

  • асинхронность сохранения по умолчанию

  • использование данных с materialized views

  • Materialized View как резервная копия для индексации

  • Дополнительная сложность репликации данных

Как-то мы смогли выкрутиться =)

Как итог

  • Историчность - Event Sourcing

  • Быстрое чтение - CQRS

  • Отчетность - CQRS

  • Быстрая(реактивная) запись - Event Sourcing

  • Full-text search - CQRS

 

Когда и почему

Идеальный пример событийной модели

Создали график

Открыли доступ

Сделали резервацию

Поменяли резервацию

Приостановили резервацию

Отменили

Неидеальный пример

Сохранил

Иногда отредактировал

Все

Мало событий! Важно финальное состояние!

В чем подвох

Когда надо знать лишь последнее состояние

Когда надо отследить каждое действие

Опции для реализации

Apache Kafka

  • У на свой Event Store DB
  • Синхронизатор
  • Read Model DB

У нас есть просто Apache Kafka =)

AWS вариант

Anton Udovychenko, Java Day Kyiev

Evantuate

Трудности и патерны

Concurrent изменения агрегата

переименовать хочу

хочу видеть этот доклад с 3:00 до 4:00

Шикарный доклад с 3:00 до 5:00

Root "Конференция"

Concurrent изменения агрегата

Locking/Блокировка

Забудьте об этом

У нас нет финального состояния

aggregate id + revision number

Вспоминаем уникальность ревизии

Нет

Ура!

AddNewSpeech {
    day: "one",
    start: "3:00",
    end: "4:00",
    stream: "1"
}
RenameConference{
    newName: "Joker rules!"
}
AddNewSpeech {
    day: "one",
    start: "3:00",
    end: "4:00",
    stream: "1"
}
RenameConference{
    newName: "Joker rules!"
}

События никак не конфликтуют между собой

AddNewSpeech {
    day: "one",
    start: "3:00",
    end: "4:00",
    stream: "1"
}
AddNewSpeech {
    day: "one",
    start: "3:00",
    end: "5:00",
    stream: "1"
}

Да

AddNewSpeech {
    day: "one",
    start: "3:00",
    end: "4:00",
    stream: "1"
}
AddNewSpeech {
    day: "one",
    start: "3:00",
    end: "5:00",
    stream: "1"
}

Это конфликт!

Да

не решаемый
конфликт

...

решаемый
конфликт

...

Ура!

Крупнозернистые события

Тип конфликта очевиден

Тип конфликта НЕ очевиден

Мелкозернистые события

Тип конфликта очевиден

Тип конфликта очевиден

Shapshots. Все-таки когда?

В какой-то момент стоит задуматься об производительности

Shapshots. Все-таки когда?

ключевое изменение состояния

черновик

активирован

Shapshots. Все-таки когда?

если чтение только с модели для чтения

Snapshot особо-то и не надо делать

Актуальность данных в модели для чтения

event лог

модель для чтения

Запись

Чтение

Удалить

Запись еще не удалена

Причины

  • Произошла ошибка обработки

  • Очень долгая обработка

  • Недоступность сервиса для чтения

Небольшие задержки

небольшие задержки это вполне нормально в большинстве своем

Инвестируйте в Message Broker

Но если вдруг...

Состояние

материализованное состояние

событие n-1

событие n

Проверки перед сохранением?

Root "Конференция"

Отправлю-ка я заявку на Joker в Санкт-Петербург

Ничего не проверяем

Root "Конференция"

Отправлю-ка я заявку на Joker в Санкт-Петербург

Command Sourcing подход

Root "Конференция"

Отправлю-ка я заявку на Joker в Санкт-Петербург

Event Sourcing подход

Root "Конференция"

Отправлю-ка я заявку на Joker в Санкт-Петербург

Запомнить

Event Sourcing подход

Сохранить заявку

Проверка качества

Заявка принята

Заявка отменена

Сохраняем наш event - заявка принята(даже если она не очень)

Запускаем все нужные проверки

Добавляем event после верификаций

Хьюстон, у нас проблемы

Посмотри что там в БД!

Ништяк

Запросы в БД?

Или

Используйте модель для чтения

Сколько может быть моделей для чтения?

В теории сколько угодно

Нам хватило и меньше

Подумай дважды

Из пушки по воробьям

Чем проще тем лучше

Из пушки по воробьям

C большим доменов

C одним субдоменом

Сервис компаний и пользователей

  • Историчность не нужна
  • Поиск пользователей по критериям
  • Поиск компаний по критериям
  • Минимальная работа с UI

Реляционная схема

Иной способ осмысления данных

Образование

Курсы

Первые проекты

Мы привыкли работать с финальным состоянием

Сложность для новичков

Также как все

ORM, RDBMS

Event Sourcing

Подытожим

Когда Event Soursing друг и враг

Друг

История и лог изменений

Event-Driven Architecture

Прокрутить события назад

Когда Event Soursing ваш ВРАГ

отсутствие событий как таковых

важно финальное состояние

много сложных запросов

Когда же CQRS?

много сложных запросов

неплох при event sourcing

большая нагрузка на чтение

разные модели для чтения

Что почитать и посмотреть

Книги и статьи

Люди

Chris Richardson

Steve Pember

Sebastian Daschner

Martin Fowler

Greg Young

Вопросы и ответы

Всем спасибо

Просыпаемся и идем на следующие доклады =))