Программное управление¶
TransactionTemplate - транзакция в коде, без аннотации. Аннотации удобнее, программное управление точнее.
@Service
public class OrderService {
private final TransactionTemplate txTemplate;
public OrderService(PlatformTransactionManager txManager) {
this.txTemplate = new TransactionTemplate(txManager);
this.txTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_REPEATABLE_READ);
this.txTemplate.setTimeout(10);
}
public Order createOrder(OrderRequest req) {
return txTemplate.execute(status -> {
try {
Order order = orderRepo.save(new Order(req));
inventoryService.reserve(order);
return order;
} catch (InsufficientInventoryException e) {
status.setRollbackOnly(); // откат без проброса исключения из транзакции
throw e;
}
});
}
}
Когда лучше аннотаций:
- сложная ветвящаяся логика, параметры транзакции выбираются динамически
- нужна узкая граница вокруг маленькой критической секции
- нужно обойти ограничения прокси (self-invocation, утилитные классы)
- нужно явно показать транзакционный поток в коде
- тесты
В реактивном стеке то же самое делает TransactionalOperator.
Действия после коммита¶
TransactionSynchronization - хуки жизненного цикла текущей транзакции.
@Transactional
public void placeOrder(Order order) {
orderRepo.save(order);
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
eventPublisher.publish(new OrderCreatedEvent(order.getId())); // данные уже в БД
}
});
}
@TransactionalEventListener - то же декларативно (Spring 4.2+). По умолчанию слушатель срабатывает в фазе
AFTER_COMMIT.
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void onOrderCreated(OrderCreatedEvent event) {
notificationService.notify(event);
}
Полезно для отправки доменных событий и любой логики «реагируй на уже закоммиченное состояние». Это не распределённая транзакция: если приложение упадёт между коммитом и отправкой, событие потеряется. Надёжная доставка - через Outbox.
TransactionSynchronizationRegistry (JTA) - тот же приём в JTA-транзакции Jakarta EE. Пример из лабы: файл кладётся в MinIO перед коммитом БД, а при откате удаляется (см. Best-effort 2PC).
ChainedTransactionManager (deprecated, но встречается) - коммитит несколько менеджеров по очереди. Это не атомарно: если второй коммит упал, первый уже закоммичен. Best effort, а не распределённая транзакция.
@Bean
public PlatformTransactionManager chainedTxManager(DataSourceTransactionManager ds1TxManager,
DataSourceTransactionManager ds2TxManager) {
return new ChainedTransactionManager(ds1TxManager, ds2TxManager);
}
Маршрутизация DataSource¶
AbstractRoutingDataSource - выбирает DataSource на каждый запрос. Типично: запись на primary, чтение на реплику
по флагу readOnly.
public class ReadWriteRoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return TransactionSynchronizationManager.isCurrentTransactionReadOnly() ? "replica" : "primary";
}
}
@Bean
public DataSource routingDataSource(DataSource primaryDataSource, DataSource replicaDataSource) {
ReadWriteRoutingDataSource ds = new ReadWriteRoutingDataSource();
ds.setTargetDataSources(Map.of(
"primary", primaryDataSource,
"replica", replicaDataSource
));
ds.setDefaultTargetDataSource(primaryDataSource);
return ds;
}
Note
Чтобы ключ вычислялся уже с известным флагом readOnly, routing-DataSource оборачивают в
LazyConnectionDataSourceProxy: соединение берётся при первом запросе, а не в начале транзакции.
Транзакции и потоки¶
Одну imperative-транзакцию нельзя разделить между потоками: контекст в ThreadLocal текущего потока. Новый поток -
новый контекст.
@Transactional
public void processInParallel(List<Item> items) {
items.parallelStream().forEach(item -> processItem(item)); // НЕПРАВИЛЬНО: потоки без транзакции вызывающего
CompletableFuture.supplyAsync(() -> heavyComputation(item)); // НЕПРАВИЛЬНО: транзакция не наследуется
}
Правильно: у каждого параллельного обработчика своя транзакционная граница. Согласованность между задачами обеспечивают не общей транзакцией, а координацией: барьер, saga, конечный автомат, outbox, идемпотентное слияние.
Подход 1 - каждый поток вызывает транзакционный метод другого бина:
@Service
public class BatchService {
private final ItemProcessor itemProcessor; // отдельный бин - свой прокси - своя транзакция
public void processBatch(List<Item> items) {
List<CompletableFuture<Void>> futures = items.stream()
.map(item -> CompletableFuture.runAsync(() -> itemProcessor.processItem(item), taskExecutor))
.toList();
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
}
}
@Service
public class ItemProcessor {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void processItem(Item item) {
// каждый вызов в своей транзакции
}
}
Подход 2 - TransactionTemplate в рабочем потоке:
public void processBatch(List<Item> items) {
ExecutorService executor = Executors.newFixedThreadPool(4);
List<Future<?>> futures = items.stream()
.map(item -> executor.submit(() ->
txTemplate.execute(status -> { // транзакция создаётся в рабочем потоке
processItem(item);
return null;
})))
.toList();
for (Future<?> f : futures) {
try {
f.get();
} catch (ExecutionException e) { /* обработка ошибки задачи */ }
}
}
Подход 3 - @Async + @Transactional:
@Service
public class AsyncProcessor {
@Async("taskExecutor") // новый поток
@Transactional // транзакция в этом потоке
public CompletableFuture<Result> processAsync(Item item) {
return CompletableFuture.completedFuture(doWork(item));
}
}
Работает, только если оба прокси выстроены правильно. Надёжнее разнести @Async и @Transactional по разным бинам.
Virtual threads (Java 21+):
@Bean
public TaskExecutor taskExecutor() {
return new TaskExecutorAdapter(Executors.newVirtualThreadPerTaskExecutor());
}
ThreadLocalу виртуального потока свой, транзакционный контекст привязывается так же, как к обычному- до Java 24 блок
synchronizedвнутри JDBC-драйвера «пинил» carrier-поток. Драйвер PostgreSQL заменилsynchronizedнаReentrantLock, у других драйверов проверяй версию. С Java 24 (JEP 491)synchronizedбольше не пинит
Чего не делать:
- ждать, что
@Async«унаследует» транзакцию - держать долгую транзакцию и распараллеливать внутри неё операции с БД
- ходить в сеть, ждать брокер или долго считать внутри транзакции
Чем дольше живёт транзакция, тем больше ожидание блокировок, дедлоков, нагрузка на пул соединений, шанс отката и
хвостовые задержки. Особенно с REQUIRES_NEW, которому нужно второе соединение.
Итого: транзакция как можно короче, критическая секция как можно уже.