При каких вариантах изоляции могут возникнуть аномалия Non-Repeatable Read
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Аномалия 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)
}
- Механизм возникновения:
- Транзакция A читает строку
- Транзакция B изменяет и фиксирует эту строку
- Транзакция 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-разработчика
-
PostgreSQL:
Read Committed— уровень по умолчанию, допускает Non-Repeatable ReadRepeatable Readпредотвращает Non-Repeatable Read через механизм snapshot isolation
-
MySQL/InnoDB:
Repeatable Read— уровень по умолчанию- При
Read Committedаномалия возможна
-
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, где целостность данных между несколькими операциями чтения критична для бизнес-логики.