Задача перевода денег в первом приближении сводится к обновлению пары строк и кажется простой — но обеспечение корректности при параллельном доступе может быть неожиданно сложным для только знакомящихся с уровнями изоляций БД.
В этом посте я покажу примеры решения упрощенной задачи, сосредоточив внимание на проблеме обновления нескольких строк БД, уровнях изоляции и особенностях каждого подхода. Мы будем использовать 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 |
Простой, но поведение зависит от конкретной СУБД |
Заключение
Решения имеют свои особенности.
Независимо от выбранной стратегии, всегда тестируйте её под нагрузкой и изучайте особенности реализации изоляции в вашей СУБД.
Материалы:
Известные мне популярные задачи на собеседованиях.
Designing Data-Intensive Applications, Martin Kleppmann, Глава 7. Транзакции
Комментарии (74)
Mapar
10.05.2025 08:16Последние 2 варианта содержат ошибку в реализации - фиксация транзакции на шаге 2, до update - обнуляет все усилия. Между шагом 2 и 3 могут втиснуться любые транзакции, а блокировки уже сняты.
Вот этот текст, на мой взгляд спорный:
"Особенности: зависит от реализации: при Snapshot Isolation — аналог оптимистической блокировки с версией, при блокировках — аналог пессимистической блокировки"
В стандарте SQL, для этого уровня блокировки, при совершении обновления строк измененных с момента начала транзакции должно выдать ошибку (которую приложение должно быть готово обработать), в той же версионной СУБД PostgreSQL возникает по факту ожидание снятие блокировки изменённой другой транзакцией строки (вдруг там rollback и все же можно), что приводит нас что даже в версионниках будет ожидание как в пессимистическом варианте. Тут конечно автор может предположить о существовании другой версионной СУБД где такого ожидания не происходит, но я о такой не знаю (в другом популярном версионнике Oracle - нет Repeatable Read). В целом такие истории, наверное, лучше рассматривать в привязке к СУБД.
askv
10.05.2025 08:16В статье достаточно примитивное представление о технологии перевода денег со счёта на счёт: уменьшить один счёт на некую сумму и увеличить другой счёт на эту же сумму. Если посмотреть на банковскую практику, то перевод раскладывается на технологические этапы, как минимум следующие (допустим, это внутри одного филиала одного банка):
Принять к исполнению документ о переводе со счёта на счёт (поручение/заявление владельца счёта, инкассо, безакцептное и т.д.).
Проверить наличие денег на счёте, заблокировать соответствующую сумму.
Создать проводку о переводе денег со счёта на счёт.
Пометить исходный документ исполненным, снять блокировку суммы.
Создать документы для получателя о приходе денег на счёт.
И каждый этап — добавление (корректировка) записей нескольких таблиц (таблицы с документами, таблицы с проводками и т.д. и т.п.). Соответственно, если там суммы в итоге где-то не сойдутся, то регулярные сверки (контроль) это выявят.
Mapar
10.05.2025 08:16Это понятно, я писал реальную банковскую систему и видел внутри еще 2-3. И там все сложнее. Понятно что там есть стадийность принятия решения, есть системы где есть остаток на начало дня и все остальное досчитывается, есть системы где есть несколько остатков (прогнозный и реальный). Если добавить карты то там еще "чудесатее" - и остаток на карте и счете - несколько дней после совершения операции разный.
Но это все не важно. Я воспринимаю статью как шаблон для того что бы показать подходы борьбы с проблемами параллельного доступа к данным в транзакционных системах. И тут важно исправить ошибки, о которых я писал выше.
askv
10.05.2025 08:16Я не против исправления ошибок. Я скорее о том, что технические проблемы, с которыми человек героически борется, могут возникать из-за неправильного решения (даже не обязательно технического), принятого совсем в другом месте. И вместо хитроумного технического решения нужно просто вернуться на этап раньше, и обычно достаточно просто скорректировать предыдущий этап, чтобы не создавать проблем на следующем.
У меня есть знакомый, который регулярно делает управленческие отчёты в своей организации, и при этом из года в год борется с тем, что эти отчёты с чем-то не совпадают (возникают необъяснимые исчезновения денег или наоборот, они появляются ниоткуда). Но реальная проблема у него не в отчётах, а в учёте. И исправлять нужно сначала учёт...
Mapar
10.05.2025 08:16Ой, с отчетами конечно оффтопик: но там основная причина это исправление данных задним числом и отсутствие версионности данных в системе. Если система не позволяет получить отчет за январь, в том виде как он выглядел в момент принятия решения, скажем 10 февраля, то такая фигня будет постоянно.
У меня был опыт разработки хранилища с полной версионности и присутствие на забавном диалоге гендира и главбуха, когда гендир получил отчет за прошлый год как он выглядел в январе и потом как в марте, и главбух имел очень бледный вид.
askv
10.05.2025 08:16Конкретно в описанном мной случае причина в том, что в учёте сохраняются не все данные, которые в итоге необходимы для построения отчётов. В формализованной отчётности такое редко бывает, а вот управленческий учёт чаще всего требует дополнительных данных, которые по умолчанию не сохраняются.
Также иногда путают порядок этапов: например, сначала сдают отчётность, а потом начинают проверять корректность учёта. В мелких организациях такое бывает.
askv
10.05.2025 08:16Если уж офтопить, то до конца: вспомнил случай, когда в банке сидела проверка ЦБ РФ, мы сделали переоценку стоимости одного актива, причём сильно в плюс. Эта переоценка очень не понравилась председателю, было совещание, я сам не присутствовал, но мне сказали, что скандал был жуткий. В обычной ситуации могли бы успеть исправить, но отчётные документы уже успели передать проверяющим...
SpiderEkb
10.05.2025 08:16Плюс еще комплекс контроля платежей. Лимиты, комплаенс контроль...
И кроме внутренних платежей (оба счета в одном банке), есть входящие (из другого банка) и исходящие (в другой банк)
Mapar
10.05.2025 08:16Коллеги, вы слишком глубоко копаете, на мой взгляд, статья простая и тема ее не финансовые системы - тут полный швах. А просто рассказ об уровнях изоляции и проблемах с ними связанных.
askv
10.05.2025 08:16Заголовок и первая фраза статьи задают основную тему. )
Mapar
10.05.2025 08:16Да к ИТшникам просто так не приходи, народ дотошный )))
Нужно очень аккуратно формулировать тему и пример, а то получишь 100500 замечаний на другом уровне абстракции )))
askv
10.05.2025 08:16У меня начальник возмущался, почему он от меня получает ответ на тот вопрос, который он задал, а не на тот, который он имел в виду...
zebin Автор
10.05.2025 08:16Спасибо, добавил пояснение что все блоки в одной транзакции. спорный текст - также уточнил что я имел в виду.
Mapar
10.05.2025 08:16Еще одно замечание: в реальном коде нужно разделять почему не получился перевод: из-за отсутствия средств на счете или из-за ошибки параллельного доступа к счету. Во втором случае код должен содержать повтор операции, о чем в статье не написано. Это применимо ко всем вариантам, кроме варианта с пессимистичной блокировкой.
SpiderEkb
10.05.2025 08:16Вот поэтому и создается платежный документ, сумма ставится на холд, а потом уже платежный документ проверяется. И если он подтвержден, тога уже осуществляется сам перевод. Деньги при этом списываются с холда.
Если платкж заблокировпн, сумма с холда возвращается обратно.
Описывать технические проблемы в отрыве от бизеслогики в корне неверно
askv
10.05.2025 08:16Описывать технические проблемы в отрыве от бизеслогики в корне неверно
100%. Причём ошибки в бизнес-логике чаще всего ведут к проблемам, которые техническими средствами неустранимы в принципе.
PrinceKorwin
10.05.2025 08:16Статья напомнила про фееричный баг у разработчиков банкоматов в Индии. Его уже пофиксили, но это было шикарно. Суть проста - при снятии наличных забираете не всю выданную наличность, а оставляете пару купюр. Банкомат видит, что деньги не забраны и забирает их обратно внутрь и откатывает всю транзакцию.
Этим багом люди полгода пользовались и сняли в сумме дохренилион денег. Пока не смогли понять в чем именно был баг.
Mapar
10.05.2025 08:16Ну так тут еще фееричнее, 3 и 4 вариант, делают все проверки, а потом закрывают транзакцию, и потом просто фигачат update, а то что между закрытием транзакции и update может быть все что угодно, автор не думает.
zebin Автор
10.05.2025 08:16Спасибо, добавил пояснение что все блоки в одной транзакции
Mapar
10.05.2025 08:16Исправьте код, выше писал, у Вас там явно commit который явно закроет транзакцию, толку от таких пояснений нет, если код кривой.
Если не понимаете как, то просто уберите из 3 и 4 варианта на 2 шаге:
}
else
{ commit();
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 + :transferAmountEND
WHERE id IN (:debitAccountId, :creditAccountId);А неотрицательность баланса, если он обязан быть неотрицательным, вообще должна проверяться CHECK-ограничением в структуре таблицы.
SpiderEkb
10.05.2025 08:16И еще раз - что делать когда счета плательщика и аолучателя в разных банках?
Что делать, когда платкж по каким-то причинам отправлен на ручной контроль и подтверждение его будет через час, два а то и вообще только завтра?
Еще более сложное. В банке каждый день наступает фаза закрытия опредня и перехода на следующий день. Фактически - сведение баланса. Но при этом платежи принимаются и обрабатываются. Но не в том юните, где идет переход на следующий лень, а в другом - юните ночного ваода. А потом, когда основной юниттперешел на следующий день, в него накатываются все изменения из ночи... Как с этим быть?
Статья про сферического коня в вакууме, а не про реальную жизнь.
Akina
10.05.2025 08:16Статья про сферического коня в вакууме, а не про реальную жизнь.
Несомненно. Но я-то тут каким боком?
inkelyad
10.05.2025 08:16И еще раз - что делать когда счета плательщика и аолучателя в разных банках?
А там есть транзакции в том смысле, что в статье? Нет, слово-то одно и то же. Но что мне кажется что банковская транзакция (что бы они там ни имели в виду) и транзакция БД (которые с уровнем изоляции) - сильно не то же самое.
ogregor
10.05.2025 08:16Ребята лучше почитайте Алекса Петрова с его книгой "Распределенные базы данных" там достаточно информации на эту тему и даже больше. Кроме того могу посоветовать Алекса Ху. С его книгами "System design interview" там достаточно информации чтобы увидеть архитектуру подобных решений.
LKU
10.05.2025 08:16Вот интересно: статья написана из базовой инженерной логики, что если мы в рамках одной БД перекидывает сумму с одного счета на другой, то делать это надо в рамках одной транзакции БД, т.е. консистентно.
На практике в банковских приложениях разных банков РФ последние годы при переводе денег между собственными счетами внутри банка регулярно наблюдаю дикую дичь:
Деньги задваиваются (на исходящем счёте ещё не списались, а на новом уже отражаются)
Деньги испаряются (на исходящем счёте уже списались, а на целевом все ещё показывает ноль
Это временные проблемы, обычно в течение 10 секунд, реже минуты все устаканивается. Но, судя по пользовательскому интерфейсу, нынче элементарные переводы внутри счетов одного пользователя в одном банке стало принято реализовывать с помощью асинхронных обновлений каждого счета по отдельности, т.е под капотом микросервисы, eventual consistency и вот это всё.
Как пользователя, меня такое поведение банковских приложений расстраивает неимоверно и думаю я не один такой.
Вопрос знатокам - почему всё же банки на практике ушли от нормальных транзакций в рамках одной БД к распределенной асинхронщине?
askv
10.05.2025 08:16Потому что бизнес-логика асинхронная изначально. Просто рассмотрите случай, когда счёта получателя и отправителя находятся в разных банках (иногда в разных юрисдикциях, разных часовых поясах и т.д.). Для бизнеса важна юридическая составляющая, чем потом отмахиваться от клиента, если он скажет, что что-то пошло не так. То есть, нужно сохранить все документы, подписи, переписку и т.д. А отражение остатка на счёте это уже следствие, а не причина. Никогда не было бизнес-задачи менять остатки на счётах синхронно, и законы это позволяют.
inkelyad
10.05.2025 08:16Но, судя по пользовательскому интерфейсу, нынче элементарные переводы внутри счетов одного пользователя в одном банке стало принято реализовывать с помощью асинхронных обновлений каждого счета по отдельности, т.е под капотом микросервисы, eventual consistency и вот это всё.
В соседней теме про 1С я пытался то ли выяснить то ли показать, что когда-то давно, в бумажно-доэлектронную эпоху с медленной связью, когда деньги и документы вот буквально транспортом возили - оно в значительной мере так и работало, ибо никаких транзакций в современном понимании не было.
Я в том обсужденнии даже пару старых книг находил. Прошлых веков. Там этих журналов, обеспечивающих работу транзакций - пачками.
Вопрос знатокам - почему всё же банки на практике ушли от нормальных транзакций в рамках одной БД к распределенной асинхронщине?
Одна БольшаяБазаДанных обладает недостатками. Прежде всего по железу. Мейнфрейм - он дорогой и труднодоступный.
Потому нарезали, географически распределили, и откатились назад по времени, когда сообщения ходят между 'клерками-микросервисами'. И каждый из них сам по себе их обрабатывает. Только поскольку протоколы и правила оформления немного забыли - все и показывается в виде 'деньги временно непонятно где'.
askv
10.05.2025 08:16Ну я в соседнем комментарии пытался сказать, что это не баг, а фича. Поскольку задача не только отразить текущее состояние счетов, а ещё и сохранить взаимосвязи, почему и на каком основании сделана та или иная операция. И, по-моему, последнее сильно важнее, чем непонятно зачем нужная синхронность. Всё равно банки баланс сводят не чаще, чем ежедневно.
Не знаю как сейчас, а когда был рейсовый механизм взаиморасчётов в ЦБ РФ, то банки могли рассчитаться, даже если ни у кого денег на счёте. Например, банк А платит банку Б 1000 рублей, банк Б платит банку В 1000 рублей, банк В платит банку А 1000 рублей. ЦБ РФ мог обработать все эти платёжки, даже если у каждого из них не было этой тысячи на счёте. Если потребовать синхронности отражений остатков и обрабатывать платёжки по очереди, то такой финт не удалось бы проворачивать без лимита овердрафта.
inkelyad
10.05.2025 08:16Ну я в соседнем комментарии пытался сказать, что это не баг, а фича.
Да понятно, что фича. Но жалоба была, как я понял, не на межбанковский обмен, а в пределах банка и одного интерфейса.
И вот тут, если уж асинхронщина - оно должно отображаться не как человек жаловался "на одном счете пропали, а на другом еще не появились", а "На одном счете пропали, но видны как 'Перевод в обработке'- XYZруб".
askv
10.05.2025 08:16Так от того, что это внутри одного банка и один клиент, юридическая составляющая такая же, как если бы это были разные банки и разные клиенты. То есть у клиента один счёт по одному договору, второй счёт по другому договору, и нужно соблюсти все процедуры для списания и все процедуры для зачисления, какие предусмотрены правилами. Поэтому прорубание отдельного туннеля для отдельно взятого частного случая, видимо, особого смысла не имеет.
inkelyad
10.05.2025 08:16Да не про отдельный туннель речь. Вот мне тут расписывали процедуру перевода. Сильно многоступенчато, да. Объясняя, что все так сложно как раз для того, чтобы в каждый момент знать, где деньги и все балансы сходились. А жалоба была на
Деньги задваиваются (на исходящем счёте ещё не списались, а на новом уже отражаются)
Деньги испаряются (на исходящем счёте уже списались, а на целевом все ещё показывает ноль
Т.е. эти самые процедуры, предусмотренные правилами, и не выполнятся - деньги берутся из ниоткуда или пропадают.
askv
10.05.2025 08:16Ну потому что в реальности эти цифры в базе они юридического значения не имеют. Даже если списание/зачисление прошло несинхронно, то вряд ли кто-то сможет этими деньгами воспользоваться повторно. Это просто картинка лагает (что для конечных пользователей, как я понимаю, существенного значения не имеет, так как каждый пользователь и так видит операции и может понять, сколько у него должно быть денег). Если даже в силу сбоя банк проведёт операций больше, чем было денег на счёте, то они потом откатят. Если не получится откатить, подадут иск. Для системы юридическая составляющая важнее, чем техническая.
PrinceKorwin
10.05.2025 08:16Т.е. эти самые процедуры, предусмотренные правилами, и не выполнятся - деньги берутся из ниоткуда или пропадают.
Не переживайте, с деньгами все в порядке. Просто никто в здравом уме не даёт доступ для онлайн банкинг приложений в боевые таблицы. Они все работают по снапшотам и по event source таблицам. Отсюда и возможные времменые такие эффекты, что описаны выше.
SpiderEkb
10.05.2025 08:16Ну не все :-) У нас, слава богу, это дичи нет.
Скорее всего проблема в микросервисах у которых каждый имеет свою копию БД. И начинаются задержки репликации и синхронизации этих копий. Тут от микросервисов бльше проблем чем пользы.
На самом деле там все не так. А примерно так:
С баланса счета плательщика сумма переводится на холд
Проводится комплекс проверок платежа.
Если все проверки пройдены, сумма зачисляется на счет получателя
При успешном зачислении суммы на счет получателя она списывается с холда на счету плательщика.
Это универсальная схема как для случая когда счета в одном, так и для случая когда счета в разных банках.
При запросе баланса счета показывается только баланс, без учета холдов. Работаем с одной БД, без репликаций (точнее, они есть, но для разных "внешних систем" и там, естесвтенно, не вся БД, а только нужные этим внешним системам данные реплицируются). И да, там могут быть лаги, но для тех систем это несущественно по их логике работы.
askv
10.05.2025 08:16так и для случая когда счета в разных банках.
Когда в разных банках, у вас в общем случае нет возможности за разумное время пройти этап
сумма зачисляется на счет получателя
Там этот критерий как-то иначе формулируется (списано с корсчёта или как-то иначе). Если СБП, то может быть, там же онлайн всё.
askv
Насколько я понимаю, в бухгалтерских системах обычно хранятся не балансы счетов, а транзакции. То есть, при переводе со счёта на счёт в таблицу транзакций добавляется строка с номерами счетов и суммой. А баланс счетов подсчитывается уже по базе транзакций и там невозможна ситуация, когда деньги ушли, но не никуда не пришли или наоборот.
skymal4ik
Как в такой системе показываются баланс и сделки за последние n дней например?
Понятно что при создании счета его баланс устанавливается в 0, а потом сверху накидывают транзакции.
Но как оптимизировать показ последних операций, например за сутки, если счету десятки лет и сотни тысяч транзакций? Не пересчитывать же с первой транзакции каждый раз?
agratoth
По джобе раз в сутки считать баланс на 00:00, в течение дня брать из бд этот предпосчитанный баланс и прибавлять к нему транзакции за сутки
askv
Я сам внутри не копался, но думаю, что просто периодически подводятся итоги и балансы сохраняются. Поэтому достаточно посчитать транзакции за период с последнего подведения итога + баланс на тот момент времени. У банков есть понятие закрытого опердня (в который новые транзакции уже не вносятся) и открытого опердня, который ещё может пополняться новыми данными.
SpiderEkb
На счете текущий баланс и холды - суммы, которые еще не списаны (получение денег плательщиком не подтверждено), но уже "обещаны к списанию".
Плюс платежные документы, связанные с этим счетом
PrinceKorwin
Как уже ответили - иметь снепшот/подсчитанный актуальный баланс на определенную дату / с определенной транзакции. Вы наверняка слышали термин опердень (операционный день).
Sanchous98
Вы слышали про такой подход, как event sourcing? Вот тут такая же логика: ваш баланс - это результат всех операций. При совершении транзакции, восстанавливается баланс исходя из всех событий. Естественно когда событий много, это может быть проблемой в производительности. Для таких случаев делаются снапшоты каждые n-событий или периодически. Таким образом оптимизированная версия будет восстанавливать баланс из снапшота и событий после снапшота. При грамотном подходе к блокировкам можно добиться хорошего уровня параллелизма
PrinceKorwin
Верно. Также есть 2 механизма (на самом деле больше) механизма отката транзакции:
проведение поверх корректирующей
полностью удаление информации о транзакции
Последняя весьма раздражающая, но используется банками в случае быстрого возврата денежных средств. Например при возврате на кассе в течение короткого (до дня) времени.
askv
Насколько я понимаю, речь идёт о не до конца проведенной транзакции. То есть, клиент приходник выписал, а по факту деньги не внёс. Но у транзакций ещё статусы могут быть разные, так что этот неисполненный приходник может сохраниться в одной таблице со статусом "отменено" и потом не попасть в таблицу проводок по счетам, куда попадают только исполненные.
PrinceKorwin
Не обязательно. Это может быть зафиксированная транзакция. Например провели на кассе картой. И через 10 минут передумали и решили сделать возврат. Банк имеет право такую транзакцию полностью удалить и вы ее нигде не увидите. Это зависит от многих факторов, но такое имеет место быть.
askv
Я думаю, такая транзакция сохранится на уровне платёжной системы, просто в банковскую систему не попадёт.
PrinceKorwin
Попадает :)
Вы сначала ее видете у себя в банк онлайн, а потом она исчезает.
И эта операция также не светится в отчётах в ЦБ
SpiderEkb
Не транзакции, а платежные документы. Платкльщик, счет плательщика, получатель, счет получателя, сумма платежа...
askv
Вот понятие платёжных документов, оно довольно странное. Их форма и содержание — это полная свобода творчества заинтересованных сторон. Например, если я, находясь в РФ, прошу своего заграничного друга оплатить мне за границей какой-то сервис (например, Zoom), то наша переписка в чате является платёжным документом или нет? Ведь эта переписка потом служит основанием для подсчёта наших взаимных обязательств друг перед другом, кто кому сколько должен, по аналогии с тем, как банки ведут учёт денег на счетах клиентов...
agratoth
Платежным поручением может считаться)
zebin Автор
Замечание справедливое. Исправил вступление. Туториал c фокусом на понимание изоляций и свойств стэйтментов. В этой статье рассматриваю только проблему обновления двух счетов. Бухгалтерские системы гораздо сложнее.
Mapar
А вот тут еще и фантомные чтения надо рассматривать в тему статьи.
zebin Автор
Спасибо. В этих примерах фантомные чтения не помешают. Но их можно будет рассмотреть на другой подобной задаче
SpiderEkb
Вообще, это основа. Прочитал запись, что-то в ней сделал, перед тем как записывать - проверь. Прочитай еще раз - не изменилось ли что-то пока ты работал с записью
Mapar
Комментарий по фантомным чтениям относился к варианту досчета остатка по транзакциям.
SpiderEkb
И все равно вопрос - вы начинате транзакцию на списание 300р со счета. И вэтот же момент начинается транзакция на списание 400р. А на счете вмего 500р. Каждая транзакция по отдельности валидна. Вместе - нет. Что делать?
Mapar
Вопрос наверное не ко мне, а к автору статьи.
Собственно он статьей на Ваш вопрос и отвечает, что делать и какие патерны серилизации применять.
SpiderEkb
К сожалению, там нет ответа на поставленный вопрос.
Я уже писал как это происходит в реальной жизни.
Формируется платежный документ где указывается плательщик, получатель, счет плательщика, счет получателя. Причем, эти счета на обязательно в одном банке. Скорее всего, даже в разных. И сложности возникают только со списанием денег со счета плательщика. С зачислением на счет получателя все проще.
Если счет плательщика в нашем банке, Сумма по платежному документу переводится со счета на холд - резервируется (если на счете достаточно денег). Это быстрая операция, она делается с блокировкой записи.
Платежный документ отправляется на контроль. Результатом может быть безусловное "разрешить" (все автоматические проверки пройдены) - тогда документ передается на исполнение. Или документ может быть отправлен на ручной контроль в службу комплаенса. Оттуда может прийти решение "разрешить" - тогда на исполнение. Или "запретить" - тогда формируется отказ в операции и сумма с холда возвращается обратно на счет (опять с полной блокировкой записи - это одна запись, там есть поле текущего баланса, есть поле суммарного холда).
В целом контроль платежей штука достаточно сложная - там много разных проверок проводится.
Сумма холда уменьшается на сумму платежного документа только тогда, когда от получателя придет подтверждение о том, что деньги реально пришли на его счет (иногда это может занимать несколько секунд, иногда несколько минут).
Т.е. одной транзакцией тут никак не обойтись - между переводом денег с баланса на холд и списанием их с холда или возвратом обратно на баланс (в случае отказа в операции или неподтверждения операции в установленные сроки) может пройти существенное время.
Это и есть та самая (причем, достаточно упрощенная) бизнес-логика, в отрыве от которой все это превращается в сферического коня в вакууме.
Кроме того, транзакции достаточно сильно грузят сервер. Я скажу крамольную вещь, но мы работаем с COMMITMENT CONTROL (*NONE). Т.е. без коммитов и роллбеков. Вместо этого ведется журналирование всех операций (в специальные журналы пишутся образы записей "до" и "после" изменения (или только "после" в случае добавления или только "до" в случае удаления). Плюс двойное чтение - прочитали запись "до", внесли изменения (получили "после"), перед записью изменений еще раз читаем запись с проверкой - совпадет она с образом "до" - если да, то записываем изменения, если нет - возвращаем ошибку "кто-то другой изменил запись" (и дальше она уже по бизнес-логике обрабатывается).
По журналам можно откатить что угодно - хоть вчерашнее, хоть позавчерашнее. Кроме того, по журналам в начале нового дня накатываются все изменения из юнита ночного ввода (писал ранее об этом).
Правда, опять крамола, мы не используем SQL для изменений в продуктовых таблицах. Наш стек позволяет напрямую работать с БД (SQL используется там, где нужный сложные выборки по нескольким таблицам и многим условиям, и то, там не так все просто с точки зрения производительности).
Строго говоря, для каждой продуктовой таблицы есть "опция ведения". Которая состоит из 4-х модулей:
Update модуль - собственно работа с таблицей и ее журналом. Чтение образа "до", запись образа "после" (с проверкой что "до" не изменился). В реальной жизни может быть достаточно сложным т.к. может работать не с одной, а с несколькими логически связанными таблицами.
Validate модуль - контроль валидности данных записи в соответствии с бизнес-логикой
Модуль интерактивной работы с записью (позволяет вносить ручные изменения). Вводим уникальный ключ - если запись есть - работаем режиме изменения, если нет - в режиме добавления. После внесенных изменений вызывается Validate модуль и, если ошибки нет - Update модуль.
Модуль внешнего ввода. Получает на вход образ "после", читает из таблицы образ "до" (опять, есть запись - изменение, нет - добавление), вызывает Validete модуль, если ошибок нет - Update модуль. Этот же модуль позволяет делать "накат по журналу". Для этого на вход передается не образ "после", а имя библиотеки где лежит журнал и ключ записи с образом "после". В этом случае образ "после" берется из журнала.
Данная схема кажется сложной, но стабильно работает в условиях очень больших нагрузок.
Ivan22
у автора же для этого как раз поле с версией заведено. Логическая transaction_id так сказать
Mapar
Мне кажется автор просто не удачно назвал статью, тем самым притянув не ту аудиторию. Статья вообще не про "денежные переводы". И тут я понимаю Вы все прекрасно расписали.
В моем понимании, статья только как выполнить конкурентный update без lost update и не более того, даже другие проблемы типа фантомных чтений не рассматриваются.
Но безусловно интересно почитать детали Вашей реализации.
askv
Это действительно так, но раз уж это произошло, то почему бы и не обсудить смежную тему?
Я не один раз наблюдал, когда неправильно прописанная (или неправильно понятая) бизнес-логика приводила к непомерному росту затрат на техническую реализацию.
Например, для реализации одной задачи мы сводили все сделки в одну таблицу Excel (выгрузка из системы). Всё хорошо работало, пока все сделки были в одной IT-системе. Но как только появилась вторая система со сделками (при живой первой), технари тут же бросились реализовывать перекачку сделок из одной системы в другую (через какие-то шины, хранилища данных и т.д. и т.п.), чтобы потом объединить все сделки в одной табличке. Хорошо, что я вовремя это заметил и сказал, что их необязательно объединять в системе, исходя из бизнес-задачи табличек может быть больше чем одна, их легче объединить вовне системы, чем внутри и т.д. и т.п., что вызвало значительный вздох облегчения у технолога на другом конце телефонной линии...
inkelyad
И дальше, похоже, вручную делается то, что в более простых случаях (или более умных базах) делается самим движком базы данных.
Без уточнения что происходит - не гонка? Потому что между "еще раз читаем с проверкой' и 'то записываем изменения' - записи могут и измениться.
SpiderEkb
Тут вопрос не в том, что может БД. Она (DB2 for i) много что может.
Вопрос в производительности в условиях больших нагрузок. Наши исследования показали что SQL может начать работать нестабильно (в смысле скорости выполнения) в ситуациях, когда один и тот же запрос начинает выполняться параллельно из многих заданий (job) с большой плотностью вызовов. При этом прямая работа с БД не только работает стабильно в таких условиях, но и еще обеспечивает меньшую загрузку системы.
Поэтому на использование SQL есть ряд ограничений, сформулированных в нефункциональных требованиях, основанных на реальной практике работы системы.
Это то, что бывает сложно понять людям, не имеющим иных возможностей работы с БД, помимо SQL. Но у нас, слава богу, такие возможности в системе заложены.
Ну и SQL (точнее, commitment control) не решает проблему журналирования, возможность которого для нашей архитектуры является ключевой.
Да. Могут измениться. Именно поэтому и проводится проверка с выдачей ошибки. Как эта ошибка обрабатывается - тут все зависит от бизнес-логики процеса.
Но блокировка записи на длительное время (пока идет какая-то обработка - это прямой путь к дедлокам и деградации производительности. Что в нашем случае неприемлемо.
Я описал как в реальности происходит обработка платежа - кратковременная блокировка при переносе суммы платежа с баланса на холд. Дальше платеж может обрабатываться сколь угодно долго - баланс счета уже уменьшился, но в случае отката (отказа в проведении платежа) может быть увеличен обратно обратной операцией - возврата суммы с холда обратно на баланс. И транзакцией это не решается т.к. вы не можете в такой системе держать транзакцию открытой в течении длительного времени (а это может достигать суток - пока пройдет проверки, пройдет при необходимости ручной контроль, будет получено подтверждение из того банка, на счет в котором уходит платеж...)
А чтобы перевести со счета на холд транзакция не нужна - это одна запись (счет - сумма текущего баланса - сумма холдов по счету) - read (с блокировкой), cerbal -= n, curhold += n, update (с разблокировкой). А транзакция по определению, это цепочка связанных изменений в нескольких записях.
inkelyad
Только главное не налететь на синонимы терминологии. Потому что с точки зрения клиента: пока холд не нулевой - это разве не означает, что есть открытая транзакция(другая, не БД-ная) перевода денег? Ну вот и висит эта транзакция несколько дней.
И все описанный операции - это ручное выполнение протокола этой транзакции, а не БД-ной. Чтобы было то самое 'либо целиком выполнилось, либо
сделаем вид чтоничего не было'.askv
Тут во всей переписке надо различать, где платёжная транзакция, а где транзакции баз данных.
SpiderEkb
Ну на самом деле Холды часто бывают не нулевые. Например, на счет у вас 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р оформляются как "возврат" т с холда переходят обратно на счет. И так и этак встречал.
izibrizi2
У бухгалтеров это называется - проводки. В 1с есть еще регистр накопления для оперативного отображения сумм (агрегат, чтобы в реальном времени транзакции не считать). Всё хранится в журналах, sqrs + event sourcing до того как это стало мейнстримом.
Если балансы корректировать через ячейки - выгонят на мороз и спасибо если не посадят