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

Как избежать грязного чтения?

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

Комментарии (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()

Лучшие практики

  1. Используй READ_COMMITTED по умолчанию — это защищает от грязного чтения
  2. Используй SELECT FOR UPDATE для критичных операций — это блокирует строки
  3. Минимизируй время транзакции — держи транзакции короткими
  4. Тестируй с несколькими процессами — используй concurrent запросы в тестах
  5. Логируй конфликты — отслеживай оптимистичные ошибки блокировки

Сравнение методов

МетодПроизводительностьСложностьЗащита
READ_COMMITTEDВысокаяНизкаяОт грязного чтения
SELECT FOR UPDATEСредняяСредняяОт грязного и повторного
Оптимистичная блокировкаВысокаяВысокаяОт конкурентных изменений
SERIALIZABLEНизкаяНизкаяПолная изоляция

Заключение

Грязное чтение — серьёзная проблема в многопроцессных системах. Главный способ избежать — использовать правильный уровень изоляции транзакций (READ_COMMITTED) и явную блокировку (SELECT FOR UPDATE) для критичных операций.