Ответ
Non-Repeatable Read (неповторяющееся чтение) возникает, когда транзакция повторно читает те же данные и обнаруживает, что они были изменены (и изменения закоммичены) другой параллельной транзакцией между двумя чтениями.
Уровни изоляции, где это возможно:
- Read Uncommitted (чтение незафиксированных данных): На этом уровне транзакция может видеть изменения, сделанные другими транзакциями, даже если они ещё не закоммичены (что также приводит к Dirty Read). Если другая транзакция закоммитит изменения между чтениями, это вызовет Non-Repeatable Read.
- Read Committed (чтение зафиксированных данных): Это уровень изоляции по умолчанию во многих СУБД (например, PostgreSQL, SQL Server, Oracle). Транзакция видит только закоммиченные изменения. Однако, если другая транзакция закоммитит изменения после первого чтения, но до второго чтения текущей транзакции, возникнет Non-Repeatable Read.
Пример на Go с использованием SQL:
Предположим, у нас есть таблица accounts с полем balance.
package main
import (
"database/sql"
"fmt"
"log"
"time"
_ "github.com/lib/pq" // Пример для PostgreSQL
)
func main() {
// Предполагается, что у вас есть запущенный PostgreSQL и база данных
// db, err := sql.Open("postgres", "user=postgres password=postgres dbname=test_db sslmode=disable")
// if err != nil {
// log.Fatal(err)
// }
// defer db.Close()
// // Инициализация данных
// db.Exec("DROP TABLE IF EXISTS accounts")
// db.Exec("CREATE TABLE accounts (id INT PRIMARY KEY, balance INT)")
// db.Exec("INSERT INTO accounts (id, balance) VALUES (1, 100)")
// Имитация работы с базой данных
fmt.Println("Начальный баланс: 100")
// Транзакция 1 (читающая)
go func() {
// tx1, err := db.BeginTx(context.Background(), &sql.TxOptions{Isolation: sql.LevelReadCommitted})
// if err != nil { log.Fatal(err) }
fmt.Println("nТранзакция 1: Начинается")
var balance1 int
// err = tx1.QueryRow("SELECT balance FROM accounts WHERE id = 1").Scan(&balance1)
// if err != nil { log.Fatal(err) }
balance1 = 100 // Имитация первого чтения
fmt.Printf("Транзакция 1: Первое чтение баланса: %dn", balance1)
time.Sleep(50 * time.Millisecond) // Даем время для выполнения Транзакции 2
var balance2 int
// err = tx1.QueryRow("SELECT balance FROM accounts WHERE id = 1").Scan(&balance2)
// if err != nil { log.Fatal(err) }
balance2 = 200 // Имитация второго чтения после изменения другой транзакцией
fmt.Printf("Транзакция 1: Второе чтение баланса: %dn", balance2)
if balance1 != balance2 {
fmt.Println("Транзакция 1: Обнаружена аномалия Non-Repeatable Read!")
}
// tx1.Commit()
fmt.Println("Транзакция 1: Завершена")
}()
// Транзакция 2 (изменяющая)
go func() {
time.Sleep(20 * time.Millisecond) // Запускаем чуть позже
// tx2, err := db.BeginTx(context.Background(), &sql.TxOptions{Isolation: sql.LevelReadCommitted})
// if err != nil { log.Fatal(err) }
fmt.Println("Транзакция 2: Начинается")
// _, err = tx2.Exec("UPDATE accounts SET balance = 200 WHERE id = 1")
// if err != nil { log.Fatal(err) }
// tx2.Commit()
fmt.Println("Транзакция 2: Баланс обновлен до 200 и закоммичен")
fmt.Println("Транзакция 2: Завершена")
}()
time.Sleep(100 * time.Millisecond) // Ждем завершения горутин
}Решение:
Для предотвращения Non-Repeatable Read необходимо использовать более высокие уровни изоляции:
- Repeatable Read (повторяемое чтение): На этом уровне транзакция видит только те данные, которые были зафиксированы до её начала. Все последующие чтения в рамках той же транзакции будут возвращать те же данные, что и первое чтение, даже если другие транзакции их изменили и закоммитили. Однако на этом уровне всё ещё возможна аномалия Phantom Read.
- Serializable (сериализуемый): Самый строгий уровень изоляции. Гарантирует, что параллельное выполнение транзакций будет эквивалентно некоторому последовательному выполнению. Предотвращает все аномалии, включая Non-Repeatable Read и Phantom Read.