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

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

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

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

План

  • Оптимистическая блокировка (с использованием поля version)

  • Оптимистическая стратегия с пост-проверкой на овердрафт

  • Пессимистическая блокировка (с использованием явных блокировок строк)

  • Использование уровня изоляции Repeatable Read

  • Итоги

  • Заключения


Оптимистическая блокировка (с использованием поля version)

  • Уровень изоляции: READ COMMITTED

  • Особености: Подойдет для счетов с низкой вероятностью одновременных операций

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

Пошагово:

-- Шаг 1: Чтение балансов и версий счетов
SELECT id, amount, version FROM accounts WHERE id in (:debitAccountId, :creditAccountId);
// Шаг 2: Проверка наличия средств на списываемом счёте
if (debit.getAmount() < transferAmount) {
    rollback();
    throw new OverdraftException();
}
-- Шаг 3: Обновление обоих счетов с использованием подхода типа CAS (Compare-And-Set)
UPDATE accounts
SET amount = amount - :transferAmount, version = :debitAccountIdVersionAfter
WHERE id = :debitAccountId AND version = :debitAccountIdVersionBefore;

UPDATE accounts
SET amount = amount + :transferAmount, version = :creditAccountIdVersionAfter
WHERE id = :creditAccountId AND version = :creditAccountIdVersionBefore;
// Шаг 4: Проверка, были ли обновления успешными
if (affectedUpdates < 2) {
    rollback();
    throw new ConcurrencyException();
} else {
    commit();
}

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


Оптимистическая стратегия с пост-проверкой на овердрафт

  • Уровень изоляции: READ COMMITTED

  • Особенности: Подойдет для высоко конкурентных операций с низкой вероятностью овердрафта

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

Пошагово:

-- Шаг 1: Применение обновлений
UPDATE accounts SET amount = amount - :transferAmount WHERE id = :debitAccountId;
UPDATE accounts SET amount = amount + :transferAmount WHERE id = :creditAccountId;

-- Шаг 2: Повторная проверка баланса
SELECT amount FROM accounts WHERE id = :debitAccountId;
// Шаг 3: Проверка перерасхода. Исключение при отрицательном балансе
if (debit.getAmount() < 0) {
    rollback();
    throw new OverdraftException();
} else {
    commit();
}

Пессимистическая блокировка (с использованием явных блокировок строк)

  • Уровень изоляции: READ COMMITTED

  • Особенности: Оптимально для счетов с высокой вероятностью параллельных операций

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

Пошагово:

-- Шаг 1: Блокировка строк счетов
SELECT * FROM accounts WHERE id IN (:debitAccountId, :creditAccountId) FOR UPDATE;
// Шаг 2: После захвата блокировок
if (debit.getAmount() < transferAmount) {
    rollback();
    throw new OverdraftException();
}
-- Шаг 3: Применение перевода
UPDATE accounts SET amount = amount - :transferAmount WHERE id = :debitAccountId;
UPDATE accounts SET amount = amount + :transferAmount WHERE id = :creditAccountId;

Надёжный, но снижает уровень параллелизма из-за блокировки строк.


Использование уровня изоляции Repeatable Read

  • Уровень изоляции: REPEATABLE READ

  • Особенности: зависит от реализации: при Snapshot Isolation (e.g. PostgreSQL (Материалы #3) https://www.postgresql.org/docs/current/transaction-iso.html#XACT-REPEATABLE-READ) — высокая вероятность ошибок сериализации (похоже на проблемы при оптимистических методах) + блокировки как при пессимистическом способе; при Locks Implementation — аналог пессимистической блокировки

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

Пошагово:

-- Шаг 1: Чтение балансов (гарантированная повторяемость в рамках транзакции)
SELECT amount FROM accounts WHERE id = :debitAccountId;
SELECT amount FROM accounts WHERE id = :creditAccountId;
// Шаг 2: Проверка наличия достаточных средств
if (debit.getAmount() < transferAmount) {
    rollback();
    throw new OverdraftException();
}
-- Шаг 3: Применение перевода
UPDATE accounts SET amount = amount - :transferAmount WHERE id = :debitAccountId;
UPDATE accounts SET amount = amount + :transferAmount WHERE id = :creditAccountId;

Полагание на механизм изоляции базы данных без явной блокировки строк.


Итоги

Подход

Уровень изоляции

Особенности

Оптимистический с версией

READ COMMITTED

Быстрый, но уязвим при высокой вероятности параллельных операций

Оптимистический без проверки версии

READ COMMITTED

Быстрый, но уязвим на рубеже перехода в овердравт

Пессимистический с блокировками

READ COMMITTED

Разумен при наличии параллельных операций на счетах

Repeatable Read

REPEATABLE READ

Простой, но поведение зависит от конкретной СУБД


Заключение

Решения имеют свои особенности.

Независимо от выбранной стратегии, всегда тестируйте её под нагрузкой и изучайте особенности реализации изоляции в вашей СУБД.

Материалы:

  1. Известные мне популярные задачи на собеседованиях.

  2. Designing Data-Intensive Applications, Martin Kleppmann, Глава 7. Транзакции

  3. PostgreSQL: Transaction Isolation

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


  1. askv
    10.05.2025 08:16

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


    1. skymal4ik
      10.05.2025 08:16

      Как в такой системе показываются баланс и сделки за последние n дней например?

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

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


      1. agratoth
        10.05.2025 08:16

        По джобе раз в сутки считать баланс на 00:00, в течение дня брать из бд этот предпосчитанный баланс и прибавлять к нему транзакции за сутки


      1. askv
        10.05.2025 08:16

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


        1. SpiderEkb
          10.05.2025 08:16

          про "закрытый опердень" не слышал. Возможно, мы работает несколько по иной схеме.

          Обычная работа - "дневной юнит". Это текущий день. Примерно в полночь по мск начинается процедура закрытия опердня и перехода на новый (у нас это называется EOD - End-Of-Day). Длится она 3-4 часа примерно - это фактически сведение всего баланса.

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

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

          В целом, это не единственная схема. Есть и другие. Например, без ночного юнита (т.е. в момент EOD никакие операции не проводятся). Там работает "кешир" - все операции ставятся в очередь (кеш), которая потом будет разбираться после завеhшения EOD уже в новом дне.

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

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

          Честно говоря, АБС в банке настолько сложная и большая система, с таким большим количеством бизнес-процессов, что так просто описать ее практически невозможно. Только в каких-то общих словах.


          1. askv
            10.05.2025 08:16

            про "закрытый опердень" не слышал. 

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

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


            1. SpiderEkb
              10.05.2025 08:16

              Не. Я ж не по бухгалтерии. Я по банку. Тут немного иначе все.

              А работаю на уровне АБС. Т.е. то, что на центральных серверах. Но направление - автоматизация процессов комплаенс-контроля + клиентские данные. Со счетами мало работаем, наше - это во-первых, типы клиентов (сами клиенты, доверенные и уполномоченные лица, держатели карт и все вот это вот), списки росфина (экстремисты-террористы) и совпадения субъектов списков с клиентами, всякие ДУЛы (документы удостоверяющие личность), адреса и т.п.

              Ну и проверки разные, само собой. Т.е. приходит некий СУЛТАНОВ ЛЕЧИ РАСУЛОВИЧ и говорит "хочу счет открыть у вас". Девочка начинает его данные вводить в систему, а ей выскакивает "найдено совпадение со списком ПЭ (подозреваемых в экстремизме)". Наша проверка отработала на попытку ввода данных.

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

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

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


              1. askv
                10.05.2025 08:16

                Вообще по правилам ЦБ опердень они должны закрыть к 12-00 следующего рабочего дня. Но по факту это почти никогда не соблюдается. Я где-то в правилах вычитал, что у банка должны быть распечатки операций по всем счетам за день (это ещё в доэлектронную эпоху было, наверное, сейчас это требование отменили). На мой вопрос замглавбуха сказала, что такой фигнёй никто не занимается. Если проверка придёт, то, конечно, распечатки им дадут.


                1. SpiderEkb
                  10.05.2025 08:16

                  Бухгалтерия (а она ив банке есть) - это одно. Собственно банк (АБС) - это другое. АБС живет своей жизнью. И да, она (АБС) подчиняется [достаточно жестким] регламентам ЦБ.

                  EOD начинается в полночь по МСК. Заканчивается... Ну точно не скажу, но часам к трем-четырем утра (опять по МСК же). Дальше там есть еще фаза USRAFT - ряд обязательных операций в начале нового дня (это где-то 5-6 часов утра - время, когда нагрузка на систему относительно невелика). Что там происходит в полном объеме не скажу - много чего. По нашей линии, к примеру, сверка клиентских данных со списками росфина (того, что обновилось за прошлый день - где-то клиентские данные поменялись, также может прийти и загрузиться новая версия списков) и обновления стоплистов и списков совпадений... Или рассылка разного рода уведомлений клиентам (на стороне АБС, конечно, не сама рассылка - этим одна из внешних систем занимается, но подготовкой кому что послать занимается АБС). Например, идет выборка клиентов у которых заканчивается срок действия ДУЛ - их надо предупредить что через ... дней, если они не предоставят обновленные данные, наступит блокировка.

                  Так или иначе, к началу рабочего дня (9 МСК) АБС уже гарантировано работает в новом дне и готова к работе с полной нагрузкой.

                  А бухгалтерия банка живет сама по себе и подчиняется другим законам. Думаю, что она является таким же клиентом для АБС, как и другие.


      1. SpiderEkb
        10.05.2025 08:16

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

        Плюс платежные документы, связанные с этим счетом


      1. PrinceKorwin
        10.05.2025 08:16

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

        Как уже ответили - иметь снепшот/подсчитанный актуальный баланс на определенную дату / с определенной транзакции. Вы наверняка слышали термин опердень (операционный день).


      1. Sanchous98
        10.05.2025 08:16

        Вы слышали про такой подход, как event sourcing? Вот тут такая же логика: ваш баланс - это результат всех операций. При совершении транзакции, восстанавливается баланс исходя из всех событий. Естественно когда событий много, это может быть проблемой в производительности. Для таких случаев делаются снапшоты каждые n-событий или периодически. Таким образом оптимизированная версия будет восстанавливать баланс из снапшота и событий после снапшота. При грамотном подходе к блокировкам можно добиться хорошего уровня параллелизма


      1. pwn3r
        10.05.2025 08:16

        Тут, кажется, подойдёт комбинация: агрегаты на конкретные даты + инкрементальный подсчёт по дневным транзакциям. Плюс индексы по дате и счёту обязательны. В Postgre часто делают материализованные представления для таких целей.


        1. SpiderEkb
          10.05.2025 08:16

          Строго говоря, я не встречал где было бы нужна история балансов по счетам на конкретные даты. История операций - да (выписка по счету). Текущий баланс и холды - да. Но вот вся история балансов и холдов... Впрочем, я со счетами работаю мало - мое направление это комплаенс и клиентские данные.

          Со счетами приходится работать, но косвенно. Типа такого:

          1. Отбираем клиентов ФЛ открывшие счет на определенных балансовых позициях в предыдущую дату:

          1.1.  отбираем базовый номер клиента:

          1.1.1. счет в статусе «Открыт» и счет не зарезервирован;

          1.1.2. счет открыт день назад;

          1.1.3. маска счета входит в список разрешенных;

          1.1.4. исключить запрещенные маски счетов

          Или

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

          Рассчитать сумму вклада, получив актуальный курс, и сконвертированную в рублёвый эквивалент сумму на счетах, тип которых входит в список ...

          Или

          1. Для отправителя и получателя выполнить проверки:

          1.1. Получить все незарезервированные счета клиента, маски которых начинаются на ...

          1.2. Определить самую раннюю дату открытия счета

          Если дата меньше или равна граничной ...

          Ну и так далее... Это уже из комплекса "Онлайн контроль платежей" (собственно, это команда Системы Расчетов делает, мы только делали комплекс комплаенс-проверок для них).


          1. askv
            10.05.2025 08:16

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

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


            1. SpiderEkb
              10.05.2025 08:16

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

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


              1. askv
                10.05.2025 08:16

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


                1. SpiderEkb
                  10.05.2025 08:16

                  Это да... В точку :-)

                  Команд много. Даже на уровне АБС. И каждая работает по своему направлению. Но при этом все интегрированы друг с другом.

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


          1. pwn3r
            10.05.2025 08:16

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

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

            В Postgre мы делали так: агрегаты на дату (daily_balances) + инкрементальный пересчёт из транзакций, плюс индексы по счёту и дате. Работает стабильно, главное — не забывать про дедупликацию и контроль полноты. Иногда это ещё кладут в материализованные представления, если перфоманс начинает страдать.

            Так что, в целом, понимаю позицию. Но бывают сценарии, где без этого — никак.


            1. SpiderEkb
              10.05.2025 08:16

              Да, посмотрел по нашей документации. Есть отдельная таблица

              Остатки в оборотно-сальдовой ведомости

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

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

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

              Просто я никогда с этой таблицей не сталкивался... Иногда приходится работать с основной таблицей счетов, изредка - таблица проводок.


      1. SpiderEkb
        10.05.2025 08:16

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

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


    1. PrinceKorwin
      10.05.2025 08:16

      Верно. Также есть 2 механизма (на самом деле больше) механизма отката транзакции:

      1. проведение поверх корректирующей

      2. полностью удаление информации о транзакции

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


      1. askv
        10.05.2025 08:16

        Например при возврате на кассе в течение короткого (до дня) времени.

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


        1. PrinceKorwin
          10.05.2025 08:16

          Насколько я понимаю, речь идёт о не до конца проведенной транзакции.

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


          1. askv
            10.05.2025 08:16

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


            1. PrinceKorwin
              10.05.2025 08:16

              Попадает :)

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

              И эта операция также не светится в отчётах в ЦБ


    1. SpiderEkb
      10.05.2025 08:16

      Не транзакции, а платежные документы. Платкльщик, счет плательщика, получатель, счет получателя, сумма платежа...


      1. askv
        10.05.2025 08:16

        Вот понятие платёжных документов, оно довольно странное. Их форма и содержание — это полная свобода творчества заинтересованных сторон. Например, если я, находясь в РФ, прошу своего заграничного друга оплатить мне за границей какой-то сервис (например, Zoom), то наша переписка в чате является платёжным документом или нет? Ведь эта переписка потом служит основанием для подсчёта наших взаимных обязательств друг перед другом, кто кому сколько должен, по аналогии с тем, как банки ведут учёт денег на счетах клиентов...


        1. agratoth
          10.05.2025 08:16

          Платежным поручением может считаться)


    1. zebin Автор
      10.05.2025 08:16

      Замечание справедливое. Исправил вступление. Туториал c фокусом на понимание изоляций и свойств стэйтментов. В этой статье рассматриваю только проблему обновления двух счетов. Бухгалтерские системы гораздо сложнее.


    1. Mapar
      10.05.2025 08:16

      А вот тут еще и фантомные чтения надо рассматривать в тему статьи.


      1. zebin Автор
        10.05.2025 08:16

        Спасибо. В этих примерах фантомные чтения не помешают. Но их можно будет рассмотреть на другой подобной задаче


        1. SpiderEkb
          10.05.2025 08:16

          Вообще, это основа. Прочитал запись, что-то в ней сделал, перед тем как записывать - проверь. Прочитай еще раз - не изменилось ли что-то пока ты работал с записью


        1. Mapar
          10.05.2025 08:16

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


          1. SpiderEkb
            10.05.2025 08:16

            И все равно вопрос - вы начинате транзакцию на списание 300р со счета. И вэтот же момент начинается транзакция на списание 400р. А на счете вмего 500р. Каждая транзакция по отдельности валидна. Вместе - нет. Что делать?


            1. Mapar
              10.05.2025 08:16

              Вопрос наверное не ко мне, а к автору статьи.

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


              1. SpiderEkb
                10.05.2025 08:16

                К сожалению, там нет ответа на поставленный вопрос.

                Я уже писал как это происходит в реальной жизни.

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

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

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

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

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

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

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

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

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

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

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

                • Validate модуль - контроль валидности данных записи в соответствии с бизнес-логикой

                • Модуль интерактивной работы с записью (позволяет вносить ручные изменения). Вводим уникальный ключ - если запись есть - работаем режиме изменения, если нет - в режиме добавления. После внесенных изменений вызывается Validate модуль и, если ошибки нет - Update модуль.

                • Модуль внешнего ввода. Получает на вход образ "после", читает из таблицы образ "до" (опять, есть запись - изменение, нет - добавление), вызывает Validete модуль, если ошибок нет - Update модуль. Этот же модуль позволяет делать "накат по журналу". Для этого на вход передается не образ "после", а имя библиотеки где лежит журнал и ключ записи с образом "после". В этом случае образ "после" берется из журнала.

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


                1. Ivan22
                  10.05.2025 08:16

                  у автора же для этого как раз поле с версией заведено. Логическая transaction_id так сказать


                1. Mapar
                  10.05.2025 08:16

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

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

                  Но безусловно интересно почитать детали Вашей реализации.


                  1. askv
                    10.05.2025 08:16

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

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

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

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


                1. inkelyad
                  10.05.2025 08:16

                  Кроме того, транзакции достаточно сильно грузят сервер. Я скажу крамольную вещь, но мы работаем с COMMITMENT CONTROL (*NONE).

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

                   Плюс двойное чтение - прочитали запись "до", внесли изменения (получили "после"), перед записью изменений еще раз читаем запись с проверкой - совпадет она с образом "до" - если да, то записываем изменения,

                  Без уточнения что происходит - не гонка? Потому что между "еще раз читаем с проверкой' и 'то записываем изменения' - записи могут и измениться.


                  1. SpiderEkb
                    10.05.2025 08:16

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

                    Тут вопрос не в том, что может БД. Она (DB2 for i) много что может.

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

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

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

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

                    Без уточнения что происходит - не гонка? Потому что между "еще раз читаем с проверкой' и 'то записываем изменения' - записи могут и измениться.

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

                    Но блокировка записи на длительное время (пока идет какая-то обработка - это прямой путь к дедлокам и деградации производительности. Что в нашем случае неприемлемо.

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

                    А чтобы перевести со счета на холд транзакция не нужна - это одна запись (счет - сумма текущего баланса - сумма холдов по счету) - read (с блокировкой), cerbal -= n, curhold += n, update (с разблокировкой). А транзакция по определению, это цепочка связанных изменений в нескольких записях.


                    1. inkelyad
                      10.05.2025 08:16

                      И транзакцией это не решается т.к. вы не можете в такой системе держать транзакцию открытой в течении длительного времени

                      Только главное не налететь на синонимы терминологии. Потому что с точки зрения клиента: пока холд не нулевой - это разве не означает, что есть открытая транзакция(другая, не БД-ная) перевода денег? Ну вот и висит эта транзакция несколько дней.

                      И все описанный операции - это ручное выполнение протокола этой транзакции, а не БД-ной. Чтобы было то самое 'либо целиком выполнилось, либо сделаем вид что ничего не было'.


                      1. askv
                        10.05.2025 08:16

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


                      1. SpiderEkb
                        10.05.2025 08:16

                        Ну на самом деле Холды часто бывают не нулевые. Например, на счет у вас 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. izibrizi2
      10.05.2025 08:16

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

      Если балансы корректировать через ячейки - выгонят на мороз и спасибо если не посадят


  1. Mapar
    10.05.2025 08:16

    Последние 2 варианта содержат ошибку в реализации - фиксация транзакции на шаге 2, до update - обнуляет все усилия. Между шагом 2 и 3 могут втиснуться любые транзакции, а блокировки уже сняты.

    Вот этот текст, на мой взгляд спорный:

    "Особенности: зависит от реализации: при Snapshot Isolation — аналог оптимистической блокировки с версией, при блокировках — аналог пессимистической блокировки"

    В стандарте SQL, для этого уровня блокировки, при совершении обновления строк измененных с момента начала транзакции должно выдать ошибку (которую приложение должно быть готово обработать), в той же версионной СУБД PostgreSQL возникает по факту ожидание снятие блокировки изменённой другой транзакцией строки (вдруг там rollback и все же можно), что приводит нас что даже в версионниках будет ожидание как в пессимистическом варианте. Тут конечно автор может предположить о существовании другой версионной СУБД где такого ожидания не происходит, но я о такой не знаю (в другом популярном версионнике Oracle - нет Repeatable Read). В целом такие истории, наверное, лучше рассматривать в привязке к СУБД.


    1. askv
      10.05.2025 08:16

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

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

      2. Проверить наличие денег на счёте, заблокировать соответствующую сумму.

      3. Создать проводку о переводе денег со счёта на счёт.

      4. Пометить исходный документ исполненным, снять блокировку суммы.

      5. Создать документы для получателя о приходе денег на счёт.

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


      1. Mapar
        10.05.2025 08:16

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

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


        1. askv
          10.05.2025 08:16

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

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


          1. Mapar
            10.05.2025 08:16

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

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


            1. askv
              10.05.2025 08:16

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

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


            1. askv
              10.05.2025 08:16

              Если уж офтопить, то до конца: вспомнил случай, когда в банке сидела проверка ЦБ РФ, мы сделали переоценку стоимости одного актива, причём сильно в плюс. Эта переоценка очень не понравилась председателю, было совещание, я сам не присутствовал, но мне сказали, что скандал был жуткий. В обычной ситуации могли бы успеть исправить, но отчётные документы уже успели передать проверяющим...


        1. SpiderEkb
          10.05.2025 08:16

          В обработке платежей это наименьшая из проблем


      1. SpiderEkb
        10.05.2025 08:16

        Плюс еще комплекс контроля платежей. Лимиты, комплаенс контроль...

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


        1. Mapar
          10.05.2025 08:16

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


          1. askv
            10.05.2025 08:16

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


            1. Mapar
              10.05.2025 08:16

              Да к ИТшникам просто так не приходи, народ дотошный )))

              Нужно очень аккуратно формулировать тему и пример, а то получишь 100500 замечаний на другом уровне абстракции )))


              1. askv
                10.05.2025 08:16

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


    1. zebin Автор
      10.05.2025 08:16

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


      1. Mapar
        10.05.2025 08:16

        От ваших пояснений толку нет, у вас в коде явно commit на 2 шаге, который закроет транзакцию и 3 шаг будет в новой транзакции. Код лучшее пояснение, а он с ошибками.

        Новичок скопирует код и получит ерунду.


        1. zebin Автор
          10.05.2025 08:16

          Оу, большое спасибо, исправил.


  1. Mapar
    10.05.2025 08:16

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


    1. SpiderEkb
      10.05.2025 08:16

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

      Если платкж заблокировпн, сумма с холда возвращается обратно.

      Описывать технические проблемы в отрыве от бизеслогики в корне неверно


      1. askv
        10.05.2025 08:16

        Описывать технические проблемы в отрыве от бизеслогики в корне неверно

        100%. Причём ошибки в бизнес-логике чаще всего ведут к проблемам, которые техническими средствами неустранимы в принципе.


  1. PrinceKorwin
    10.05.2025 08:16

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

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


    1. Mapar
      10.05.2025 08:16

      Ну так тут еще фееричнее, 3 и 4 вариант, делают все проверки, а потом закрывают транзакцию, и потом просто фигачат update, а то что между закрытием транзакции и update может быть все что угодно, автор не думает.


      1. zebin Автор
        10.05.2025 08:16

        Спасибо, добавил пояснение что все блоки в одной транзакции


        1. Mapar
          10.05.2025 08:16

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

          Если не понимаете как, то просто уберите из 3 и 4 варианта на 2 шаге:
          } else { commit();


          1. zebin Автор
            10.05.2025 08:16

            Большое спасибо, моя грубая ошибка - исправил


            1. Mapar
              10.05.2025 08:16

              Да, так будет работать.


  1. Akina
    10.05.2025 08:16

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

    Да? Давайте посмотрим попристальнее.

    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-ограничением в структуре таблицы.


    1. SpiderEkb
      10.05.2025 08:16

      И еще раз - что делать когда счета плательщика и аолучателя в разных банках?

      Что делать, когда платкж по каким-то причинам отправлен на ручной контроль и подтверждение его будет через час, два а то и вообще только завтра?

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

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


      1. Akina
        10.05.2025 08:16

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

        Несомненно. Но я-то тут каким боком?


      1. inkelyad
        10.05.2025 08:16

        И еще раз - что делать когда счета плательщика и аолучателя в разных банках?

        А там есть транзакции в том смысле, что в статье? Нет, слово-то одно и то же. Но что мне кажется что банковская транзакция (что бы они там ни имели в виду) и транзакция БД (которые с уровнем изоляции) - сильно не то же самое.


  1. gnub
    10.05.2025 08:16

    спасибо! очень наглядно


  1. ogregor
    10.05.2025 08:16

    Ребята лучше почитайте Алекса Петрова с его книгой "Распределенные базы данных" там достаточно информации на эту тему и даже больше. Кроме того могу посоветовать Алекса Ху. С его книгами "System design interview" там достаточно информации чтобы увидеть архитектуру подобных решений.


  1. LKU
    10.05.2025 08:16

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

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

    • Деньги задваиваются (на исходящем счёте ещё не списались, а на новом уже отражаются)

    • Деньги испаряются (на исходящем счёте уже списались, а на целевом все ещё показывает ноль

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

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

    Вопрос знатокам - почему всё же банки на практике ушли от нормальных транзакций в рамках одной БД к распределенной асинхронщине?


    1. askv
      10.05.2025 08:16

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


    1. inkelyad
      10.05.2025 08:16

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

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

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

      Вопрос знатокам - почему всё же банки на практике ушли от нормальных транзакций в рамках одной БД к распределенной асинхронщине?

      Одна БольшаяБазаДанных обладает недостатками. Прежде всего по железу. Мейнфрейм - он дорогой и труднодоступный.

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


      1. askv
        10.05.2025 08:16

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

        Не знаю как сейчас, а когда был рейсовый механизм взаиморасчётов в ЦБ РФ, то банки могли рассчитаться, даже если ни у кого денег на счёте. Например, банк А платит банку Б 1000 рублей, банк Б платит банку В 1000 рублей, банк В платит банку А 1000 рублей. ЦБ РФ мог обработать все эти платёжки, даже если у каждого из них не было этой тысячи на счёте. Если потребовать синхронности отражений остатков и обрабатывать платёжки по очереди, то такой финт не удалось бы проворачивать без лимита овердрафта.


        1. inkelyad
          10.05.2025 08:16

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

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

          И вот тут, если уж асинхронщина - оно должно отображаться не как человек жаловался "на одном счете пропали, а на другом еще не появились", а "На одном счете пропали, но видны как 'Перевод в обработке'- XYZруб".


          1. askv
            10.05.2025 08:16

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


            1. inkelyad
              10.05.2025 08:16

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

              • Деньги задваиваются (на исходящем счёте ещё не списались, а на новом уже отражаются)

              • Деньги испаряются (на исходящем счёте уже списались, а на целевом все ещё показывает ноль

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


              1. askv
                10.05.2025 08:16

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


              1. PrinceKorwin
                10.05.2025 08:16

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

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


    1. SpiderEkb
      10.05.2025 08:16

      Ну не все :-) У нас, слава богу, это дичи нет.

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

      На самом деле там все не так. А примерно так:

      • С баланса счета плательщика сумма переводится на холд

      • Проводится комплекс проверок платежа.

      • Если все проверки пройдены, сумма зачисляется на счет получателя

      • При успешном зачислении суммы на счет получателя она списывается с холда на счету плательщика.

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

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


      1. askv
        10.05.2025 08:16

        так и для случая когда счета в разных банках.

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

        сумма зачисляется на счет получателя

        Там этот критерий как-то иначе формулируется (списано с корсчёта или как-то иначе). Если СБП, то может быть, там же онлайн всё.


    1. Sergeygs
      10.05.2025 08:16

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


      1. LKU
        10.05.2025 08:16

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

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


        1. SpiderEkb
          10.05.2025 08:16

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

          Связкой между REST API и соответствующим веб-сервисом на умной шине занимаются другие люди. Третьи люди (это уже мы, кто на АБС работает) занимаются разработкой сервис-модулей для веб-сервисов. Т.е. собственно того, что отвечает за формирование выборки из БД по соответствующему запросу. На нашей стороне точно нет никаких кешей. Получили параметры запроса, сформировали resulset, отдали его вебсервису.

          Кеширование, если оно и есть, возможно где-то на уровне умной шины.


  1. michael_v89
    10.05.2025 08:16

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

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

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


    1. SpiderEkb
      10.05.2025 08:16

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


      1. michael_v89
        10.05.2025 08:16

        Ну так естественно, в том и смысл, чтобы операции с одним ресурсом выполнялись последовательно. Зачем их делать параллельно, а потом некоторые откатывать потому что version не совпадает. Если повторять все расчеты заново после отката, то нагрузка на сервер будет еще больше.


        1. askv
          10.05.2025 08:16

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


          1. inkelyad
            10.05.2025 08:16

             Потому как у банка должен быть определённый лимит исходящих переводов, при этом при параллельной обработке есть риск его нарушить

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


            1. askv
              10.05.2025 08:16

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


              1. inkelyad
                10.05.2025 08:16

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

                Ну и пускай. Окажется(еще больше) должен всем тем банкам, куда выводили. Те, конечно, всплакнут, но ЦБ потом им поможет, как водится.


        1. SpiderEkb
          10.05.2025 08:16

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

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


          1. michael_v89
            10.05.2025 08:16

            более 100 000 000 операций
            Делать все последовательно - просто времени не хватит.

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

            Со счета на холд перекинули сумму и дальше уже не держим запись.

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


            1. askv
              10.05.2025 08:16

              Если расчеты происходят в приложении

              Расчёты не происходят в приложении. Приложение это средство доставки поручения клиента в банк (одно из). Также клиент может передать поручение в отделении или ещё как-то, как предусмотрено договором. Когда приложение доставило поручение клиента в банк, задача считается выполненной, с чего бы это оно должно ждать обновления каких-то счетов?


              1. michael_v89
                10.05.2025 08:16

                А в банке это поручение не приложение обрабатывает, а операторы вручную считают на бумаге и потом заносят получившееся значение в базу через SQL-клиент?


                1. askv
                  10.05.2025 08:16

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


                  1. SpiderEkb
                    10.05.2025 08:16

                    Ну там не совсем так.

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

                    Каждая проверка возвращает код - ок/не ок (если не ок, то там свой код у каждой проверки). Если очередная проверка вернула не ок, то платеж отправляется в комплаенс на ручной контроль с этим самым кодом (чтобы знали что не понравилось). Там уже руками ставят статус разрешить или запретит и возвращают обратно в расчетный конвейер.

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

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


                  1. michael_v89
                    10.05.2025 08:16

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


            1. SpiderEkb
              10.05.2025 08:16

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

              Компания в рамках зарплатного проекта распределяет з/п на счета 1000 своих сотрудников. И что, пока не распределит все, с ее счетом ничего нельзя сделать?

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

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


              1. michael_v89
                10.05.2025 08:16

                Если вы надолго залочили ресурс и при этом у вас высокая интенсивность работы с БД - рано или поздно вы столкнетесь с дедлоками

                Поэтому я написал, что если в операции нужно изменять 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. Если вам важна консистентность данных, то это единственный правильный вариант. Сколько времени он занимает это другой вопрос.


                1. inkelyad
                  10.05.2025 08:16

                  Вы же о немного разных вещах говорите?

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

                  И тут один человек говорит о платежной транзакции, а другой - о транзакции базы данных.


                  Речь идет о случаях, когда transferSolary - платежная операция и может быть долгой (дни)

                  Тогда как описанный цикл - он про блокировки и перевод только на уровне базы данных.


                  1. michael_v89
                    10.05.2025 08:16

                    Насколько я вижу, во всей этой ветке разговор только о транзакции БД.


                    1. inkelyad
                      10.05.2025 08:16

                      Насколько я вижу, во всей этой ветке разговор только о транзакции БД.

                      Вот тут

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

                      Это никак не про транзакции БД.

                      И тут

                      Компания в рамках зарплатного проекта распределяет з/п на счета 1000 своих сотрудников. И что, пока не распределит все, с ее счетом ничего нельзя сделать?

                      Это тоже не про транзакции БД.

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


                      1. michael_v89
                        10.05.2025 08:16

                        А с холда они спишутся когда из другого банка подтверждение придет

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

                        Компания распределяет з/п на счета своих сотрудников

                        Здесь подразумевалась долгая работа с БД в рамках одного бизнес-действия. Это тоже можно назвать "операция с ресурсами", но она состоит из более мелких независимых операций. Которые кстати можно распараллелить, и локи тут как раз помогут. Хотя наверно тут распараллеливание не даст большого эффекта.

                        employeeAccount - вообще может быть в другом банке, а не в нашей базе

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


                      1. askv
                        10.05.2025 08:16

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

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


                      1. inkelyad
                        10.05.2025 08:16

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

                        Эт я знаю. У меня один из любимых вопросов - почему обмен электронными бумажками

                        "Тут покупатель (счет A) хочет деньги продавцу (Счет B) перевести. Ну ОК, одобрено/принято".
                        происходящий при помощи кассовой машинки и с одобрением всех причастных - это всего лишь 'документ про деньги'.

                        А столь же электронный обмен
                        "Тут со счета A хотят на счет B деньги перевести. ОК, принято.", происходящий в системе межбанковских платежей - это уже обмен 'настоящими' деньгами.


                      1. askv
                        10.05.2025 08:16

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

                        Просто в случае с деньгами та же путаница, что и с информацией и документами из предыдущего комментария. Фактически обязательство любого лица может считаться деньгами, если выполняет функцию денег. Но на практике деньгами считаются обязательства спецсубъектов (т.е. кредитных организаций), причём фактически только при наличии лицензии.

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


                      1. inkelyad
                        10.05.2025 08:16

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

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

                        И после нескольких оборотов все сходится - к 'традиции такие'.


                      1. askv
                        10.05.2025 08:16

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