Разпространение и изолиране на транзакции през пролетта @Transactional

1. Въведение

В този урок ще разгледаме анотацията @Transactional и нейните настройки за изолиране и разпространение .

2. Какво е @Transactional?

Можем да използваме @Transactional, за да обгърнем метод в транзакция на база данни.

Позволява ни да зададем условия за разпространение, изолиране, време за изчакване, само за четене и възстановяване за нашата транзакция. Също така можем да посочим мениджъра на транзакциите.

2.1. @ Транзакционни подробности за изпълнение

Spring създава прокси или манипулира байт-кода на класа, за да управлява създаването, фиксирането и връщането на транзакцията. В случай на прокси, Spring игнорира @Transactional във вътрешни извиквания на методи.

Просто казано, ако имаме метод като callMethod и го маркираме като @Transactional, Spring ще обгърне някакъв код за управление на транзакции около извикването: @Transactional метод, наречен:

createTransactionIfNecessary(); try { callMethod(); commitTransactionAfterReturning(); } catch (exception) { completeTransactionAfterThrowing(); throw exception; }

2.2. Как да използвам @Transactional

Можем да поставим анотацията върху дефиниции на интерфейси, класове или директно върху методи. Те се отменят взаимно според приоритетния ред; от най-ниското до най-високото имаме: Интерфейс, суперклас, клас, метод на интерфейс, метод на суперклас и метод на клас.

Spring прилага анотацията на ниво клас към всички публични методи от този клас, които не сме коментирали с @Transactional .

Ако обаче поставим анотацията на частен или защитен метод, Spring ще я игнорира без грешка.

Нека започнем с пример за интерфейс:

@Transactional public interface TransferService { void transfer(String user1, String user2, double val); } 

Обикновено не се препоръчва да зададете @Transactional на интерфейса. Въпреки това е приемливо за случаи като @Repository с Spring Data.

Можем да поставим анотацията в дефиниция на клас, за да заменим настройката за транзакция на интерфейса / суперкласа:

@Service @Transactional public class TransferServiceImpl implements TransferService { @Override public void transfer(String user1, String user2, double val) { // ... } }

Сега нека го заменим, като зададем анотацията директно върху метода:

@Transactional public void transfer(String user1, String user2, double val) { // ... }

3. Разпространение на транзакции

Разпространението определя границата на транзакциите на нашата бизнес логика. Spring успява да стартира и постави на пауза транзакция според нашата настройка за разпространение .

Spring извиква TransactionManager :: getTransaction, за да получи или създаде транзакция според разпространението. Той поддържа някои от разпространенията за всички видове TransactionManager , но има няколко от тях, които се поддържат само от специфични реализации на TransactionManager .

Сега да разгледаме различните разпространения и как те работят.

3.1. ЗАДЪЛЖИТЕЛНО Размножаване

REQUIRED е разпространението по подразбиране. Spring проверява дали има активна транзакция, след което създава нова, ако нищо не е съществувало. В противен случай бизнес логиката се добавя към текущата активна транзакция:

@Transactional(propagation = Propagation.REQUIRED) public void requiredExample(String user) { // ... }

Също тъй като REQUIRED е разпространението по подразбиране, можем да опростим кода, като го пуснем:

@Transactional public void requiredExample(String user) { // ... }

Нека да видим псевдокода на това как работи създаването на транзакции за ИЗИСКВАНО разпространение:

if (isExistingTransaction()) { if (isValidateExistingTransaction()) { validateExisitingAndThrowExceptionIfNotValid(); } return existing; } return createNewTransaction();

3.2. ПОДДЪРЖА Размножаване

За SUPPORTS Spring първо проверява дали съществува активна транзакция. Ако съществува транзакция, тогава ще се използва съществуващата транзакция. Ако няма транзакция, тя се изпълнява без транзакция:

@Transactional(propagation = Propagation.SUPPORTS) public void supportsExample(String user) { // ... }

Нека видим псевдокода за създаване на транзакция за SUPPORTS :

if (isExistingTransaction()) { if (isValidateExistingTransaction()) { validateExisitingAndThrowExceptionIfNotValid(); } return existing; } return emptyTransaction;

3.3. ЗАДЪЛЖИТЕЛНО Размножаване

Когато разпространението е ЗАДЪЛЖИТЕЛНО , ако има активна транзакция, тя ще бъде използвана. Ако няма активна транзакция, тогава Spring извежда изключение:

@Transactional(propagation = Propagation.MANDATORY) public void mandatoryExample(String user) { // ... }

И нека отново видим псевдокода:

if (isExistingTransaction()) { if (isValidateExistingTransaction()) { validateExisitingAndThrowExceptionIfNotValid(); } return existing; } throw IllegalTransactionStateException;

3.4. НИКОГА Размножаване

За логика на транзакции с разпространение НИКОГА Spring извежда изключение, ако има активна транзакция:

@Transactional(propagation = Propagation.NEVER) public void neverExample(String user) { // ... }

Нека видим псевдокода на това как създаването на транзакции работи за НИКОГА разпространение:

if (isExistingTransaction()) { throw IllegalTransactionStateException; } return emptyTransaction;

3.5. NOT_SUPPORTED Размножаване

Spring at first suspends the current transaction if it exists, then the business logic is executed without a transaction.

@Transactional(propagation = Propagation.NOT_SUPPORTED) public void notSupportedExample(String user) { // ... }

The JTATransactionManager supports real transaction suspension out-of-the-box. Others simulate the suspension by holding a reference to the existing one and then clearing it from the thread context

3.6. REQUIRES_NEW Propagation

When the propagation is REQUIRES_NEW, Spring suspends the current transaction if it exists and then creates a new one:

@Transactional(propagation = Propagation.REQUIRES_NEW) public void requiresNewExample(String user) { // ... }

Similar to NOT_SUPPORTED, we need the JTATransactionManager for actual transaction suspension.

And the pseudo-code looks like so:

if (isExistingTransaction()) { suspend(existing); try { return createNewTransaction(); } catch (exception) { resumeAfterBeginException(); throw exception; } } return createNewTransaction();

3.7. NESTED Propagation

For NESTED propagation, Spring checks if a transaction exists, then if yes, it marks a savepoint. This means if our business logic execution throws an exception, then transaction rollbacks to this savepoint. If there's no active transaction, it works like REQUIRED .

DataSourceTransactionManager supports this propagation out-of-the-box. Also, some implementations of JTATransactionManager may support this.

JpaTransactionManager supports NESTED only for JDBC connections. However, if we set nestedTransactionAllowed flag to true, it also works for JDBC access code in JPA transactions if our JDBC driver supports savepoints.

Finally, let's set the propagation to NESTED:

@Transactional(propagation = Propagation.NESTED) public void nestedExample(String user) { // ... }

4. Transaction Isolation

Isolation is one of the common ACID properties: Atomicity, Consistency, Isolation, and Durability. Isolation describes how changes applied by concurrent transactions are visible to each other.

Each isolation level prevents zero or more concurrency side effects on a transaction:

  • Dirty read: read the uncommitted change of a concurrent transaction
  • Nonrepeatable read: get different value on re-read of a row if a concurrent transaction updates the same row and commits
  • Phantom read: get different rows after re-execution of a range query if another transaction adds or removes some rows in the range and commits

We can set the isolation level of a transaction by @Transactional::isolation. It has these five enumerations in Spring: DEFAULT, READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE.

4.1. Isolation Management in Spring

The default isolation level is DEFAULT. So when Spring creates a new transaction, the isolation level will be the default isolation of our RDBMS. Therefore, we should be careful if we change the database.

We should also consider cases when we call a chain of methods with different isolation. In the normal flow, the isolation only applies when a new transaction created. Thus if for any reason we don't want to allow a method to execute in different isolation, we have to set TransactionManager::setValidateExistingTransaction to true. Then the pseudo-code of transaction validation will be:

if (isolationLevel != ISOLATION_DEFAULT) { if (currentTransactionIsolationLevel() != isolationLevel) { throw IllegalTransactionStateException } }

Now let's get deep in different isolation levels and their effects.

4.2. READ_UNCOMMITTED Isolation

READ_UNCOMMITTED is the lowest isolation level and allows for most concurrent access.

As a result, it suffers from all three mentioned concurrency side effects. So a transaction with this isolation reads uncommitted data of other concurrent transactions. Also, both non-repeatable and phantom reads can happen. Thus we can get a different result on re-read of a row or re-execution of a range query.

We can set the isolation level for a method or class:

@Transactional(isolation = Isolation.READ_UNCOMMITTED) public void log(String message) { // ... }

Postgres does not support READ_UNCOMMITTED isolation and falls back to READ_COMMITED instead. Also, Oracle does not support and allow READ_UNCOMMITTED.

4.3. READ_COMMITTED Isolation

The second level of isolation, READ_COMMITTED, prevents dirty reads.

The rest of the concurrency side effects still could happen. So uncommitted changes in concurrent transactions have no impact on us, but if a transaction commits its changes, our result could change by re-querying.

Here, we set the isolation level:

@Transactional(isolation = Isolation.READ_COMMITTED) public void log(String message){ // ... }

READ_COMMITTED is the default level with Postgres, SQL Server, and Oracle.

4.4. REPEATABLE_READ Isolation

The third level of isolation, REPEATABLE_READ, prevents dirty, and non-repeatable reads. So we are not affected by uncommitted changes in concurrent transactions.

Also, when we re-query for a row, we don't get a different result. But in the re-execution of range-queries, we may get newly added or removed rows.

Moreover, it is the lowest required level to prevent the lost update. The lost update occurs when two or more concurrent transactions read and update the same row. REPEATABLE_READ does not allow simultaneous access to a row at all. Hence the lost update can't happen.

Here is how to set the isolation level for a method:

@Transactional(isolation = Isolation.REPEATABLE_READ) public void log(String message){ // ... }

REPEATABLE_READ is the default level in Mysql. Oracle does not support REPEATABLE_READ.

4.5. SERIALIZABLE Isolation

SERIALIZABLE is the highest level of isolation. It prevents all mentioned concurrency side effects but can lead to the lowest concurrent access rate because it executes concurrent calls sequentially.

In other words, concurrent execution of a group of serializable transactions has the same result as executing them in serial.

Now let's see how to set SERIALIZABLE as the isolation level:

@Transactional(isolation = Isolation.SERIALIZABLE) public void log(String message){ // ... }

5. Conclusion

In this tutorial, we explored the propagation property of @Transaction in detail. Afterward, we learned about concurrency side effects and isolation levels.

As always, you can find the complete code over on GitHub.