Что происходит с транзакциями в параллельном режиме
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Транзакции в параллельном режиме (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 Read | Non-repeatable | Phantom | Производительность |
|---|---|---|---|---|
| READ_UNCOMMITTED | Да | Да | Да | Высокая |
| READ_COMMITTED | Нет | Да | Да | Хорошая |
| REPEATABLE_READ | Нет | Нет | Да | Средняя |
| SERIALIZABLE | Нет | Нет | Нет | Низкая |
Лучшие практики
- Минимизируйте область транзакции - держите транзакции короткими
- Избегайте вложенных транзакций - они не пересекаются правильно
- Используйте нужный уровень изоляции - не переусложняйте
- Обрабатывайте оптимистические блокировки - повторяйте при конфликте
- Тестируйте параллелизм - race conditions трудно ловить
- Логируйте deadlocks - они признак проблем с дизайном
Заключение
В параллельном режиме транзакции должны соблюдать ACID свойства. Основной вызов - изоляция между одновременными операциями. Java разработчик должен понимать разные уровни изоляции и применять подходящий для своего случая (пессимистическую или оптимистическую блокировку), не переусложняя архитектуру приложения.