Короткий ответ
Serializable даёт самую строгую гарантию без необходимости вручную расставлять блокировки, но требует обязательной retry-логики на serialization failure и может снижать пропускную способность под высокой конкуренцией; Repeatable Read/Read Committed с явным SELECT ... FOR UPDATE или атомарными UPDATE-выражениями даёт больше контроля и обычно выше производительность, но требует аккуратного ручного проектирования каждого критичного пути.
Как это работает подробнее
Serializable в PostgreSQL реализован через SSI (Serializable Snapshot Isolation) — он детектирует потенциальные аномалии между конкурентными транзакциями и прерывает одну из них с ошибкой serialization failure; при Repeatable Read/Read Committed корректность операции обеспечивается явно — через атомарный UPDATE с условием (WHERE balance >= amount) или SELECT ... FOR UPDATE, блокирующий строку до конца транзакции.
Сравнение
- Компромисс. Выбор зависит от частоты конкурентных обращений к одним и тем же счетам, сложности бизнес-логики внутри транзакции, терпимости к retry на стороне клиента и готовности команды поддерживать явные блокировки против декларативного Serializable; критерий — профилирование реальной конкуренции и нагрузочное тестирование обоих вариантов перед продакшеном, а не выбор «по умолчанию».
Пример кода
from django.db import transaction, DatabaseError, connection
import time
def debit_serializable(account_id, amount, attempts=5):
for attempt in range(attempts):
try:
with transaction.atomic():
connection.cursor().execute("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE")
account = Account.objects.get(id=account_id)
if account.balance < amount:
raise ValueError("Insufficient funds")
account.balance -= amount
account.save()
return
except DatabaseError as e:
if "could not serialize access" in str(e) and attempt < attempts - 1:
time.sleep(0.05 * (attempt + 1))
continue
raiseЧто использовать на практике
- Практика. Для систем с умеренной конкуренцией и сложной бизнес-логикой Serializable снижает риск пропустить неочевидную аномалию, но требует retry-цикла, так как откат под Serializable — штатная ситуация; для высокочастотных простых операций часто выгоднее явный атомарный UPDATE или FOR UPDATE, так как это предсказуемо по производительности.
Подводные камни
- Ошибка. Включают Serializable глобально «на всякий случай», не реализовав retry-логику, из-за чего под нагрузкой пользователи получают случайные ошибки; реализуют FOR UPDATE в одних частях кода и обычные UPDATE без условий в других, создавая непоследовательную защиту.