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

Можно ли встретить ошибку Non-Repeatable Read в уровне изоляции Read Commited?

1.8 Middle🔥 192 комментариев
#Базы данных

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

🐱
deepseek-v3.2PrepBro AI5 апр. 2026 г.(ред.)

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

Можно ли встретить ошибку Non-Repeatable Read на уровне изоляции Read Committed?

Да, ошибка Non-Repeatable Read (неповторяемое чтение) — это характерная проблема уровня изоляции READ COMMITTED. Это один из основных недостатков данного уровня, который допускает такие ситуации в рамках своей гарантийной модели.

Краткий ответ

В транзакционной модели ACID, уровень изоляции READ COMMITTED гарантирует только то, что транзакция будет читать данные, которые были уже фиксированы (committed) другими транзакциями. Однако он не гарантирует, что если транзакция повторно читает одну и ту же строку (или набор строк) в ходе своей работы, она получит то же самое значение. Другая транзакция может изменить эти данные и зафиксировать изменения между двумя чтениями первой транзакции. Именно эта ситуация называется Non-Repeatable Read.

Уровни изоляции ANSI SQL и проблема Non-Repeatable Read

Согласно стандарту ANSI SQL, уровни изоляции определяют, какие аномалии (phenomena) они допускают или предотвращают. Основные аномалии:

  1. Lost Update (потерянное обновление)
  2. Dirty Read (чтение "грязных" данных)
  3. Non-Repeatable Read (неповторяемое чтение)
  4. Phantom Read (чтение "фантомов")

READ COMMITTED (наиболее распространенный базовый уровень в большинстве СУБД, включая PostgreSQL, Oracle, SQL Server с настройками по умолчанию) предотвращает только Dirty Read. Он позволяет (permits) как Non-Repeatable Read, так и Phantom Read.

Пример Non-Repeatable Read на уровне READ COMMITED

Рассмотрим классический пример в контексте банковского приложения. Две транзакции выполняются параллельно:

Транзакция A (чтение баланса клиента):

BEGIN TRANSACTION;
-- Первое чтение баланса клиента ID = 1
SELECT balance FROM accounts WHERE id = 1;
-- Результат: 1000 рублей
-- Транзакция выполняет некоторую логику/вычисления...
-- ... и позже читает баланс повторно для проверки
SELECT balance FROM accounts WHERE id = 1;
-- Результат теперь может быть: 900 рублей
COMMIT;

Транзакция B (изменение баланса того же клиента):

BEGIN TRANSACTION;
-- Между двумя SELECT транзакции A, транзакция B
-- изменяет и фиксирует данные
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
COMMIT;

Результат: Транзакция A в своих двух операциях SELECT получила два разных значения для одного и того же поля (balance) одной и той же строки (id = 1). Это классическая Non-Repeatable Read.

Почему это важно?

Эта аномалия может привести к логическим ошибкам в приложении:

  • Неверные вычисления, основанные на предположении о неизменности данных.
  • Проблемы с проверкой условий (например, проверка достаточности средств перед операцией).
  • Сложности с формированием отчетов, где данные должны быть консистентными в момент начала транзакции.

Как предотвратить Non-Repeatable Read?

Чтобы избежать этой проблемы, необходимо использовать более строгий уровень изоляции, который не допускает (disallows) Non-Repeatable Read:

  • REPEATABLE READ (или SNAPSHOT в некоторых СУБД) — гарантирует, что все данные, читаемые в течение транзакции, будут оставаться неизменными для этой транзакции до ее завершения. Другие транзакции могут их изменять, но изменения не будут видимы для читающей транзакции.
  • SERIALIZABLE — самый строгий уровень, предотвращающий все аномалии.

Пример использования REPEATABLE READ в PostgreSQL:

BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SELECT balance FROM accounts WHERE id = 1; -- 1000
-- Транзакция B не сможет изменить balance для id=1 и зафиксировать
-- так, чтобы это изменение стало видимым для этой транзакции до ее завершения.
SELECT balance FROM accounts WHERE id = 1; -- Гарантированно 1000
COMMIT;

Особенности реализации в разных СУБД

  • PostgreSQL: READ COMMITTED — уровень по умолчанию. Non-Repeatable Read возможен. Для предотвращения используется REPEATABLE READ или SERIALIZABLE.
  • MySQL/InnoDB: По умолчанию используется REPEATABLE READ, поэтому Non-Repeatable Read предотвращается на базовом уровне. Это особенность реализации InnoDB.
  • Oracle: Уровень READ COMMITTED также является базовым и допускает Non-Repeatable Read. Oracle для предотвращения использует механизм SELECT FOR UPDATE (блокировки) или расширенный контроль через SET TRANSACTION ISOLATION LEVEL SERIALIZABLE.
  • SQL Server: По умолчанию также READ COMMITTED. Можно использовать REPEATABLE READ или блокировку через WITH (REPEATABLEREAD) в SELECT.

Альтернативные подходы в прикладном коде (Go)

Вместо повышения уровня изоляции всей транзакции иногда можно использовать более узкие методы:

  • Оптимистичные блокировки (версионирование).
  • Повторное чтение с проверкой в логике приложения.
  • Явные блокировки строк (SELECT ... FOR UPDATE в SQL).

Пример использования SELECT FOR UPDATE для предотвращения Non-Repeatable Read в Go с драйвером lib/pq для PostgreSQL:

func updateAccountBalance(db *sql.DB, accountID int, amount float64) error {
    tx, err := db.Begin()
    if err != nil {
        return err
    }

    // Явная блокировка строки для предотвращения изменения другими транзакциями
    var currentBalance float64
    err = tx.QueryRow("SELECT balance FROM accounts WHERE id = $1 FOR UPDATE", accountID).Scan(&currentBalance)
    if err != nil {
        tx.Rollback()
        return err
    }

    // Проверка бизнес-логики...
    if currentBalance < amount {
        tx.Rollback()
        return fmt.Errorf("недостаточно средств")
    }

    // Обновление данных
    _, err = tx.Exec("UPDATE accounts SET balance = balance - $1 WHERE id = $2", amount, accountID)
    if err != nil {
        tx.Rollback()
        return err
    }

    return tx.Commit()
}

FOR UPDATE блокирует строки на чтение, предотвращая их изменение другими транзакциями до завершения текущей, что эффективно устраняет риск Non-Repeatable Read для этих строк даже на уровне READ COMMITTED.

Заключение

Non-Repeatable Read — это допустимая и ожидаемая аномалия на уровне изоляции READ COMMITTED. Его основная задача — предотвратить только Dirty Reads, обеспечивая баланс между консистентностью и производительностью (меньше блокировок). Для приложений, где повторяемое чтение критично, необходимо либо повышать уровень изоляции транзакции, либо использовать дополнительные механизмы синхронизации (явные блокировки). Понимание этого поведения является ключевым для правильного проектирования надежных и консистентных систем, работающих с базами данных.