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

Какие есть проблемы у readcommited?

2.4 Senior🔥 151 комментариев
#SQL и базы данных#Архитектура и проектирование

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

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

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

Проблемы READ COMMITTED уровня изоляции

READ COMMITTED — второй по строгости уровень изоляции транзакций. Несмотря на то, что это стандартный уровень изоляции во многих БД, он имеет серьезные недостатки для certain операций.

Определение READ COMMITTED

READ COMMITTED гарантирует:

  • Грязное чтение (dirty read) невозможно
  • Но допускает: non-repeatable read и phantom read

Проблема 1: Non-Repeatable Read

Одна и та же строка может быть прочитана по-разному в одной транзакции:

-- Транзакция A
BEGIN;
SELECT balance FROM accounts WHERE id = 1;  -- Читает balance = 100
-- ... другие операции ...
SELECT balance FROM accounts WHERE id = 1;  -- Может быть balance = 150!
COMMIT;

-- Одновременно Транзакция B
BEGIN;
UPDATE accounts SET balance = balance + 50 WHERE id = 1;
COMMIT;

Проблема: Один и тот же SELECT дает разные результаты внутри одной транзакции.

Impact: Итерационные калькуляции, проверки balance-после-вычисления могут быть некорректны.

Проблема 2: Phantom Read

Множество строк, возвращаемых одним запросом, может отличаться при повторном выполнении:

-- Транзакция A
BEGIN;
SELECT COUNT(*) FROM orders WHERE status = 'pending';  -- Читает 10
-- ... какие-то операции ...
SELECT COUNT(*) FROM orders WHERE status = 'pending';  -- Может быть 15!
COMMIT;

-- Одновременно Транзакция B вставляет новые pending orders
BEGIN;
INSERT INTO orders (status) VALUES ('pending');
COMMIT;

Проблема: Новые строки появляются между двумя SELECT в одной транзакции.

Impact: Расчеты на основе COUNT, SUM, WHERE условии могут быть инконсистентны.

Проблема 3: Lost Update (Write Skew)

Реда-модифай-пиши паттерн может привести к потере данных:

-- Транзакция A
BEGIN;
SELECT balance FROM accounts WHERE id = 1;  -- balance = 100
-- ... вычисления ...
UPDATE accounts SET balance = balance + 50 WHERE id = 1;
COMMIT;  -- balance = 150

-- Одновременно Транзакция B (в READ COMMITTED)
BEGIN;
SELECT balance FROM accounts WHERE id = 1;  -- Читает СТАРОЕ balance = 100
-- ... вычисления ...
UPDATE accounts SET balance = balance + 30 WHERE id = 1;
COMMIT;  -- balance = 130! (потеря 50 от TX A)

Проблема: Обе транзакции читают одно значение, модифицируют и пишут. Последняя запись перезаписывает изменения первой.

Impact: Финансовые системы, инвентарь, балансы счетов могут быть некорректны.

Проблема 4: Read Skew

Несогласованное состояние при чтении связанных данных:

-- Транзакция A
BEGIN;
SELECT balance1, balance2 FROM accounts WHERE id IN (1, 2);
-- balance1 = 1000, balance2 = 500 (сумма = 1500)
-- ... какие-то операции ...
COMMIT;

-- Одновременно Транзакция B переводит деньги
BEGIN;
UPDATE accounts SET balance1 = balance1 - 200 WHERE id = 1;
UPDATE accounts SET balance2 = balance2 + 200 WHERE id = 2;
COMMIT;

Проблема: TX A видит частичное состояние изменений TX B.

Impact: Проверки инвариантов (например, sum(balances) = X) нарушаются.

Пример: Конкурентное снижение запаса

# Django/SQLAlchemy с READ COMMITTED (по умолчанию)

def reduce_inventory(product_id, quantity):
    with db.session.begin():
        # TX A и TX B одновременно
        product = Product.query.get(product_id)  # stock = 100
        
        if product.stock >= quantity:
            product.stock -= quantity  # Может стать отрицательным!
            db.session.commit()
        else:
            raise InsufficientStock()

# TX A: reduce_inventory(1, 50)  # stock = 100 - 50 = 50
# TX B: reduce_inventory(1, 60)  # stock = 100 - 60 = 40 (ОШИБКА!)
# Результат: stock = 40, но продано 110 единиц

Решения

Решение 1: Использовать более высокий уровень изоляции

-- REPEATABLE READ (более строгий)
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;

BEGIN;
SELECT balance FROM accounts WHERE id = 1;
-- ... операции ...
SELECT balance FROM accounts WHERE id = 1;  -- Гарантированно то же значение
COMMIT;

Решение 2: Использовать SELECT FOR UPDATE

Эксклюзивная блокировка для read-modify-write:

BEGIN;
SELECT balance FROM accounts WHERE id = 1 FOR UPDATE;  -- Блокирует строку
-- Теперь никакая другая TX не может изменить эту строку до нашего commit
UPDATE accounts SET balance = balance + 50 WHERE id = 1;
COMMIT;
# В SQLAlchemy
from sqlalchemy import text

def update_balance(account_id, amount):
    session.execute(
        text("SELECT * FROM accounts WHERE id = :id FOR UPDATE"),
        {"id": account_id}
    )
    account = session.query(Account).filter_by(id=account_id).one()
    account.balance += amount
    session.commit()

Решение 3: Использовать atomic operations

-- Вместо SELECT + UPDATE (2 операции)
UPDATE accounts SET balance = balance + 50 WHERE id = 1;

-- Это атомично и безопасно в READ COMMITTED

Решение 4: Использовать версионирование (optimistic locking)

class Account(Base):
    __tablename__ = 'accounts'
    id = Column(Integer, primary_key=True)
    balance = Column(Float)
    version = Column(Integer, default=0)  # Версия строки

def update_balance(account_id, amount):
    account = session.query(Account).filter_by(id=account_id).one()
    old_version = account.version
    account.balance += amount
    account.version += 1
    
    try:
        session.execute(
            Account.__table__.update()
            .where((Account.id == account_id) & (Account.version == old_version))
            .values(balance=account.balance, version=account.version)
        )
        session.commit()
    except Exception:
        # Версия изменилась, retry
        session.rollback()
        raise ConcurrencyException()

Решение 5: Использовать SERIALIZABLE

SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;

-- Гарантирует полную изоляцию, но медленнее
-- Используй только для критичных операций

Сравнение уровней изоляции

УровеньDirty ReadNon-Repeatable ReadPhantom ReadPerformance
READ UNCOMMITTED⚡⚡⚡
READ COMMITTED⚡⚡
REPEATABLE READ
SERIALIZABLEМедленно

Чеклист для production

  • Финансовые операции: используй SELECT FOR UPDATE или SERIALIZABLE
  • Инвентарь/запасы: используй SELECT FOR UPDATE
  • Чтения с проверками: REPEATABLE READ или FOR UPDATE
  • Обычные чтения: READ COMMITTED безопасен
  • Высокая конкурентность: избегай SELECT FOR UPDATE (узкое место)
  • Критичные операции: тестируй с concurrent transactions

Заключение

READ COMMITTED допускает non-repeatable reads, phantom reads и lost updates. Для операций, зависящих от консистентности данных, нужны SELECT FOR UPDATE, атомичные операции или более высокие уровни изоляции.