Transaction Propagation and Isolation in Spring @transactional
Transaction Propagation and Isolation in Spring @transactional
Spring @Transactional
1. Introduction
In this tutorial, we'll cover the @Transactional annotation, as well as
its isolation and propagation settings.
2. What Is @Transactional?
We can use @Transactional to wrap a method in a database transaction.
It allows us to set propagation, isolation, timeout, read-only, and rollback conditions
for our transaction. We can also specify the transaction manager.
2.1. @Transactional Implementation Details
Spring creates a proxy, or manipulates the class byte-code, to manage the creation,
commit, and rollback of the transaction. In the case of a proxy, Spring
ignores @Transactional in internal method calls.
Simply put, if we have a method like callMethod and we mark it
as @Transactional, Spring will wrap some transaction management code around the
invocation@Transactional method called:
createTransactionIfNecessary();
try {
callMethod();
commitTransactionAfterReturning();
} catch (exception) {
completeTransactionAfterThrowing();
throw exception;
}
3. Transaction Propagation
Propagation defines our business logic's transaction boundary. Spring manages to start
and pause a transaction according to our propagation setting.
Spring calls TransactionManager::getTransaction to get or create a transaction
according to the propagation. It supports some of the propagations for all types
of TransactionManager, but there are a few of them that are only supported by
specific implementations of TransactionManager.
Let's go through the different propagations and how they work.
3.1. REQUIRED Propagation
REQUIRED is the default propagation. Spring checks if there is an active transaction,
and if nothing exists, it creates a new one. Otherwise, the business logic appends to
the currently active transaction:
@Transactional(propagation = Propagation.REQUIRED)
public void requiredExample(String user) {
// ...
}
Furthermore, since REQUIRED is the default propagation, we can simplify the code
by dropping it:
@Transactional
public void requiredExample(String user) {
// ...
}
Let's see the pseudo-code of how transaction creation works
for REQUIRED propagation:
if (isExistingTransaction()) {
if (isValidateExistingTransaction()) {
validateExisitingAndThrowExceptionIfNotValid();
}
return existing;
}
return createNewTransaction();
3.2. SUPPORTS Propagation
For SUPPORTS, Spring first checks if an active transaction exists. If a transaction
exists, then the existing transaction will be used. If there isn't a transaction, it is
executed non-transactional:
@Transactional(propagation = Propagation.SUPPORTS)
public void supportsExample(String user) {
// ...
}
Let's see the transaction creation's pseudo-code for SUPPORTS:
if (isExistingTransaction()) {
if (isValidateExistingTransaction()) {
validateExisitingAndThrowExceptionIfNotValid();
}
return existing;
}
return emptyTransaction;
3.3. MANDATORY Propagation
When the propagation is MANDATORY, if there is an active transaction, then it will
be used. If there isn't an active transaction, then Spring throws an exception:
@Transactional(propagation = Propagation.MANDATORY)
public void mandatoryExample(String user) {
// ...
}
Let's again see the pseudo-code:
if (isExistingTransaction()) {
if (isValidateExistingTransaction()) {
validateExisitingAndThrowExceptionIfNotValid();
}
return existing;
}
throw IllegalTransactionStateException;
3.4. NEVER Propagation
For transactional logic with NEVER propagation, Spring throws an exception if there's
an active transaction:
@Transactional(propagation = Propagation.NEVER)
public void neverExample(String user) {
// ...
}
Let's see the pseudo-code of how transaction creation works for NEVER propagation:
if (isExistingTransaction()) {
throw IllegalTransactionStateException;
}
return emptyTransaction;
3.5. NOT_SUPPORTED Propagation
If a current transaction exists, first Spring suspends it, and 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.
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, and if so, it marks a
save point. This means that if our business logic execution throws an exception, then
the transaction rollbacks to this save point. If there's no active transaction, it works
like REQUIRED.
DataSourceTransactionManager supports this propagation out-of-the-box. Some
implementations of JTATransactionManager may also support this.
JpaTransactionManager supports NESTED only for JDBC connections.
However, if we set the nestedTransactionAllowed flag to true, it also works for
JDBC access code in JPA transactions if our JDBC driver supports save points.
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:
4.3. READ_COMMITTED Isolation
The second level of isolation, READ_COMMITTED, prevents dirty reads.
The rest of the concurrency side effects could still 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. However, 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.
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){
// ...
}
java.lang.Object
java.lang.Enum<Propagation>
org.springframework.transaction.annotation.Propagation
Enum Constants
MANDATORY
NESTED
NEVER
NOT_SUPPORTED
REQUIRED
REQUIRES_NEW
Create a new transaction, and suspend the current transaction if one exists.
SUPPORTS
REQUIRED
SUPPORTS
See Also:
AbstractPlatformTransactionManager.setTransactionSynchronization(int
)
MANDATORY
See Also:
JtaTransactionManager.setTransactionManager(javax.transaction.Transa
ctionManager)
NOT_SUPPORTED
See Also:
JtaTransactionManager.setTransactionManager(javax.transaction.Transa
ctionManager)
NEVER
See Also:
DataSourceTransactionManager