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

Приведи пример изоляции транзакции

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

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

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

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

Пример изоляции транзакции

Изоляция транзакции (Isolation) — это одно из четырёх свойств ACID. Это правило определяет, как параллельные транзакции видят друг друга изменения. Рассмотрим практические примеры с разными уровнями изоляции.

Уровни изоляции

Любая СУБД имеет 4 основных уровня изоляции (от слабого к сильному):

  1. READ UNCOMMITTED — читаешь незафиксированные (грязные) данные
  2. READ COMMITTED — читаешь только зафиксированные данные
  3. REPEATABLE READ — одна и та же выборка возвращает одно и то же
  4. SERIALIZABLE — полная сериализация (как если бы транзакции шли по очереди)

Проблемы параллелизма

# Исходное состояние счёта: balance = 100

# ❌ DIRTY READ (Грязное чтение)
# Транзакция 1              | Транзакция 2
# BEGIN                     |
#                           | BEGIN
# UPDATE accounts           |
# SET balance = 50          |
# WHERE id = 1              |
#                           | SELECT balance FROM accounts  # 50 — грязное чтение!
#                           | WHERE id = 1
# ROLLBACK                  |
#                           | COMMIT  # Прочитала откатанное значение!

# ❌ NON-REPEATABLE READ (Неповторяющееся чтение)
# Транзакция 1              | Транзакция 2
# BEGIN                     |
# SELECT balance = 100      |
#                           | BEGIN
#                           | UPDATE accounts
#                           | SET balance = 200
#                           | WHERE id = 1
#                           | COMMIT
# SELECT balance = 200      | # Разные результаты!
# WHERE id = 1              |
# COMMIT                    |

# ❌ PHANTOM READ (Фантомное чтение)
# Транзакция 1              | Транзакция 2
# BEGIN                     |
# SELECT COUNT(*)           |
# FROM orders               | # Результат: 5
#                           | BEGIN
#                           | INSERT INTO orders (...)  # +1
#                           | COMMIT
# SELECT COUNT(*)           |
# FROM orders               | # Результат: 6 (фантом!)
# COMMIT                    |

Пример 1: PostgreSQL с READ UNCOMMITTED → READ COMMITTED

import psycopg2
import threading
import time

# Подключение к PostgreSQL
conn = psycopg2.connect(
    dbname="bank",
    user="postgres",
    password="password",
    host="localhost"
)

# Создаём таблицу счётов
cursor = conn.cursor()
cursor.execute("""
    CREATE TABLE IF NOT EXISTS accounts (
        id SERIAL PRIMARY KEY,
        owner VARCHAR(50),
        balance DECIMAL(10, 2)
    )
""")
cursor.execute("DELETE FROM accounts")  # Очистим
cursor.execute(
    "INSERT INTO accounts (owner, balance) VALUES (Alice, 100)"
)
conn.commit()

# Демонстрация DIRTY READ (невозможна в READ COMMITTED по умолчанию)
def transaction_1():
    """Пытается читать незафиксированные изменения"""
    conn1 = psycopg2.connect(
        dbname="bank",
        user="postgres",
        password="password",
        host="localhost"
    )
    
    # Устанавливаем READ UNCOMMITTED (если база поддерживает)
    cursor1 = conn1.cursor()
    cursor1.execute("SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED")
    cursor1.execute("BEGIN")
    
    time.sleep(0.5)  # Даём транзакции 2 время на изменение
    
    cursor1.execute("SELECT balance FROM accounts WHERE id = 1")
    balance = cursor1.fetchone()[0]
    print(f"[TX1] Прочитала balance: {balance}")
    
    time.sleep(1)  # Даём время транзакции 2 на откат
    cursor1.execute("COMMIT")
    conn1.close()

def transaction_2():
    """Делает изменение и откатывает его"""
    conn2 = psycopg2.connect(
        dbname="bank",
        user="postgres",
        password="password",
        host="localhost"
    )
    
    cursor2 = conn2.cursor()
    cursor2.execute("BEGIN")
    
    # Изменяем баланс, но не коммитим
    cursor2.execute(
        "UPDATE accounts SET balance = 50 WHERE id = 1"
    )
    print("[TX2] Обновила balance на 50")
    
    time.sleep(2)  # Даём TX1 время на чтение
    
    cursor2.execute("ROLLBACK")  # Откатываем изменение
    print("[TX2] ROLLBACK")
    
    conn2.close()

# Запускаем параллельно
thread1 = threading.Thread(target=transaction_1)
thread2 = threading.Thread(target=transaction_2)

thread2.start()
thread1.start()

thread1.join()
thread2.join()

print("\nРезультат: DIRTY READ произошла в READ UNCOMMITTED!")
conn.close()

Пример 2: NON-REPEATABLE READ

import psycopg2
import threading
import time

conn = psycopg2.connect(
    dbname="bank",
    user="postgres",
    password="password",
    host="localhost"
)

def transaction_a():
    """Читает один и тот же счёт дважды"""
    conn_a = psycopg2.connect(
        dbname="bank",
        user="postgres",
        password="password",
        host="localhost"
    )
    
    # READ COMMITTED уровень (по умолчанию)
    cursor_a = conn_a.cursor()
    cursor_a.execute("BEGIN")
    
    # Первое чтение
    cursor_a.execute("SELECT balance FROM accounts WHERE id = 1")
    balance_1 = cursor_a.fetchone()[0]
    print(f"[TXA] Первое чтение: balance = {balance_1}")
    
    time.sleep(1)  # Даём TXB время на изменение
    
    # Второе чтение того же счёта
    cursor_a.execute("SELECT balance FROM accounts WHERE id = 1")
    balance_2 = cursor_a.fetchone()[0]
    print(f"[TXA] Второе чтение: balance = {balance_2}")
    
    if balance_1 != balance_2:
        print("❌ NON-REPEATABLE READ: одна транзакция увидела разные значения!")
    
    cursor_a.execute("COMMIT")
    conn_a.close()

def transaction_b():
    """Изменяет счёт"""
    conn_b = psycopg2.connect(
        dbname="bank",
        user="postgres",
        password="password",
        host="localhost"
    )
    
    cursor_b = conn_b.cursor()
    cursor_b.execute("BEGIN")
    
    time.sleep(0.5)  # Даём TXA время на первое чтение
    
    cursor_b.execute("UPDATE accounts SET balance = 75 WHERE id = 1")
    print("[TXB] Обновила balance на 75")
    cursor_b.execute("COMMIT")
    
    conn_b.close()

thread_a = threading.Thread(target=transaction_a)
thread_b = threading.Thread(target=transaction_b)

thread_b.start()
thread_a.start()

thread_a.join()
thread_b.join()

print("\nЭто проблема уровня READ COMMITTED")
conn.close()

Пример 3: SQLAlchemy с SERIALIZABLE

from sqlalchemy import create_engine, Column, Integer, String, Numeric
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, Session
from sqlalchemy.pool import StaticPool
from decimal import Decimal
import threading
import time

# Создаём БД
engine = create_engine(
    sqlite:///:memory:,
    poolclass=StaticPool,
    isolation_level="SERIALIZABLE"  # Максимальная изоляция
)

Base = declarative_base()

class Account(Base):
    __tablename__ = accounts
    
    id = Column(Integer, primary_key=True)
    owner = Column(String(50))
    balance = Column(Numeric(10, 2))

Base.metadata.create_all(engine)
SessionLocal = sessionmaker(bind=engine)

# Инициализируем данные
session = SessionLocal()
session.add(Account(id=1, owner=Alice, balance=Decimal(100)))
session.add(Account(id=2, owner=Bob, balance=Decimal(100)))
session.commit()
session.close()

def transaction_transfer_a():
    """Переводит деньги от Alice к Bob"""
    session = SessionLocal()
    
    try:
        session.execute(
            "BEGIN IMMEDIATE"  # Явно блокируем
        )
        
        # Прочитаем баланс Alice
        alice = session.query(Account).filter(Account.id == 1).with_for_update().first()
        print(f"[TA] Alice balance before: {alice.balance}")
        
        time.sleep(1)  # Даём TB время
        
        # Обновляем
        alice.balance -= Decimal(25)
        session.commit()
        print(f"[TA] Alice balance after: {alice.balance}")
        
    except Exception as e:
        session.rollback()
        print(f"[TA] Ошибка: {e}")
    finally:
        session.close()

def transaction_transfer_b():
    """Переводит деньги от Bob к Alice"""
    session = SessionLocal()
    
    try:
        session.execute("BEGIN IMMEDIATE")
        
        time.sleep(0.5)  # Даём TA время на начало
        
        # Прочитаем баланс Bob
        bob = session.query(Account).filter(Account.id == 2).with_for_update().first()
        print(f"[TB] Bob balance before: {bob.balance}")
        
        # При SERIALIZABLE это может заблокироваться
        bob.balance -= Decimal(25)
        session.commit()
        print(f"[TB] Bob balance after: {bob.balance}")
        
    except Exception as e:
        session.rollback()
        print(f"[TB] Ошибка: {e}")
    finally:
        session.close()

thread_a = threading.Thread(target=transaction_transfer_a)
thread_b = threading.Thread(target=transaction_transfer_b)

thread_a.start()
thread_b.start()

thread_a.join()
thread_b.join()

print("\nС SERIALIZABLE: транзакции не видят друг друга изменения")

Пример 4: Django ORM и изоляция

from django.db import transaction, connection
from django.db.models import F
from myapp.models import BankAccount

# Настройка уровня изоляции
with transaction.atomic(durable=True):
    # DURABLE = используется более строгий уровень изоляции
    
    # Транзакция 1: Снять деньги со счёта
    account = BankAccount.objects.select_for_update().get(id=1)
    print(f"Баланс: {account.balance}")
    
    account.balance -= 50
    account.save()
    
    # select_for_update() захватывает строку
    # Другие транзакции не смогут её изменить, пока мы не закончим

# Пример с REPEATABLE READ эмуляцией
with transaction.atomic():
    # Читаем данные в начале транзакции
    initial_balance = BankAccount.objects.filter(id=1).values_list(
        balance, flat=True
    ).first()
    
    # Делаем вычисления
    new_balance = initial_balance - 25
    
    # Обновляем (если никто не менял за время вычисления)
    BankAccount.objects.filter(
        id=1,
        balance=initial_balance  # Оптимистичная блокировка
    ).update(
        balance=new_balance
    )

Пример 5: MongoDB транзакции

from pymongo import MongoClient
from pymongo.errors import OperationFailure

client = MongoClient()
db = client[bank]

# MongoDB использует Snapshot Isolation
def transfer_money():
    session = client.start_session()
    
    try:
        session.start_transaction(
            isolation_level=snapshot  # Snapshot Isolation
        )
        
        # Транзакция видит снимок БД на момент начала
        # И не видит изменений других параллельных транзакций
        
        accounts = db.accounts
        
        # Вычесть со счёта A
        accounts.update_one(
            {_id: alice},
            {: {balance: -50}},
            session=session
        )
        
        # Добавить на счёт B
        accounts.update_one(
            {_id: bob},
            {: {balance: 50}},
            session=session
        )
        
        session.commit_transaction()
        print("Транзакция успешна")
        
    except OperationFailure as e:
        session.abort_transaction()
        print(f"Ошибка: {e}")
    finally:
        session.end_session()

Таблица: Какие проблемы решают уровни изоляции

Уровень              | Dirty | Non-Rep | Phantom
                     | Read  | Read    | Read
─────────────────────┼───────┼─────────┼────────
READ UNCOMMITTED     | ❌    | ❌      | ❌
READ COMMITTED       | ✅    | ❌      | ❌
REPEATABLE READ      | ✅    | ✅      | ❌
SERIALIZABLE        | ✅    | ✅      | ✅

Правильная использование в production

# ✅ Используй правильный уровень
from sqlalchemy.pool import QueuePool

engine = create_engine(
    postgresql://user:pass@localhost/db,
    isolation_level=READ_COMMITTED,  # По умолчанию
    pool_pre_ping=True,
    pool_size=20,
    max_overflow=40
)

# Для критичных операций
with engine.connect() as conn:
    with conn.begin():
        # Автоматический COMMIT при успехе
        # ROLLBACK при ошибке
        conn.execute(sql)

Заключение

Изоляция транзакции гарантирует, что:

  • Одна транзакция не видит грязные данные другой
  • Данные остаются консистентными
  • Параллельные операции не мешают друг другу

Выбор уровня изоляции зависит от требований:

  • READ COMMITTED — стандартный для большинства приложений
  • REPEATABLE READ — для аналитики и отчётов
  • SERIALIZABLE — только для критичных финансовых операций (с учётом performance penalty)
Приведи пример изоляции транзакции | PrepBro