Как реализована транзакционность в Spring?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Транзакционность в Spring: @Transactional
Это один из самых важных механизмов Spring Framework. Аннотация @Transactional обеспечивает декларативное управление транзакциями ACID (Atomicity, Consistency, Isolation, Durability).
Что такое транзакция?
Транзакция — это набор операций, которые должны выполниться атомарно (либо все, либо ничего).
// БЕЗ транзакции — может сломаться:
1. Снять $100 со счёта A
2. (приложение упало здесь)
3. Добавить $100 на счёт B
// Результат: деньги исчезли!
// С транзакцией — гарантия ACID:
@Transactional
public void transferMoney(Long fromId, Long toId, BigDecimal amount) {
Account from = accountRepo.findById(fromId).get();
Account to = accountRepo.findById(toId).get();
from.withdraw(amount); // 1
to.deposit(amount); // 2
accountRepo.save(from);
accountRepo.save(to);
// Либо обе операции успешны, либо откатываются обе
}
Как Spring реализует @Transactional
1. AOP (Aspect-Oriented Programming) и прокси
Spring использует прокси для перехвата вызовов методов:
@Service
public class AccountService {
@Transactional
public void transfer(Long fromId, Long toId, BigDecimal amount) {
// бизнес-логика
}
}
// Spring создаёт прокси примерно так:
public class AccountServiceProxy extends AccountService {
private TransactionManager txManager;
@Override
public void transfer(Long fromId, Long toId, BigDecimal amount) {
// ДО метода:
Transaction tx = txManager.begin();
try {
// САМА ЛОГИКА:
super.transfer(fromId, toId, amount);
// ПОСЛЕ метода:
tx.commit(); // успех
} catch (Exception e) {
tx.rollback(); // откат
throw e;
}
}
}
Важно: Spring создаёт прокси только для public методов!
@Service
public class MyService {
@Transactional
public void publicMethod() { } // ✅ прокси создаёт перехват
@Transactional
private void privateMethod() { } // ❌ прокси НЕ работает!
@Transactional
protected void protectedMethod() { } // ⚠️ зависит от конфига
}
2. TransactionManager
Spring использует TransactionManager для управления транзакциями БД:
// В Spring Boot автоматически конфигурируется:
// для JPA: JpaTransactionManager
// для JDBC: DataSourceTransactionManager
// для Hibernate: HibernateTransactionManager
@Configuration
public class TransactionConfig {
@Bean
public PlatformTransactionManager transactionManager(EntityManagerFactory emf) {
return new JpaTransactionManager(emf);
}
}
3. Thread-Local: хранение текущей транзакции
Текущая транзакция хранится в ThreadLocal, чтобы все операции в этом потоке использовали одну транзакцию:
// Внутри Spring (упрощённо):
public class TransactionManager {
private static final ThreadLocal<Transaction> currentTx = new ThreadLocal<>();
public Transaction begin() {
Transaction tx = new Transaction();
currentTx.set(tx); // сохраняем в текущий поток
return tx;
}
public static Transaction getCurrentTransaction() {
return currentTx.get(); // все операции в потоке используют эту tx
}
}
// EntityManager использует текущую транзакцию:
public class EntityManager {
public void persist(Object entity) {
Transaction tx = TransactionManager.getCurrentTransaction();
tx.addEntity(entity); // добавляет в буфер текущей транзакции
}
}
Полный пример с объяснением
@Service
public class AccountService {
@Autowired
private AccountRepository accountRepository; // JPA
@Transactional
public void transfer(Long fromId, Long toId, BigDecimal amount) {
// 1. BEGIN TRANSACTION (Spring создал новую транзакцию)
Account from = accountRepository.findById(fromId).get();
Account to = accountRepository.findById(toId).get();
from.balance = from.balance.subtract(amount);
to.balance = to.balance.add(amount);
// Сохранение отправляет UPDATE в БД, но они в буфере транзакции
accountRepository.save(from);
accountRepository.save(to);
// Метод завершён успешно → COMMIT
// Все UPDATE выполняются атомарно
}
@Transactional
public void transferWithError(Long fromId, Long toId, BigDecimal amount) {
Account from = accountRepository.findById(fromId).get();
Account to = accountRepository.findById(toId).get();
from.balance = from.balance.subtract(amount);
to.balance = to.balance.add(amount);
accountRepository.save(from);
if (amount.compareTo(BigDecimal.valueOf(10000)) > 0) {
throw new IllegalArgumentException("Сумма слишком большая");
// ROLLBACK! Все UPDATE откатываются
// from и to вернут свои старые значения
}
accountRepository.save(to);
}
}
Параметры @Transactional
propagation (распространение)
Что делать при вложенных транзакциях:
@Transactional(propagation = Propagation.REQUIRED) // по умолчанию
public void parent() {
// Создаёт новую транзакцию
child(); // используется та же транзакция
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void child() {
// Создаёт НОВУЮ транзакцию, независимо от parent
// Если child откатится, parent НЕ откатится
}
@Transactional(propagation = Propagation.NESTED)
public void nested() {
// Создаёт savepoint в текущей транзакции
// Может откатиться независимо, но входит в parent
}
isolation (изоляция)
Какой уровень изоляции для транзакции:
@Transactional(isolation = Isolation.READ_COMMITTED) // по умолчанию
public void readData() { }
// Уровни (от слабого к сильному):
// READ_UNCOMMITTED - грязное чтение (быстро)
// READ_COMMITTED - нет грязного чтения
// REPEATABLE_READ - нет грязного + фантомного чтения
// SERIALIZABLE - полная изоляция (медленно)
timeout (таймаут)
@Transactional(timeout = 10) // секунды
public void longOperation() {
// Если выполняется > 10 сек, откатывается
}
readOnly (только чтение)
@Transactional(readOnly = true) // оптимизация для SELECT
public List<Account> getAccounts() {
return accountRepository.findAll();
}
rollbackFor (откат при исключении)
@Transactional(rollbackFor = Exception.class) // откат при любом Exception
public void riskyOperation() {
// По умолчанию откатывается только при RuntimeException
// Checked exceptions НЕ откатывают (нужно явно указать)
}
Частые ошибки
❌ Ошибка 1: вызов @Transactional из того же класса
@Service
public class Service {
public void publicMethod() {
privateTransactionalMethod(); // ❌ Прокси НЕ применится!
}
@Transactional
private void privateTransactionalMethod() {
// БЕЗ транзакции, потому что вызов напрямую
}
}
Решение: внедрить себя или использовать другой класс
@Service
public class Service {
@Autowired
private Service self; // самоинъекция (странно, но работает)
public void publicMethod() {
self.privateTransactionalMethod(); // ✅ Теперь через прокси
}
}
❌ Ошибка 2: не-checked исключения НЕ откатывают
@Transactional
public void transfer() {
account1.withdraw(100);
try {
account2.deposit(100);
} catch (Exception e) {
// Откат не произойдёт! Exception поймана
}
}
Решение: пробросить исключение
@Transactional
public void transfer() throws Exception {
account1.withdraw(100);
account2.deposit(100); // если исключение — откат
}
❌ Ошибка 3: Checked исключения НЕ откатывают
@Transactional
public void process() throws IOException { // Checked
// IOException НЕ откатит транзакцию!
}
@Transactional(rollbackFor = IOException.class) // ✅ явно указать
public void process() throws IOException {
// Теперь откатит
}
Как работает COMMIT/ROLLBACK
// 1. BEGIN — открыть соединение с БД
Connection conn = dataSource.getConnection();
conn.setAutoCommit(false); // отключить auto-commit
// 2. Выполнять SQL:
Statement stmt = conn.createStatement();
stmt.execute("UPDATE accounts SET balance = balance - 100 WHERE id = 1");
stmt.execute("UPDATE accounts SET balance = balance + 100 WHERE id = 2");
// Запросы в памяти, не выполнены в БД!
// 3. При успехе — COMMIT
try {
conn.commit(); // все запросы выполняются атомарно
} catch (Exception e) {
conn.rollback(); // откатить все запросы
throw e;
}
Итог
Spring реализует @Transactional через:
- AOP-прокси — перехватывает вызовы методов
- TransactionManager — управляет BEGIN/COMMIT/ROLLBACK
- ThreadLocal — хранит текущую транзакцию в потоке
- Декларативный подход — просто добавь @Transactional
Правила:
- Только public методы
- Откатывается только при RuntimeException
- Параметры: propagation, isolation, timeout, readOnly, rollbackFor