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

При каких вариантах изоляции могут возникнуть аномалия Non-Repeatable Read

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

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

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

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

Аномалия Non-Repeatable Read и уровни изоляции в Go

Non-Repeatable Read — это аномалия параллельного доступа к данным в системах управления базами данных (СУБД), при которой повторное чтение одних и тех же данных в рамках одной транзакции возвращает разные результаты из-за изменений, внесенных другими параллельными транзакциями.

Суть аномалии

В Go-разработке при работе с базами данных (например, через database/sql и драйверы PostgreSQL/MySQL) эта проблема особенно актуальна. Рассмотрим классический сценарий:

// Пример в Go, демонстрирующий риск Non-Repeatable Read
func transferCheck(tx *sql.Tx, accountID int) error {
    var balance1, balance2 float64
    
    // Первое чтение баланса
    err := tx.QueryRow("SELECT balance FROM accounts WHERE id = $1", accountID).Scan(&balance1)
    if err != nil {
        return err
    }
    
    // Какая-то логика между чтениями...
    time.Sleep(100 * time.Millisecond)
    
    // Второе чтение ТОГО ЖЕ баланса
    err = tx.QueryRow("SELECT balance FROM accounts WHERE id = $1", accountID).Scan(&balance2)
    if err != nil {
        return err
    }
    
    // balance1 != balance2 - аномалия Non-Repeatable Read!
    if balance1 != balance2 {
        return fmt.Errorf("баланс изменился между чтениями: было %.2f, стало %.2f", balance1, balance2)
    }
    return nil
}

Уровни изоляции, допускающие Non-Repeatable Read

1. Read Uncommitted (Чтение незафиксированных данных)

  • Наиболее слабый уровень изоляции
  • Позволяет все аномалии: dirty reads, non-repeatable reads, phantom reads
  • В Go при использовании database/sql устанавливается через:
// Для PostgreSQL
_, err := db.Exec("SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED")
// Для MySQL/SQLite аналогично
  • Non-Repeatable Read гарантированно возможна, так как транзакция видит незафиксированные изменения других транзакций

2. Read Committed (Чтение зафиксированных данных)

  • Уровень по умолчанию в PostgreSQL и многих других СУБД
  • Запрещает Dirty Reads, но разрешает Non-Repeatable Reads
  • В Go для явного установления:
// Начало транзакции с уровнем изоляции
tx, err := db.BeginTx(ctx, &sql.TxOptions{
    Isolation: sql.LevelReadCommitted,
})
if err != nil {
    log.Fatal(err)
}
  • Механизм возникновения:
    1. Транзакция A читает строку
    2. Транзакция B изменяет и фиксирует эту строку
    3. Транзакция A повторно читает ту же строку и видит новые данные

Практический пример в Go с PostgreSQL

func demonstrateNonRepeatableRead(db *sql.DB) error {
    ctx := context.Background()
    
    // Транзакция 1: два последовательных чтения
    go func() {
        tx, _ := db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelReadCommitted})
        defer tx.Rollback()
        
        var balance1, balance2 float64
        tx.QueryRow("SELECT balance FROM accounts WHERE id = 1").Scan(&balance1)
        
        // Пауза, дающая время другой транзакции изменить данные
        time.Sleep(200 * time.Millisecond)
        
        tx.QueryRow("SELECT balance FROM accounts WHERE id = 1").Scan(&balance2)
        
        if balance1 != balance2 {
            fmt.Printf("Non-Repeatable Read обнаружена: %.2f -> %.2f\n", balance1, balance2)
        }
    }()
    
    // Транзакция 2: изменение данных между чтениями транзакции 1
    time.Sleep(100 * time.Millisecond)
    
    go func() {
        tx, _ := db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelReadCommitted})
        defer tx.Rollback()
        
        _, err := tx.Exec("UPDATE accounts SET balance = balance + 100 WHERE id = 1")
        if err != nil {
            return
        }
        tx.Commit() // Фиксация изменений
    }()
    
    return nil
}

Уровни изоляции, защищающие от Non-Repeatable Read

Repeatable Read (Повторяемое чтение)

  • Гарантирует, что данные, прочитанные один раз, не изменятся при повторном чтении
  • В Go:
tx, err := db.BeginTx(ctx, &sql.TxOptions{
    Isolation: sql.LevelRepeatableRead,
})

Serializable (Сериализуемый)

  • Самый строгий уровень, полностью предотвращающий все аномалии
  • В Go:
tx, err := db.BeginTx(ctx, &sql.TxOptions{
    Isolation: sql.LevelSerializable,
})

Особенности в популярных СУБД для Go-разработчика

  1. PostgreSQL:

    • Read Committed — уровень по умолчанию, допускает Non-Repeatable Read
    • Repeatable Read предотвращает Non-Repeatable Read через механизм snapshot isolation
  2. MySQL/InnoDB:

    • Repeatable Read — уровень по умолчанию
    • При Read Committed аномалия возможна
  3. SQLite:

    • По умолчанию использует Serializable, но может работать в режиме Read Uncommitted при указании PRAGMA read_uncommitted = 1

Рекомендации для Go-разработчиков

  • Явно указывайте уровень изоляции при начале транзакции через BeginTx()
  • Понимайте семантику по умолчанию для вашей целевой СУБД
  • Используйте Repeatable Read или Serializable, когда логика приложения требует консистентности данных между несколькими чтениями
  • Учитывайте компромисс: более строгие уровни изоляции уменьшают параллелизм и могут привести к deadlock'ам
// Правильный подход: выбор адекватного уровня изоляции
func updateAccountBalance(db *sql.DB, fromID, toID int, amount float64) error {
    ctx := context.Background()
    
    // Используем Repeatable Read, чтобы избежать Non-Repeatable Read при проверках
    tx, err := db.BeginTx(ctx, &sql.TxOptions{
        Isolation: sql.LevelRepeatableRead,
    })
    if err != nil {
        return err
    }
    defer tx.Rollback()
    
    // Многократные чтения баланса будут консистентны
    var balance float64
    err = tx.QueryRow("SELECT balance FROM accounts WHERE id = $1", fromID).Scan(&balance)
    if err != nil {
        return err
    }
    
    if balance < amount {
        return fmt.Errorf("недостаточно средств")
    }
    
    // ... дальнейшая логика с гарантией консистентности данных
    return tx.Commit()
}

В заключение, Non-Repeatable Read возникает на уровнях изоляции Read Uncommitted и Read Committed, что важно учитывать при проектировании конкурентных приложений на Go, где целостность данных между несколькими операциями чтения критична для бизнес-логики.