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

Какие знаешь проблемы параллельного выполнения транзакций в БД?

1.0 Junior🔥 151 комментариев
#Базы данных и SQL

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

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

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

Проблемы параллельного выполнения транзакций в БД

Когда несколько транзакций выполняются одновременно, возникают сложные проблемы, которые могут привести к потере данных и нарушению консистентности. Это ключевая область для Java разработчика, работающего с БД.

Основные проблемы (Anomalies)

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

Это когда одна транзакция читает незафиксированные изменения другой транзакции, которая потом откатывается.

Транзакция 1 (T1)        Транзакция 2 (T2)
─────────────────────────────────────────
BEGIN;
                         BEGIN;
UPDATE account 
  SET balance = 500      
  WHERE id = 1;
  
                         SELECT balance 
                         FROM account 
                         WHERE id = 1;  -- Читает 500
                         
ROLLBACK;                -- T1 откатывается!
                         -- Теперь у T2 есть значение 500,
                         -- которого никогда не было!

Проблема: T2 использует некорректное значение в дальнейших расчетах.

2. Non-Repeatable Read (Неповторяемое чтение)

Если транзакция два раза читает один и тот же ряд, она может получить разные значения.

Транзакция 1 (T1)        Транзакция 2 (T2)
─────────────────────────────────────────
BEGIN;
SELECT balance = 500
FROM account WHERE id=1;
                         BEGIN;
                         UPDATE account 
                         SET balance = 600 
                         WHERE id = 1;
                         COMMIT;
SELECT balance = 600     -- Другое значение!
FROM account WHERE id=1;
COMMIT;

Проблема: Один и тот же запрос в одной транзакции возвращает разные результаты.

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

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

Транзакция 1 (T1)        Транзакция 2 (T2)
─────────────────────────────────────────
BEGIN;
SELECT COUNT(*) = 5
FROM accounts 
WHERE balance > 1000;
                         BEGIN;
                         INSERT INTO accounts 
                         VALUES (6, New, 2000);
                         COMMIT;
SELECT COUNT(*) = 6      -- Фантомный ряд!
FROM accounts 
WHERE balance > 1000;
COMMIT;

Проблема: Запрос возвращает разные наборы рядов при выполнении в одной транзакции.

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

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

Транзакция 1 (T1)        Транзакция 2 (T2)
─────────────────────────────────────────
BEGIN;
SELECT balance = 1000
FROM account WHERE id=1;
                         BEGIN;
                         SELECT balance = 1000
                         FROM account WHERE id=1;
                         UPDATE account 
                         SET balance = 1000 - 50 = 950
                         WHERE id = 1;
                         COMMIT;
                         
UPDATE account 
SET balance = 1000 - 100 = 900  -- Перезписывает 950!
WHERE id = 1;
COMMIT;

Результат: T2 вычитал 50, но потом T1 вычитал только 100 с исходного значения, потеряв 50.

5. Write Skew (Конфликт записи)

Две транзакции читают одни и те же данные, но изменяют разные строки. При проверке инварианта возникает нарушение.

// Пример: в отделе должно быть минимум 1 разработчик
Транзакция 1: "Дан отпуск разработчику 1"
Транзакция 2: "Дан отпуск разработчику 2"

// Обе проверяют: COUNT(developers) > 1 ✓
// Обе обновляют разные строки
// Результат: COUNT(developers) = 0 ✗ (нарушение инварианта)

Уровни изоляции транзакций (Isolation Levels)

БД предоставляет несколько уровней изоляции для борьбы с этими проблемами:

READ UNCOMMITTED (уровень 0)

Самый низкий уровень - разрешены все проблемы.

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
BEGIN;
  SELECT * FROM accounts; -- Может прочитать грязные данные
COMMIT;

READ COMMITTED (уровень 1)

Предотвращает Dirty Read, но разрешены Non-Repeatable Read и Phantom Read.

// Spring/Hibernate:
@Transactional(isolation = Isolation.READ_COMMITTED)
public void transferMoney(Long fromId, Long toId, BigDecimal amount) {
  // Защита от грязного чтения, но не от других проблем
}

REPEATABLE READ (уровень 2)

Предотвращает Dirty Read и Non-Repeatable Read, но разрешены Phantom Read и Write Skew.

@Transactional(isolation = Isolation.REPEATABLE_READ)
public void checkAccountConsistency() {
  // Каждый SELECT вернет одно и то же значение
  // Но новые строки могут появиться
}

SERIALIZABLE (уровень 3)

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

@Transactional(isolation = Isolation.SERIALIZABLE)
public void criticalOperation() {
  // Максимальная безопасность, но пониженная производительность
}

Решения и стратегии

Пессимистическое блокирование (Pessimistic Locking)

SELECT FOR UPDATE блокирует строку для других транзакций:

@Repository
public interface AccountRepository extends JpaRepository<Account, Long> {
  @Query("SELECT a FROM Account a WHERE a.id = :id FOR UPDATE")
  Account findByIdWithLock(@Param("id") Long id);
}

@Service
public class TransferService {
  public void transfer(Long fromId, Long toId, BigDecimal amount) {
    // Блокирует обе строки
    Account from = accountRepository.findByIdWithLock(fromId);
    Account to = accountRepository.findByIdWithLock(toId);
    
    from.setBalance(from.getBalance().subtract(amount));
    to.setBalance(to.getBalance().add(amount));
    // COMMIT - другие транзакции могут начать работу
  }
}

Оптимистическое блокирование (Optimistic Locking)

Используется версионирование для обнаружения конфликтов:

@Entity
public class Account {
  @Id
  private Long id;
  private BigDecimal balance;
  
  @Version  // Автоматически увеличивается при UPDATE
  private Long version;
}

@Service
public class TransferService {
  public void transfer(Long fromId, Long toId, BigDecimal amount) {
    Account from = accountRepository.findById(fromId).orElse(null);
    Account to = accountRepository.findById(toId).orElse(null);
    
    from.setBalance(from.getBalance().subtract(amount));
    to.setBalance(to.getBalance().add(amount));
    
    try {
      accountRepository.save(from);
      accountRepository.save(to);
    } catch (OptimisticLockingFailureException e) {
      // Перезапросить и повторить
      transfer(fromId, toId, amount);
    }
  }
}

Использование правильного уровня изоляции

@Transactional(isolation = Isolation.REPEATABLE_READ)
public void criticalTransfer() {
  // Для финансовых операций минимум REPEATABLE READ
}

Серийная обработка (Batching)

Вместо одновременных небольших обновлений, группировать их:

@Transactional
public void batchTransfer(List<TransferRequest> transfers) {
  for (TransferRequest transfer : transfers) {
    Account from = accountRepository.findByIdWithLock(transfer.getFromId());
    Account to = accountRepository.findByIdWithLock(transfer.getToId());
    // Обновить
  }
  // Один COMMIT для всех
}

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

  • Для финансовых операций: SERIALIZABLE или пессимистическое блокирование
  • Для обычных CRUD: READ_COMMITTED достаточно
  • Для аналитики: Используй отдельные read-only реплики
  • Мониторь deadlocks: PostgreSQL будет выкидывать исключение, перехвати и перезапроси
  • Минимизируй время транзакции: Меньше вероятность конфликтов

Понимание этих проблем критично для написания надежного production кода!

Какие знаешь проблемы параллельного выполнения транзакций в БД? | PrepBro