Регідратація агрегатів із проекції "знімків", а не магазину подій


14

Тож я деякий час фліртував з Event Sourcing та CQRS, хоча ніколи не мав можливості застосувати шаблони до реального проекту.

Я розумію переваги розділення ваших проблем з читанням і записом, і я ціную, як Sourcing подій спрощує проектування змін стану баз даних "Read Model", які відрізняються від вашого Event Store.

Що мені не дуже зрозуміло, це те, чому ви коли-небудь повторно зневоднюєте свої агрегати із самого магазину подій.

Якщо проектування змін до "читати" баз даних так легко, чому б не завжди проектувати зміни в базу даних "запис", схема якої ідеально відповідає вашій моделі домену? Це фактично буде базою даних знімків.

Я думаю, це повинно бути досить поширеним у ES + CQRS-програмах у дикій природі.

Якщо це так, чи корисний лише Магазин подій під час перебудови вашої бази даних "запис" у результаті змін схеми? Або я пропускаю щось велике?


Немає нічого поганого в тому, щоб асинхронно записувати до магазину моделі запису і використовувати це виключно для завантаження об'єктів. Про те, що ви робите це чи ні, є ті самі питання точності. Ключовим фактором для вирішення цих питань є узгодження моделей ваших організацій по-різному. Немає нічого магічного в пошуку подій, що вирішує ці проблеми послідовності. Магія полягає в моделюванні, а не в турботі. Існують конкретні програми, які вимагають послідовності на тому рівні, які мають суперечливі сутності, незалежно від того, як ви їх моделюєте, та потребуватимуть особливої ​​уваги незалежно.
Ендрю Ларссон

Поки ви можете гарантувати доставку заходів. Для цього вашій програмі просто необхідно синхронно опублікувати подію на довговічній шині подій. Після публікації завдання програми закінчено. Потім шина доставить його різним обробникам подій: один для оновлення магазину подій, один для оновлення магазину стану та будь-який інший, необхідний для оновлення магазинів читання. Причина, по якій ви використовуєте пошук подій, полягає в тому, що ви вже не піклуєтесь про негайну послідовність. Прийняти це.
Ендрю Ларссон

Немає причини, щоб ви постійно завантажували свої об'єкти з магазину подій. Це не його призначення. Її мета - забезпечити сировинну, постійну книгу всього, що відбулося в системі. Державні магазини суб'єктів господарювання та денормалізовані моделі зчитування призначені для завантаження та читання.
Ендрю Ларссон

Відповіді:


14

Що мені не дуже зрозуміло, це те, чому ви коли-небудь повторно зневоднюєте свої агрегати із самого магазину подій.

Тому що «події» - це книга рекордів.

Якщо проектування змін до "читати" баз даних так легко, чому б не завжди проектувати зміни в базу даних "запис", схема якої ідеально відповідає вашій моделі домену? Це фактично буде базою даних знімків.

Так; іноді корисна оптимізація продуктивності використовувати кешовану копію сукупного стану, а не регенерувати цей стан з нуля кожного разу. Пам'ятайте: перше правило оптимізації продуктивності - "Не". Це додає рішення додаткову складність, і ви вважаєте за краще уникати цього, поки не отримаєте переконливої ​​бізнес-мотивації.

Якщо це так, чи корисний лише Магазин подій під час перебудови вашої бази даних "запис" у результаті змін схеми? Або я пропускаю щось велике?

Вам не вистачає чогось більшого.

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

Тому ми взагалі пишемо в магазин подій.

Зокрема, це означає, що кожну зміну потрібно записувати в магазин подій.

Змагаючі письменники можуть потенційно або знищити записи один одного, або привести систему до ненавмисного стану, якщо вони не знають про редагування один одного. Отже, звичайний підхід, коли вам потрібна послідовність, - це звернення записів до певної позиції в журналі (аналогічно умовному PUT у програмі HTTP). Невдале написання повідомляє письменнику, що їхнє поточне розуміння журналу не синхронізоване і що їм слід відновитись.

Повернення до відомого хорошого становища та повторення будь-яких додаткових подій, оскільки цей момент є загальною стратегією відновлення. Ця відома хороша позиція може бути копією того, що є в локальному кеші, або представлення у вашому магазині знімків.

На щасливому шляху ви можете зберегти в пам’яті знімок агрегату; вам потрібно звернутися до зовнішнього магазину лише тоді, коли немає локальної копії.

Крім того, вам не потрібно буде повністю загравати, якщо у вас є доступ до книги записів.

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

Існує багато випадків, коли ця складність не представляє інтересу, оскільки дрібнозернисті агрегати із обмеженим терміном життя зазвичай не збирають достатньо подій для користі, щоб перевищити витрати на підтримку кешу знімків.

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


Це все звучить розумно. Коли я пограв з фіктивним впровадженням ES деякий час тому, я використовував EventStore, щоб гарантувати послідовність, але я також синхронно написав те, що ви називаєте "сховище знімків". Це означало, що поточний стан завжди було готове читати без необхідності повторювати події. Я підозрював, що це не буде гарно, але, оскільки це була лише вправа, я не проти.
MetaFight

6

Оскільки ви не вказуєте, якою була б мета бази даних "написати", я припускаю тут, що ви маєте на увазі саме це: під час реєстрації нового оновлення до агрегату, замість того, щоб збирати агрегат із магазину подій, ви підніміть його з бази даних "написати", підтвердіть зміни та опублікуйте подію.

Якщо це саме ви маєте на увазі, то ця стратегія створить умову для невідповідності: якщо нове оновлення відбудеться до того, як останнє отримало шанс внести його в базу даних «запису», нове оновлення закінчиться перевіреним щодо застарілих даних, таким чином потенційно видаючи "неможливу" (тобто "заборонену") подію і пошкоджуючи стан системи.

Наприклад, розглянемо постійний приклад бронювання місць у театрі. Щоб запобігти подвійному бронюванню, потрібно переконатися, що місце, яке забронюється, вже не зайняте - саме так ви називаєте "перевірку". Для цього ви зберігаєте список уже забронюваних місць у базі даних "написати". Потім, коли надходить запит на бронювання, ви перевіряєте, чи запитується місце у списку, а якщо ні, ви опублікуєте подію "заброньовано", інакше відповісте повідомленням про помилку. Потім ви запускаєте процес прогнозування, коли ви слухаєте "заброньовані" події та додаєте заброньовані місця до списку в базі даних "написати".

Зазвичай система функціонує так:

1. Request to book seat #1
2. Check in the "already booked" list: the list is empty.
3. Issue a "booked seat #1" event.
4. Projection process catches the event, adds seat #1 to the "already booked" list.
5. Another request to book seat #1.
6. Check in the list: the list contains seat #1
7. Respond with an error message.

Однак що робити, якщо запити надходять занадто швидко, а крок 5 відбувається перед кроком 4?

1. Request to book seat #1
2. Check in the "already booked" list: the list is empty.
3. Issue a "booked seat #1" event.
4. Another request to book seat #1.
5. Check in the list: the list is still empty.
6. Issue another "booked seat #1" event.

Тепер у вас є два події для бронювання одного місця. Пошкоджено стан системи.

Щоб цього не сталося, ніколи не слід перевіряти оновлення щодо проекції. Щоб перевірити оновлення, ви збираєте сукупність із сховища подій, а потім перевіряєте оновлення відносно нього. Після цього ви видаєте подію, але використовуйте захист часових міток, щоб переконатися, що жодних нових подій не було видано з моменту останнього читання з магазину. Якщо це не вдалося, ви просто спробуйте.

Перебудова агрегатів із магазину подій може призвести до покарання за ефективність. Щоб пом'якшити це, ви можете зберігати сукупні знімки прямо у потоці подій, позначені ідентифікатором події, з якої створено знімок. Таким чином, ви можете відновити сукупність, завантаживши останній знімок і відтворюючи лише події, що відбулися після нього, на відміну від того, щоб завжди відтворювати весь потік подій з початку часу.


Дякуємо за вашу відповідь (і вибачте за те, що ви так довго відповіли). Те, що ви говорите про перевірку даних щодо бази даних, не обов'язково відповідає дійсності. Як я вже згадував в іншому коментарі, в прикладі реалізації ES, з яким я грав, я переконався, що синхронно оновлював свою базу даних записів (і зберігав concurrencyId / timetamp). Це дозволило мені виявити оптимістичні порушення одночасності, не потребуючи готовності до EventStore. Зрозуміло, синхронні записи самі по собі не захищають від пошкодження даних, але я також робив записи з одним доступом (однопоточні).
MetaFight

Отже, у мене були вирішені питання щодо їх узгодженості. Хоча я припускав, що це відбувається за рахунок масштабованості.
MetaFight

Синхронне записування до бази запису все ще несе небезпеку пошкодження: що станеться, якщо ваш запис у магазин подій успішний, але ваш запис у базу даних запису не вдається?
Федір Сойкін

1
Якщо прочитане проекція не вдасться, воно буде просто спробувати, поки це не вдасться. Якщо він вибивається прямо, він прокинеться і продовжить там, де врізався - іншими словами, також спробуйте. Зовнішній ефект, що спостерігається, нічим не відрізнятиметься від нього, лише працює трохи повільно. Якщо проекція буде постійно провалюватися та виходити з ладу, це означає, що в ній є помилка, і це доведеться виправити. Після виправлення воно відновиться з останнього хорошого стану. Якщо вся прочитана база даних пошкодиться в результаті помилки, я просто відновити базу даних з нуля, використовуючи історію подій.
Федір Сойкін

1
Жодні дані ніколи не втрачаються, це важливо. Дані можуть на деякий час застрягти у незручній (для читання) формі, але вона ніколи не втрачається.
Федір Сойкін

3

Основна причина - продуктивність. Ви можете зберігати знімок для кожної фіксації (фіксація = події, що генеруються однією командою, як правило, лише одна подія), але це дорого. Поряд із знімком потрібно також зберігати фіксацію, інакше це не буде пошук подій. І все це потрібно робити атомно, все або нічого. Ваше запитання справедливе лише в тому випадку, якщо використовуються окремі бази / таблиці / колекції (інакше було б саме пошук подій), тому ви змушені використовувати транзакції , щоб гарантувати послідовність. Операції не масштабуються. Потік подій, що додається тільки (магазин сховищ) - це мати масштабованості.

Друга причина - сукупна інкапсуляція. Вам потрібно захистити його. Це означає, що Агрегат повинен бути вільний змінити своє внутрішнє представництво в будь-який час. Якщо ви зберігаєте його і сильно залежатимете від нього, тоді вам доведеться дуже важко працювати з версіями, що станеться точно. У ситуації, коли ви використовуєте знімок лише як оптимізацію, при зміні схеми ви просто ігноруєте ці знімки ( просто ? Я не думаю, що так; удача визначивши, що схема агрегату змінюється - включаючи всі вкладені об'єкти та об'єкти цінності - у ефективний спосіб та управління цим).


Коли моя схема сукупності змінюється, чи не було б простим питанням відтворення моїх подій для створення оновленої бази даних "написати"?
MetaFight

Проблема полягає в виявленні цієї зміни. Агрегат може бути дуже великим, з великою кількістю файлів / класів.
Костянтин Гальбену

Я не розумію. Зміна відбудеться з випуском програмного забезпечення. Випуск, ймовірно, буде поставлений із сценарієм бази даних для відновлення бази даних "написати".
MetaFight

Потрібно багато роботи над сценарієм міграції. Під час роботи програми додаток має бути вимкненим.
Костянтин Гальбену

@MetaFight, якщо потік дуже великий, знадобиться багато часу, щоб відновити нову сукупну схему ... Я зараз думаю про знімок, який є станом живої проекції, який міг би працювати до виходу нового сукупності схема
Narvalex
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.