Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
# Как избежать грязного чтения (Dirty Read)
Грязное чтение (Dirty Read) — это проблема, когда одна транзакция читает данные, которые были изменены другой транзакцией, но ещё не закоммичены (зафиксированы). Если вторая транзакция откатывается, первая работает с невалидными данными.
Визуализация проблемы
Транзакция A Транзакция B
1. SELECT balance (100)
2. UPDATE balance = 50 (незакоммичено)
3. ЧИТАЕТ balance = 50 (грязное!)
4. ROLLBACK
5. Работает с balance = 50
Но на самом деле это 100! ❌
Уровни изоляции транзакций в SQL
Грязное чтение предотвращается установкой нужного уровня изоляции:
-- 1. READ UNCOMMITTED — самый низкий, позволяет грязное чтение (ПЛОХО)
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
-- 2. READ COMMITTED — не позволяет грязное чтение (хорошо)
SET TRANSACTION ISOLATION LEVEL READ COMMITTED; -- по умолчанию в большинстве БД
-- 3. REPEATABLE READ — выше защита от фантомных чтений
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- 4. SERIALIZABLE — самый высокий, максимальная защита
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
Метод 1: Использование правильного уровня изоляции
PostgreSQL
import psycopg2
conn = psycopg2.connect("dbname=mydb user=postgres")
# Установить уровень изоляции
conn.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_READ_COMMITTED)
cursor = conn.cursor()
cursor.execute("SELECT balance FROM accounts WHERE id = 1")
balance = cursor.fetchone()[0]
print(balance) # Данные гарантированно закоммичены
conn.close()
SQLAlchemy
from sqlalchemy import create_engine, text
from sqlalchemy.orm import sessionmaker
from sqlalchemy.pool import NullPool
engine = create_engine(
"postgresql://user:password@localhost/mydb",
isolation_level="READ_COMMITTED" # Предотвращает грязное чтение
)
Session = sessionmaker(bind=engine)
session = Session()
# Или для конкретной транзакции
with engine.begin() as conn:
result = conn.execute(text("SELECT balance FROM accounts WHERE id = 1"))
balance = result.scalar()
Метод 2: Явная блокировка строк
from sqlalchemy import text
from sqlalchemy.orm import Session
def transfer_money(session: Session, from_id: int, to_id: int, amount: float):
# Заблокируем строки для чтения (SELECT FOR UPDATE)
from_account = session.execute(
text(
"SELECT * FROM accounts WHERE id = :id FOR UPDATE"
),
{"id": from_id}
).fetchone()
if from_account['balance'] < amount:
raise ValueError("Insufficient funds")
# Другие транзакции не смогут читать/писать эти строки
# до конца нашей транзакции
# Выполняем операцию
session.execute(
text("UPDATE accounts SET balance = balance - :amount WHERE id = :id"),
{"amount": amount, "id": from_id}
)
session.execute(
text("UPDATE accounts SET balance = balance + :amount WHERE id = :id"),
{"amount": amount, "id": to_id}
)
session.commit()
Метод 3: Оптимистичная блокировка с версией
from sqlalchemy import Column, Integer, String, Float, DateTime
from sqlalchemy.orm import declarative_base
from datetime import datetime
Base = declarative_base()
class Account(Base):
__tablename__ = "accounts"
id = Column(Integer, primary_key=True)
balance = Column(Float)
version = Column(Integer, default=0) # Версия для оптимистичной блокировки
updated_at = Column(DateTime, default=datetime.utcnow)
def update_balance(session: Session, account_id: int, new_balance: float):
account = session.query(Account).filter(Account.id == account_id).first()
# Попытка обновления с проверкой версии
result = session.execute(
text(
"""UPDATE accounts
SET balance = :balance, version = version + 1
WHERE id = :id AND version = :version"""
),
{"id": account_id, "balance": new_balance, "version": account.version}
)
if result.rowcount == 0:
# Версия не совпадает — данные были изменены другой транзакцией
raise Exception("Concurrent modification detected")
session.commit()
Метод 4: Read-Only транзакции
from sqlalchemy import text
def read_account_safely(session: Session, account_id: int):
# Для только-читающих операций используй read-only режим
with session.begin():
# Установи транзакцию в read-only
session.execute(text("SET TRANSACTION READ ONLY"))
result = session.execute(
text("SELECT * FROM accounts WHERE id = :id"),
{"id": account_id}
).fetchone()
return result
Практический пример: Банковский перевод
from sqlalchemy import create_engine, text
from sqlalchemy.orm import sessionmaker
from contextlib import contextmanager
engine = create_engine("postgresql://localhost/bank")
Session = sessionmaker(bind=engine)
@contextmanager
def transaction():
session = Session()
try:
yield session
session.commit()
except Exception:
session.rollback()
raise
finally:
session.close()
def safe_transfer(from_id: int, to_id: int, amount: float):
with transaction() as session:
# BEGIN TRANSACTION с READ COMMITTED
session.begin()
# Заблокируй счёт отправителя для избежания грязного чтения
from_account = session.execute(
text(
"""SELECT id, balance FROM accounts
WHERE id = :id FOR UPDATE"""
),
{"id": from_id}
).fetchone()
if not from_account:
raise ValueError(f"Account {from_id} not found")
if from_account['balance'] < amount:
raise ValueError("Insufficient funds")
# Выполни операцию
session.execute(
text(
"""UPDATE accounts
SET balance = balance - :amount
WHERE id = :id"""
),
{"amount": amount, "id": from_id}
)
session.execute(
text(
"""UPDATE accounts
SET balance = balance + :amount
WHERE id = :id"""
),
{"amount": amount, "id": to_id}
)
# COMMIT фиксирует изменения
session.commit()
Лучшие практики
- Используй READ_COMMITTED по умолчанию — это защищает от грязного чтения
- Используй SELECT FOR UPDATE для критичных операций — это блокирует строки
- Минимизируй время транзакции — держи транзакции короткими
- Тестируй с несколькими процессами — используй concurrent запросы в тестах
- Логируй конфликты — отслеживай оптимистичные ошибки блокировки
Сравнение методов
| Метод | Производительность | Сложность | Защита |
|---|---|---|---|
| READ_COMMITTED | Высокая | Низкая | От грязного чтения |
| SELECT FOR UPDATE | Средняя | Средняя | От грязного и повторного |
| Оптимистичная блокировка | Высокая | Высокая | От конкурентных изменений |
| SERIALIZABLE | Низкая | Низкая | Полная изоляция |
Заключение
Грязное чтение — серьёзная проблема в многопроцессных системах. Главный способ избежать — использовать правильный уровень изоляции транзакций (READ_COMMITTED) и явную блокировку (SELECT FOR UPDATE) для критичных операций.