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

Что происходит с транзакциями в параллельном режиме

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

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

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

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

Транзакции в параллельном режиме (Concurrency Control)

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

ACID свойства транзакций

ACID - это четыре основных свойства, которые гарантируют надежность транзакций:

1. Atomicity (Атомарность)

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

@Transactional
public void transferMoney(Account from, Account to, BigDecimal amount) {
    from.withdraw(amount);      // Шаг 1
    to.deposit(amount);         // Шаг 2
    // Если что-то сломалось на шаге 2, оба шага откатываются
    // Деньги либо переведены полностью, либо вообще не переведены
}

Проблема без атомарности:

  • Деньги списались с счёта 1
  • Сломалось при добавлении на счёт 2
  • Результат: деньги исчезли!

2. Consistency (Согласованность)

Определение: База данных переходит из одного консистентного состояния в другое.

// Правило: сумма всех денег на счетах не должна измениться
BigDecimal totalBefore = accountRepository.sumAll();
transferMoney(account1, account2, amount);
BigDecimal totalAfter = accountRepository.sumAll();
assert totalBefore.equals(totalAfter); // Всегда true

Проблемы без консистентности:

  • Нарушение целостности данных
  • Деньги появляются или исчезают
  • Нарушение бизнес-правил

3. Isolation (Изоляция)

Определение: Транзакции не видят незафиксированные изменения друг друга.

Это самое сложное в параллельной работе!

4. Durability (Долговечность)

Определение: После успешного commit, данные сохранены несмотря на ошибки оборудования.

Уровни изоляции (Isolation Levels)

PostgreSQL, MySQL и другие СУБД поддерживают разные уровни изоляции. Это баланс между консистентностью и производительностью:

1. READ UNCOMMITTED (Грязное чтение)

Описание: Самый низкий уровень. Транзакция видит незафиксированные изменения других транзакций.

// Транзакция A
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public void processData() {
    Account account = accountRepository.findById(1);
    // Может прочитать незакоммиченные данные
    System.out.println(account.getBalance()); // Может быть неверно
}

Проблема: Транзакция A видит данные, которые транзакция B потом откатит.

Транзакция B: balance = 100 (не закоммичено)
Транзакция A: читает balance = 100
Транзакция B: ROLLBACK (вернулось к 50)
Транзакция A: использует неверное значение 100

2. READ COMMITTED (Чтение закоммиченных данных)

Описание: По умолчанию в большинстве СУБД. Видны только закоммиченные данные.

@Transactional(isolation = Isolation.READ_COMMITTED)
public void safeRead() {
    // Видит только закоммиченные данные
    Account account = accountRepository.findById(1);
    System.out.println(account.getBalance()); // Безопасно
}

Проблема - Phantom Read:

Транзакция A: SELECT * FROM accounts WHERE balance > 100
             Результат: 2 счета
Транзакция B: INSERT новый счет с balance = 150 и COMMIT
Транзакция A: SELECT * FROM accounts WHERE balance > 100
             Результат: 3 счета (phantom - призрак!)

3. REPEATABLE READ (Повторяемое чтение)

Описание: Гарантирует, что данные, прочитанные один раз, будут одинаковы при повторном чтении.

@Transactional(isolation = Isolation.REPEATABLE_READ)
public void consistentRead() {
    Account account = accountRepository.findById(1);
    BigDecimal balance1 = account.getBalance(); // 100
    
    // Другая транзакция меняет данные
    
    Account account2 = accountRepository.findById(1);
    BigDecimal balance2 = account2.getBalance(); // Тоже 100
    
    assert balance1.equals(balance2); // Гарантировано true
}

4. SERIALIZABLE (Сериализуемость)

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

@Transactional(isolation = Isolation.SERIALIZABLE)
public void completeIsolation() {
    // Полная изоляция, как при последовательном выполнении
    // Но производительность может упасть
}

Проблемы конкурентного доступа

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

// Транзакция A: читает незакоммиченные данные
Transaction A: SELECT balance FROM accounts WHERE id = 1; // 100
Transaction B: UPDATE accounts SET balance = 200; -- (не закоммичено)
Transaction A: видит balance = 200 (грязные данные!)
Transaction B: ROLLBACK (вернулось к 100)
// Транзакция A использует неверные данные

Решение: READ COMMITTED или выше

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

public void incrementCount() {
    // Транзакция A читает count = 10
    int count = getCount(); // 10
    
    // Транзакция B тоже читает count = 10
    // Транзакция B: count++ -> 11, коммит
    
    // Транзакция A: count++ -> 11, коммит
    // Результат: count = 11, хотя должен быть 12!
    
    setCount(count + 1);
}

Решение: Пессимистическая блокировка или оптимистическая с версией

@Entity
public class Counter {
    @Id
    private Long id;
    private Integer value;
    
    @Version  // Оптимистическая блокировка
    private Integer version;
}

@Transactional
public void incrementCount() {
    Counter counter = counterRepository.findById(1);
    counter.setValue(counter.getValue() + 1);
    // Если версия изменилась -> OptimisticLockingFailureException
}

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

Transaction A: SELECT balance FROM accounts; // 100
Transaction B: UPDATE accounts SET balance = 150; COMMIT;
Transaction A: SELECT balance FROM accounts; // 150 (разные значения!)

Решение: REPEATABLE READ или SERIALIZABLE

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

Transaction A: SELECT COUNT(*) FROM orders; // 5
Transaction B: INSERT INTO orders VALUES (...); COMMIT;
Transaction A: SELECT COUNT(*) FROM orders; // 6 (появился призрак!)

Решение: SERIALIZABLE

Практические подходы в Java

1. Пессимистическая блокировка

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

@Transactional
public void transferMoney(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));
    // Другие транзакции ждут разблокировки
}

2. Оптимистическая блокировка

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

@Transactional
public void transferMoney(Long fromId, Long toId, BigDecimal amount) {
    Account from = accountRepository.findById(fromId).get();
    Account to = accountRepository.findById(toId).get();
    
    from.setBalance(from.getBalance().subtract(amount));
    to.setBalance(to.getBalance().add(amount));
    
    try {
        accountRepository.saveAll(List.of(from, to));
    } catch (OptimisticLockingFailureException e) {
        // Версия изменилась, транзакция откатилась
        // Нужно повторить
    }
}

3. SELECT FOR UPDATE

@Transactional
public void criticalOperation() {
    // Заблокирует строку на уровне БД
    entityManager.createQuery(
        "SELECT a FROM Account a WHERE a.id = :id", 
        Account.class
    ).setParameter("id", 1)
     .setLockMode(LockModeType.PESSIMISTIC_WRITE)
     .getSingleResult();
}

Таблица уровней изоляции

УровеньDirty ReadNon-repeatablePhantomПроизводительность
READ_UNCOMMITTEDДаДаДаВысокая
READ_COMMITTEDНетДаДаХорошая
REPEATABLE_READНетНетДаСредняя
SERIALIZABLEНетНетНетНизкая

Лучшие практики

  1. Минимизируйте область транзакции - держите транзакции короткими
  2. Избегайте вложенных транзакций - они не пересекаются правильно
  3. Используйте нужный уровень изоляции - не переусложняйте
  4. Обрабатывайте оптимистические блокировки - повторяйте при конфликте
  5. Тестируйте параллелизм - race conditions трудно ловить
  6. Логируйте deadlocks - они признак проблем с дизайном

Заключение

В параллельном режиме транзакции должны соблюдать ACID свойства. Основной вызов - изоляция между одновременными операциями. Java разработчик должен понимать разные уровни изоляции и применять подходящий для своего случая (пессимистическую или оптимистическую блокировку), не переусложняя архитектуру приложения.

Что происходит с транзакциями в параллельном режиме | PrepBro