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

Комментарии 23

А не проще собрать список записей для изменения (добавления, удаления), а затем разом одной транзакцией их вкатить?

Единственное что тут потребуется - журнал, в котором хранится два образа записи - "до" и "после" (это в случае изменения записи, в случае удаления только "до", добавления - только "после").

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

Если в процессе наката что-то пошло не так - отменяем транзакцию (хотя наличие журнала позволяет просто развернуться и вернуть все взад руками - вся история есть).

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


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

Можно и не отказываться. У нас так (правда, причины не совсем те, но...)

Есть таблица, для каждой таблицы есть соответствующий ей журнал, повторяющий ее структуру, но имеющий дополнительные поля:

Имя рабочей станции
Дата
Время
Сиквенс
Флаг образа (B - before, A - after)

Также для каждой таблицы есть "модуль внешнего ввода" - ему передается на вход запись и флаг A - append, M - modify, D - delete. Он уже разберется что делать. Есть, например, изменение или удаление записи, он прочитает запись из таблицы, сравнит ее с образом B в журнале и есть они совпадут - изменит или удалит. Если нет - выдаст ошибку что запись была кем-то изменена в промежутке между тем, как вы захотели ее поменять и тем как дело дошло до конкретных изменений.

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

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

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

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

И тут возможен "накат с разворотом" - вам надо вкатить 100 записей в 10 таблиц. Идете по головному журналу. на 50-й записи сломались - просто разворачиваетесь и идете обратно меняя местами образы A и B. Все восстановилось.

У нас десятки тысяч таблиц, тысячи одновременно работающих с ними процессов (это не совсем микросервисы, скорее, акторы) и все это работает.

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

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

Не совсем. Журналы это несколько большее чем транзакция. Это еще и историчность.

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

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

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

Oracle undo log как раз для этих целей есть. Там все операции отмены лежат. Если транзакция хочет прочитать старую версию - берет последнее состояние таблицы и накатывает undo однин за одиним пока не дойдет до нужной версии транзакции

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


Например, гипотетический сервис продажи билетов, продающий не более Rk билетов на дату k. Допустим, есть таблица с двумя столбцами: первичный ключ — дата k, доступно к продаже — R. Два агента параллельно продают билет на одну дату. Они оба получают остаток, видят, что его хватает, и передают в журнал записи на вливание? Так они при вливании получат конфликт, что запись изменена, и вторая продажа ошибочно отменится.

У вас это работает, потому что все процессы спроектированы под такую архитектуру.

Да.

Любой произвольный бизнес-процесс со своей историей, не так просто перевести.

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

Например, гипотетический сервис продажи билетов, продающий не более Rk билетов на дату k.

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

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

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

Такие проблемы решаются иными способами

для небольших масштабов, не банковских — это SELECT FOR UPDATE.


через систему холдов, или через систему квот агентам

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


Как будто в такой системе невозможно создавать аггрегаты, а всё должно вливаться/меняться атомарными строчками, а аггрегаты не могут храниться, а должны всегда считаться на лету по фактам. Шаблон Event Sourcing в чистом виде?

для небольших масштабов, не банковских — это SELECT FOR UPDATE

А если речь о 10-ти таблицах по которым надо раскидать данные?

Пытаюсь сообразить, как это может выглядеть.

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

Если продажа прошла - все ок. Если нет - холд снимается и количество доступных единиц обратно увеличивается на 1. Это быстрые операции, тут коллизии маловероятны.

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

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

Считать по фактам в hi-load системах такое себе. И "атомарная строчка" может раскладываться по 10-ти таблицам.

Наиболее плотно используемые вещи хранятся в т.н. "витринах", которые обновляются журнальными мониторами по изменению записи в таблицах.

Холд — это когда агент начинает продажу и количество проданных единиц сразу уменьшается на 1. В момент запроса на начало продажи. Т.е. продаваемая единица "ставится на холд" (как платеж по банковской карте).

Если продажа прошла — все ок. Если нет — холд снимается и количество доступных единиц обратно увеличивается на 1. Это быстрые операции, тут коллизии маловероятны.


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


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

Система хорошая, но когда осталась последняя единица товара — не работает.

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

Это понятно. Вопрос, как это реализуется на уровне БД.
Уменьшение на 1 значения в какой-то строчке таблицы? Тогда это конфликт.


Квоты — это когда у каждого агента есть своя строчка с выделенным ему количество единиц

Если агент последовательно продал 3 билета, в "ночной" пакет обновлений войдёт 3 последовательных апдейта одной строки, и тогда реализуется негативная ветка сценария ниже?


Есть, например, изменение или удаление записи, он прочитает запись из таблицы, сравнит ее с образом B в журнале и есть они совпадут — изменит или удалит. Если нет — выдаст ошибку что запись была кем-то изменена в промежутке между тем, как вы захотели ее поменять и тем как дело дошло до конкретных изменений
А если речь о 10-ти таблицах по которым надо раскидать данные?

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

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

Второй вариант - оптимистический. Который подразумевает что скорее всего никому наш ресурс не появится и поэтому блокировать его не нужно, а в конце когда надо фиксировать транзакцию - надо делать проверку не изменилось состояние нашего объекта - если нет, все норм, накатываем обновления, ну а если изменилось - отваливаемся с ошибкой (snapshot too old или "извините, пока вы думали, этот билет кто-то уже купил").

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

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

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


Так делает Firebird или MS SQL Server в режиме версионной БД (READ_COMMITTED_SNAPSHOT ON, ALLOW_SNAPSHOT_ISOLATION ON)


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

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

Для меня конец транзакции — это исполнение оператора COMMIT (или ROLLBACK).

Решили написать свое решение. 

Что помешало научиться готовить temporal.io (единорог, на минуточку)? Пилить инфраструктурный велосипед - это значит откладывать на потом "business value".

Ещё один пример в мою коллекцию:
https://habr.com/ru/companies/oleg-bunin/articles/418235/
https://habr.com/ru/companies/ozontech/articles/590709/

Тема с Saga также хорошо описана в книге - Крис Ричардсон. Микросервисы. Паттерны разработки и рефакторинга

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

А что тогда почитать?

Почитать про что?
Про МСА - практически нечего, разве что статьи Фаулера на сайте.
Есть неплохие доклады (э, мой, например), есть очень неплохие специалисты в некоторых чатах в TG (Сергей Баранов, например). С книжками - совсем сложно.

Если конкретно про саги - то можно посмотреть мой рассказ, например: https://www.youtube.com/watch?v=0_ziFXXEW_M. Но вообще тема довольно плохо проработана (

А какую производительность получили? А то PG не очень удачное решение для подобных задач. А вообще получилось очень похоже на то, что я описывал в https://youtu.be/hXuyT6T3fNU?t=1471, даже код описания сценария похожий.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий