Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Зачем нужна изоляция транзакций
Отличный вопрос про ACID и основы баз данных! Изоляция транзакций — это один из столпов надёжности. Давайте разберёмся по порядку.
Проблема: Конфликты параллельных транзакций
Представь, что два пользователя одновременно переводят деньги со счёта:
Счёт А: 1000 рублей
Счёт Б: 2000 рублей
Транзакция 1: Перевести 100 от А к Б
Транзакция 2: Читать баланс А и Б
Без изоляции может произойти:
1. T1: Прочитала баланс А (1000)
2. T2: Прочитала баланс А (1000) ← Невидит изменений T1
3. T1: Уменьшила А, увеличила Б
4. T2: Видит A=900, B=2100
Отчёт T2 противоречив: деньги из неоткуда!
ACID и буква I (Isolation)
ACID — четыре свойства надежной БД:
- Atomicity — "всё или ничего"
- Consistency — целостность данных
- Isolation — изоляция (наша тема)
- Durability — долговечность
Изоляция означает: параллельные транзакции не должны мешать друг другу.
Проблемы без изоляции
1. Dirty Read (Грязное чтение)
Транзакция A Транзакция B
├─ Прочитала X=100
└─ Изменила X=200
├─ Прочитала X=200 ← Видит незафиксированное!
└─ Откатилась (ROLLBACK)
У B осталась "грязная" информация X=200, но в А это откатилось!
Реальный пример:
# Без изоляции (опасно!)
# Транзакция 1 (в банке)
begin_transaction()
deposit = Deposit.objects.get(id=1)
deposit.amount = 1000 # Я погасил долг
deposit.save() # Сохранил в БД
# В этот момент Транзакция 2 видит 1000, думает погашено
# Но потом T1 сделает rollback из-за ошибки
# T2 будет работать с фантомными данными!
2. Non-Repeatable Read (Неповторяемое чтение)
Транзакция A Транзакция B
├─ Прочитала X (500)
│
├─ Изменила X на 700
└─ Завершилась (COMMIT)
│
├─ Прочитала X снова (700) ← Другое значение!
└─ Конец транзакции
В одной транзакции один и тот же X имеет разные значения!
Реальный пример:
# Пересчёт налога
begin_transaction()
total = 0
for item in Order.objects.all():
total += item.price
print(f"Total before tax: {total}")
# Между тем другая транзакция добавляет товар
total_again = sum(item.price for item in Order.objects.all())
print(f"Total after: {total_again}") # Разные значения!
3. Phantom Read (Фантомное чтение)
Транзакция A Транзакция B
├─ SELECT COUNT(*) где age > 18
│ Результат: 1000
│
├─ INSERT новых 100 пользователей age=20
└─ COMMIT
│
├─ SELECT COUNT(*) где age > 18
│ Результат: 1100 ← Новые строки!
└─ Конец
4. Lost Update (Потерянное обновление)
Транзакция A Транзакция B
├─ Прочитала counter=10
│ ├─ Прочитала counter=10
│
├─ Увеличила counter+=1 │
├─ Сохранила (11) │
│ ├─ Увеличила counter+=1
│ ├─ Сохранила (11) ← Потеряно!
│ └─ COMMIT
│
└─ COMMIT (11)
Оба сделали +=1, но результат 11 вместо 12!
Уровни изоляции
Базы данных предоставляют разные уровни изоляции (компромисс между безопасностью и производительностью):
1. READ UNCOMMITTED (Чтение незафиксированных данных)
Самый слабый уровень — разрешены все проблемы выше:
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
BEGIN;
SELECT balance FROM accounts WHERE id = 1; -- Может быть незафиксировано
COMMIT;
Когда использовать: Отчёты о примерных данных, статистика
# Django
with transaction.atomic(durable=False):
# Быстрое чтение, но может быть неточно
rough_stats = User.objects.count()
2. READ COMMITTED (Чтение только фиксированных данных)
По умолчанию в PostgreSQL — предотвращает Dirty Read:
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN;
SELECT balance FROM accounts WHERE id = 1; -- Только фиксированные
COMMIT;
Проблемы остаются:
- Non-repeatable reads ✓ (может быть)
- Phantom reads ✓ (может быть)
Когда использовать: Большинство приложений
# Django (по умолчанию)
with transaction.atomic():
user = User.objects.get(id=1)
user.balance = 500
user.save()
3. REPEATABLE READ (Повторяемое чтение)
По умолчанию в MySQL — предотвращает Dirty Read и Non-repeatable Read:
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
SELECT balance FROM accounts WHERE id = 1; -- Блокируется
time.sleep(10) # Даже если другой update, я вижу старое значение
SELECT balance FROM accounts WHERE id = 1; -- Зато значение
COMMIT;
Проблемы:
- Phantom reads ✓ (может быть)
Когда использовать: Критичные отчёты, финансовые операции
# Django
from django.db import transaction
with transaction.atomic():
# Инициируем сеанс на REPEATABLE READ
with connection.cursor() as cursor:
cursor.execute(
"SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;"
)
# Все запросы в этом контексте имеют стабильный снимок
accounts = Account.objects.all()
for acc in accounts:
if acc.balance < 0:
handle_negative(acc)
4. SERIALIZABLE (Строгая сериализуемость)
Самый сильный уровень — транзакции выполняются как будто последовательно:
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN;
SELECT * FROM accounts WHERE id IN (1, 2, 3);
-- Эти строки заблокированы от других транзакций
UPDATE accounts SET balance = balance + 100;
COMMIT;
Гарантии: Нет никаких проблем, 100% правильность
Цена: Медленнее, много конфликтов
Когда использовать: Критичные аудиты, законодательные требования
# Django
with transaction.atomic():
with connection.cursor() as cursor:
cursor.execute(
"SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;"
)
# Например, перевод денег между счётами
account1 = Account.objects.select_for_update().get(id=1)
account2 = Account.objects.select_for_update().get(id=2)
if account1.balance >= 100:
account1.balance -= 100
account2.balance += 100
account1.save()
account2.save()
Таблица сравнения
| Уровень | Dirty Read | Non-repeatable | Phantom | Скорость |
|---|---|---|---|---|
| READ UNCOMMITTED | ✓ | ✓ | ✓ | ⚡⚡⚡ |
| READ COMMITTED | ✗ | ✓ | ✓ | ⚡⚡ |
| REPEATABLE READ | ✗ | ✗ | ✓ | ⚡ |
| SERIALIZABLE | ✗ | ✗ | ✗ | 🐢 |
Реальный пример: Банковский учет
from django.db import transaction, connection
from django.db.models import F
class BankService:
@transaction.atomic
def transfer_money(self, from_id, to_id, amount):
"""Критичная операция — используем сильную изоляцию"""
with connection.cursor() as cursor:
# Используем SERIALIZABLE для абсолютной надежности
cursor.execute(
"SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;"
)
# Блокируем счета в одном порядке (предотвращаем deadlock)
first_id = min(from_id, to_id)
second_id = max(from_id, to_id)
acc1 = Account.objects.select_for_update().get(id=first_id)
acc2 = Account.objects.select_for_update().get(id=second_id)
# Определяем отправителя/получателя
sender = acc1 if acc1.id == from_id else acc2
receiver = acc2 if acc1.id == from_id else acc1
# Проверяем баланс
if sender.balance < amount:
raise InsufficientFundsError()
# Переводим (в одной атомарной транзакции)
sender.balance -= amount
receiver.balance += amount
sender.save(update_fields=['balance'])
receiver.save(update_fields=['balance'])
# Логируем транзакцию
Transaction.objects.create(
from_account=sender,
to_account=receiver,
amount=amount,
status='completed'
)
return True
# Использование
service = BankService()
try:
service.transfer_money(from_id=1, to_id=2, amount=100)
print("Transfer successful")
except InsufficientFundsError:
print("Transfer failed: insufficient funds")
PostgreSQL Snapshot Isolation
PostgreSQL использует MVCC (Multi-Version Concurrency Control) для изоляции:
# PostgreSQL создаёт "снимок" данных для каждой транзакции
# Это снимок видит данные такие, какие они были в начале транзакции
BEGIN; -- Создаётся снимок на момент t=10:00:00
SELECT * FROM users; -- Видит состояние на 10:00:00
time.sleep(10) # Другие транзакции меняют данные
SELECT * FROM users; -- Всё равно видит состояние на 10:00:00
COMMIT; -- Снимок больше не используется
Вывод
Изоляция транзакций нужна потому что:
- ✅ Данные остаются консистентными — финансовые операции не теряются
- ✅ Предотвращает гонки данных — каждая транзакция видит согласованное состояние
- ✅ Гарантирует ACID свойства — надёжность базы
- ✅ Разные уровни для разных задач — скорость vs надежность
Как выбрать уровень:
- Отчёты/статистика → READ UNCOMMITTED
- Большинство операций → READ COMMITTED (по умолчанию)
- Финансовые операции → REPEATABLE READ или SERIALIZABLE
- Критичные аудиты → SERIALIZABLE
Главное: Всегда думай про параллелизм, даже если приложение кажется однопоточным. Production всегда многопроцессный!