Представьте ситуацию: вам нужно реализовать механизм перевода денег между двумя банковскими счетами. Очевидно, что операция состоит из двух шагов:
- Списание средств с одного счёта.
- Зачисление средств на другой счёт.
Если в процессе что-то пошло не так, например, сбой на втором шаге, важно откатить всю операцию, чтобы избежать ситуации, когда деньги "исчезли". Здесь в игру вступает аннотация @Transactional, позволяя вам сосредоточиться на бизнес-логике, а сам Spring берет на себя управление транзакцией, делая код чище и проще.
Как работает @Transactional?
@Transactional — это аннотация, которая задаёт поведение транзакции для метода или класса. Spring использует аспектно-ориентированное программирование (AOP), чтобы обернуть ваш код в "транзакционные прокси". Это значит, что когда метод запускается, Spring автоматически начинает транзакцию, а по завершении метода — либо фиксирует изменения (commit), либо откатывает их (rollback), если произошло исключение.
Вот пример использования @Transactional для операции перевода денег между счетами:
@Service
public class BankService {
@Transactional
public void transferMoney(Long fromAccountId, Long toAccountId, BigDecimal amount) {
// Шаг 1: Списание средств с одного счёта
accountRepository.debit(fromAccountId, amount);
// Шаг 2: Зачисление средств на другой счёт
accountRepository.credit(toAccountId, amount);
}
}
Просто? Но это только верхушка айсберга.
Опции аннотации @Transactional
Аннотация @Transactional предоставляет множество опций для управления поведением транзакций. Давайте разберём самые важные.
1. Propagation (распространение)
Это указывает, как должна быть выполнена транзакция, если метод вызывается внутри другой транзакции. Например:
REQUIRED(по умолчанию): Использует существующую транзакцию, если она есть. Если нет, создаёт новую.REQUIRES_NEW: всегда создаёт новую транзакцию, приостанавливая текущую.MANDATORY: требует существующей транзакции. Если её нет – выбросит исключение.SUPPORTS: работает в рамках транзакции, если она есть, но может быть вызван и без неё.
Пример использования:
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveAuditLog(AuditLog log) {
auditLogRepository.save(log);
}
2.Isolation (уровень изоляции)
Указывает, как транзакция должна видеть изменения, сделанные другими транзакциями. Это важно для работы в многопользовательских системах.
Вот основные уровни изоляции:
READ_UNCOMMITTED: транзакция может видеть неприменённые изменения других транзакций (грязное чтение).READ_COMMITTED: исключает грязное чтение.REPEATABLE_READ: защита от немодифицированных чтений (consistent reads).SERIALIZABLE: полная изоляция, максимальная защита от конфликтов, но с падением производительности.
Пример:
@Transactional(isolation = Isolation.SERIALIZABLE)
public void performSensitiveOperation() {
// Критично важная операция
}
3. Timeout (время выполнения)
Иногда транзакции могут заблокироваться или работать слишком долго. Вы можете задать лимит времени (в секундах), после которого транзакция будет прервана.
Пример:
@Transactional(timeout = 5) // Максимум 5 секунд
public void executeLongQuery() {
// Долгая операция
}
4. ReadOnly (только для чтения)
Если транзакция только читает данные (без изменений), вы можете указать readOnly=true. Это может оптимизировать производительность.
Пример:
@Transactional(readOnly = true)
public List<Account> getAllAccounts() {
return accountRepository.findAll();
}
5. RollbackFor (откат транзакции при ошибке)
По умолчанию Spring откатывает транзакцию только в случае RuntimeException или Error. Если вы хотите откатывать транзакцию при других исключениях (например, Exception), нужно указать это явно:
@Transactional(rollbackFor = Exception.class)
public void processTransaction() throws Exception {
// Код, который может выбросить исключение
}
Реальные примеры использования
Пример 1: Обработка заказов в интернет-магазине
Представьте обработку заказа, которая включает:
- Вычитание товара со склада
- Списание средств с карты клиента
- Запись данных о заказе
@Service
public class OrderService {
@Transactional
public void processOrder(Order order) {
// Проверка наличия товара
stockService.decreaseStock(order.getProductId(), order.getQuantity());
// Списание средств
paymentService.charge(order.getPaymentInfo());
// Сохранение заказа
orderRepository.save(order);
}
}
Если на любом этапе произойдёт ошибка, все изменения откатятся.
Пример 2: Журналирование ошибок
Иногда требуется сохранять журнал событий вне основной транзакции. Используем REQUIRES_NEW:
@Service
public class LogService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logError(String message) {
errorLogRepository.save(new ErrorLog(message));
}
}
Типичные ошибки при работе с @Transactional
- Аннотация не сработала. Это может произойти, если метод вызван из того же класса (сквозь
this). Помните, что Spring применяет прокси, и вызов изнутри одного класса обходит механизм AOP. Решение? Вынесите метод в другой@Service. - Совмещение внешних и внутренних транзакций. Например, использование
@Transactional(propagation = Propagation.REQUIRES_NEW)внутри метода может создавать накладные расходы. Используйте только при необходимости. - Неправильное поведение rollback. Если вы забыли указать
rollbackFor, Spring может не откатить транзакцию. Всегда явно указывайте нужное поведение.
Практика: Внедрение @Transactional в приложение
Создадим метод для обработки заказа с автоматическим откатом транзакции в случае ошибки:
@Service
public class ProductService {
@Autowired
private ProductRepository productRepository;
@Autowired
private OrderRepository orderRepository;
@Transactional
public void placeOrder(Long productId, int quantity) {
// Уменьшаем количество товара
Product product = productRepository.findById(productId)
.orElseThrow(() -> new RuntimeException("Продукт не найден!"));
if (product.getStock() < quantity) {
throw new RuntimeException("Не хватает товара на складе!");
}
product.setStock(product.getStock() - quantity);
productRepository.save(product);
// Сохраняем заказ
Order order = new Order();
order.setProductId(productId);
order.setQuantity(quantity);
orderRepository.save(order);
// Искусственный сбой для проверки rollback
if (quantity > 10) {
throw new RuntimeException("Сбой при обработке заказа!");
}
}
}
Проверка:
- Вызов метода
placeOrderс корректными данными — создаст заказ и уменьшит количество товара. - Запуск с большим количеством (
quantity > 10) — вызовет сбой. Spring откатит изменения, оставив данные консистентными.
@Transactional — мощный инструмент, снимающий с нас головную боль управления транзакциями вручную. Используйте её ответственно, оптимизируйте параметры и всегда проверяйте работу через тесты!
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ