Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Пример изоляции транзакции
Изоляция транзакции (Isolation) — это одно из четырёх свойств ACID. Это правило определяет, как параллельные транзакции видят друг друга изменения. Рассмотрим практические примеры с разными уровнями изоляции.
Уровни изоляции
Любая СУБД имеет 4 основных уровня изоляции (от слабого к сильному):
- READ UNCOMMITTED — читаешь незафиксированные (грязные) данные
- READ COMMITTED — читаешь только зафиксированные данные
- REPEATABLE READ — одна и та же выборка возвращает одно и то же
- 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)