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

Что такое конкуренция между транзакциями?

2.0 Middle🔥 191 комментариев
#Базы данных и SQL#Многопоточность

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

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

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

Конкуренция между транзакциями (Transaction Concurrency)

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

Основной концепт

В распределенной системе несколько пользователей/приложений могут одновременно выполнять транзакции. Без правильного управления, это может привести к:

// Сценарий: Два пользователя одновременно переводят деньги

// Транзакция 1 (Пользователь А):
// SELECT balance FROM accounts WHERE id = 1;  // баланс = 1000
// UPDATE accounts SET balance = balance - 100 WHERE id = 1;

// Транзакция 2 (Пользователь Б) - одновременно
// SELECT balance FROM accounts WHERE id = 1;  // баланс = 1000
// UPDATE accounts SET balance = balance - 200 WHERE id = 1;

// Результат: баланс становится 800 вместо 700!

Проблемы конкуренции (Concurrency Issues)

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

Чтение незафиксированных изменений другой транзакции.

// Транзакция 1:
update accounts set balance = balance - 100 where id = 1;

// Транзакция 2 (одновременно):
select balance from accounts where id = 1;  // может прочитать незафиксированное значение!

// Если Транзакция 1 откатится - Транзакция 2 прочитает неправильное значение

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

Одна и та же строка, прочитанная дважды в одной транзакции, дает разные значения.

// Транзакция 1:
select balance from accounts where id = 1;  // 1000

// Транзакция 2 (одновременно):
update accounts set balance = 1200 where id = 1;
commit;

// Транзакция 1 (продолжение):
select balance from accounts where id = 1;  // 1200 - изменилось!

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

Новые строки, добавленные другой транзакцией, видны в результатах запроса.

// Транзакция 1:
select count(*) from accounts where balance > 1000;  // 5 строк

// Транзакция 2 (одновременно):
insert into accounts values (...);
commit;

// Транзакция 1 (повторный запрос):
select count(*) from accounts where balance > 1000;  // 6 строк - появилась "фантомная" строка

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

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

// Транзакция 1:
select balance from accounts where id = 1;  // 1000
update accounts set balance = 900 where id = 1;  // -100

// Транзакция 2 (одновременно):
select balance from accounts where id = 1;  // 1000
update accounts set balance = 800 where id = 1;  // -200

// Транзакция 2 коммитит первой → баланс = 800
// Транзакция 1 коммитит → баланс = 900 (перезаписала изменение Т2)
// Вместо ожидаемых 700, баланс = 900

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

Проблемы конкуренции решаются через Isolation Levels:

1. READ_UNCOMMITTED (0)

@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public void process() {
    // Может читать незафиксированные данные
    // Возможны все проблемы
}

2. READ_COMMITTED (1) — по умолчанию в PostgreSQL

@Transactional(isolation = Isolation.READ_COMMITTED)
public void process() {
    // Читает только зафиксированные данные
    // Защита от Dirty Read
    // Но возможны Non-Repeatable Read и Phantom Read
}

3. REPEATABLE_READ (2) — по умолчанию в MySQL

@Transactional(isolation = Isolation.REPEATABLE_READ)
public void process() {
    // Защита от Dirty Read и Non-Repeatable Read
    // Но возможны Phantom Read
}

4. SERIALIZABLE (3) — максимальная безопасность

@Transactional(isolation = Isolation.SERIALIZABLE)
public void process() {
    // Максимальная изоляция
    // Транзакции выполняются последовательно
    // Защита от всех проблем
    // Но очень медленно!
}

Матрица проблем и уровней изоляции

                 Dirty Read | Non-Rep Read | Phantom Read
READ UNCOMMITTED     +      |      +       |      +
READ COMMITTED       -      |      +       |      +
REPEATABLE READ      -      |      -       |      +
SERIALIZABLE         -      |      -       |      -

Практический пример на Spring Data

@Service
public class AccountService {
    @Autowired
    private AccountRepository accountRepository;
    
    // Безопасное снятие денег
    @Transactional(isolation = Isolation.SERIALIZABLE)
    public void withdraw(Long accountId, BigDecimal amount) {
        Account account = accountRepository.findById(accountId)
            .orElseThrow(() -> new AccountNotFoundException());
        
        if (account.getBalance().compareTo(amount) < 0) {
            throw new InsufficientFundsException();
        }
        
        account.setBalance(account.getBalance().subtract(amount));
        accountRepository.save(account);
    }
    
    // Пример с использованием явной блокировки
    @Transactional
    public void safeTransfer(Long fromId, Long toId, BigDecimal amount) {
        Account from = accountRepository.findByIdForUpdate(fromId);
        Account to = accountRepository.findByIdForUpdate(toId);
        
        from.setBalance(from.getBalance().subtract(amount));
        to.setBalance(to.getBalance().add(amount));
        
        accountRepository.save(from);
        accountRepository.save(to);
    }
}

@Repository
public interface AccountRepository extends JpaRepository<Account, Long> {
    // SELECT FOR UPDATE - явная блокировка строки
    @Query("SELECT a FROM Account a WHERE a.id = :id")
    @Lock(LockModeType.PESSIMISTIC_WRITE)
    Account findByIdForUpdate(@Param("id") Long id);
}

Стратегии управления конкуренцией

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

@Entity
public class Account {
    @Id
    private Long id;
    
    @Version  // Версия для обнаружения конфликтов
    private Long version;
    
    private BigDecimal balance;
}

// При обновлении проверяется версия
// Если версия изменилась - StaleObjectStateException

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

@Transactional
public void updateWithLock(Long id) {
    Account account = accountRepository.findByIdWithLock(id);
    // Строка заблокирована, другие транзакции ждут
    account.setBalance(account.getBalance().add(BigDecimal.TEN));
    accountRepository.save(account);
}

@Repository
public interface AccountRepository extends JpaRepository<Account, Long> {
    @Query("SELECT a FROM Account a WHERE a.id = ?1")
    @Lock(LockModeType.PESSIMISTIC_WRITE)
    Account findByIdWithLock(Long id);
}

3. MVCC (Multi-Version Concurrency Control)

Каждая транзакция работает со своей версией данных (PostgreSQL, MySQL использют MVCC).

Пример конкурентного тестирования

@Test
public void testConcurrency() throws InterruptedException {
    ExecutorService executor = Executors.newFixedThreadPool(10);
    
    for (int i = 0; i < 10; i++) {
        executor.submit(() -> {
            accountService.withdraw(1L, BigDecimal.TEN);
        });
    }
    
    executor.shutdown();
    executor.awaitTermination(10, TimeUnit.SECONDS);
    
    Account account = accountRepository.findById(1L).orElse(null);
    assertEquals(BigDecimal.valueOf(900), account.getBalance());
}

Итого

Конкуренция между транзакциями — это ситуация когда несколько транзакций одновременно работают с данными. Это может привести к Dirty Read, Non-Repeatable Read, Phantom Read и Lost Update. Решение: использовать правильный Isolation Level, явные блокировки (pessimistic/optimistic), или MVCC. Выбор стратегии зависит от требований приложения к производительности и консистентности данных.

Что такое конкуренция между транзакциями? | PrepBro