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

С какими аномалиями, возникающими при параллельных транзакциях, сталкивался

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

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

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

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

Аномалии при параллельных транзакциях в Go

При работе с параллельными транзакциями в Go, особенно при использовании баз данных или конкурентных структур данных, разработчики сталкиваются с классическими проблемами параллелизма, которые могут привести к некорсистентности данных. Вот основные аномалии, с которыми я сталкивался:

1. Потерянное обновление (Lost Update)

Возникает, когда две транзакции читают одну запись, затем независимо обновляют её, и второе обновление перезаписывает первое без учёта изменений первой транзакции.

// Пример опасного кода
var balance int64 = 100

func transferLostUpdate(amount int64, wg *sync.WaitGroup) {
    defer wg.Done()
    current := balance          // Чтение
    time.Sleep(time.Millisecond) // Имитация задержки
    balance = current + amount  // Обновление
}

func main() {
    var wg sync.WaitGroup
    wg.Add(2)
    go transferLostUpdate(50, &wg)  // Ожидаем 150
    go transferLostUpdate(30, &wg)  // Ожидаем 180
    wg.Wait()
    fmt.Println("Final balance:", balance) // Может быть 130 или 150 вместо 180!
}

2. Грязное чтение (Dirty Read)

Транзакция читает данные, которые были изменены другой транзакцией, но ещё не зафиксированы. Если вторая транзакция откатывается, первая работает с некорректными данными.

// Пример в контексте разделяемой памяти
type Account struct {
    mu    sync.RWMutex
    dirty bool
    value int
}

func dirtyReadExample() {
    acc := &Account{value: 100}
    
    go func() {
        acc.mu.Lock()
        acc.value = 200  // Нефиксированное изменение
        acc.dirty = true
        time.Sleep(100 * time.Millisecond)
        // Если здесь произойдёт откат...
        acc.value = 100
        acc.dirty = false
        acc.mu.Unlock()
    }()
    
    time.Sleep(50 * time.Millisecond)
    acc.mu.RLock()
    if acc.dirty {
        fmt.Println("Прочитали грязные данные:", acc.value) // 200!
    }
    acc.mu.RUnlock()
}

3. Неповторяющееся чтение (Non-repeatable Read)

Одна транзакция дважды читает одну запись, а между этими чтениями другая транзакция изменяет эту запись. Результаты двух чтений различаются.

func nonRepeatableReadExample(db *sql.DB) error {
    tx1, _ := db.Begin()
    tx2, _ := db.Begin()
    
    var value1, value2 int
    
    // Первое чтение в транзакции 1
    tx1.QueryRow("SELECT balance FROM accounts WHERE id=1").Scan(&value1)
    
    // Транзакция 2 изменяет данные
    tx2.Exec("UPDATE accounts SET balance = balance + 100 WHERE id=1")
    tx2.Commit()
    
    // Второе чтение в транзакции 1
    tx1.QueryRow("SELECT balance FROM accounts WHERE id=1").Scan(&value2)
    
    fmt.Printf("Разные значения: %d vs %d\n", value1, value2)
    return tx1.Commit()
}

4. Фантомное чтение (Phantom Read)

Транзакция повторно выполняет запрос, возвращающий набор строк по некоторому условию, и обнаруживает, что количество строк изменилось из-за другой транзакции.

func phantomReadExample(db *sql.DB) error {
    tx1, _ := db.Begin()
    tx2, _ := db.Begin()
    
    // Первый запрос
    rows1, _ := tx1.Query("SELECT COUNT(*) FROM users WHERE active=true")
    // Допустим, получили 5 записей
    
    // Другая транзакция добавляет запись
    tx2.Exec("INSERT INTO users (name, active) VALUES ('new_user', true)")
    tx2.Commit()
    
    // Повторный запрос
    rows2, _ := tx1.Query("SELECT COUNT(*) FROM users WHERE active=true")
    // Теперь получим 6 записей!
    
    return tx1.Commit()
}

5. Зацикливание (Deadlock)

Две или более транзакций блокируют друг друга, каждая ожидает ресурс, заблокированный другой транзакцией.

func deadlockExample(db *sql.DB) {
    go func() {
        tx, _ := db.Begin()
        tx.Exec("UPDATE accounts SET balance = balance - 100 WHERE id=1") // Блокировка строки 1
        time.Sleep(100 * time.Millisecond)
        tx.Exec("UPDATE accounts SET balance = balance + 100 WHERE id=2") // Ожидание блокировки строки 2
        tx.Commit()
    }()
    
    go func() {
        tx, _ := db.Begin()
        tx.Exec("UPDATE accounts SET balance = balance - 50 WHERE id=2")  // Блокировка строки 2
        time.Sleep(100 * time.Millisecond)
        tx.Exec("UPDATE accounts SET balance = balance + 50 WHERE id=1")  // Ожидание блокировки строки 1
        tx.Commit()
    }()
}

Методы борьбы с аномалиями в Go

Изоляция с помощью sync примитивов

  • sync.Mutex для исключительной блокировки
  • sync.RWMutex для разделения на чтение/запись
  • sync/atomic для атомарных операций

Уровни изоляции транзакций в СУБД

В Go при работе с базами данных важно правильно выбирать уровни изоляции:

// Установка уровня изоляции
db.SetIsolationLevel(sql.LevelSerializable)

// Или при начале транзакции
tx, err := db.BeginTx(ctx, &sql.TxOptions{
    Isolation: sql.LevelRepeatableRead,
    ReadOnly:  false,
})

Оптимистичный и пессимистичный контроль

  • Оптимистичный: Проверка версий/меток времени
  • Пессимистический: Блокировки на уровне СУБД
// Оптимистичный контроль версий
type Record struct {
    ID      int
    Value   string
    Version int  // Контрольная версия
}

func updateWithOptimisticLock(db *sql.DB, id int, newValue string) error {
    var currentVersion int
    err := db.QueryRow("SELECT version FROM records WHERE id=$1", id).Scan(&currentVersion)
    if err != nil {
        return err
    }
    
    result, err := db.Exec(
        "UPDATE records SET value=$1, version=version+1 WHERE id=$2 AND version=$3",
        newValue, id, currentVersion,
    )
    
    rows, _ := result.RowsAffected()
    if rows == 0 {
        return errors.New("конфликт версий, обновление отменено")
    }
    return nil
}

Паттерны в Go для избежания аномалий

  1. Принцип "Share memory by communicating" - использование каналов вместо разделяемой памяти
  2. Локальные копии данных для обработки
  3. Идемпотентные операции
  4. Компенсирующие транзакции (Saga-паттерн)

На практике в Go наиболее эффективно комбинировать механизмы изоляции базы данных с конкурентными примитивами языка, адаптируя решение под конкретные требования к консистентности и производительности. Особое внимание нужно уделять тестированию в конкурентных сценариях с использованием -race флага для детектирования гонок данных.