← Назад к вопросам

Зачем нужно изолировать транзакции?

1.7 Middle🔥 121 комментариев
#Базы данных (SQL)

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

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Зачем нужна изоляция транзакций

Отличный вопрос про 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 ReadNon-repeatablePhantomСкорость
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;  -- Снимок больше не используется

Вывод

Изоляция транзакций нужна потому что:

  1. Данные остаются консистентными — финансовые операции не теряются
  2. Предотвращает гонки данных — каждая транзакция видит согласованное состояние
  3. Гарантирует ACID свойства — надёжность базы
  4. Разные уровни для разных задач — скорость vs надежность

Как выбрать уровень:

  • Отчёты/статистика → READ UNCOMMITTED
  • Большинство операций → READ COMMITTED (по умолчанию)
  • Финансовые операции → REPEATABLE READ или SERIALIZABLE
  • Критичные аудиты → SERIALIZABLE

Главное: Всегда думай про параллелизм, даже если приложение кажется однопоточным. Production всегда многопроцессный!