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

Как реализована транзакционность в Spring?

2.0 Middle🔥 211 комментариев
#Spring Framework

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

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

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

Транзакционность в 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 через:

  1. AOP-прокси — перехватывает вызовы методов
  2. TransactionManager — управляет BEGIN/COMMIT/ROLLBACK
  3. ThreadLocal — хранит текущую транзакцию в потоке
  4. Декларативный подход — просто добавь @Transactional

Правила:

  • Только public методы
  • Откатывается только при RuntimeException
  • Параметры: propagation, isolation, timeout, readOnly, rollbackFor
Как реализована транзакционность в Spring? | PrepBro