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

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

Пример системы

Начну сразу с примера системы. Система состоит из двух сервисов, оба написаны с применением эвент-сорсинга.

Первый сервис отвечает за проведение платежей. Так выглядит стейт-машина воображаемого платежа.

1. Стейт-машина платежа
1. Стейт-машина платежа

Второй сервис отвечает за проведение заказов. Заказ представлен такой стейт-машиной.

2.Стейт-машина заказа
2.Стейт-машина заказа

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

3. Флоу заказа целиком
3. Флоу заказа целиком

А так система выглядит на уровне компонентов.

4. Система на уровне компонентов
4. Система на уровне компонентов

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

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

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

По ходу статьи я буду ссылаться на некоторые части системы, поэтому пробегитесь хотя бы по картинкам!

Что такое эвент-сорсинг

Теперь давайте разбираться что такое эвент-сорсинг. Цель главы — понять основные составляющие и выделить термины.

Оттолкнёмся от определения из статьи майкрософта. Да и, в целом, вся статья неплохая, рекомендую к прочтению, но только после моей!

Instead of storing just the current state of the data in a domain, use an append-only store to record the full series of actions taken on that data. The store acts as the system of record and can be used to materialize the domain objects. This can simplify tasks in complex domains, by avoiding the need to synchronize the data model and the business domain, while improving performance, scalability, and responsiveness. It can also provide consistency for transactional data, and maintain full audit trails and history that can enable compensating actions.

Ссылка: https://learn.microsoft.com/en-us/azure/architecture/patterns/event-sourcing

Объяснять буду на примере сервиса заказов.

5. Стейт-машина заказа
5. Стейт-машина заказа

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

6. От снимков состояний к событиям
6. От снимков состояний к событиям

Когда состояние системы хранится в виде снимков состояния — состояние объекта в системе представлено изменяемой строкой или строками в базе данных. При обновлении состояния объекта мы перезаписываем строку новыми значениями.

Когда применяется эвент-сорсинг — состояние хранится в виде последовательности неизменяемых событий. При обновлении состояния объекта нужно сохранить новые события не меняя старые.

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

В коде это некоторый неизменяемый объект, пример.

7. Стрим
7. Стрим

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

Стрим в коде удобно представить в виде DDD-агрегата, потому что стрим как и DDD-агрегат образует границу согласованности данных.

An Event Stream is a list of Events that form a consistency unit that you might call an aggregate if you practice Domain-Driven Design or otherwise an entity, business object, …

Ссылка: https://itnext.io/event-sourcing-explained-b19ccaa93ae4#:~:text=An Event Stream is a,keep it simple for now

8. Лог
8. Лог

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

9. Эвент-стор
9. Эвент-стор

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

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

10. Лог
10. Лог

Начнем с проекций.

Проекции или как их ещё называют материализованные представления это состояние объекта или объектов в оптимизированном для чтения виде. В оптимизированном для чтения значит, что данные для обработки некоторого [get-]запроса подготовлены заранее и для их получения не требуется выполнения сложных операций, в частности, не нужно восстанавливать состояние объекта из стрима событий, а так же не нужно делать серии вызовов для того, чтобы получить все нужные для обработки запроса данные.

Примерами проекций могут быть снимки состояний объектов, отчеты и другие данные, которые необходимо показать на UI. К слову, если захочется сделать какой-то джоин (join), то сделать это можно будет только с помощью проекций, события джойнить сложновато :-)

11. Фоновая работа
11. Фоновая работа

Теперь про фоновую работу.

Под этой фразой я понимаю запрос к некоторому внешнему сервису, который потребляет много ресурсов, CPU или времени — обращение к модулю text-to-speech или публикация интеграционных эвентов в шину, всё что угодно, что не относится напрямую к обработке текущего запроса. Имея возможность подписки на новые события, становится возможным выполнять эти задачи асинхронно, реактивно и в фоне, что делает нашу систему более отзывчивой и более контролируемой.

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

12. Оффсеты
12. Оффсеты

Концептуально обработка эвентов в фоне выглядит так же как в кафке, концепции буквально те же, лог = топик, партиции = шарды, консьюмер группы и консьюмеры даже можно называть так же.

13. Обработка эвентов в фоне как в кафке
13. Обработка эвентов в фоне как в кафке

Мы можем добавлять новых консьюмеров на события в любое время, и так как мы храним все данные, мы можем обрабатывать события с нужного нам момента, с самого первого эвента в системе или с текущего момента, например.

Что касается транзакций.

Фоновая обработка эвентов избавляет нас от необходимости в транзакциях. Вообще, транзакции нужны для того, чтобы обеспечить консистентность данных. В случае когда мы используем транзакции мы имеем строгую консистентность (strong consistency), т.е. когда база данных нам отвечает ОК, это значит что все объекты изменились и все изменения записаны. В случае с эвент-сорсингом мы имеем консистентность в конечном счете (eventual consistency), но за то наша система становится отзывчивой, задачи очень хорошо отделяются друг от друга, что делает код проще и надёжнее.

Вкратце, это вся теория.

Влияние на архитектуру приложения

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

Command Query Responsibility Segregation

14. CQRS
14. CQRS

Следствием того, что в приложении с применением эвент-сорсинга нет транзакций, но есть проекции является то, что в таких приложениях изначально логически разделены модели данных для чтения (read) и для записи (write). По-сути, CQRS.

Domain Driven Design

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

Фоновая обработка

15. Тяжелые операции смещаются в фоновые сервисы
15. Тяжелые операции смещаются в фоновые сервисы

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

Акторы

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

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

Что ещё стоит знать

В этом разделе я собрал вопросы, в которых точно стоит разобраться до того, как применять эвент-сорсинг.

Чем отличаются эвенты от команд

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

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

Ещё есть такая концепция как команд-сорсинг. Суть заключается в том, чтобы хранить команды, которые исполняет система, для того, чтобы иметь возможность проиграть их заново. В результате такого воспроизведения мы получим другое (новое) системы. В эвент-сорсинге же мы храним состояние и можем воспроизводить одно конкретное состояние в разные моменты времени. Почитать ещё про команд-сорсинг можно тут.

Как скейлить допустимую read нагрузку

Представим, что у есть система, которая:

  • состоит из одного НЕшардированного лога

  • в рамках шарда эвенты упорядочены

  • есть обработчик, который обрабатывает эвенты последовательно 0, 1, 2, 3...

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

16. Скейлим read компоненты
16. Скейлим read компоненты

В случае проблем с нагрузкой на read компоненты мы можем просто увеличивать количество инстансов API и реплик базы данных и таким образом справляться с любой нужной нам нагрузкой. По-умолчанию всё скейлится горизонтально.

Более интересный случай, когда нам не хватает скорости, с которой работает какой-нибудь подписчик на write базу данных.

17. Скейлим обработчики эвентов
17. Скейлим обработчики эвентов

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

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

Если вы используете, например, MySql, то придётся всё сделать самостоятельно, если же вы используете, например, Azure Cosmos DB, то вопрос скейлинга решен из коробки. Тут можно почитать о том как это сделано в Azure Cosmos DB, обратите внимание на шардирование, ченжфид и балансировку.

Подходы могут быть разные в зависимости от задачи, например, если важно, чтобы события одного стрима обрабатывались последовательно, то нужно распределять работу так, чтобы события одного стрима попадали в один и тот же обработчик, если нет, то можно распределять опираясь на SequenceId, round-robin-ом или как-то ещё.

Как скейлить допустимую write нагрузку

В прошлом параграфе мы обсудили скейлинг read нагрузки в случае когда лог не шардирован. Шардирование лога позволяет скейлить допустимую write нагрузку.

18. Скейлинг write компонентов
18. Скейлинг write компонентов

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

19. Несколько шардов
19. Несколько шардов

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

Как избежать head-of-line блокировок при обработке эвентов

С head-of-line блокировками вы столкнётесь, если будете делать медленные операций напрямую в обработчике эвентов из базы данных. Медленные операции это, например, вызовы АПИ внешних систем или вызовы какого-то ресурсоёмкого кода. Если это не учитывать, то возможна ситуация, когда обработка эвента 5 в первой линии затормозит и все последующие эвенты будут ожидать своей очереди до тех пор, пока эвент 5 не обработается.

20. Head-of-line blocking
20. Head-of-line blocking

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

Что делать если нужно удалить некоторые данные

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

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

  2. Перезаписать данные некоторого эвента — это приведёт к тому, что все проекции, которые использовали этот эвент требуется перестроить

  3. Удалить один стрим физически — это приведёт к тому, все проекции, которые использовали этот эвент требуется перестроить

Но это ещё не все — если данные эвента как-то распространялись по системе, то вычистить их повсеместно может быть очень сложно.

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

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

Как делать UI

21. Задержка обновления read моделей
21. Задержка обновления read моделей

Допустим мы показываем пользователю страницу на сайте, для чего используем некоторую проекцию и пользователь имеет возможность как-то менять данные на странице. Так как проекции (read модели) не строго-консистентны с write моделью, то они обновляются с некоторой задержкой, и нам нужно обработать ситуацию когда пользователь нажал на кнопку сохранения, но проекции ещё не обновились.

Какие решения:

  1. Делать честный task-based UI. Это решение подразумевает то, что нужна некоторая инфраструктура для работы с [асинхронным] задачами, и, в частности, отслеживания состояния задач (в очереди → исполняется → результат). Главный недостаток этого решения в его высокой сложности.

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

  3. Ещё более дешёвое — поллинг на бэкенде. Суть заключается в том, чтобы не завершать запрос в момент, когда мы сохранили эвент, а поллить read базу до тех пор пока нужная модель не достигнет нужной версии, примерно тоже самое, что в предыдущем пункте. Недостаток заключается в том, что ресурсы тоже будут расходоваться на поллинг, но ещё и сетевые подключения будут существовать относительно долго.

  4. Ничего не делать. Главный недостаток — пользователи будут страдать, а с ними и вы от количество вопросов о том, почему всё не работает :-)

Стоит ли интегрировать сервисы с помощью эвент-сорсинга

Несколько раз мне попадалась идея о том, чтобы использовать эвент-сорсинг как средство интеграции [микро] сервисов. Я считаю, что это плохое решение потому что приводит к излишней связности и хрупкости. Если доменные эвенты используются для решения задач интеграции — это уже интеграционные эвенты и они предполагают наличие некоторого контракта, который нужно соблюдать, что накладывает ограничения на внесение изменений в код сервиса.

22. Разделение интеграционных и доменных эвентов
22. Разделение интеграционных и доменных эвентов

Я за разумный подход, стоит применять эвент-сорсинг в ограниченных рамках, например, в рамках одного bounded context, допустим, сервис платежей может быть написан с применением event-sourcing, или сервис заказов может быть написан с применением event-sourcing. Но каждый из них — отдельный сервис и интегрируются они стандартно — с помощью интеграционных эвентов или АПИ.

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

Почитать ещё о проблемах интеграции сервисов с помощью эвент-сорсинга можно тут.

Что если в транзакции обновлять агрегаты

Возможно в предыдущем пункте у вас возникла идея использовать [распределенные] транзакции для обновления проекций. Думаю, это возможно но есть и свои минусы:

  • Неконтролируемо растет сложность и время записей

  • Место где происходит запись в базу становится хрупким и сложным, так как требует внесения изменений каждый раз при изменении проекций

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

Если имеет место необходимость иметь read модель строго-консистентную с write моделью, то возможно эвент-сорсинг не самое подходящее решение. Стоит рассмотреть возможность сохранения ревизий объектов — тоже append-only сторедж, но хранить нужно не события, а версии снимков состояния объектов. Так делают блоговые движки, вроде wordpress — сохраняют ревизии постов.

Выбираем технологию для event store

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

Предлагаю дополнить ваш список требований к технологии такими вопросами:

  1. Есть ли возможность упорядочить данные?

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

  2. Есть ли готовые решения для подписки на новые эвенты?

  3. Возможно прочитать отдельный стрим?

  4. Возможно ли прочитать данные сразу после записи? (Уровень консистентности)

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

  5. Как скейлить запись и чтение?

    Нам интересно что предлагается из коробки для решения задачи и или существование популярные готовые решения.

  6. Возможность модифицировать записанные данные

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

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

???? MySQL (или другие rdbms)

  1. Есть ли возможность упорядочить данные?

    Есть, с помощью авто-инкремента.

  2. Есть ли готовые решения для подписки на новые эвенты?

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

  3. Возможно прочитать отдельный стрим?

    Да

  4. Возможно ли прочитать данные сразу после записи? (Уровень консистентности)

    По умолчанию у нас строгий уровень консистентности и такая гарантия есть.

  5. Как скейлить запись и чтение?

    Шардирование. Коробочных решений нет, но есть условно готовые решения, вроде ProxySQL.

  6. Возможность модифицировать записанные данные

    Да, полный контроль над данными

На мой взгляд это самый гибкий вариант, но и ручной работы при этом много.

❌ MongoDB

  1. Есть ли возможность упорядочить данные?

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

    Если использовать ченж стримы — https://www.mongodb.com/docs/manual/changeStreams/, то возможно упорядочивать данные вручную не нужно, так как записи в оплоге будут упорядочены за нас, но это решение со звёздочкой.

  2. Есть ли готовые решения для подписки на новые эвенты?

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

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

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

  3. Возможно прочитать отдельный стрим?

    Да

  4. Возможно ли прочитать данные сразу после записи? (Уровень консистентности)

    По умолчанию у нас строгий уровень консистентности и такая гарантия есть

  5. Как скейлить запись и чтение?

    Шардирование, решено из коробки

  6. Возможность модифицировать записанные данные

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

В моём понимании mongo не подходит для использования в качестве event-store так как слишком хрупка, хотя очень даже подходит для использования как read база данных.

???? Azure CosmosDB

  1. Есть ли возможность упорядочить данные?

    Нет, но данные упорядочены для нас в ченж фидах.

  2. Есть ли готовые решения для подписки на новые эвенты?

    Да, причем проблемы скейлинга и балансировки нагрузки решены за нас из коробки. https://learn.microsoft.com/en-us/azure/cosmos-db/change-feed

    Когда я пробовал эту функцию пару лет назад было ограничение в скорости поллинга, кажется, не быстрее ~500ms. А так же каждый запрос к ченжфиду биллится и это просто напросто может оказаться дорогим решением. Нужно считать в каждом конкретном случае.

  3. Возможно прочитать отдельный стрим?

    Да

  4. Возможно ли прочитать данные сразу после записи? (Уровень консистентности)

    Да, доступна строгая консистентность

  5. Как скейлить запись и чтение?

    Решено из коробки с помощью шардирования — https://learn.microsoft.com/en-us/azure/cosmos-db/partitioning-overview. Но есть нюанс, что мы напрямую не контролируем скейлинг, алгоритм скрыт от нас и полностью автоматический.

  6. Возможность модифицировать записанные данные

    Да, но апдейты появятся в ченж фиде, их нужно будет игнорировать

На мой взгляд подходит, тут есть пример как делать эвент-сорсинг на Azure CosmosDB https://medium.com/@thomasweiss_io/planet-scale-event-sourcing-with-azure-cosmos-db-48a557757c8d. Из аргументов против может быть то, что это решение вендорлок на облако Azure.

???? EventStoreDB

  1. Есть ли возможность упорядочить данные?

    Сделано за нас из коробки

  2. Есть ли готовые решения для подписки на новые эвенты?

    Сделано за нас из коробки

  3. Возможно прочитать отдельный стрим?

    Да

  4. Возможно ли прочитать данные сразу после записи? (Уровень консистентности)

    Да

  5. Как скейлить запись и чтение?

    Как я понял из документации, только вертикальное, значит шардирование придётся делать руками

  6. Возможность модифицировать записанные данные

    Нет, предлагают делать через копирование — https://www.eventstore.com/blog/why-cant-i-update-an-event

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

❌ Kafka

  1. Есть ли возможность упорядочить данные?

    Сделано за нас из коробки

  2. Есть ли готовые решения для подписки на новые эвенты?

    Сделано за нас из коробки

  3. Возможно прочитать отдельный стрим?

    Нет!

  4. Возможно ли прочитать данные сразу после записи? (Уровень консистентности)

    Нет! Так как мы не можем читать отдельные данные, то есть делать запросы, то нам нужно материализовать данные, а значит уровень консистентности eventual.

  5. Как скейлить запись и чтение?

    Скейлится из коробки

  6. Возможность модифицировать записанные данные

    Нет, но можно сделать какой-то механизм на основе этой идеи — https://www.eventstore.com/blog/why-cant-i-update-an-event

Кафку очень часто форсят в книгах и статьях как серебряную пулю решающую все проблемы и в том числе когда речь про эвент-сорсинг. Но это точно не так — кейсы где кафка действительно подходит очень специфичные и нужно очень тонко понимать все ограничения. Я считаю что кафка точно НЕ подходит по-умолчанию.

Так как это очень спорная тема хочу оставить тут ряд статей от разных авторов с разными взглядами.

  • Тут утверждают, что можно сделать — раз.

  • Тут поняли проблему с тем, что нужно стронг консистенси и пытаются её решить — раз.

  • А тут утверждают что не подходит — раз, два.

Когда применять эвент-сорсинг

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

Когда применять

  • Когда нужен полный аудит лог событий в системе

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

  • Когда не хочется использовать транзакции

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

  • Когда нужна высокая пропускная способность

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

  • Когда хочется получить однонаправленный поток данных

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

    Система становится очень понятной за счет того, что у каждого типа операций есть определенное место в коде.

  • Когда не хочется терять данные by design

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

Когда не применять

  • Когда у вас простой домен. Эвенты вида created → updated → updated … → deleted имеют мало смысла с точки зрения эвент-сорсинга и в таком случае можно ограничиться более простым подходом. В таком случае можно рассмотреть применение CRUD или хранения ревизий объектов. Например, в wordpress посты хранят в виде ревизий.

  • Когда необходима строгая согласованность между read и write моделями данных. В таком случае скорее всего не обойтись без транзакций и эвент-сорсинг теряет свои полезные свойства.

Литература

Тут некоторые из материалов и технологий, которые так или иначе повлияли на мое понимание эвент-сорсинга.

Статьи

Книги

  • Domain-Driven Design by Eric Evans

  • Implementing Domain-driven Design by Vaughn Vernon

  • Building Microservices by Sam Newman

  • Building Event-Driven Microservices by Adam Bellemare

Технологии

Ищу работу

Я открыт к предложениям о работе тимлидом, backend-разработчиком, fullstack-разработчиком, SRE или консультантом. Для меня важны понятные перспективы продукта, над которым нужно будет работать, сильная команда и достойная оплата. Остальное обсуждаемо :-)

Мой CV — тут, моя телега — @aurokk

Комментарии (103)


  1. tsvettsih
    00.00.0000 00:00
    +1

    Для платежей и заказов нужно хранить историю перехода между состояниями независимо от того, используется ES или нет. Так что пример в статье неудачный (
    Можете привести пример системы, где хранение агрегата в виде списка ивентов действительно дает профит?
    И второй вопрос: можно хранить не ивенты, а завести таблицу журнала и хранить там историю состояний, чем это лучше/хуже, чем хранение ивентов?


    1. aurokk Автор
      00.00.0000 00:00

      Для платежей и заказов нужно хранить историю перехода между состояниями независимо от того, используется ES или нет. Так что пример в статье неудачный (

      Так как эту задачу можно решить и без ES, например писать логи дополнительно или хранить ревизии, то это не такой и плохой пример.

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

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

      И второй вопрос: можно хранить не ивенты, а завести таблицу журнала и хранить там историю состояний, чем это лучше/хуже, чем хранение ивентов?

      Если я правильно понял, под таблицей журнала вы имеете ввиду append-only табличку где вы будете хранить целиком версии объектов при каждом изменении.

      Нормальный подход, особенно, для блогов или каких-то объектов настроек, потому что они меняются редко и [почти] целиком при каждой записи, к тому же там нет разных причин изменения состояний. Условно, при работе с такими данными есть просто одно действие — сохранить новую версию данных и поэтому моделировать какие-то эвенты — оверхед.

      Если стейт-машина сложная и много всяких разных воздействий и состояний, то лучше хранить эвенты, потому что это [сильно] дешевле, к тому же так как с эвентами нет дублей данных, то они с меньшей вероятностью "разьедутся", в эвент-сорсинге же каждый кусочек данных это источник правды и просто необходим. Ещё кажется что эвент-сорсинг предлагает более дженериковый подход к работе с данными, добавляя новый агрегат не придётся менять ничего в write базе данных, вроде создания новых табличек или коллекций, и вы сразу получите историю состояний с причиной изменений и прочими деталями. Ну и ещё поинт, что если вы захотите CQRS, отдельную read модель, то вам придётся примерно все те же механизмы написать что и для эвент-сорсинга, только у вас в базе будет храниться больше данных.


      1. blib
        00.00.0000 00:00
        +1

        Не плохо работает для весьма узкого класса задач, так как тащит за собой ряд проблем про которые совсем не говорится в статье. Банально сделать какой нибудь отчёт, который традиционно можно сделать одним SQL запросом, становится весьма не тривиальной задачей.


        1. aurokk Автор
          00.00.0000 00:00

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

          Отвечу тут.

          На большом кол-ве данных в oltp делать запросы для построения отчетов — это путь вникуда. Я думаю что эвент-сорсинг для мелких приложений вряд ли стоит применять, поэтому предполагаю, что это наш кейс приложение с большим кол-вом данных. Большинство решений задачи в таком случае сводится к переливке данных из oltp с помощью change data capture в olap хранилище и там уже строятся отчеты. Особенно актуально в системах с большим количеством сервисов, когда нужны отчеты по данным множества сервисов.

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

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


          1. powerman
            00.00.0000 00:00

            Я думаю что эвент-сорсинг для мелких приложений вряд ли стоит применять

            Такое очень полезно писать в начале статьи, желательно сопроводив количественным определением понятие "мелкий".


          1. dph
            00.00.0000 00:00

            Ну, прием заказов - это как раз про "небольшой объем данных", там не нужен ES.


            1. powerman
              00.00.0000 00:00

              А если это приём заказов где-нить на Али или Амазон в чёрную пятницу - там тоже "небольшой объём данных"?


              1. dph
                00.00.0000 00:00

                Вот когда дело дойдет до, примерно, трех компаний в мире, где прием заказов в черную пятницу реально требует специальных решений - можно будет думать про ES или какие-то другие паттерны.
                Я не нашел актуальных рекордов на Амазоне, в 2018м в черную пятницу было всего 500 продаж в секунду, это вполне решается и без ES.


              1. gandjustas
                00.00.0000 00:00
                +1

                А в чем проблема большого количества продаж в секунду? Интернет-магазин прекрасно масштабируется горизонтально, так как всю базу можно разбить на шарды по товарам (каталог, запасы) и клиентам (корзины и оплаты).

                Для этого совсем не нужен ES.


                1. dph
                  00.00.0000 00:00

                  И тоже верно.
                  У меня тоже проблема с пониманием, когда может пригодится ES. Другое дело, что какой-нибудь тактический DDD без ES получается довольно криво, но это скорее проблема DDD.


                  1. gandjustas
                    00.00.0000 00:00

                    ES вырос из DDD. В целом DDD не менее бесполезен, чем ES.


                    1. dph
                      00.00.0000 00:00

                      Кажется, что ES как "хайповая идея" постарше, нежели хайп вокруг DDD. Но это не важно.
                      В DDD очень неплохие стратегические паттерны. А вот тактические - да, бесполезны...


  1. powerman
    00.00.0000 00:00

    Как делать UI

    Есть одна байка, как эту проблему решили в Facebook. Очень дешёвое, и истинно "инженерное" решение: тупо добавили "sleep(1 second)" между запросом на изменение данных и запросом на чтение (где конкретно - на фронте или бэке - не знаю, да и не важно это).

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

    Можете немного развернуть эту тему?

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

    Поправьте, если я ошибаюсь, но в моём понимании Event Sourcing - это аналог API, и обращаться с ним нужно как с любым API: документировать формат всех сообщений (событий), версионировать, поддерживать совместимость… с тем единственным отличием, что от совместимости с устаревшим API часто можно позволить себе отказаться (потому что все клиенты и сервисы использующие его уже обновились), а вот от поддержки старых форматов событий в логе отказаться нельзя никогда (что дополнительно раздувает и усложняет код и тестирование).


    1. aurokk Автор
      00.00.0000 00:00

      Про facebook смешно и это вполне рабочий вариант, без шуток :-)

      Про доменные и интеграционные эвенты — ну да, всё так, я называю доменными то, что внутри домена (домен может обслуживать один сервис или группа сервисов), а те эвенты, что используются для интеграции с другими сервисами — интеграционными.

      Я думаю, что использовать доменные эвенты для интеграции не хорошо, по нескольким причинам:

      1. Внешние сервисы могут требовать больше информации, чем содержится в конкретном эвенте. Допустим, можно представить, что у нас есть пара эвентов — о том, что платёж создан (в этом эвенте будет фигурировать orderId, который пришел в запросе создания платежа), и о том, что платёж авторизован (в этом платеже мы зафиксировали факт оплаты orderId фигурировать не будет). В нашем домене мы данные не дублируем, и условно каждый эвент содержит только некоторую дельту данных Если внешнему сервису интересен только факт авторизации платежа для некоторого orderId, то ему придётся консьюмить два эвента и created и authorized (и все промежуточные, в реальном мире от создания платежа до авторизации эвентов будет с десяток) и самому как-то их объединять, т.е. воспроизводить логику из нашего домена. Для интеграции удобно иметь эвент который содержит те данные которые нужны для интеграции :-)

      2. Это ограничивает наши возможности по изменению нашего сервиса. Условно, если нам нужно будет переделать как-то эвенты, убрать одни, добавить другие, то тут может возникнуть проблема, что интеграция сломается. Ну либо идти и менять все зависимые сервисы. Жесткая связность. Думаю, можно интегрироваться и на доменных эвентах, но нужно отдавать отчет в том, какую связность это вносит и поддерживать некоторый баланс.


      1. powerman
        00.00.0000 00:00

        Про facebook смешно и это вполне рабочий вариант, без шуток :-)

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

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

        Так и с внутренними та же фигня, разве нет? Вчера внутренний консьюмер обходился без этих данных, а сегодня бизнес поменял требования и эти данные резко стали нужны. Да, внутренний может и не события вычитывать, а прямо из БД с представлением данные брать, где есть все поля, но - это же уже совсем другой сервис получится, который не только и не столько на событиях работает…

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

        Я веду к тому, что не совсем понимаю в чём принципиальная разница между доменными и интеграционными событиями. Если только в том, для какого получателя они предназначены (кстати, в статье на медиуме, на которую Вы ссылались, рассматривался кейс когда для каждого внешнего сервиса публикуются свои события) - то чем внутренний получатель "домен" принципиально отличается от внешних "интеграции"?

        Версионировать нужно и те и другие, поддерживать все версии формата событий во всех консьюмерах вечно нужно и там и там, полей может нехватить и там и там (и в обоих случаях это можно решать либо вычитыванием "лишних" событий либо построением собственного представления), документировать формат событий нужно и там и там, хранить вечно нужно…

        Я что-то упускаю?


      1. powerman
        00.00.0000 00:00

        Забыл на второй пункт ответить:

        Это ограничивает наши возможности по изменению нашего сервиса. Условно, если нам нужно будет переделать как-то эвенты, убрать одни, добавить другие, то тут может возникнуть проблема, что интеграция сломается. Ну либо идти и менять все зависимые сервисы. Жесткая связность.

        И снова - а в чём разница? Внутри домена ведь тоже есть один или несколько сервисов-консьюмеров, которые должны быть готовы обработать события в новом формате (реальная потребность в котором может быть только у одного из них, ради которого это изменение и делалось). Именно поэтому я воспринимаю event sourcing именно как аналог API - изменения в события надо вносить ровно так же, как в API: либо совместимым с существующими клиентами образом, либо сначала обновить все клиенты. И в любом случае несовместимые изменения создадут проблемы и подумать о всех возможных клиентах при этом необходимо, где бы они не находились - внутри домена или снаружи.


        1. aurokk Автор
          00.00.0000 00:00

          Неуместно сравнивать что ли, тут дело в том, чтобы был уровень косвенности и понимание того какое количество кода придётся поменять в случае чего.

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

          Если у вас вся система обслуживает один контекст, то может вам и не нужны уровни косвенности такие как интеграционные эвенты, или апи (если это монолит, например).


  1. powerman
    00.00.0000 00:00

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

    Это логично, но не работает. Что конкретно считать ПД - не всегда заранее известно. ПД определяются не здравым смыслом, а текущими требованиями закона/регулятора, которые меняются непредсказуемым образом. Реализация этого факта неизбежно приводит нас к тому, что этот уровень косвенности нам нужен для всех данных (вводимых пользователем), что в свою очередь делает концепцию "неизменяемых событий" иллюзией, т.к. по факту БД событий становится очень даже изменяемой чуть менее чем полностью.


    1. aurokk Автор
      00.00.0000 00:00

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

      Удалить и изменить данные конечно можно, примерно такие шаги можно предпринять:

      1. Сделать новую версию эвентов и поправить код для работы с этой версией эвентов

      2. Начать работать с новыми эвентами

      3. Старые эвенты скопировать в новый сторедж, приведя к новой версии

      4. Удалить старые эвенты

      Ну, это я так, утрированно. Вопрос лишь в кол-ве работы которое придётся проделать. Ну и про ПД всё же тут можно продумать, как и про данные которые в скоуп pci-dss попадают. Можно самые очевидные вещи — карточные данные или персональные данные сразу не хранить в сервисе, который за них не отвечает, а запрашивать по-необходимости.


  1. powerman
    00.00.0000 00:00

    Акторы

    ... В дотнете особенно удачным решением выглядит фреймворк orleans и их концепция виртуальных акторов.

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

    Я неплохо понимаю что такое акторы в их стандартном определении (в т.ч. упомянутом в https://learn.microsoft.com/en-us/dotnet/orleans/overview#the-actor-model), но вот попытка найти определение что такое "виртуальные акторы изобретённые Orleans" через три ссылки привела к research paper (https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/Orleans-MSR-TR-2014-41.pdf) на 12 страниц мелким шрифтом в pdf, в которые вникать желания нет.

    Если несложно, поясните своими словами что это и чем ценно в контексте статьи для тех, кто далёк от технологий M$ - у Вас отлично получается рассказывать такие вещи. :) Из контекста это звучит как обычный кеш в памяти записей из read DB, но тогда неясно почему это "актор".


    1. aurokk Автор
      00.00.0000 00:00

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

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


  1. gandjustas
    00.00.0000 00:00
    +1

    Дочитал до пункта "когда не применять"

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

    Когда у вас появляются деньги вам нужна строгая согласованность.

    В вашем примере вы легко можете взять с клиента оплату за заказ дважды:

    1. Клиент жмет "оплатить" на странице заказа, в момент проведения транзакции связь ломается, клиент не получает ответа от платежного сервиса, но информация об оплате попадает в эвентстор.

    2. Далее клиент перезагружает страницу заказа, видит что статус заказа все еще "ожидает оплаты", так как трансформер не отработал, и оплачивает еще раз.

    3. Вы потом долго разбираетесь в эвентсторе откуда два раза изменение статуса с "не оплачен" на "оплачен" (кстати что делать если в эвентсторе оказались несовместимые события, как разруливается конкурентный доступ?)

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

    Может быть у вас есть пример эвентсорсинга, который несет только полезные свойства, а не tradeoff с отрицательной суммой?


    1. powerman
      00.00.0000 00:00
      +1

      IMHO положительный tradeoff возникает тогда, когда система на транзакциях тупо не тянет необходимую нагрузку (напр. RDBMS) и/или неприемлемо тормозит (напр. распределённые транзакции). Ну т.е. это крайне редкий кейс на практике, очередной пример концепции "вы не гугл". В остальных случаях event sourcing - это дикое переусложнение и боль.


      1. gandjustas
        00.00.0000 00:00

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


    1. aurokk Автор
      00.00.0000 00:00

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

      Может быть у вас есть пример эвентсорсинга, который несет только полезные свойства, а не tradeoff с отрицательной суммой?

      Вопрос не понял, уточните? Плюсы перечислены перед "когда не применять" в "когда применять" :-)


      1. gandjustas
        00.00.0000 00:00
        +2

        Вопросы межсервисного взаимодействия к эвент-сорсингу не относятся

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

        Но даже если представить что сервис ваш, то это никак не помешает вам дважды принять оплату в эвент-сорсинг архитектуре.

        Условно, все те же проблемы нужно решать как и без эвент-сорсинга — делать пессиместичные и дюрабельные блокировки, вызовы идемпотентными и прочее.

        Чтобы делать пессимистичные и дюрабельные блокировки вам нужна согласованность write и read моделей (внезапно!)

        Как идемпотентность вызовов поможет вам не потерять данные об оплате клиента в случае eventual consistency?

        Вопрос не понял, уточните?

        Ваш пример в статье такой, что если переписать его без эвенсорсинга на обычную РСУБД, то он станет работать не просто надежнее, но и быстрее. А аудит можно прикрутить отдельно.

        Есть у вас пример, который не получится переписать без эвенсорсинга без потери ключевых характеристик?


    1. powerman
      00.00.0000 00:00
      -1

      Когда у вас появляются деньги вам нужна строгая согласованность.

      Кстати, на этом стоит остановиться подробнее. Забавно, но это - иллюзия.

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


      1. gandjustas
        00.00.0000 00:00
        +1

        У вас два неявных предположения:

        • Вы предполагаете что проблемы в отсутствии транзакционности будут возникать редко.

        • Вы предполагаете что поддержание ACID гарантий сильно замедляет скорость разработки.

        Оба предположения неверные.

        В моей практике был один интересный случай. Я был тогда еще очень молод, только закончил универ, где на предпоследнем курсе мы изучали реляционные СУБД. К нам в контору устроился весьма опытный программист, много чего написал в своей жизни и умел реально быстро делать.

        И как-то раз мы с товарищем, с которым учились вместе, посмотрели его код. Там было так: запрос "Получить значение А", небольшие вычисления со значением А, которые занимали едины миллисекунд, запрос "Записать в А новое значение". Запросы были не в транзакции.

        Мы с товарищем сразу почувствовали неладное. Но опытный программист сказал нам: "там задержка между запросами минимальна, не успеет другой запрос туда вклиниться, я всегда так делал. Если написать транзакцию, то будут дедлоки, а так проблем никогда не было".

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

        В итоге в код все равно были добавлены транзакции и updlock. Но если бы это было сделано сразу, то не было бы ущерба репутации и не было бы потрачено несколько часов работы саппорта.

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

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


        1. powerman
          00.00.0000 00:00

          У вас два неявных предположения:

          Всё верно. :)

          Оба предположения неверные.

          Не-а. :)

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


          1. gandjustas
            00.00.0000 00:00

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

            Пока не доказано обратное.


        1. aurokk Автор
          00.00.0000 00:00

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

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

          Вы предполагаете что поддержание ACID гарантий сильно замедляет скорость разработки.

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

          В моей практике был один интересный случай...

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


          1. gandjustas
            00.00.0000 00:00

            у вас не будет транзакций концептуально

            В реальном мире транзакции существуют. Любая сделка это транзакция. Обязательства возникают одновременно у множества контрагентов.

            Более того, любое взаимодействие двух и более субъектов это транзакция. Мы нажимаем кнопку "отправить" и хабр размещает наш ответ под постом, видимых другим участникам. Это происходит атомарно: нажали - получили результат.

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

            Один из таких алгоритмов, который применяется практически во всех СУБД, это Write Ahead Log (WAL). Мы сначала атомарной операцией записи сохраняем то, что мы хотим поменять, а потом уже меняем и если все ок, то помечаем данные меткой этой транзакции, а если нет, то транзакцию откатываем - просто не сохраняем изменения.

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

            У вас уже есть WAL. Даже если нет и вы нашли систему без WAL, вы его изобретаете в виде eventstore.

            код обновления проекций, или там взаимодействия с другими сервисами, по-задумке, находится в другом месте и работает с задержкой

            Как можно оплату с задержкой принять? Пользователь сформировал заказ, нажал оплатить, дальше что?

            Я посмотрел ваш код, у вас там тест пинает многократно апи, пока заказ не станет в нужном состоянии. Но в реальности это не бездушная машина делает, а пользователь нажимает кнопки на сайте. Если он увидит, что после добавления товара в корзину количество не поменялось он уйдет с вашего сайта и никогда не вернется.

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

            Не понимаю откуда взялся тут ACID, ACID он как бы про другое

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

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

            Что значит "становится хрупким" ? Почему имеет тенденцию разрастаться? И почему на это должны влиять проекции? Можете привести пример кода?

            Мой пример без эвенсорсинга:

            // Этот эндпоинт вызывает робокасса или другой платежный сервис
            // если он вернул ошибку, то деньги не списываются
            public void Authorize(int orderId)
            {
                var changed = db.Order
                  .Where(o => o.Id == orderId && o.Status < OrderStatus.Paid )
                  .ExecuteUpdate(s => s.SetProperty(o => o.Status, OrderStatus.Paid));
                if(changed != 1) throw new Exception();
            }
            
            // Этот эндпоинт вызывает клиент
            // order.Status всегда содержит актуальный статус заказа
            // клиент не сможет перейти к оплате 
            // даже с помощью хаков не сможет оплатить еще раз
            public Order Get(int orderId)
            {
                return db.Order.First(o => o.Id == orderId);
            }
            

            Даже если вы можете работать с EF7 или не приучены писать ExecuteUpdate, то можно написать такой код:

            public void Authorize(int orderId)
            {
                var order = db.Order.First(o => o.Id == orderId && o.Status < OrderStatus.Paid);
                order.Status = OrderStatus.Paid;
                db.SaveChanges();
            }

            Если кто-то межу строками 3 и 5 поменяет статус заказа, то saveChanges отвалится из-за оптимистичной конкуренции и оплата не пройдет.

            Что в этом примере "становится хрупким" ? Что будет разрастаться?

            Как вы решите проблему двойной оплаты в своем коде?

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

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

            PS. Открыл ваше резюме и увидел, что вы занимались payment gateway в Додо. Там все так плохо как у вас в статье?


            1. linefight
              00.00.0000 00:00

              Я, конечно, не сварщик, но для решения проблемы двойной оплаты вижу навскидку такие варианты:

              в случае с event sourcing Authorize будет командой, а в обработчике этой команды в агрегате мы будем работать с состоянием, собранным из write представления, которое консистентно. В случае, если заказ уже оплачен, то возвращаем робокассе ошибку, заодно можно сохранить событие о том, что была попытка двойной оплаты. В случае успеха, соответственно, меняем состояние заказа на "оплачен". Если представить, что PSP не вызывает наш сервис, чтобы проверить, что деньги можно списать, и списывает их безусловно, то можно пойти немного другим путем. Сделать кнопку оплаты таким образом, чтобы она сперва создала семантической лок в системе (то есть это будет команда) путем перевода заказа в состояние а-ля PAYMENT_IN_PROGRESS, и потом бы происходил редирект на страницу PSP. Но кажется, что конкретно в этой ситуации без event sourcing пришлось сделать то же самое. Ещё один вариант: если задетектили двойную оплату каким-либо способом, то всегда одну из транзакций можно откатить в PSP в автоматическом режиме. Короче, как мне кажется, варианты побороть проблему есть. И способы решения проблем, как всегда, сильно зависят от бизнеса и конкретных кейсов.


              1. gandjustas
                00.00.0000 00:00

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

                Во-первых это убивает все преимущества ES, которые обозначены в статье.

                Во-вторых блокировку поддерживают в основном РСУБД, а имея РСУБД для чтения write-модели не надо делать ES. Вы можете просто читать и записывать состояние. При этом вы вполне можете вместе с изменением состояния писать лог этого изменения, когда логирование нужно для бизнеса.

                Сделать кнопку оплаты таким образом, чтобы она сперва создала семантической лок в системе

                Лок требует строгой консистентности, которой нет в ES.

                Ещё один вариант: если задетектили двойную оплату каким-либо способом, то всегда одну из транзакций можно откатить в PSP в автоматическом режиме.

                Это прекрасный вариант с точки зрения user experience. Пользователь идет оплачивает корзину, переходит на страницу подтверждения, раудется, уходит в вашего сайта. В этот момент фоновый процесс обрабатывает его оплату, понимает что на складе товара уже не осталось и транзакцию нужно откатить, возвращает деньги (хорошо если это произойдёт моментально) и отправляет письмо "извините, несмогла", которое почти 100% попадает в спам. Угадайте сколько еще раз этот пользователь будет покупать в вашем магазине?

                И собственно в чем таком особенном заключается преимущество ES архитектуры, что можно терпеть подобные недостатки?


                1. linefight
                  00.00.0000 00:00

                  Блокировка запси во время чтения создает строгую консистентность.

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

                  Во-первых это убивает все преимущества ES, которые обозначены в статье.

                  Ну почему убивает? Во-первых, наличие аудит лога и возможность "путешествия во времени" точно не убивает. Во-вторых, из коробки механизм атомарной и упорядоченной публикации событий (в системе без ES пришлось бы использовать Transactional Outbox pattern или, не дай боже, распределенные транзакции). В-третьих, CQRS - хороший паттерн, который отделяет мух от котлет, и позволяет более гранулярно оптимизировать различные части приложения. Конечно, ES и CQRS - вещи ортогональные, но в связке интересно выглядит.

                  Лок требует строгой консистентности, которой нет в ES.

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

                  Это прекрасный вариант с точки зрения user experience.

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

                  P.S. Все же согласен, что в 99% можно обойтись без ES. И сразу начинать проектировать приложение с помощью этого подхода не стоит. Серебряной пули не существует, и за всё приходится платить. Однако я уверен, что ES может и находит свое применение.


                  1. gandjustas
                    00.00.0000 00:00

                    Можно делать оптимистичную блокировку

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

                    Вы два раза продали товар, который у вас в одном экземпляре. Зато теперь вы сможете автоматически транзакцию откатить. Но все равно пользователь №2 уже ушел с сайта и вам придется сделать много работы, чтобы ему сообщить и он 100% не вернется на ваш сайт.

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

                    Аудит возможен и без ES. Более того, без ES даже выгоднее, так как можно логировать только то, что нужно будет потом смотреть, а не все подряд.

                    Во-вторых, из коробки механизм атомарной и упорядоченной публикации событий

                    Вот это я не понял, можно подробнее?

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

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

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

                    Выше привел пример почему оптимистичная блокировка не поможет.

                    Однако я уверен, что ES может и находит свое применение.

                    Я все еще жду пример, где с ES будет работать лучше чем без него.


                    1. linefight
                      00.00.0000 00:00

                      А что это даст?

                      Давайте попробуем пофантазировать. Допустим, мы написали 2 прототипа системы (с ES и с традиционным подходом). Есть Ваня и Петя, которые хотят купить один товар, но на складе он последний. Предположим, что у нас есть система хранения, которая понимает SQL, поддерживает уникальные индексы, но не умеет в транзакции (но операцию UPDATE все же считаем атомарной, чтобы предотвратить потерянные обновления). Эту систему хранения мы будем использовать для обоих прототипов. Оба пользователя добавляют товар в корзину, переходят на чекаут, вбивают свои данные и нажимают на кнопку "оплатить". В этом случае нам нужно временно зарезервировать товар на складе, пока производится оплата. Далее имеем примерно следующие SQL запросы к базе, которые генерируются после нажатия на кнопку оплаты:

                      Без ES с использованием оптимистической блокировки:

                      SELECT * FROM inventory WHERE id = stock_id;
                      UPDATE inventory SET quantity = quantity — 1 WHERE id = stock_id AND version = expected_version;

                      Без ES с использованием семантического лока:

                      SELECT * FROM inventory WHERE id = stock_id;
                      UPDATE inventory SET quantity = quantity — 1 WHERE id = stock_id AND quantity > 0;

                      В случае, если UPDATE возвращает 0 строк, то повторяем запросы. Если при повторении обнаруживается, что товара на складе больше нет, то показываем пользователю, что купить товар он не успел.

                      Если все-таки транзакции поддерживаем, то можно и через пессемистичную блокировку с использованием SELECT FOR UPDATE

                      С ES пока вижу только такой вариант, при условии, что есть уникальный индекс на SequenceId (в качестве примера используем таблицу с событиями из статьи):

                      SELECT * FROM Events WHERE AggregateType = 'Stock' AND AggregateId = stock_id;
                      INSERT INTO Events VALUES (expected_sequence_id, stock_id, 'Stock', event_id, 'QuantityReduced', data);

                      Детали получения актуального SequenceId я опустил. В случае, если валимся на уникальном индексе, то делаем то же самое, что и в примере с оптимистичными локами без использование ES.

                      Во всех примерах это происходит синхронно и консистентно при нажатии на кнопку оплаты.

                      Аудит возможен и без ES.

                      Вы правы. И по сути это будет тот же ES, но только вы на каждое событие создаете снимок состояния агрегата и используете его при чтении. Возможно, я натянул сову на глобус сейчас, но тем не менее :)

                      Вот это я не понял, можно подробнее?

                      Это если у вас есть, например, брокер сообщений, и вам нужно бросать в него сообщение только в том случае, если транзакция в базе данных успешно закомитилась или не бросать его вовсе. То есть нужны ACID гарантии между базой данных и брокером сообщений. Это достигается либо с использованием распределенных транзакций, что является злом, либо с использованием шаблона Transactional Outbox, где вы не бросаете событие в брокер напрямую, а вставляете его в таблицу в рамках одной транзакции. События из этой таблицы асинхронно будут выгребаться оттуда через какой-нибудь CDC или тупым поллингом с помощью селекта. Само собой, здесь также очень важна упорядоченность событий, если они не являются коммутативными. Например, будет странно, если сначала прилетит событие OrderCancelled, а потом OrderCreated для одного заказа. В случае с ES этот паттерн уже реализован из коробки, так как у вас таблица Events сама по себе является Outbox таблицей. При этом здесь уже нет необходимости делать в рамках одной транзакции обновление строк в разных таблицах, нужна только вставка в одну таблицу, что снимает ряд требований на хранилище событий.

                      но на практике просто база данных работает быстрее, чем оптимизированный CQRS, в режиме строгой согласованности

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

                      Я все еще жду пример, где с ES будет работать лучше чем без него.

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


                      1. gandjustas
                        00.00.0000 00:00

                        Детали получения актуального SequenceId я опустил.

                        Так эти детали самые важные. У вас SequenceId может получится только после пересчета агрегата, по сути это и есть версия агрегата. Нигде больше вы SequenceId не сможете взять.

                        То есть в вашем между select и insert нужно обновить агрегат, причем пока он обновляется вы или блокируете запись в эвенты (как это в вашем примере) и получаете строгую консистентность, которая тормозит, сильнее чем без ES, или не блокируете если у вас EventStoreDb, как у автора поста и получаете продажу одного товара два раза.


                      1. linefight
                        00.00.0000 00:00

                        Так эти детали самые важные.

                        Можно просто взять

                        SELECT MAX(SequenceId) FROM Events;

                        А еще лучше, чтобы уменьшить количество конфликтов, можно сделать уникальный индекс по колонкам AggregateType и EventId и использовать EventId в качестве "версии".

                        То есть в вашем между select и insert нужно обновить агрегат

                        Не нужно. Вставка события - это и есть "обновление" агрегата. Агрегат уже потом собирается путем проигрывания всех событий из таблицы Events.

                        Кстати говоря, в моих примерах с оптимистичными блокировками каждый SQL запрос может выполняться в отдельной транзакции. Главное, чтобы для случая "без ES" UPDATE был атомарным.


                      1. linefight
                        00.00.0000 00:00

                        Еще кое-что поясню: между select и insert для случая с ES и между select и update для случая без ES происходит какая-то бизнес-логика на стороне приложения, то есть не на уровне БД.


                      1. gandjustas
                        00.00.0000 00:00

                        Можно просто взять SELECT MAX(SequenceId) FROM Events

                        Если вы возьмете MAX(SequenceId), то у вас будет конфликт уникальности при каждой вставке, если возьмете MAX(SequenceId)+1, то у вас этот SequenceId будет уникальным и никакой оптимистичной блокировки не будет.

                        использовать EventId в качестве "версии"

                        Никакой "оптимистичной блокировки" не будет, у вас кажый раз будет новый EventId.

                        Не нужно. Вставка события - это и есть "обновление" агрегата. Агрегат уже потом собирается путем проигрывания всех событий из таблицы Events.

                        Когда "потом"? Нам сейчас надо проверить, что количество не меньше нуля. Откуда мы это количество возьмем, не собирая агрегат? Из таблицы событий? Это значит вы просто агрегат переместили в таблицу событий и фактически отказались от ES.


                      1. linefight
                        00.00.0000 00:00

                        У нас, мне кажется, возникло недопонимание. Давайте я более подробно разъясню пример с ES:

                        -- БД:
                        SELECT * FROM Events WHERE AggregateType = 'Stock' AND AggregateId = stock_id;
                        
                        -- приложение:
                        -- Берем последний EventId. Если он равен, например, 1, то следующий EventId будет 2
                        -- Собираем из выборки агрегат, проигрывая все события по порядку
                        -- Проверяем, что quantity в агрегате больше 0
                        -- Если больше 0, то переходим к вставке события QuantityReduced c EventId = 2
                        -- Если 0, то отдаем неуспешный ответ
                        
                        -- БД:
                        INSERT INTO Events VALUES (next_sequence_id, stock_id, 'Stock', 2, 'QuantityReduced', data);
                        -- Если вставка прошла успешно, то отдаем успешный ответ клиенту
                        -- Если вставка фэйлится на уникальности ключа AggregateType + AggregateId + EventId, то сработала оптимистичная блокировка.
                        -- То есть кто-то уже вставил для этого агрегата событие с EventId = 2, а значит состояние агрегата изменилось.
                        -- В таком случае повторяем сначала

                        Там, где я в своем предыдущем сообщении говорил, что лучше повесить уникальный индекс на AggregateType и EventId, то был не прав, так как нужно к индексу добавить еще AggregateId.

                        Когда "потом"?

                        Тут я имел ввиду, что собираем после SELECT * FROM Events


                      1. gandjustas
                        00.00.0000 00:00

                        -- Берем последний EventId. Если он равен, например, 1, то следующий EventId будет 2
                        -- Собираем из выборки агрегат, проигрывая все события по порядку
                        -- Проверяем, что quantity в агрегате больше 0
                        -- Если больше 0, то переходим к вставке события QuantityReduced c EventId = 2
                        -- Если 0, то отдаем неуспешный ответ

                        Поздравляю, вы изобрели READ COMMITED SNAPSHOT уровень изоляции.

                        Во-первых вам теперь не нужен лог (если надо для бизнеса, то просто пишите отдельным запросом). Вы можете писать сразу в агрегат и сравнивать его версию, то есть делать:

                        SELECT @ver = Version, @qty = Quantity FROM AggregateType WHERE AggregateId = stock_id
                        UPDATE SET Version = @ver+1, Quantity = @qty-1 WHERE AggregateId = stock_id and Version = @ver
                        

                        Во-вторых база уже умеет делать READ COMMITED или даже READ COMMITED SNAPSHOT и вы можете просто написать:

                        UPDATE SET Quantity = Quantity-1 WHERE AggregateId = stock_id

                        А проверку, что Quantity >= 0 повесить в CHECK CONSTRAINT

                        Пытаясь решить проблемы ES мы получили код без ES.

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


                      1. linefight
                        00.00.0000 00:00

                        Поздравляю, вы изобрели READ COMMITED SNAPSHOT уровень изоляции.

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

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


                      1. gandjustas
                        00.00.0000 00:00

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


                      1. gandjustas
                        00.00.0000 00:00

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

                        Конечно же Аудит не похож на ES, потому что ES требует сохранять все. А в своем аудите мы можем сохранять только то что нужно - можем выбрать поля, можем сохранять метаинформацию, можем сохранять только новое значение или предыдущее тоже.

                        А можем вообще таблицу иситории завести. Это уже будет зависеть от бизнес-требований.


                      1. linefight
                        00.00.0000 00:00

                        Поэтому я и говорю, что натягиваю сову на глобус. Смотря какой аудит, смотря где и какие требования, тут все верно.


          1. dph
            00.00.0000 00:00
            +1

            Кстати, это тоже не верно.
            Обработка команды с генерацией сопутствующих эвентов - это уже транзакция. И для нее требуется атомарность.
            Любая зависимость инварианта агрегата от его состояния (например "мы не можем уходить ниже нуля на остатках на счете) уже требует транзакции.


            1. linefight
              00.00.0000 00:00

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


      1. gybson_63
        00.00.0000 00:00

        Кому нужна надежность в оплате и отгрузке заказов, те купят 1С. Ну или битрикс. Ну другое готовое решение.
        В здравом уме никто не закажет разработку такого.

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


        1. gandjustas
          00.00.0000 00:00

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


          1. gybson_63
            00.00.0000 00:00

            Это готовое решение, готовая платформа. Как ни крути. Притом концептуально это именно ES, на уровне платформы.

            Я думаю лучше брать какие-то примеры по сбору и анализу метрик, например. Не лезть в бизнес и тем более корпоративные решения.

            Простой gps-трэкер тоже хороший пример. Накидываем координаты - лог, видим маршрут на карте - вью. А если бы мы просто фиксировали текущие координаты, то маршрут не отследить.

            В биллинг системе мы просто фиксируем факт звонка и длительность. А потом мы все это складываем и получаем счет.

            И главный вопрос возникает - а кто делает иначе?


            1. gandjustas
              00.00.0000 00:00

              Тут как анекдоте про нюанс. Он всегда есть.

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

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

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


              1. gybson_63
                00.00.0000 00:00
                -3

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

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

                То же со скидками.

                И главное, никогда не ставится под сомнение весь результат. Можно проанализировать весь лог событий, скорректировать отдельные позиции и получить правильный результат. А если вы произвели 1000 операций, не запоминая их, то сомнительный результат идет в корзину. Невозможно разрешить конфликт о достоверности результата.

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

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


                1. gandjustas
                  00.00.0000 00:00

                  Вы подменяете понятия.

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

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

                  Вы сами говорите что всегда надо знать сколько товара на складе. Теперь объясните как гарантированно не продать больше чем есть на складе при отложенной согласованности.


                  1. gybson_63
                    00.00.0000 00:00
                    -1

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


                    1. gandjustas
                      00.00.0000 00:00

                      Да вы что?

                      Списывать (резервировать) единицу товара надо в момент оплаты. Иначе до физического выбытия вы её ещё раз продадите.

                      Как это можно сделать в es?


                      1. gybson_63
                        00.00.0000 00:00
                        -1

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

                        А как это делать в другой методологии?


                      1. gandjustas
                        00.00.0000 00:00
                        +1

                        Точно также, но есть нюанс, который вы как обычно игнорируете.

                        Пользователь видит не логи, а суммую. Сумма получается через неопредлеленное время после записи в лог.

                        У вам обязательно случится ситуация, когда вы записали в лог -1, но сумма не успела обновиться, а в это время другой пользователь сделал действие (оплатил товар) и в лог еще раз записали -1. Сумма пересчиталась и стала отрицательной, а такого случаться не должно.

                        Вы осознаете что такая проблема существует? Я что-то уже начал сомневаться в этом.

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


                      1. gybson_63
                        00.00.0000 00:00
                        -1

                        Нет никакого обновления суммы. Она вычисляется каждый раз.

                        Event Sourcing pattern - Azure Architecture Center | Microsoft Learn


                      1. gandjustas
                        00.00.0000 00:00

                        Почему же нет? При строгой консистентность Я могу явно делать -1 и откатывать если результат отрицательный.

                        Но это неважно. Важно как не уйти в минус. Что эвентсорсинг предлагает для решения проблемы?


                      1. powerman
                        00.00.0000 00:00

                        Поллинг в ожидании когда нужное событие будет обработано. (Ведь эвентсорсинг не зря относится к категории eventually consistent - мы всё ещё имеем консистентность, просто не сразу, а через какое-то время. Соответственно, если нужна гарантия консистентности перед какой-то операции, то нужно дождаться этого момента времени.)

                        1. Отправляем событие "создан заказ 42" со списком товаров.

                        2. Ждём события "товары зарезервированы для заказа 42" либо события "заказ 42 отклонён, причина: не удалось зарезервировать товар".

                        3. Если получено первое событие - показываем форму оплаты. Если второе - сообщение о необходимости отредактировать корзину и повторно создать заказ.

                        4. Если в течении заданного периода времени не отправляется событие "заказ 42 оплачен", то система сама выполняет отмену резервирования товаров этого заказа. Разумеется, форма оплаты не должна позволять выполнить оплату позднее этого же периода времени.

                        При этом процесс резервирования товаров (шаг 2) выполняется в обычной БД. Причём поддержка транзакций даже в этой БД не является обязательной т.к. нередко процесс, который это резервирование выполняет - один и работает в один поток: принял событие, зарезервировал товар если он доступен, отправил событие, повторить. Транзакции тут могут быть нужны, к примеру, чтобы атомарно отметить в БД резервирование разных товаров плюс ID события, в рамках обработки которого это было сделано (для идемпотентности обработки событий) - но это зависит от особенностей конкретного проекта (напр. товар в заказе всегда один) плюс есть альтернативные варианты реализации без транзакций (напр. через формирование представления из суммы всех событий связанных с добавлением/удалением/резервированием/отменой резервирования товаров).

                        Иными словами - обеспечить надёжность при использовании эвентсорсинга можно. Только это заметно сложнее, чем при использовании транзакций.

                        P.S. Не поймите неправильно, лично я не сторонник эвентсорсинга, на мой взгляд это совершенно излишняя сложность для абсолютного большинства проектов. Но это не значит, что на нём нельзя написать надёжно.


                      1. gandjustas
                        00.00.0000 00:00
                        +1

                        Поллинг в ожидании когда нужное событие будет обработано.

                        Простите, а кто будет поллить?

                        Клиент? Вы реально считаете, что нажав кнопку "добавить в корзину" пользователь будет обновлять страницу пока товар не появится в корзине? (именно такое решение предлагает автор поста, это есть в исходниках)

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

                        Но это не значит, что на нём нельзя написать надёжно.

                        На ES можно писать надежно, но тормозить оно будет СИЛЬНЕЕ чем без ES. Никаких других преимуществ ES тоже не будет.

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


                      1. powerman
                        00.00.0000 00:00

                        но тормозить оно будет СИЛЬНЕЕ чем без ES

                        В местах где нужна строгая консистентность.

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

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

                        Поэтому, на мой взгляд, ценно учитывать и то, что строгая консистентность нужна не везде, и даже где вроде бы нужна цена её обеспечения иногда выше чем стоимость проблем вызванных отсутствием консистентности в этом месте. И, аналогично, высокая производительность важна для UX, но иногда цена этого слишком высокая, и использование технологий вроде ES (включая и ранее упомянутый трюк со `sleep(1 second)`) позволяет получить приемлемый UX намного-намного дешевле. Иными словами, нужно учитывая требования бизнеса, а не делать идеальное техническое решение не обращая внимание на то, сколько это стоит - как денег так и времени. И я вполне допускаю, что иногда поиски этого баланса могут привести и к ES (хотя лично я до сих пор находил более простые альтернативы, всё-таки ES довольно сложен в реализации).


                      1. gandjustas
                        00.00.0000 00:00

                        В местах где нужна строгая консистентность.

                        Я свой первый комментарий закончил как раз просьбой указать где ES будет работать лучше, чем без него.

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

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

                        Верно

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

                        Неверно. Шаридинг БД никто не отменял. В тот же azure он встроен. Вы замучаетесь придумывать задачу, которая плохо шардится на БД.

                        Попытки это решать приводят к разному, в т.ч. к ES

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

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

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


        1. dph
          00.00.0000 00:00

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


      1. dph
        00.00.0000 00:00
        +1

        Вообще говоря увод счета клиента в минус (если нет договора по кредитованию и при этом это не банк) - является достаточным поводов для отзыва лицензии у соответствующей платежной системы. Так что вопрос "не ухода в минус" при работе с деньгами - он крайне важный.
        Для работы в ecom данный подход тоже не работает, так как приводит к куче злобных пользователей, купивших товар, которого нет на складе.


        1. gybson_63
          00.00.0000 00:00
          +1

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

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


          1. dph
            00.00.0000 00:00

            Э, как именно в ES можно сделать на агрегате гарантию положительности баланса всегда? При обработке команды "авторизация счета" нужно в одной serializable транзакции поднять состояние счета, проверить его достаточность, зарезервировать сумму на авторизацию". Но в ES это нереально провести без дополнительных усилий (я, кстати, знаю, каких - но в статье про это ничего не сказано).


            1. gybson_63
              00.00.0000 00:00

              Так же как и не в ES поменять состояние объектов. Заблокировать на время транзакции. Вопросы странные. Как так можно ездить на велосипеде, упадешь же, а на машине фыр-фыр и едешь.
              Бывает так, что не нужна информация о конечном остатке на строго текущий момент всегда, а нужна по запросу и на любой момент времени.

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


              1. dph
                00.00.0000 00:00

                Хм, что в рамках ES можно заблокировать и на время какой транзакции? Вот конкретный пример для агрегата счет - можешь описать? Именно в рамках реализации ES, с разделением read и write моделей?


                1. gybson_63
                  00.00.0000 00:00

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

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


                  1. dph
                    00.00.0000 00:00
                    -1

                    Ага, значит описать решение конкретной простой задачи - не можешь.
                    Ну, что и хотелось понять.


  1. gybson_63
    00.00.0000 00:00

    Табличка с заказом это на самом деле 3 таблички. 1 таблица - id заказа и данные о заказчике. 2 таблица - состояния заказов, 3 табличка - содержимое заказа. При оплате заказа, создается задание на отгрузку, которое отражается также тремя таблицами, аналогично. Далее запросом можно уже получать количество и содержимое заказов, оплаты по ним и отгрузки.

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


  1. gatoazul
    00.00.0000 00:00
    -1

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


  1. ggo
    00.00.0000 00:00

    Пример, где event-sourcing себя проявляет с хорошей стороны.

    Там где нужно правильно считать деньги, уже выше обсудили, event-sourcing не подходит.

    Но там, где есть highload, и нет жестких требований по одномоментной консистентности, event-sourcing весьма полезен. Например, в твите надо показывать количество лайков и репостов. Для показа твита, данные извлечаются из какого-нибудь очень быстрого на чтение хранилища. А лайки и репосты складываем к отдельное очень быстрое на вставку хранилище. Затем отдельным процессом обновляем лайки и репосты.

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


    1. gandjustas
      00.00.0000 00:00
      +3

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


    1. gybson_63
      00.00.0000 00:00

      Деньги испокон века на ES, с первых бухгалтерских книг. В основе работы с деньгами транзакции и аудит. Как производить аудит операций, если их не записывать? Мол вот написано 100 рублей на счете и всё, компутер так видит.


      1. gandjustas
        00.00.0000 00:00

        Что за бред вы несете?

        Лог операций это не эвентсорсинг. Лог операций при строгой консистентности изменений применяли за много (тысяч?) лет до изобретения эвентсорсинга.


        1. gybson_63
          00.00.0000 00:00
          -2

          Продублирую еще раз

          Event Sourcing pattern - Azure Architecture Center | Microsoft Learn

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

          Просто пример. На кассе человек выкладывает товар, а у вас он не числится (не успели, проморгали, разное). Вы не продадите товар, потому что в минус уйдете? Ну удачи вам с таким бизнесом.


          1. gandjustas
            00.00.0000 00:00
            +1

            В реальности строгая консистентность описана в законе. Например розничная торговля: при оплате покупателем строго консистентно возникает обязанность передать товар.

            Ваши объяснения в суде о том, что у вас "отложенная согласованность" не примут если вы продали товар два раза, а у вас всего одна единица была. Вы не только вернёте стоимость покупателю, потеряете на эквайринге, но и оплатите судебные издержки, и, самое главное, потеряете репутацию.

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


            1. gybson_63
              00.00.0000 00:00

              Договор публичной оферты. Работает когда товар выставлен непосредственно на полке и вы пришли с ним на кассу.

              Но Вы же сами говорите, что наличие остатка в программе (а именно это и есть, "числится") не имеет отношения к архитектуре программы. Тогда о чем в сущности разговор?


              1. gandjustas
                00.00.0000 00:00

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

                А вот если у вас интернет-магазин, который торгует со склада, то вам нужно вести учет в реальном времени и эвентсорсинг поможет вам продать больше товара чем есть на складе. Закон о розничной торговле применим также в этом случае.


            1. gybson_63
              00.00.0000 00:00

              Вот вам конкретно про консистентность при покупке товара, который заканчивается

              "It is worth bearing in mind that an application may not actually require data to be consistent all of the time. For example, in a typical ecommerce web application that enables a user to browse and purchase goods ..." и далее по ссылке

              Data Consistency Primer | Microsoft Learn


              1. gandjustas
                00.00.0000 00:00
                +1

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

                Такой бред мог написать только тот, кто никогда не занимался интернет-магазинами.

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

                Какие преимущества должна нести архитектура, чтобы оправдать подобные косяки?

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


                1. gybson_63
                  00.00.0000 00:00

                  А вы что делаете, когда товар на складе закончился, что сообщаете юзеру? Просто "иди на хер"?


                  1. gandjustas
                    00.00.0000 00:00

                    В каталоге отмечается как "нет на складе".

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

                    В других вариантах если кто-то успел добавить в корзину, но не успел оплатить, то на этапе чекаута получает ошибку "товар закончился и предложение купить что-то еще".

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

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


        1. gybson_63
          00.00.0000 00:00
          -1

          Вся страна ведет бухгалтерию и торговлю в продуктах на основе ES. Такова реальность, нет смысла ее отрицать.


          1. gandjustas
            00.00.0000 00:00

            Вы бред пишите.

            1с всегда использовала строгую консистентность. Пересчет регистров всегда был транзакции при проведении документов.

            Вы все ещё путаете лог изменений и эвенсорсинг.


            1. gybson_63
              00.00.0000 00:00

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

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

              Провел - в таблицу записали строку +1
              Распровел - из таблицы убрали запись.

              Так что я ничего не путаю


              1. gandjustas
                00.00.0000 00:00

                Нет, вы именно путаете.

                В бухгалтерии до выполнения движений получаются остатки и проверяется, что они не уйдут в минус

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

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

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

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

                А как оно внутри устроено - через сохранение значения или "виртуальные" таблицы - дело десятое. Материализованные представления даже границу этих различий стирают.


                1. gybson_63
                  00.00.0000 00:00

                  Вы какой-то свой ES придумываете, который будет удобно обличать. Официальный документ гласит
                  "At any point, it's possible for applications to read the history of events. You can then use it to materialize the current state of an entity by playing back and consuming all the events that are related to that entity. This process can occur on demand to materialize a domain object when handling a request. Or, the process occurs through a scheduled task so that the state of the entity can be stored as a materialized view, to support the presentation layer."

                  Event Sourcing pattern - Azure Architecture Center | Microsoft Learn


                  1. gandjustas
                    00.00.0000 00:00

                    Я ничего не придумываю, это в статье описано, к которой вы пишите комменты.

                    Но давайте рассмотрим ваш источник правды. Он предполагает два варианта:

                    • through a scheduled task - это как раз так, как описано в данной статье со всеми преимуществами в скорости и недостатками в согласованности (хорошо что вы перестали спорить с ними)

                    • on demand when handling a request - рассмотрим этот вариант поподробнее.

                    Предположим вы пишите движения товара в эвентстор, а материализуете в момент создания нового движения.

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

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

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

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

                    Именно блокировка эвентстора на запись в момент расчета "остатков" создает строгую консистентность и сериализуемость транзакций. Поэтому в большинстве систем выгоднее остаток пересчитывать при записи (материализовать), а не при обработке запроса.

                    Механизм блокирования записи данных во время чтения данных уже встроен в большинство СУБД и вам для этого не надо ничего дополнительно делать.


                    1. gybson_63
                      00.00.0000 00:00

                      Сейчас блокируется не весь лог, а только по конкретным товарам и складам. Актуальный достаток доступен на любой момент времени.

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


                      1. gandjustas
                        00.00.0000 00:00
                        +1

                        При использовании РСУБД блокировки конечно же навешиваются не на всю таблицу лога, а только на страницы индексов и отдельные записи\диапазоны. Если конечно эскалация не случится.

                        Но если у вас уже есть РСУБД, то делать ВСЕ изменения через лог крайне неэффективно. Например в случае интернет-магазина удобнее менять непосредственно остаток\резерв когда пользователь оплачивает или добавляет товар в корзину.

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


  1. dph
    00.00.0000 00:00
    +1

    Стоит еще заметить, что
    1) использование ES почти всегда очень сильно понижает производительность, но, если проектирование сделано правильно, может дать выигрыш в масштабировании. Т.е. решение будет много дороже, но может быть его можно будет залить железом.
    2) ES очень сильно зависит от EventStore, при этом на рынке есть только одно решение, которое как-то удовлетворяет требованиям (EventStoreDB), но при этом документации по нему очень мало, архитектурной документации нет вообще, реализуемые гарантии не описаны, знающих решение админов на рынке не найти.
    3) Проектов, которым реально нужен ES - крайне мало. И это скорее социальные сети, нежели ecom или финтех.


    1. gybson_63
      00.00.0000 00:00

      MS вот считают, что для ecom это здорово. 1С каноничная ES.

      Ну и событийная интеграция.

      Реализация взаимодействия между микрослужбами на основе событий (события интеграции) | Microsoft Learn


      1. dph
        00.00.0000 00:00

        Нет, 1С вообще не про ES, да и не про ecom, это уже тут сто раз обсуждалось.
        Что считает MS - не имеет никакого значения, им нужно продавать облако подороже, а не предлагать оптимальные решения для задач (а ES как раз дает возможность потратить в разы больше ресурсов в облаке, чем другие подходы).


        1. gybson_63
          00.00.0000 00:00

          В 1С остатки и обороты всегда сумма событий. Там конечно еще куча остального есть, которое не ES.

          Чтобы понимать экономию на облаках, надо понимать сколько компания вообще тратит штат программистов, разрабатывающих более эффективное решение. Т.е. вот сэкономили дискового пространства на 30 000 рублей, программистам заплатили 3 000 000 рублей, хороший бизнес ... у программистов.


  1. gybson_63
    00.00.0000 00:00

    Меня тут осенило. Когда хотят сделать редактор с функцией отмены, то там фиксируется лог действий пользователя. Вот это прикольный кейс.

    Пример SWT Undo Redo : Undo Redo « SWT JFace Eclipse « Java (java2s.com)


    1. dph
      00.00.0000 00:00

      Это паттерн "Команда", описанный еще в Gang4. К ES не имеет никакого отношения.


  1. LaRN
    00.00.0000 00:00

    При таком подходе скорость роста размера БД в которой храняться события будет космической. А с ростом БД появится куча проблем с хранение резервных копий (диски может и не дорогие, но цена отличная от нуля), подъёмом дампа (в случае сбоя например), замедлением работы индексов если они есть. При этом написано, что удаление из БД не предусмотрено. Как тогда быть?


    1. aurokk Автор
      00.00.0000 00:00

      Привет!

      Перемещать (копировать, затем удалять) данные из операционного хранища в некоторое холодное хранилище точно нужно.

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

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

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