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

Что такое потерянное обновление в БД?

2.0 Middle🔥 241 комментариев
#Базы данных и SQL

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

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

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

Потерянное обновление в БД

Потерянное обновление (Lost Update) — это проблема конкурентности, которая возникает, когда два или более транзакций одновременно модифицируют один и тот же объект данных, и изменения одной транзакции перезаписывают изменения другой, как будто второе изменение никогда не происходило.

Простой пример

Представьте, что есть банковский счёт с балансом 1000 долларов. Два пользователя одновременно пополняют счёт на 500 долларов каждый. Ожидаемый результат: 2000 долларов. Но при потерянном обновлении результат может быть всего 1500 долларов.

Как это происходит

Время | Транзакция 1 (User A) | Транзакция 2 (User B) | Баланс в БД
------|----------------------|----------------------|-------------
T1    | Прочитать баланс     |                      | 1000
T2    |                      | Прочитать баланс     | 1000
T3    | Добавить 500         |                      | 
T4    |                      | Добавить 500         | 
T5    | Записать: 1500       |                      | 1500
T6    |                      | Записать: 1500       | 1500 (ПОТЕРЯ!)

Обе транзакции прочитали исходное значение 1000, каждая добавила 500, но последняя запись перезаписала первую, и в итоге вместо 2000 получилось 1500.

Реальный код на Java

Уязвимый подход:

public class BankAccount {
    private int balance;
    
    public void deposit(int amount) {
        // Проблема: три отдельных операции
        int currentBalance = balance;           // T1: Чтение
        currentBalance = currentBalance + amount; // T2: Вычисление
        balance = currentBalance;               // T3: Запись
    }
}

// Использование
BankAccount account = new BankAccount(1000);

Thread t1 = new Thread(() -> account.deposit(500));
Thread t2 = new Thread(() -> account.deposit(500));

t1.start();
t2.start();
t1.join();
t2.join();

System.out.println(account.balance); // Может быть 1500 вместо 2000!

Решение 1: Синхронизация (Synchronized)

public class SynchronizedBankAccount {
    private int balance;
    
    public synchronized void deposit(int amount) {
        int currentBalance = balance;
        currentBalance = currentBalance + amount;
        balance = currentBalance;
    }
    
    public synchronized int getBalance() {
        return balance;
    }
}

Здесь synchronized гарантирует, что только один поток может выполнять этот метод одновременно. Второй поток будет ждать, пока первый не закончит.

Решение 2: AtomicInteger

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicBankAccount {
    private AtomicInteger balance = new AtomicInteger(0);
    
    public void deposit(int amount) {
        balance.addAndGet(amount);  // Атомарная операция
    }
    
    public int getBalance() {
        return balance.get();
    }
}

AtomicInteger использует lock-free алгоритмы для безопасной конкурентной работы без блокировки.

Решение 3: ReentrantLock

import java.util.concurrent.locks.ReentrantLock;

public class LockBankAccount {
    private int balance;
    private ReentrantLock lock = new ReentrantLock();
    
    public void deposit(int amount) {
        lock.lock();
        try {
            int currentBalance = balance;
            currentBalance = currentBalance + amount;
            balance = currentBalance;
        } finally {
            lock.unlock();  // Гарантированное освобождение
        }
    }
}

Решение 4: На уровне БД (SELECT FOR UPDATE)

public class DatabaseBankAccount {
    public void deposit(Long accountId, int amount) throws SQLException {
        try (Connection conn = getConnection();
             Statement stmt = conn.createStatement()) {
            
            // Заблокировать строку в БД на время транзакции
            stmt.executeUpdate(
                "SELECT * FROM accounts WHERE id = " + accountId + " FOR UPDATE"
            );
            
            // Теперь безопасно обновить
            stmt.executeUpdate(
                "UPDATE accounts SET balance = balance + " + amount + 
                " WHERE id = " + accountId
            );
            
            conn.commit();
        }
    }
}

Решение 5: Уровень изоляции транзакций

// SQL
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN TRANSACTION;

SELECT balance FROM accounts WHERE id = 1;
UPDATE accounts SET balance = balance + 500 WHERE id = 1;

COMMIT;

Уровни изоляции:

  • READ UNCOMMITTED — нет защиты от потерянного обновления
  • READ COMMITTED — защита от потерянного обновления благодаря блокировкам
  • REPEATABLE READ — более строгая защита
  • SERIALIZABLE — максимальная защита, но может влиять на производительность

Сравнение решений

РешениеСложностьПроизводительностьОбласть применения
synchronizedНизкаяСредняяПростые случаи
AtomicIntegerНизкаяВысокаяЧисловые счётчики
ReentrantLockСредняяСредняяСложная логика
SELECT FOR UPDATEСредняяНизкаяКритичные операции
Уровень изоляцииСредняяВарьируетсяВсе БД операции

Практическая рекомендация

Для банковского приложения лучший подход — это использование SELECT FOR UPDATE на уровне БД в сочетании с уровнем изоляции SERIALIZABLE:

public void transferMoney(Long fromAccount, Long toAccount, BigDecimal amount) {
    jdbcTemplate.update(sql, params);
    // Операция выполняется как одна атомарная единица
}

Потерянное обновление — критическая проблема при разработке многопользовательских систем, особенно с финансовыми данными. Её решение требует понимания как механизмов синхронизации в Java, так и транзакционных моделей в БД.

Что такое потерянное обновление в БД? | PrepBro