Что такое конкуренция между транзакциями?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Конкуренция между транзакциями (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. Выбор стратегии зависит от требований приложения к производительности и консистентности данных.