Комментарии 147
Насколько я понимаю, в бухгалтерских системах обычно хранятся не балансы счетов, а транзакции. То есть, при переводе со счёта на счёт в таблицу транзакций добавляется строка с номерами счетов и суммой. А баланс счетов подсчитывается уже по базе транзакций и там невозможна ситуация, когда деньги ушли, но не никуда не пришли или наоборот.
Как в такой системе показываются баланс и сделки за последние n дней например?
Понятно что при создании счета его баланс устанавливается в 0, а потом сверху накидывают транзакции.
Но как оптимизировать показ последних операций, например за сутки, если счету десятки лет и сотни тысяч транзакций? Не пересчитывать же с первой транзакции каждый раз?
По джобе раз в сутки считать баланс на 00:00, в течение дня брать из бд этот предпосчитанный баланс и прибавлять к нему транзакции за сутки
Я сам внутри не копался, но думаю, что просто периодически подводятся итоги и балансы сохраняются. Поэтому достаточно посчитать транзакции за период с последнего подведения итога + баланс на тот момент времени. У банков есть понятие закрытого опердня (в который новые транзакции уже не вносятся) и открытого опердня, который ещё может пополняться новыми данными.
про "закрытый опердень" не слышал. Возможно, мы работает несколько по иной схеме.
Обычная работа - "дневной юнит". Это текущий день. Примерно в полночь по мск начинается процедура закрытия опердня и перехода на новый (у нас это называется EOD - End-Of-Day). Длится она 3-4 часа примерно - это фактически сведение всего баланса.
Чтобы операции могли продолжаться в течении EOD, перед его началом создается т.н. "юнит ночного ввода" - копия дневного юнита на момент начала EOD. Дальше, в дневном юните начинается процедура EOD, а все операции продолжатся в ночном юните.
Когда EOD завершился и дневной юнит перешел на новый день, в него "накатывается" (по журналам, писал тут про это уже) все, что случилось в ночи. Т.е. все изменения, которые произошли в течении EOD. И дальше уже обычная работа в новом дне.
В целом, это не единственная схема. Есть и другие. Например, без ночного юнита (т.е. в момент EOD никакие операции не проводятся). Там работает "кешир" - все операции ставятся в очередь (кеш), которая потом будет разбираться после завеhшения EOD уже в новом дне.
Считается, что наша схема работает "в реальном времени" т.е. банк работает без перерыва на EOD, схема с кеширом - в перерывом (операции принимаются к исполнению, но откладываются до следующего дня и реально будут совершены уже в следующем дне).
Возможно, есть и другие схемы работы в реальном времени, но там подробностей не знаю.
Честно говоря, АБС в банке настолько сложная и большая система, с таким большим количеством бизнес-процессов, что так просто описать ее практически невозможно. Только в каких-то общих словах.
про "закрытый опердень" не слышал.
Потому что, судя по тому, что Вы пишете, вы ближе к операционному блоку работаете. Закрытый опердень это из бухгалтерской лексики, про отражение операций в бухучёте. У них, как правило, несколько опердней открыто одновременно, так как документы для отражения в учёте могут досылаться несколько дней. Соответственно, остатки на счетах по открытым опердням они неокончательные ещё несколько дней (в январе из-за праздников и для подведения итога года могут до середины января держать открытыми). После закрытия опердня остатки на конец этого дня становятся окончательными, завести проводки в этот день уже невозможно. Иногда по распоряжению главбуха закрытый опердень открывают заново и вносят необходимые исправления. Или вносят в более поздние даты.
Тут как и транзакция, слово счёт многогранное. Счёт в бухучёте это иная сущность, чем счёт клиента. Но механизмы где-то схожие.
Не. Я ж не по бухгалтерии. Я по банку. Тут немного иначе все.
А работаю на уровне АБС. Т.е. то, что на центральных серверах. Но направление - автоматизация процессов комплаенс-контроля + клиентские данные. Со счетами мало работаем, наше - это во-первых, типы клиентов (сами клиенты, доверенные и уполномоченные лица, держатели карт и все вот это вот), списки росфина (экстремисты-террористы) и совпадения субъектов списков с клиентами, всякие ДУЛы (документы удостоверяющие личность), адреса и т.п.
Ну и проверки разные, само собой. Т.е. приходит некий СУЛТАНОВ ЛЕЧИ РАСУЛОВИЧ и говорит "хочу счет открыть у вас". Девочка начинает его данные вводить в систему, а ей выскакивает "найдено совпадение со списком ПЭ (подозреваемых в экстремизме)". Наша проверка отработала на попытку ввода данных.
Аналогично если кто-то из клиентов попробует этому персонажу деньги куда-то в другой банк перевести. Тоже сработает - платеж отправится на ручной контроль с службу комплаенса, те уже будут детально изучать что к чему (и скорее всего заблокируют перевод).
Закрытый опердень это из бухгалтерской лексики, про отражение операций в бухучёте. У них, как правило, несколько опердней открыто одновременно, так как документы для отражения в учёте могут досылаться несколько дней.
В банке с этим проще, слава богу. Тут все в текущем дне делается т.к. работа с платежными поручениями идет.
Вообще по правилам ЦБ опердень они должны закрыть к 12-00 следующего рабочего дня. Но по факту это почти никогда не соблюдается. Я где-то в правилах вычитал, что у банка должны быть распечатки операций по всем счетам за день (это ещё в доэлектронную эпоху было, наверное, сейчас это требование отменили). На мой вопрос замглавбуха сказала, что такой фигнёй никто не занимается. Если проверка придёт, то, конечно, распечатки им дадут.
Бухгалтерия (а она ив банке есть) - это одно. Собственно банк (АБС) - это другое. АБС живет своей жизнью. И да, она (АБС) подчиняется [достаточно жестким] регламентам ЦБ.
EOD начинается в полночь по МСК. Заканчивается... Ну точно не скажу, но часам к трем-четырем утра (опять по МСК же). Дальше там есть еще фаза USRAFT - ряд обязательных операций в начале нового дня (это где-то 5-6 часов утра - время, когда нагрузка на систему относительно невелика). Что там происходит в полном объеме не скажу - много чего. По нашей линии, к примеру, сверка клиентских данных со списками росфина (того, что обновилось за прошлый день - где-то клиентские данные поменялись, также может прийти и загрузиться новая версия списков) и обновления стоплистов и списков совпадений... Или рассылка разного рода уведомлений клиентам (на стороне АБС, конечно, не сама рассылка - этим одна из внешних систем занимается, но подготовкой кому что послать занимается АБС). Например, идет выборка клиентов у которых заканчивается срок действия ДУЛ - их надо предупредить что через ... дней, если они не предоставят обновленные данные, наступит блокировка.
Так или иначе, к началу рабочего дня (9 МСК) АБС уже гарантировано работает в новом дне и готова к работе с полной нагрузкой.
А бухгалтерия банка живет сама по себе и подчиняется другим законам. Думаю, что она является таким же клиентом для АБС, как и другие.
На счете текущий баланс и холды - суммы, которые еще не списаны (получение денег плательщиком не подтверждено), но уже "обещаны к списанию".
Плюс платежные документы, связанные с этим счетом
Но как оптимизировать показ последних операций, например за сутки
Как уже ответили - иметь снепшот/подсчитанный актуальный баланс на определенную дату / с определенной транзакции. Вы наверняка слышали термин опердень (операционный день).
Вы слышали про такой подход, как event sourcing? Вот тут такая же логика: ваш баланс - это результат всех операций. При совершении транзакции, восстанавливается баланс исходя из всех событий. Естественно когда событий много, это может быть проблемой в производительности. Для таких случаев делаются снапшоты каждые n-событий или периодически. Таким образом оптимизированная версия будет восстанавливать баланс из снапшота и событий после снапшота. При грамотном подходе к блокировкам можно добиться хорошего уровня параллелизма
Тут, кажется, подойдёт комбинация: агрегаты на конкретные даты + инкрементальный подсчёт по дневным транзакциям. Плюс индексы по дате и счёту обязательны. В Postgre часто делают материализованные представления для таких целей.
Строго говоря, я не встречал где было бы нужна история балансов по счетам на конкретные даты. История операций - да (выписка по счету). Текущий баланс и холды - да. Но вот вся история балансов и холдов... Впрочем, я со счетами работаю мало - мое направление это комплаенс и клиентские данные.
Со счетами приходится работать, но косвенно. Типа такого:
1. Отбираем клиентов ФЛ открывшие счет на определенных балансовых позициях в предыдущую дату:
1.1. отбираем базовый номер клиента:
1.1.1. счет в статусе «Открыт» и счет не зарезервирован;
1.1.2. счет открыт день назад;
1.1.3. маска счета входит в список разрешенных;
1.1.4. исключить запрещенные маски счетов
Или
Рассчитать сумму займа, получив актуальный курс, и сконвертированную в рублёвый эквивалент сумму на счетах, тип которых входит в список ...
Рассчитать сумму вклада, получив актуальный курс, и сконвертированную в рублёвый эквивалент сумму на счетах, тип которых входит в список ...
Или
1. Для отправителя и получателя выполнить проверки:
1.1. Получить все незарезервированные счета клиента, маски которых начинаются на ...
1.2. Определить самую раннюю дату открытия счета
Если дата меньше или равна граничной ...
Ну и так далее... Это уже из комплекса "Онлайн контроль платежей" (собственно, это команда Системы Расчетов делает, мы только делали комплекс комплаенс-проверок для них).
Строго говоря, я не встречал где было бы нужна история балансов по счетам на конкретные даты.
Например, для начисления процентов на остаток по счёту. Берутся балансы за каждый день месяца, потом на среднемесячный остаток начисляется процент. Ну и т.д.
Да, возможно. С этим не сталкивался совсем. Подозреваю, там какие-то отдельные исторические таблицы ведутся для тех счетов, где это надо.
В основной таблице счетов этого нет - там и так очень много всяких данных (дата открытия/закрытия счета, бухрежим по счету еще куча всяких признаков) - дублировать все это в историю нет смысла.
Так работа банка настолько многоплановая, что её обсуждение часто напоминает картинку, где слепые ощупывают слона и пытаются спорить на тему, что же представляет из себя этот слон...
Это да... В точку :-)
Команд много. Даже на уровне АБС. И каждая работает по своему направлению. Но при этом все интегрированы друг с другом.
Тут бывает что прежде чем выйти с очередной поставкой на комитет по внедрению, нужно от десятка других команд получить заключение по регресс-тестам о том, что наша поставка ничего в их процессах не ломает...
Ну да, логично. В подавляющем большинстве случаев реально достаточно текущего баланса и выписки. Но у меня в практике пару раз всплывали кейсы, где прям нужна была история балансов по дням. Один из них — ретроспективные проверки по требованиям комплаенса, где важно было восстановить "что именно было на счету" на конкретную дату. Второй — расчёты по условиям договора, где привязка шла к среднесуточным остаткам.
Ещё это часто нужно для всяких регуляторных отчётов или аудита, особенно если банк более-менее крупный и не хочет потом руками в журнале копаться.
В Postgre мы делали так: агрегаты на дату (daily_balances
) + инкрементальный пересчёт из транзакций, плюс индексы по счёту и дате. Работает стабильно, главное — не забывать про дедупликацию и контроль полноты. Иногда это ещё кладут в материализованные представления, если перфоманс начинает страдать.
Так что, в целом, понимаю позицию. Но бывают сценарии, где без этого — никак.
Да, посмотрел по нашей документации. Есть отдельная таблица
Остатки в оборотно-сальдовой ведомости
В оборотно-сальдовой ведомости хранятся входящие/исходящие остатки за каждую дату по всем счетам, существующим в системе.
Данные по оборотам пишутся (линковщиком) в таблицу в режиме on-line - одновременно с записью в таблицу парных проводок (бухгалтерский журнал).
Следует учитывать, что в таблицу данные помещаются только при условии движения средств по счету (включая переоценку и дооценку).
Просто я никогда с этой таблицей не сталкивался... Иногда приходится работать с основной таблицей счетов, изредка - таблица проводок.
В банке по счету хранится текущий баланс счета, сумма текущих холдов (незавершенные операции) + "платежные документы" - от кого, кому, сумма. С привязкой к счету.
Естественно, показать операции за сутки, неделю, месяц нет проблем. Это называется "выписка по счету". Просто нужно сделать выборку всех платежных документов, привязанных к этому счету.
Верно. Также есть 2 механизма (на самом деле больше) механизма отката транзакции:
проведение поверх корректирующей
полностью удаление информации о транзакции
Последняя весьма раздражающая, но используется банками в случае быстрого возврата денежных средств. Например при возврате на кассе в течение короткого (до дня) времени.
Например при возврате на кассе в течение короткого (до дня) времени.
Насколько я понимаю, речь идёт о не до конца проведенной транзакции. То есть, клиент приходник выписал, а по факту деньги не внёс. Но у транзакций ещё статусы могут быть разные, так что этот неисполненный приходник может сохраниться в одной таблице со статусом "отменено" и потом не попасть в таблицу проводок по счетам, куда попадают только исполненные.
Насколько я понимаю, речь идёт о не до конца проведенной транзакции.
Не обязательно. Это может быть зафиксированная транзакция. Например провели на кассе картой. И через 10 минут передумали и решили сделать возврат. Банк имеет право такую транзакцию полностью удалить и вы ее нигде не увидите. Это зависит от многих факторов, но такое имеет место быть.
Не транзакции, а платежные документы. Платкльщик, счет плательщика, получатель, счет получателя, сумма платежа...
Вот понятие платёжных документов, оно довольно странное. Их форма и содержание — это полная свобода творчества заинтересованных сторон. Например, если я, находясь в РФ, прошу своего заграничного друга оплатить мне за границей какой-то сервис (например, Zoom), то наша переписка в чате является платёжным документом или нет? Ведь эта переписка потом служит основанием для подсчёта наших взаимных обязательств друг перед другом, кто кому сколько должен, по аналогии с тем, как банки ведут учёт денег на счетах клиентов...
Замечание справедливое. Исправил вступление. Туториал c фокусом на понимание изоляций и свойств стэйтментов. В этой статье рассматриваю только проблему обновления двух счетов. Бухгалтерские системы гораздо сложнее.
А вот тут еще и фантомные чтения надо рассматривать в тему статьи.
Спасибо. В этих примерах фантомные чтения не помешают. Но их можно будет рассмотреть на другой подобной задаче
Вообще, это основа. Прочитал запись, что-то в ней сделал, перед тем как записывать - проверь. Прочитай еще раз - не изменилось ли что-то пока ты работал с записью
Комментарий по фантомным чтениям относился к варианту досчета остатка по транзакциям.
И все равно вопрос - вы начинате транзакцию на списание 300р со счета. И вэтот же момент начинается транзакция на списание 400р. А на счете вмего 500р. Каждая транзакция по отдельности валидна. Вместе - нет. Что делать?
Вопрос наверное не ко мне, а к автору статьи.
Собственно он статьей на Ваш вопрос и отвечает, что делать и какие патерны серилизации применять.
К сожалению, там нет ответа на поставленный вопрос.
Я уже писал как это происходит в реальной жизни.
Формируется платежный документ где указывается плательщик, получатель, счет плательщика, счет получателя. Причем, эти счета на обязательно в одном банке. Скорее всего, даже в разных. И сложности возникают только со списанием денег со счета плательщика. С зачислением на счет получателя все проще.
Если счет плательщика в нашем банке, Сумма по платежному документу переводится со счета на холд - резервируется (если на счете достаточно денег). Это быстрая операция, она делается с блокировкой записи.
Платежный документ отправляется на контроль. Результатом может быть безусловное "разрешить" (все автоматические проверки пройдены) - тогда документ передается на исполнение. Или документ может быть отправлен на ручной контроль в службу комплаенса. Оттуда может прийти решение "разрешить" - тогда на исполнение. Или "запретить" - тогда формируется отказ в операции и сумма с холда возвращается обратно на счет (опять с полной блокировкой записи - это одна запись, там есть поле текущего баланса, есть поле суммарного холда).
В целом контроль платежей штука достаточно сложная - там много разных проверок проводится.Сумма холда уменьшается на сумму платежного документа только тогда, когда от получателя придет подтверждение о том, что деньги реально пришли на его счет (иногда это может занимать несколько секунд, иногда несколько минут).
Т.е. одной транзакцией тут никак не обойтись - между переводом денег с баланса на холд и списанием их с холда или возвратом обратно на баланс (в случае отказа в операции или неподтверждения операции в установленные сроки) может пройти существенное время.
Это и есть та самая (причем, достаточно упрощенная) бизнес-логика, в отрыве от которой все это превращается в сферического коня в вакууме.
Кроме того, транзакции достаточно сильно грузят сервер. Я скажу крамольную вещь, но мы работаем с COMMITMENT CONTROL (*NONE). Т.е. без коммитов и роллбеков. Вместо этого ведется журналирование всех операций (в специальные журналы пишутся образы записей "до" и "после" изменения (или только "после" в случае добавления или только "до" в случае удаления). Плюс двойное чтение - прочитали запись "до", внесли изменения (получили "после"), перед записью изменений еще раз читаем запись с проверкой - совпадет она с образом "до" - если да, то записываем изменения, если нет - возвращаем ошибку "кто-то другой изменил запись" (и дальше она уже по бизнес-логике обрабатывается).
По журналам можно откатить что угодно - хоть вчерашнее, хоть позавчерашнее. Кроме того, по журналам в начале нового дня накатываются все изменения из юнита ночного ввода (писал ранее об этом).
Правда, опять крамола, мы не используем SQL для изменений в продуктовых таблицах. Наш стек позволяет напрямую работать с БД (SQL используется там, где нужный сложные выборки по нескольким таблицам и многим условиям, и то, там не так все просто с точки зрения производительности).
Строго говоря, для каждой продуктовой таблицы есть "опция ведения". Которая состоит из 4-х модулей:
Update модуль - собственно работа с таблицей и ее журналом. Чтение образа "до", запись образа "после" (с проверкой что "до" не изменился). В реальной жизни может быть достаточно сложным т.к. может работать не с одной, а с несколькими логически связанными таблицами.
Validate модуль - контроль валидности данных записи в соответствии с бизнес-логикой
Модуль интерактивной работы с записью (позволяет вносить ручные изменения). Вводим уникальный ключ - если запись есть - работаем режиме изменения, если нет - в режиме добавления. После внесенных изменений вызывается Validate модуль и, если ошибки нет - Update модуль.
Модуль внешнего ввода. Получает на вход образ "после", читает из таблицы образ "до" (опять, есть запись - изменение, нет - добавление), вызывает Validete модуль, если ошибок нет - Update модуль. Этот же модуль позволяет делать "накат по журналу". Для этого на вход передается не образ "после", а имя библиотеки где лежит журнал и ключ записи с образом "после". В этом случае образ "после" берется из журнала.
Данная схема кажется сложной, но стабильно работает в условиях очень больших нагрузок.
у автора же для этого как раз поле с версией заведено. Логическая transaction_id так сказать
Мне кажется автор просто не удачно назвал статью, тем самым притянув не ту аудиторию. Статья вообще не про "денежные переводы". И тут я понимаю Вы все прекрасно расписали.
В моем понимании, статья только как выполнить конкурентный update без lost update и не более того, даже другие проблемы типа фантомных чтений не рассматриваются.
Но безусловно интересно почитать детали Вашей реализации.
Мне кажется автор просто не удачно назвал статью, тем самым притянув не ту аудиторию.
Это действительно так, но раз уж это произошло, то почему бы и не обсудить смежную тему?
Я не один раз наблюдал, когда неправильно прописанная (или неправильно понятая) бизнес-логика приводила к непомерному росту затрат на техническую реализацию.
Например, для реализации одной задачи мы сводили все сделки в одну таблицу Excel (выгрузка из системы). Всё хорошо работало, пока все сделки были в одной IT-системе. Но как только появилась вторая система со сделками (при живой первой), технари тут же бросились реализовывать перекачку сделок из одной системы в другую (через какие-то шины, хранилища данных и т.д. и т.п.), чтобы потом объединить все сделки в одной табличке. Хорошо, что я вовремя это заметил и сказал, что их необязательно объединять в системе, исходя из бизнес-задачи табличек может быть больше чем одна, их легче объединить вовне системы, чем внутри и т.д. и т.п., что вызвало значительный вздох облегчения у технолога на другом конце телефонной линии...
Кроме того, транзакции достаточно сильно грузят сервер. Я скажу крамольную вещь, но мы работаем с COMMITMENT CONTROL (*NONE).
И дальше, похоже, вручную делается то, что в более простых случаях (или более умных базах) делается самим движком базы данных.
Плюс двойное чтение - прочитали запись "до", внесли изменения (получили "после"), перед записью изменений еще раз читаем запись с проверкой - совпадет она с образом "до" - если да, то записываем изменения,
Без уточнения что происходит - не гонка? Потому что между "еще раз читаем с проверкой' и 'то записываем изменения' - записи могут и измениться.
И дальше, похоже, вручную делается то, что в более простых случаях (или более умных базах) делается самим движком базы данных.
Тут вопрос не в том, что может БД. Она (DB2 for i) много что может.
Вопрос в производительности в условиях больших нагрузок. Наши исследования показали что SQL может начать работать нестабильно (в смысле скорости выполнения) в ситуациях, когда один и тот же запрос начинает выполняться параллельно из многих заданий (job) с большой плотностью вызовов. При этом прямая работа с БД не только работает стабильно в таких условиях, но и еще обеспечивает меньшую загрузку системы.
Поэтому на использование SQL есть ряд ограничений, сформулированных в нефункциональных требованиях, основанных на реальной практике работы системы.
Это то, что бывает сложно понять людям, не имеющим иных возможностей работы с БД, помимо SQL. Но у нас, слава богу, такие возможности в системе заложены.
Ну и SQL (точнее, commitment control) не решает проблему журналирования, возможность которого для нашей архитектуры является ключевой.
Без уточнения что происходит - не гонка? Потому что между "еще раз читаем с проверкой' и 'то записываем изменения' - записи могут и измениться.
Да. Могут измениться. Именно поэтому и проводится проверка с выдачей ошибки. Как эта ошибка обрабатывается - тут все зависит от бизнес-логики процеса.
Но блокировка записи на длительное время (пока идет какая-то обработка - это прямой путь к дедлокам и деградации производительности. Что в нашем случае неприемлемо.
Я описал как в реальности происходит обработка платежа - кратковременная блокировка при переносе суммы платежа с баланса на холд. Дальше платеж может обрабатываться сколь угодно долго - баланс счета уже уменьшился, но в случае отката (отказа в проведении платежа) может быть увеличен обратно обратной операцией - возврата суммы с холда обратно на баланс. И транзакцией это не решается т.к. вы не можете в такой системе держать транзакцию открытой в течении длительного времени (а это может достигать суток - пока пройдет проверки, пройдет при необходимости ручной контроль, будет получено подтверждение из того банка, на счет в котором уходит платеж...)
А чтобы перевести со счета на холд транзакция не нужна - это одна запись (счет - сумма текущего баланса - сумма холдов по счету) - read (с блокировкой), cerbal -= n, curhold += n, update (с разблокировкой). А транзакция по определению, это цепочка связанных изменений в нескольких записях.
И транзакцией это не решается т.к. вы не можете в такой системе держать транзакцию открытой в течении длительного времени
Только главное не налететь на синонимы терминологии. Потому что с точки зрения клиента: пока холд не нулевой - это разве не означает, что есть открытая транзакция(другая, не БД-ная) перевода денег? Ну вот и висит эта транзакция несколько дней.
И все описанный операции - это ручное выполнение протокола этой транзакции, а не БД-ной. Чтобы было то самое 'либо целиком выполнилось, либо сделаем вид что ничего не было'.
Тут во всей переписке надо различать, где платёжная транзакция, а где транзакции баз данных.
Ну на самом деле Холды часто бывают не нулевые. Например, на счет у вас 10 000р На холде 0. Пошли по магазинам - тут купили на 1 000, там 2 000, здесь на 3 000... В результате "на счете" 4 000, на холде 6 000. Потом, когда начинают приходить подтверждения по платежным операциям, холд начинает уменьшаться (каждый раз на ту сумму, на которую пришло подтверждение).
Или на заправке - оплачиваете бензина на 2 500р Они встают на холд. Но в бак влезло только на 2 400р. Тут возможны варианты (это уже как банк отработает). Или сначала отмена все операции на 2 500р с возвратом всей суммы с холда на счет и сразу создание новой операции уже на 2 400р с перемещением на холд со счета 2 400р. Или 2 400р остаются на холде до подтверждения , а 100р оформляются как "возврат" т с холда переходят обратно на счет. И так и этак встречал.
перед записью изменений еще раз читаем запись с проверкой
Да. Могут измениться. Именно поэтому и проводится проверка с выдачей ошибки.
Ну так после проверки и до записи они все равно могут измениться.
Там уже нет т.к. второе чтение с проверкой блокирует запись от любых изменений.
Разница между первым и вторым чтением в том, что после первого чтения может пройти достаточно длительное время на внесение изменений в данные, валидацию и т.п. И блокировать запись от изменений на это время в высоконагруженных системах нежелательно.
После второго чтения все происходит быстро - прочитали, убедились что никто не вклинился - записали уже подготовленные изменения.
А, ну если с блокировкой тогда другое дело конечно. Только если никто не вклинивался, то и блокировка от изменений в начале процесса никому не помешает. А если вклинивался, то повторять всё заново создаст бо́льшую нагрузку на сервер, чем просто ожидание одного из процессов, и в условиях большо́й нагрузки вообще может быть невозможным, потому что постоянно будет кто-то вклиниваться.
Нет. Процессов очень много и они между собой связаны. Если один процесс встал на ожидание, то за ним может выстроиться целая цепочка и тогда встанет весь сервер (или значительная его часть).
Все это не просто так придумывалось, а на основе множества граблей и набитых шишек.
Как раз проще и дешевле решать конфликты если кто-то вклинился. Тут как в гите - что-то решается автоматом (ну просто смотрим - мы поменяли поле А, но кто-то вклинился и поменял поле Б - новое значение поля Б не противоречит нашему значению поля А - можно просто взять новое значение Б, новое значение А и сохранить запись). А что-то требует ручного вмешательства или более серьезной обработки.
за ним может выстроиться целая цепочка
Ну так если они все хотят обновить например баланс одного счета, то в вашем подходе все кроме первого откатятся, потому что первый вклинился, и повторят все действия заново. Но сохранит данные опять только один, а другие опять откатятся, потому что второй вклинился. И так далее. В результате это займет столько же времени, сколько с ожиданием, только с лишней нагрузкой на сервер.
Если они меняют разные группы полей, то можно попробовать разделить одну сущность на две со связью один-к-одному.
Я согласен, что может быть в вашей архитектуре это подходящее решение, но я бы постарался сначала использовать другие решения из-за вероятности бесконечного вклинивания. А если вероятность отсутствия вклинивания высокая, то мне кажется и с локами при правильном подходе должно быть все нормально.
В случае со счетами там проблем вообще нет.
Запись лочится на время уменьшения одного поля на сумму платежного документа для переноса этой суммы на холд. Все. Проверки платежа (а это долго) уже идут потом, вне лока.
Это именно архитектурное решение вместо залочить запись, провести весь комплекс проверок, если проверки прошли, внести изменения, разлочить запись.
Если проверки не прошли, то ничего страшного - еще раз короткий лок и возврат суммы на счет с холда. Тут логических противоречий не будет.
Это архитектурное решение для ситуации с большой плотностью обращений к записи.
Где плотность ниже - там возможно повторное чтение. И там бывают ситуации (скажем, обновление какого-то дневного агрегата), когда "ну вклинился кто-то, да и бог с ним - он его обновил (или создал), мы тогда не будем обновлять (создавать) - ничего страшного не случится".
Т.е. тут опять надо не тупо технически решать, а понимать логику бизнес-процесса.
Запись лочится на время уменьшения одного поля для переноса этой суммы на холд
а понимать логику бизнес-процесса
Еще раз говорю, я говорю о технической реализации любого (вообще любого) действия приложения, которое что-то меняет в базе данных. Для любой бизнес-логики. Действие может делать то, что вы хотите, только оно должно быть с локами до чтения данных в приложение. Если у вас обработчик запроса только переносит на холд, отлично, он всё равно должен ставить лок на запись, которую он хочет обновить или проверить, до ее чтения (и разблокировать после переноса на холд). В примере из статьи автор решил делать все проверки в одном обработчике, там тоже надо блокировать записи до их чтения.
В третий раз повторяю - не надо доказывать очевидное. Я нигде не возражал против локов.
У бухгалтеров это называется - проводки. В 1с есть еще регистр накопления для оперативного отображения сумм (агрегат, чтобы в реальном времени транзакции не считать). Всё хранится в журналах, sqrs + event sourcing до того как это стало мейнстримом.
Если балансы корректировать через ячейки - выгонят на мороз и спасибо если не посадят
Последние 2 варианта содержат ошибку в реализации - фиксация транзакции на шаге 2, до update - обнуляет все усилия. Между шагом 2 и 3 могут втиснуться любые транзакции, а блокировки уже сняты.
Вот этот текст, на мой взгляд спорный:
"Особенности: зависит от реализации: при Snapshot Isolation — аналог оптимистической блокировки с версией, при блокировках — аналог пессимистической блокировки"
В стандарте SQL, для этого уровня блокировки, при совершении обновления строк измененных с момента начала транзакции должно выдать ошибку (которую приложение должно быть готово обработать), в той же версионной СУБД PostgreSQL возникает по факту ожидание снятие блокировки изменённой другой транзакцией строки (вдруг там rollback и все же можно), что приводит нас что даже в версионниках будет ожидание как в пессимистическом варианте. Тут конечно автор может предположить о существовании другой версионной СУБД где такого ожидания не происходит, но я о такой не знаю (в другом популярном версионнике Oracle - нет Repeatable Read). В целом такие истории, наверное, лучше рассматривать в привязке к СУБД.
В статье достаточно примитивное представление о технологии перевода денег со счёта на счёт: уменьшить один счёт на некую сумму и увеличить другой счёт на эту же сумму. Если посмотреть на банковскую практику, то перевод раскладывается на технологические этапы, как минимум следующие (допустим, это внутри одного филиала одного банка):
Принять к исполнению документ о переводе со счёта на счёт (поручение/заявление владельца счёта, инкассо, безакцептное и т.д.).
Проверить наличие денег на счёте, заблокировать соответствующую сумму.
Создать проводку о переводе денег со счёта на счёт.
Пометить исходный документ исполненным, снять блокировку суммы.
Создать документы для получателя о приходе денег на счёт.
И каждый этап — добавление (корректировка) записей нескольких таблиц (таблицы с документами, таблицы с проводками и т.д. и т.п.). Соответственно, если там суммы в итоге где-то не сойдутся, то регулярные сверки (контроль) это выявят.
Это понятно, я писал реальную банковскую систему и видел внутри еще 2-3. И там все сложнее. Понятно что там есть стадийность принятия решения, есть системы где есть остаток на начало дня и все остальное досчитывается, есть системы где есть несколько остатков (прогнозный и реальный). Если добавить карты то там еще "чудесатее" - и остаток на карте и счете - несколько дней после совершения операции разный.
Но это все не важно. Я воспринимаю статью как шаблон для того что бы показать подходы борьбы с проблемами параллельного доступа к данным в транзакционных системах. И тут важно исправить ошибки, о которых я писал выше.
Я не против исправления ошибок. Я скорее о том, что технические проблемы, с которыми человек героически борется, могут возникать из-за неправильного решения (даже не обязательно технического), принятого совсем в другом месте. И вместо хитроумного технического решения нужно просто вернуться на этап раньше, и обычно достаточно просто скорректировать предыдущий этап, чтобы не создавать проблем на следующем.
У меня есть знакомый, который регулярно делает управленческие отчёты в своей организации, и при этом из года в год борется с тем, что эти отчёты с чем-то не совпадают (возникают необъяснимые исчезновения денег или наоборот, они появляются ниоткуда). Но реальная проблема у него не в отчётах, а в учёте. И исправлять нужно сначала учёт...
Ой, с отчетами конечно оффтопик: но там основная причина это исправление данных задним числом и отсутствие версионности данных в системе. Если система не позволяет получить отчет за январь, в том виде как он выглядел в момент принятия решения, скажем 10 февраля, то такая фигня будет постоянно.
У меня был опыт разработки хранилища с полной версионности и присутствие на забавном диалоге гендира и главбуха, когда гендир получил отчет за прошлый год как он выглядел в январе и потом как в марте, и главбух имел очень бледный вид.
Конкретно в описанном мной случае причина в том, что в учёте сохраняются не все данные, которые в итоге необходимы для построения отчётов. В формализованной отчётности такое редко бывает, а вот управленческий учёт чаще всего требует дополнительных данных, которые по умолчанию не сохраняются.
Также иногда путают порядок этапов: например, сначала сдают отчётность, а потом начинают проверять корректность учёта. В мелких организациях такое бывает.
Если уж офтопить, то до конца: вспомнил случай, когда в банке сидела проверка ЦБ РФ, мы сделали переоценку стоимости одного актива, причём сильно в плюс. Эта переоценка очень не понравилась председателю, было совещание, я сам не присутствовал, но мне сказали, что скандал был жуткий. В обычной ситуации могли бы успеть исправить, но отчётные документы уже успели передать проверяющим...
В обработке платежей это наименьшая из проблем
Плюс еще комплекс контроля платежей. Лимиты, комплаенс контроль...
И кроме внутренних платежей (оба счета в одном банке), есть входящие (из другого банка) и исходящие (в другой банк)
Коллеги, вы слишком глубоко копаете, на мой взгляд, статья простая и тема ее не финансовые системы - тут полный швах. А просто рассказ об уровнях изоляции и проблемах с ними связанных.
Заголовок и первая фраза статьи задают основную тему. )
Да к ИТшникам просто так не приходи, народ дотошный )))
Нужно очень аккуратно формулировать тему и пример, а то получишь 100500 замечаний на другом уровне абстракции )))
Спасибо, добавил пояснение что все блоки в одной транзакции. спорный текст - также уточнил что я имел в виду.
Еще одно замечание: в реальном коде нужно разделять почему не получился перевод: из-за отсутствия средств на счете или из-за ошибки параллельного доступа к счету. Во втором случае код должен содержать повтор операции, о чем в статье не написано. Это применимо ко всем вариантам, кроме варианта с пессимистичной блокировкой.
Вот поэтому и создается платежный документ, сумма ставится на холд, а потом уже платежный документ проверяется. И если он подтвержден, тога уже осуществляется сам перевод. Деньги при этом списываются с холда.
Если платкж заблокировпн, сумма с холда возвращается обратно.
Описывать технические проблемы в отрыве от бизеслогики в корне неверно
Статья напомнила про фееричный баг у разработчиков банкоматов в Индии. Его уже пофиксили, но это было шикарно. Суть проста - при снятии наличных забираете не всю выданную наличность, а оставляете пару купюр. Банкомат видит, что деньги не забраны и забирает их обратно внутрь и откатывает всю транзакцию.
Этим багом люди полгода пользовались и сняли в сумме дохренилион денег. Пока не смогли понять в чем именно был баг.
Ну так тут еще фееричнее, 3 и 4 вариант, делают все проверки, а потом закрывают транзакцию, и потом просто фигачат update, а то что между закрытием транзакции и update может быть все что угодно, автор не думает.
Этот подход гарантирует, что ни одно конкурентное обновление не изменило те же строки. Если одно из обновлений не прошло (несовпадение версий), транзакция откатывается.
Да? Давайте посмотрим попристальнее.
UPDATE accounts
SET amount = amount + :transferAmount,
version = :creditAccountIdVersionAfter
WHERE id = :creditAccountId
AND version = :creditAccountIdVersionBefore;
Что видим? А видим мы прилетевшие откуда-то снаружи значения параметров какой-то там версии. Ну ладно, опустим тот момент, что эта самая версия есть совершенно непонятная и неясно кому и зачем нужная фигня. Но давайте, убедите нас, что эта версия ну никак не может получить одно и то же значение в двух разных конкурентных процессах... а если это вдруг и получится, в чём я лично сомневаюсь, то объясните, почему при рассмотрении внутренних для сервера БД вопросов мы априорно закладываемся на гарантии от внешнего и совершенно неконтролируемого сервером БД процесса.
Шаг 3: Проверка перерасхода. Исключение при отрицательном балансе
Класс... то есть если получен отрицательный баланс (в скобках отмечу - неважно, сотворили это мы или процесс-конкурент), то это ошибка, но если он неотрицателен, то всё в порядке, даже будь результат хоть трижды неправильный. Походу, кто-то успел забыть, что же надо проверять...
----------------
А на самом деле всего-то и надо, что
UPDATE accounts
SET amount = CASE id
WHEN :debitAccountId THEN amount - :transferAmount
WHEN :creditAccountId THEN amount + :transferAmount END
WHERE id IN (:debitAccountId, :creditAccountId);
А неотрицательность баланса, если он обязан быть неотрицательным, вообще должна проверяться CHECK-ограничением в структуре таблицы.
И еще раз - что делать когда счета плательщика и аолучателя в разных банках?
Что делать, когда платкж по каким-то причинам отправлен на ручной контроль и подтверждение его будет через час, два а то и вообще только завтра?
Еще более сложное. В банке каждый день наступает фаза закрытия опредня и перехода на следующий день. Фактически - сведение баланса. Но при этом платежи принимаются и обрабатываются. Но не в том юните, где идет переход на следующий лень, а в другом - юните ночного ваода. А потом, когда основной юниттперешел на следующий день, в него накатываются все изменения из ночи... Как с этим быть?
Статья про сферического коня в вакууме, а не про реальную жизнь.
Статья про сферического коня в вакууме, а не про реальную жизнь.
Несомненно. Но я-то тут каким боком?
И еще раз - что делать когда счета плательщика и аолучателя в разных банках?
А там есть транзакции в том смысле, что в статье? Нет, слово-то одно и то же. Но что мне кажется что банковская транзакция (что бы они там ни имели в виду) и транзакция БД (которые с уровнем изоляции) - сильно не то же самое.
спасибо! очень наглядно
Ребята лучше почитайте Алекса Петрова с его книгой "Распределенные базы данных" там достаточно информации на эту тему и даже больше. Кроме того могу посоветовать Алекса Ху. С его книгами "System design interview" там достаточно информации чтобы увидеть архитектуру подобных решений.
Вот интересно: статья написана из базовой инженерной логики, что если мы в рамках одной БД перекидывает сумму с одного счета на другой, то делать это надо в рамках одной транзакции БД, т.е. консистентно.
На практике в банковских приложениях разных банков РФ последние годы при переводе денег между собственными счетами внутри банка регулярно наблюдаю дикую дичь:
Деньги задваиваются (на исходящем счёте ещё не списались, а на новом уже отражаются)
Деньги испаряются (на исходящем счёте уже списались, а на целевом все ещё показывает ноль
Это временные проблемы, обычно в течение 10 секунд, реже минуты все устаканивается. Но, судя по пользовательскому интерфейсу, нынче элементарные переводы внутри счетов одного пользователя в одном банке стало принято реализовывать с помощью асинхронных обновлений каждого счета по отдельности, т.е под капотом микросервисы, eventual consistency и вот это всё.
Как пользователя, меня такое поведение банковских приложений расстраивает неимоверно и думаю я не один такой.
Вопрос знатокам - почему всё же банки на практике ушли от нормальных транзакций в рамках одной БД к распределенной асинхронщине?
Потому что бизнес-логика асинхронная изначально. Просто рассмотрите случай, когда счёта получателя и отправителя находятся в разных банках (иногда в разных юрисдикциях, разных часовых поясах и т.д.). Для бизнеса важна юридическая составляющая, чем потом отмахиваться от клиента, если он скажет, что что-то пошло не так. То есть, нужно сохранить все документы, подписи, переписку и т.д. А отражение остатка на счёте это уже следствие, а не причина. Никогда не было бизнес-задачи менять остатки на счётах синхронно, и законы это позволяют.
Но, судя по пользовательскому интерфейсу, нынче элементарные переводы внутри счетов одного пользователя в одном банке стало принято реализовывать с помощью асинхронных обновлений каждого счета по отдельности, т.е под капотом микросервисы, eventual consistency и вот это всё.
В соседней теме про 1С я пытался то ли выяснить то ли показать, что когда-то давно, в бумажно-доэлектронную эпоху с медленной связью, когда деньги и документы вот буквально транспортом возили - оно в значительной мере так и работало, ибо никаких транзакций в современном понимании не было.
Я в том обсужденнии даже пару старых книг находил. Прошлых веков. Там этих журналов, обеспечивающих работу транзакций - пачками.
Вопрос знатокам - почему всё же банки на практике ушли от нормальных транзакций в рамках одной БД к распределенной асинхронщине?
Одна БольшаяБазаДанных обладает недостатками. Прежде всего по железу. Мейнфрейм - он дорогой и труднодоступный.
Потому нарезали, географически распределили, и откатились назад по времени, когда сообщения ходят между 'клерками-микросервисами'. И каждый из них сам по себе их обрабатывает. Только поскольку протоколы и правила оформления немного забыли - все и показывается в виде 'деньги временно непонятно где'.
Ну я в соседнем комментарии пытался сказать, что это не баг, а фича. Поскольку задача не только отразить текущее состояние счетов, а ещё и сохранить взаимосвязи, почему и на каком основании сделана та или иная операция. И, по-моему, последнее сильно важнее, чем непонятно зачем нужная синхронность. Всё равно банки баланс сводят не чаще, чем ежедневно.
Не знаю как сейчас, а когда был рейсовый механизм взаиморасчётов в ЦБ РФ, то банки могли рассчитаться, даже если ни у кого денег на счёте. Например, банк А платит банку Б 1000 рублей, банк Б платит банку В 1000 рублей, банк В платит банку А 1000 рублей. ЦБ РФ мог обработать все эти платёжки, даже если у каждого из них не было этой тысячи на счёте. Если потребовать синхронности отражений остатков и обрабатывать платёжки по очереди, то такой финт не удалось бы проворачивать без лимита овердрафта.
Ну я в соседнем комментарии пытался сказать, что это не баг, а фича.
Да понятно, что фича. Но жалоба была, как я понял, не на межбанковский обмен, а в пределах банка и одного интерфейса.
И вот тут, если уж асинхронщина - оно должно отображаться не как человек жаловался "на одном счете пропали, а на другом еще не появились", а "На одном счете пропали, но видны как 'Перевод в обработке'- XYZруб".
Так от того, что это внутри одного банка и один клиент, юридическая составляющая такая же, как если бы это были разные банки и разные клиенты. То есть у клиента один счёт по одному договору, второй счёт по другому договору, и нужно соблюсти все процедуры для списания и все процедуры для зачисления, какие предусмотрены правилами. Поэтому прорубание отдельного туннеля для отдельно взятого частного случая, видимо, особого смысла не имеет.
Да не про отдельный туннель речь. Вот мне тут расписывали процедуру перевода. Сильно многоступенчато, да. Объясняя, что все так сложно как раз для того, чтобы в каждый момент знать, где деньги и все балансы сходились. А жалоба была на
Деньги задваиваются (на исходящем счёте ещё не списались, а на новом уже отражаются)
Деньги испаряются (на исходящем счёте уже списались, а на целевом все ещё показывает ноль
Т.е. эти самые процедуры, предусмотренные правилами, и не выполнятся - деньги берутся из ниоткуда или пропадают.
Ну потому что в реальности эти цифры в базе они юридического значения не имеют. Даже если списание/зачисление прошло несинхронно, то вряд ли кто-то сможет этими деньгами воспользоваться повторно. Это просто картинка лагает (что для конечных пользователей, как я понимаю, существенного значения не имеет, так как каждый пользователь и так видит операции и может понять, сколько у него должно быть денег). Если даже в силу сбоя банк проведёт операций больше, чем было денег на счёте, то они потом откатят. Если не получится откатить, подадут иск. Для системы юридическая составляющая важнее, чем техническая.
Т.е. эти самые процедуры, предусмотренные правилами, и не выполнятся - деньги берутся из ниоткуда или пропадают.
Не переживайте, с деньгами все в порядке. Просто никто в здравом уме не даёт доступ для онлайн банкинг приложений в боевые таблицы. Они все работают по снапшотам и по event source таблицам. Отсюда и возможные времменые такие эффекты, что описаны выше.
Ну не все :-) У нас, слава богу, это дичи нет.
Скорее всего проблема в микросервисах у которых каждый имеет свою копию БД. И начинаются задержки репликации и синхронизации этих копий. Тут от микросервисов бльше проблем чем пользы.
На самом деле там все не так. А примерно так:
С баланса счета плательщика сумма переводится на холд
Проводится комплекс проверок платежа.
Если все проверки пройдены, сумма зачисляется на счет получателя
При успешном зачислении суммы на счет получателя она списывается с холда на счету плательщика.
Это универсальная схема как для случая когда счета в одном, так и для случая когда счета в разных банках.
При запросе баланса счета показывается только баланс, без учета холдов. Работаем с одной БД, без репликаций (точнее, они есть, но для разных "внешних систем" и там, естесвтенно, не вся БД, а только нужные этим внешним системам данные реплицируются). И да, там могут быть лаги, но для тех систем это несущественно по их логике работы.
Нет, это проблема REST бэка приложений. Для ускорения всегда везде кэши и тупо один уже обновился, второй нет. В самой банковской системе все операции проходят в ABS (что бы не путать с автомобильной) и уж поверьте, там все в порядке с транзакциями. Самих ABS не так много - банки (даже крупные) редко делают свои - берут готовые сертифицированные - и пишут обвес для мобилок/сайтов. По сути цифры на счетах в этих "обвесах" - это тоже кэш. Реальные данные только в АБС-ках, куда говнокодеров не подпустят и на пушечный выстрел.
То есть вы полагаете, что разработчики интернет-банковский приложений массово забывают принудительно обновить кеш по счетам, затронутым переводом?
Может и так конечно, но тогда это уж совсем масштабная глупость..
Не знаю где как, но у нас эти разработчики ничего ни про какие кеши не знают. Они знают только что есть REST API для получения списка счетов и остатков по ним. Или для получения истории операций (выписки) по счету.
Связкой между REST API и соответствующим веб-сервисом на умной шине занимаются другие люди. Третьи люди (это уже мы, кто на АБС работает) занимаются разработкой сервис-модулей для веб-сервисов. Т.е. собственно того, что отвечает за формирование выборки из БД по соответствующему запросу. На нашей стороне точно нет никаких кешей. Получили параметры запроса, сформировали resulset, отдали его вебсервису.
Кеширование, если оно и есть, возможно где-то на уровне умной шины.
Как мне кажется, единственный логически правильный способ - для любых параллельных операций на изменение предотвращать чтение записей, которые меняются в текущей операции. Все действия на изменение с одним аккаунтом должны выполняться последовательно. Только так бизнес-проверки будут работать правильно.
FOR UPDATE примерно это и делает, но он требует открытую транзакцию на всё время обработки в приложении. Это не всегда удобно. Поэтому в приложении лучше использовать отдельную систему мьютексов, и блокировать id сущности перед чтением из базы для изменяющих операций. Также это будет работать, если вместо базы используется стороннее API.
Если в процессе операции обновляется более одной записи, как в примере из статьи, надо блокировать id записей в порядке возрастания, тогда не должно быть взаимных блокировок. В транзакции будет только фактическое обновление записей после всех расчетов.
Тут еще надо учесть что блокировка записи на какое-то длительное время в условиях высокой плотности обращений к таблице из разных потоков (заданий) может привесть если не к дедлокам, то к общей деградации производительности всего сервера.
Ну так естественно, в том и смысл, чтобы операции с одним ресурсом выполнялись последовательно. Зачем их делать параллельно, а потом некоторые откатывать потому что version не совпадает. Если повторять все расчеты заново после отката, то нагрузка на сервер будет еще больше.
А мне вот в этом контексте интересно, например, операции в СБП, приходящие в платёжную систему от одного банка, обрабатываются последовательно или параллельно? Потому как у банка должен быть определённый лимит исходящих переводов, при этом при параллельной обработке есть риск его нарушить, а при последовательной снизится производительность, все клиенты банка выстроятся в одну общую очередь на переводы вовне.
Потому как у банка должен быть определённый лимит исходящих переводов, при этом при параллельной обработке есть риск его нарушить
Есть подозрение, что там на него плюнули. Потому что особого смысла он не имеет - граждане столько переводов не сделают, чтобы проблемы создать, даже если не следишь.
Это до тех пор, пока какой-нибудь банк не окажется на грани банкротства и клиенты не попытаются одновременно вывести все свои деньги через СБП.
Ну, так скажем, через банк за сутки проходит более 100 000 000 операций. И каждая требует обработки. Делать все последовательно - просто времени не хватит. Да и клиенты возмущаться начнут - "целых 10 секунд прошло, а деньги со счета на счет еще не перекинулись..." (забыли как лет 25 назад по межбанку платеж мог несколько дней идти)...
Поэтому да. Параллельно. Но, как уже неоднократно писал - блокировки короткие. Со счета на холд перекинули сумму и дальше уже не держим запись. Можем спокойно все проверки проводить. А с холда они спишутся реально уже когда из другого банка подтверждение придет что до них дошли деньги. Или обратно с холда на счет если была отмена операции или подтверждение не получено в установленный срок. Но это уже может завтра или послезавтра произойти (не будете же вы держать запись заблокированной - получится что клиент одну операцию в день может провести?)
более 100 000 000 операций
Делать все последовательно - просто времени не хватит.
Не надо делать все последовательно. В примере из статьи меняются 2 счета конкретных пользователей, это 2 отдельных ресурса. У других пользователей будут свои счета, это 2 других ресурса, операции с ними будут идти параллельно первым. Лок делается на связку "название сущности + id".
Со счета на холд перекинули сумму и дальше уже не держим запись.
Если расчеты происходят в приложении, то это и есть операция, в течение которой надо держать запись. После нее конечно держать не нужно. Это аналог примера из статьи, только вместо второго аккаунта некий "холд". Получили лок на id аккаунта, прочитали параметры аккаунта, проверили что сумма достаточная, перевели на холд, разблокировали. Надо ли делать лок на холд зависит от того как он устроен. Если это таблица, куда всегда делается только INSERT, то лок не нужен.
Если расчеты происходят в приложении
Расчёты не происходят в приложении. Приложение это средство доставки поручения клиента в банк (одно из). Также клиент может передать поручение в отделении или ещё как-то, как предусмотрено договором. Когда приложение доставило поручение клиента в банк, задача считается выполненной, с чего бы это оно должно ждать обновления каких-то счетов?
А в банке это поручение не приложение обрабатывает, а операторы вручную считают на бумаге и потом заносят получившееся значение в базу через SQL-клиент?
Так иногда и вручную, тут уже писали. Если комплаенс хочет проверить и т.д. То есть, потенциально оно может зависнуть на неопределённое время. У меня были случаи, когда я подавал поручение через приложение, а из банка перезванивали и уточняли, действительно ли я подавал.
Ну там не совсем так.
Есть комплекс проверок в системе расчетов. Они над каждым платежом выполняются последовательно (и там набор проверок и их порядок еще зависит от того, кто является плательщиком, а кто получателем - ФЛ, ИП, ЮЛ... И что за платеж - исходящий (в другой банк), входящий (из другого банка), внутренний (внутри банка)...
Каждая проверка возвращает код - ок/не ок (если не ок, то там свой код у каждой проверки). Если очередная проверка вернула не ок, то платеж отправляется в комплаенс на ручной контроль с этим самым кодом (чтобы знали что не понравилось). Там уже руками ставят статус разрешить или запретит и возвращают обратно в расчетный конвейер.
Все эти операции проводятся не со счетами, а с платежным документом. для запуска платежного документа на конвейер нужно только чтобы сумма в платежном документе была больше чем баланс счета плательщика и меньше лимита, установленного по счету. тогда сумма со счета переносится на холд и лок (кратковременный, только на момент переноса с баланса на холд) записи со счета снимается. А платежный документ уходит в расчетный конвейер. А дальше уже если платеж завернули, опять лок (короткий) для возврата с холда на баланс. Или ждем подтвержения зачисления суммы на счет получателя чтобы уменьшить холд на эту сумму.
Т.е. долгих локов не бывает никогда. А баланс всегда отражает тут сумму, которой владелец счета может распоряжаться.
Вы не поняли. Я говорю про приложение на сервере внутри банка, которое переводит средства с аккаунта на аккаунт, а не про мобильное приложение для пользователей.
А там нет какого-то одного "приложения". Там целый комплекс модулей-акторов, которые занимаются обработкой платежных документов. И каждый из них выполняет свою логическую функцию.
Еще раз - банк (АБС) - это очень много (и данных и логики) и очень сложно. Порядка трех десятков тысяч модулей, полутора десятков тысяч таблиц.
И перевести деньги со счета на счет - это не одна операция, а цепочка последовательных операций, растянутая по времени. И, строго говоря, в случае внутреннего перевода (оба счета в одном банке) платежный документ (проводка) разбивается на две слинкованных полупроводки - одна кредитовая (списание со счета плательщика), вторая дебетовая (зачисление на счет получателя). Каждая полупроводка обрабатывается отдельно в конечном итоге. Фактически это унифицировано с остальными типами платежей - исходящими (в другой банк) и входящими (из другого банка).
Все описания в статье - это как пытаться концепцию ООП объяснять на примере котиков и собачек или геометрических фигур. Примитивизировано до полной потери смысла.
А там нет какого-то одного "приложения"
Я вроде не говорил ничего про оценку количества приложений.
Там целый комплекс модулей-акторов, которые занимаются обработкой платежных документов.
Любые приложения, которые меняют что-то в базе, должны использовать локи, и если у них база общая, то одну и ту же систему локов.
И перевести деньги со счета на счет - это не одна операция, а цепочка последовательных операций
То, что я описал, подходит к любой операции в этой цепочке. В общем случае, к любой последовательности "прочитали - проверили - рассчитали - сохранили в одной транзакции БД".
Все описания в статье
Как я написал в первом комментарии, я говорю про любые параллельные операции на изменение данных, а не только про пример в статье.
Поскольку обработка поручения клиента может быть долгой (несколько часов, а то и дней), то никто не блокирует операции по соответствующему счёту и записи в базе. Вообще поручений может быть одновременно несколько, они параллельно обрабатываются в общем случае. Вместо блокировки базы (операций) блокируется (переносится с баланса счёта на холд, как тут уже писали) соответствующая сумма на счёте. И пока банк работает с этим поручением (иногда вручную), есть гарантия, что со счёта не будет списана сумма больше, чем остаток, это, во-первых. И записи в базе (другие операции по счёту) не блокируются, во-вторых.
Поскольку обработка поручения клиента может быть долгой (несколько часов, а то и дней)
Вместо блокировки базы (операций) блокируется (переносится с баланса счёта на холд, как тут уже писали) соответствующая сумма на счёте.
Вы не понимаете, о чем идет речь. Я говорю про технические детали реализации действия "переносится с баланса счёта на холд".
"Обработка поручения клиента" это совершенно не то, что я подразумеваю под выражением "операция с ресурсами". Я не предлагаю блокировать базу на несколько часов или дней.
Вы поймите, те, кто работает в этой предметной области, мыслят категориями бизнес-логики, а не чисто техническими. Без понимания (хотя бы на базовом уровне) как все это работает, тут ничего хорошего не сделаешь. Мало быть хорошим технарем, надо еще понимать логику процессов.
А вы упираетесь в чисто технические аспекты, которые, в целом, и так очевидны.
Вот в том и дело, что специфика бизнеса тут ни при чем. Если вы обновляете строки в базе из приложения, и у вас возможны параллельные процессы, вам нужны локи. В любом приложении и в любой предметной области.
Покажите мне где я говорил что локи не нужны...
Вы отстаиваете тезис, с которым никто не спорит.
Я говорил что локи должный быть короткими. Только на момент изменения данных. А данные для изменения должны быть уже готовы. И подготовка данных ведется вне лока. Как - это уже архитектура и понимание бизнес-логики процесса.
Вот вы гите работаете? Когда работает в репозитории - лочите на время работы все объекты с которыми работаете? Или таки нет? или все-таки даете возможность кому-то еще с этими объектами работать, а потенциальные конфликты решаете потом при мерже?
Покажите мне где я говорил что локи не нужны...
Я не говорю, что вы говорите, что локи не нужны. Вы говорите, что локи не нужны до чтения данных. Я говорю, что нужны.
Только на момент изменения данных.
А какой смысл записывать данные, которые некорректно рассчитаны?
Когда работает в репозитории - лочите на время работы все объекты с которыми работаете?
В очередной раз повторяю - я не предлагаю блокировать данные на часы и дни.
а потенциальные конфликты решаете потом при мерже?
Решение конфликтов это ручное действие. Если вы хотите разбирать и исправлять некорректные данные в миллионах строк в базе вручную, то конечно можно не использовать локи.
Да, именно так. Холд - это блокировка суммы на счете с тем, чтобы не получалось технического овердрафта (я, кстати, сталкивался с ситуацией в одном мелком банке, когда имея на счете 1000р и две привязанных к нему карты, купить сначала на 1000р по одной карте, а потом сразу (и даже не сразу, а в течении чуть не дня, минимум нескольких часов) еще на 1000р по другой карте. А на следующий день получить "технический овердрафт" в 1000р (отрицательный баланс счета -1000р).
Холд - это состояние "деньги в пути". Тот самый пример с караванщиком - вы отдаете ему сумму для передачи кому-то в другом городе - у вас в кармане денег уже нет, но и у получателя их еще нет. И при этом караванщик может не найти получателя по указанному адресу (уехал, умер и т.п.) и через какое-то время вернуть ваши деньги обратно вам в карман.
При этом вы можете через другого караванщика отправить еще кому-то денег, а не ждать окончательного результата (передал или вернул) от первого.
Это чисто архитектурное решение, давно уже являющееся стандартом в отрасли (по крайней мере в банках и платежных системах).
Ещё причины теховера: изменение курсов валют (холд был по одному курсу, списалось по более высокому), или операции без предварительной авторизации. Я помню мультивалютную карту в ВТБ24, там к одной карте было привязано 3 счёта в разных валютах и правила списания, с какого счёта в какой очерёдности сумма списывается в зависимости от валюты и наличия денег на счёте. В каких-то обстоятельствах, по-моему, невозможно было потратить деньги с карты в 0 полностью, так как доступный остаток считался в рублях по одному курсу, а при блокировке он же блокировался по более высокому. То есть, если у вас на карте $100, то их нельзя было потратить, не добавив денег, которые бы покрыли разницу в курсах. Но по факту эти дополнительные деньги только блокировались до даты расчётов, но никогда не списывались в итоге.
Тоже вариант, да... Но там вообще все сложно. Мультивалютные проводки - слышал про такое, но как оно организовано вообще не в курсе...
Я так понял, что перевод не всегда начинает исполняться от отправителя к получателю. Точка старта может быть где-то внутри платёжной системы. Первый шаг: перевод со счёта банка-отправителя на счёт банка-получателя. Потом банк получателя переводит получателю как обычный перевод. А банк отправителя в обратном порядке, компенсирует списание со своего клиента (для этого даже холд не нужен, в крайнем случае будет теховер). Примерно как движение дырок в полупроводниках.
К сожалению, уровень моего понимания тут очень поверхностный. Я по комплаенсу и клиентским данным :-)
Но если говорить о картах, то я понимаю сие так (не гарантирую что это 100% правильно): баланс по карте и баланс по счету к которому она привязана не есть одной и то же. Это разные сущности. Баланс по карте - в платежной системе (ПС). Баланс по счету - в банке. Т.е. оплачивая что-то картой, вы используете "деньги ПС". А банк имеет обязательство перед ПС погасить эту сумму. Со счета, который к этой карте привязан. Но если там не хватит денег, то из денег банка (а дальше уже будет с вами разбираться). Тот самый пример с двумя картами и одним счетом - это пример медленного взаимодействия банка с платежной системой.
На деле там, конечно, сложнее. Вы платите картой. ПС посылает в банк запрос "гарантируете оплату?" Если банк отвечает положительно, то ПС ее проводит и отсылает банку уведомление о списанной сумме (при этом уменьшая у себя баланс по карте). Банк должен его принять, и перевести с вашего счета деньги на холд. И ждать когда от получателя (не знаю, через ПС или напрямую) придет подтверждение что деньги зачислены на счет продавца (или была отмена покупки).
В случае с двумя картами и теховером, видимо, был временной разрыв между покупкой и переводом денег на холд в банке. Т.е. баланс по карте в ПС сразу уменьшился, а баланс по счету в банке остается прежним. И, соответственно, из банка в ПС не уходит уведомления о том, что и по второй карте нужно баланс поменять. Судя по всему, там эти операции как-то ставились в очередь и обрабатывались не сразу.
Короче, там все сильно заморочено...
Любые приложения, которые меняют что-то в базе, должны использовать локи, и если у них база общая, то одну и ту же систему локов.
Я с этим не спорил. Просто отметил что архитектура и логика обработки была таковой, чтобы локи были короткие по времени. Иначе начинаются проблемы со взаимными блокировками записей.
То, что я описал, подходит к любой операции в этой цепочке. В общем случае, к любой последовательности "прочитали - проверили - рассчитали - сохранили в одной транзакции БД".
А вот это не совсем так. Потому что в реальной жизни цепочка "прочитали - проверили - рассчитали - сохранили" может по времени занимать несколько часов. Или дней. Поэтому и вводится система холдов. Где в одной короткой транзакции деньги со счета переводятся на холд (там единственная проверка - чтобы денег на счете хватило), а дальше уже идут все процессуальные проверки. Но счет (запись) при этом не заблокирована уже и доступна для любых других операций.
Вторая, такая же короткая, транзакция - это возврат денег с холда обратно на счет (если по каким-то причинам платеж не проходит или отменен), или уменьшение суммы холда (когда получено подтверждение о зачислении денег на счет получателя).
Делать все это в одной транзакции БД категорически неверно. Это не просто переложить деньги из кармана в карман. Это как отправить посылку - отнести ее на почту (где ее проверят на предмет отсутствия запрещенных к пересылке предметов - ее могут еще и тут завернуть), потом дождаться или подтверждения получения или возврата с пометкой "адресат выбыл".
Просто отметил что архитектура и логика обработки была таковой, чтобы локи были короткие по времени. Иначе начинаются проблемы со взаимными блокировками записей.
Вот я и объяснил, что "взаимные блокировки" появляются не магически сами по себе, а из-за разного порядка блокировки. Один процесс блокирует записи в порядке "1, 2", другой "2, 1". Поэтому надо использовать одинаковый порядок.
Потому что в реальной жизни цепочка "прочитали - проверили - рассчитали - сохранили" может по времени занимать несколько часов. Или дней.
Нет, приложения так не работают. На веб-запросы от пользователей вида "хочу перевести деньги" вообще обычно ставится таймаут несколько десятков секунд. Я же объяснил, что говорю про обработку одного запроса в приложении, почему вы переводите речь на бизнес-процессы?
Если ваш бизнес-процесс занимает несколько дней, там будет несколько запусков нескольких разных приложений по веб-запросу или по расписанию с несколькими разными транзакциями БД. Я говорю про один конкретный запуск. Нафига держать блокировку между запусками?)
Поэтому и вводится система холдов. Где в одной короткой транзакции деньги со счета переводятся на холд
Поэтому я объяснил, что в этом сценарии я говорю именно про момент "переводятся со счета на холд". Перевод со счета на холд это одна транзакция БД, перевод с холда еще куда-то это другая транзакция БД. Это разные действия разных процессов ОС. Последовательность "получение лока - снятие лока" находится в одном действии одного процесса. Между запусками процессов/обработчиков запроса блокировка не держится.
Не надо делать все последовательно. В примере из статьи меняются 2 счета конкретных пользователей, это 2 отдельных ресурса. У других пользователей будут свои счета, это 2 других ресурса, операции с ними будут идти параллельно первым. Лок делается на связку "название сущности + id".
Компания в рамках зарплатного проекта распределяет з/п на счета 1000 своих сотрудников. И что, пока не распределит все, с ее счетом ничего нельзя сделать?
Тут рассматривается крайне примитивный пример, бесконечно далекий от реальности. В реальной жизни все может быть хуже. Если вы надолго залочили ресурс и при этом у вас высокая интенсивность работы с БД - рано или поздно вы столкнетесь с дедлоками (опыт-с...). А дедлок в высоконагруженной системе чреват появлением лавинных эффектов - одно ждет другого, то еще чего-то ждет ив результате достаточно быстро вся система встает колом.
Поэтому еще раз - блокировка нужна, но она должна быть очень кратковременной. Как этого добиться - это уже архитектура а не разработка.
Если вы надолго залочили ресурс и при этом у вас высокая интенсивность работы с БД - рано или поздно вы столкнетесь с дедлоками
Поэтому я написал, что если в операции нужно изменять 2 ресурса, то надо блокировать id по возрастанию. Тогда дедлоков быть не должно, минимальный id из пересекающихся какой-то процесс запросит первым, другие процессы будут ждать, остальные id останутся свободными, и первый процесс сможет их заблокировать.
Когда при операции блокируется только 1 ресурс, дедлоков быть не может. Может быть только таймаут ожидания мьютекса при большой нагрузке, тогда приложение покажет сообщение "Не удалось выполнить операцию, попробуйте еще раз". Если нагрузка большая, и мы и так подождали сколько можно, тут уже ничего не сделаешь.
И что, пока не распределит все, с ее счетом ничего нельзя сделать?
В данном случае "операция" это перевод на счет одного сотрудника.
$companyAccountId = $this->getCompanyAccountId();
foreach ($employees as $employee) {
$salaryAmount = $this->calculateSalary($employee);
// сортировка id по возрастанию пропущена для наглядности
// 1
$this->lockService->lock(Account::class, $companyAccountId);
$companyAccount = $this->accountRepository->getAccount($companyAccountId);
$this->lockService->lock(Account::class, $employee->accountId);
$employeeAccount = $this->accountRepository->getAccount($employee->accountId);
$this->transferSalary($companyAccount, $employeeAccount, $salaryAmount)
$this->lockService->unlock(Account::class, $employee->accountId);
$this->lockService->unlock(Account::class, $companyAccountId);
// 2
}
Между точкой 2 на текущей итерации и 1 на следующей итерации аккаунт компании не заблокирован, его может заблокировать другая операция, тогда вызов в точке 1 будет ждать освобождения.
Поэтому еще раз - блокировка нужна, но она должна быть очень кратковременной.
Если у вас есть какие-то проверки в приложении при выполнении операции, то блокировка должна быть до чтения данных, участвующих в проверках. Иначе будет race condition. Если вам важна консистентность данных, то это единственный правильный вариант. Сколько времени он занимает это другой вопрос.
Вы же о немного разных вещах говорите?
Еще вчера мне отвечали, что "Тут во всей переписке надо различать, где платёжная транзакция, а где транзакции баз данных."
И тут один человек говорит о платежной транзакции, а другой - о транзакции базы данных.
Речь идет о случаях, когда transferSolary - платежная операция и может быть долгой (дни)
Тогда как описанный цикл - он про блокировки и перевод только на уровне базы данных.
Насколько я вижу, во всей этой ветке разговор только о транзакции БД.
Насколько я вижу, во всей этой ветке разговор только о транзакции БД.
Вот тут
А с холда они спишутся реально уже когда из другого банка подтверждение придет что до них дошли деньги.
Это никак не про транзакции БД.
И тут
Компания в рамках зарплатного проекта распределяет з/п на счета 1000 своих сотрудников. И что, пока не распределит все, с ее счетом ничего нельзя сделать?
Это тоже не про транзакции БД.
В таком контексте приведенный код вообще смысла не имеет, потому что employeeAccount - вообще может быть в другом банке, а не в нашей базе и с поставкой на него лока нужно упрожняться с абстракциями.
А с холда они спишутся когда из другого банка подтверждение придет
Ну я про это и сказал, что это не входит в понятие "операция с ресурсами".
Подтверждение из банка это другой API вызов, или там другое сообщение в Кафке, то есть это другая операция с ресурсами.
Компания распределяет з/п на счета своих сотрудников
Здесь подразумевалась долгая работа с БД в рамках одного бизнес-действия. Это тоже можно назвать "операция с ресурсами", но она состоит из более мелких независимых операций. Которые кстати можно распараллелить, и локи тут как раз помогут. Хотя наверно тут распараллеливание не даст большого эффекта.
employeeAccount - вообще может быть в другом банке, а не в нашей базе
В этом случае как раз и подразумевается "холд", про который была речь.
Тут в другом посте раскопали тему различия, что именно происходит при работе IT-системы. Раньше всегда было так, что в электронной системе мы работали с представлениями объектов реального мира. Ну условно есть в реальном мире документ (та же платёжка или договор), а в компьютере мы обрабатываем информацию о документе, регистрируем его и т.д. И такого рода электронная информация никогда не переходила границу организации без потери смысла. То есть, чтобы эта информация была воспринята другой организацией, нужно было из собственной системы сформировать документ, передать его, а другая организация ввела бы его уже в свою систему в своей интерпретации.
Но когда всё больше документы и взаимодействия становятся электронными, то различие становится весьма эфемерным. Потому как бизнес работает с документами, а IT-система с их представлениями и информацией о них. Но когда и документы, и их представления и информация о документах фактически помещаются по сути в одни и те же компьютеры, то возникает потенциально опасная путаница что есть что и с чем именно мы работаем в данный момент, и кто имеет право работать с той или иной информацией (документом).
одни и те же компьютеры, то возникает потенциально опасная путаница
Эт я знаю. У меня один из любимых вопросов - почему обмен электронными бумажками
"Тут покупатель (счет A) хочет деньги продавцу (Счет B) перевести. Ну ОК, одобрено/принято".
происходящий при помощи кассовой машинки и с одобрением всех причастных - это всего лишь 'документ про деньги'.
А столь же электронный обмен
"Тут со счета A хотят на счет B деньги перевести. ОК, принято.", происходящий в системе межбанковских платежей - это уже обмен 'настоящими' деньгами.
Потому что, видимо, разный смысл придаётся разным электронным записям. В первом случае это документ, фиксирующий обязательство рассчитаться, причём подтверждённый всеми посредниками между А и В, что такой расчёт физически возможен (у всех участников достаточно средств, чтобы рассчитаться). Во втором это фиксация результатов расчётов, то есть происходит движение денег.
Просто в случае с деньгами та же путаница, что и с информацией и документами из предыдущего комментария. Фактически обязательство любого лица может считаться деньгами, если выполняет функцию денег. Но на практике деньгами считаются обязательства спецсубъектов (т.е. кредитных организаций), причём фактически только при наличии лицензии.
При отзыве лицензии у банка все деньги на счетах клиентов фактически перестают быть деньгами, а становятся обычными обязательствами организации (потенциального банкрота).
В первом случае это документ, фиксирующий обязательство рассчитаться, причём подтверждённый всеми посредниками между А и В, что такой расчёт физически возможен (у всех участников достаточно средств, чтобы рассчитаться). Во втором это фиксация результатов расчётов, то есть происходит движение денег.
Ага. именно так обычно и отвечают. Потом я задаю вопрос "почему 'фиксацией результатов' нельзя считать сразу первый документ, раз уж его все участники видели и с ним согласны'
И после нескольких оборотов все сходится - к 'традиции такие'.
Не совсем традиции. У взаимоотношений есть определённая структура и стадии развития, это объективный процесс. Условно, мы сначала вступаем в переговоры, потом делаем предложение, получаем акцепт (или подписываем договор), потом переходим к исполнению взятых на себя обязательств по совершённой сделке, потом фиксируем факт исполнения (если это необходимо). И эту этапность невозможно преодолеть, пропустить этапы и т.д., даже если всё в электронном виде происходит.
другие процессы будут ждать, остальные id останутся свободными
Вот это и есть проблема. Сколько ждать? Если ждать достаточно долго, то Второй процесс ждет первого, третий ждет второго и далее по цепочке. В итоге миллионы клиентов по всей стране не могут расплатиться картами банка в течении нескольких часов.
Сколько ждать?
Я же написал - таймаут ожидания мьютекса. 30-60 секунд обычно достаточно.
Если ждать достаточно долго, то Второй процесс ждет первого, третий ждет второго и далее по цепочке
В том сценарии, который я описал, эта цепочка не может замкнуться обратно на первый, поэтому это ничем не отличается от обычного запроса одного ресурса 2 процессами. Если у вас получается долгое ожидание, значит на какой-то ресурс слишком большая нагрузка, он является узким местом. Можно подумать о том, как изменить архитектуру, чтобы он не участвовал в таком количестве процессов, или оптимизировать сами процессы. Иногда можно заменить UPDATE (обновление существующего ресурса) на INSERT (создание нового ресурса).
Вы похоже не понимаете. Без локов в условиях большой нагрузки у вас данные будут некорректные. И какой-нибудь хакер найдет способ это использовать.
В итоге миллионы клиентов по всей стране не могут расплатиться картами банка
Значит надо разобраться и изменить архитектуру. Здесь участвует счет клиента и счет продавца, там миллионы клиентов и тысячи продавцов, каждая комбинация независима, они не должны друг друга тормозить.
В вашем варианте 2 (пост-проверка на овердрафт) проблема с отрицательным балансом не решена.
На шаге 3 там сначала выполняется проверка баланса (взятого с шага 2) на неотрицательность, а потом уже коммит транзакции. Но ничего не запрещает параллельной транзакции протиснуться между проверкой баланса и коммитом. Если на счету 100 рублей, а в транзакциях А и Б запрошены переводы на 80 и 70 рублей, то возможна ситуация:
1. проверка перерасхода на шаге 3 в транзакции А
2. проверка перерасхода на шаге 3 в транзакции Б
(Обе проверки пройдут, т.к. ни одно списание не закоммичено и соответственно другие транзакции их не видят)
3. коммит транзакции А
4. коммит транзакции Б
В итоге обе транзакции будут успешны, а проверки перерасхода не сработают. Баланс будет -50 рублей.
Если две транзакции изменили одно и то же состояние строки - закоммичена может быть только одна из них.
В PostgresSQL - изменяющий стейтмент (e.q. UPDATE) ожидает завершения параллельной транзакции изменившей ту же строку первее, а затем, перепроверит условие мэтча. В lock реализации - строка блокируется на запись до завершения транзакции.
Смотрите внимательнее. В режиме Read Committed, про который говорится в варианте 2, закоммичены могут быть обе. Об этом говорит официальная дока:
UPDATE
,DELETE
,SELECT FOR UPDATE
, andSELECT FOR SHARE
commands behave the same asSELECT
in terms of searching for target rows: they will only find target rows that were committed as of the command start time. However, such a target row might have already been updated (or deleted or locked) by another concurrent transaction by the time it is found. In this case, the would-be updater will wait for the first updating transaction to commit or roll back (if it is still in progress). ... If the first updater commits, the second updater will ignore the row if the first updater deleted it, otherwise it will attempt to apply its operation to the updated version of the row. The search condition of the command (theWHERE
clause) is re-evaluated to see if the updated version of the row still matches the search condition. If so, the second updater proceeds with its operation using the updated version of the row. ...
В данном случае вторая транзакция при выполнении commit();
выяснит, что да, произошло параллельное обновление с той же строкой, и попробует заново применить все операции (в данном случае 2 операции UPDATE accounts и операцию SELECT amount). Проблема в том, что проверка на неотрицательность баланса находится в Java-коде, а не в SQL, и СУБД до нее просто не дойдет, успешно закоммитив отрицательный баланс.
На этом уровне изоляциии проверка и блокировка конкурентов происходит на стейтменте ( UPDATE ) а не во время commit(). Апдейты должны произойти последовательно updateDebit(tx1) , updateCredit(tx1), updateDebit(tx2), updateCredit(tx2), - а Select и проверка java будет строго после апдейтов каждая в своей транзакции
Если две транзакции изменили одно и то же состояние строки - закоммичена может быть только одна из них.
В PostgresSQL - изменяющий стейтмент (e.q. UPDATE) ожидает завершения параллельной транзакции изменившей ту же строку первее, а затем, перепроверит условие мэтча. В lock реализации - строка блокируется на запись до завершения транзакции.
Задачи на собеседованиях. Денежные переводы в SQL. Обновление счетов и уровни изоляций