The post has been translated automatically. Original language: Russian
ACID in the database description is often perceived as a ready-made guarantee "data is always consistent". In practice, the letter I in ACID isolation is not a binary "yes or no" switch, but a range of levels, and the default level in most DBMSs (usually Read Committed) does not protect against all parallel access problems that can cost a business real money.
Read Committed ensures that a transaction does not see uncommitted changes to another transaction (protection against "dirty reads"), but it does not protect against non—repeated reads: if the same line is read twice in one transaction, another transaction may have time to change and commit it between reads, and the second read will return a different value. This may not be critical for the report, but for the logic of the type "we checked the account balance, then debited the amount" within the framework of a single transaction, this is a direct hole for a race of fortunes.
Repeatable Read solves this particular problem — within a transaction, re—reading the same row always gives the same result - but it is still vulnerable to phantom reads: a repeated query with the same WHERE condition may return a different set of rows if new rows matching the condition are added in parallel. And even Repeatable Read does not protect against the classic race of the form "they read the balance, considered that there were enough funds, wrote down a new balance" — if two parallel transactions do this at the same time, both can independently calculate that the operation is acceptable, although together they take the balance into negative territory.
Serializable— the strictest level that ensures that the result of parallel execution of transactions is equivalent to some kind of sequential order of their execution, closes this case, but pays for it with a significant drop in throughput under high competition: transactions more often block each other or roll back with a serialization error that the application needs to be able to intercept and retry.
In practice, for truly money-sensitive logic - debiting funds, reserving a balance, scoring decisions based on the current account status — a typical solution is not to "raise the isolation level everywhere", but to explicitly control competitive access where it is really critical: through SELECT... FOR an UPDATE to explicitly lock a specific row for the duration of an operation, through an optimistic lock with a record version check, or through atomic operations at the DBMS level (UPDATE... SET balance = balance - X WHERE balance >= X) instead of the sequence "read, calculate in the application, write". The default isolation level is not a guarantee of the correctness of the business logic, but only one of the tools, and relying solely on it in financial transactions is a risk that should be placed consciously, not by default.
ACID в описании базы данных часто воспринимается как готовая гарантия «данные всегда консистентны». На практике буква I в ACID — изоляция — не бинарный переключатель «есть или нет», а спектр уровней, и уровень по умолчанию в большинстве СУБД (обычно Read Committed) защищает далеко не от всех проблем параллельного доступа, которые могут стоить бизнесу реальных денег.
Read Committed гарантирует, что транзакция не увидит незафиксированные изменения другой транзакции (защита от «грязного чтения»), но не защищает от неповторяющегося чтения: если в рамках одной транзакции дважды прочитать одну и ту же строку, между чтениями её может успеть изменить и зафиксировать другая транзакция — и второе чтение вернёт уже другое значение. Для отчёта это может быть не критично, но для логики вида «проверили остаток на счёте, потом списали сумму» в рамках одной транзакции — это прямая дыра для гонки состояний.
Repeatable Read решает эту конкретную проблему — в рамках транзакции повторное чтение той же строки всегда даёт одинаковый результат, — но по-прежнему уязвим к фантомным чтениям: повторный запрос с тем же условием WHERE может вернуть другой набор строк, если параллельно добавились новые строки, подходящие под условие. И даже Repeatable Read не защищает от классической гонки вида «прочитали остаток, посчитали, что средств достаточно, записали новый остаток» — если две параллельные транзакции сделают это одновременно, обе могут независимо посчитать, что операция допустима, хотя совместно они уводят баланс в минус.
Serializable — самый строгий уровень, гарантирующий, что результат параллельного выполнения транзакций эквивалентен какому-то последовательному порядку их выполнения, — закрывает и этот случай, но платит за это ощутимым падением пропускной способности под высокой конкуренцией: транзакции чаще блокируют друг друга или откатываются с ошибкой сериализации, которую приложению нужно уметь перехватывать и повторять попытку.
На практике для по-настоящему денежно-чувствительной логики — списание средств, резервирование баланса, скоринговые решения на основе текущего состояния счёта — типичное решение не «поднять уровень изоляции везде», а явно контролировать конкурентный доступ там, где он действительно критичен: через SELECT ... FOR UPDATE для явной блокировки конкретной строки на время операции, через оптимистичную блокировку с проверкой версии записи, или через атомарные операции на уровне СУБД (UPDATE ... SET balance = balance - X WHERE balance >= X) вместо последовательности «прочитать, посчитать в приложении, записать». Уровень изоляции по умолчанию — это не гарантия корректности бизнес-логики, а лишь один из инструментов, и полагаться исключительно на него в финансовых операциях — риск, который стоит закладывать осознанно, а не по умолчанию.