Spring Boot Transaction Management: A Comprehensive Guide
Introduction
This article provides a comprehensive guide to using transaction management in Spring Boot applications.
Spring Boot Transaction Management
Note: If you wish to directly obtain the project code, you can jump to the bottom of the article and download it via the link.
What is a Transaction?
A transaction is a sequence of operations performed as a single logical unit of work. It must be atomic, consistent, isolated, and durable (ACID).
Transaction Management Methods
Spring supports two types of transaction management:
- Programmatic Transaction Management: Uses
TransactionTemplateor the underlyingPlatformTransactionManager. Spring recommends usingTransactionTemplate. - Declarative Transaction Management: Built on AOP. It intercepts method calls to create or join a transaction before the target method starts and commits or rolls back after execution. This method is recommended because it does not require code changes; simply adding
@Transactionalenables it.
Transaction Commit Behavior
By default, databases operate in auto-commit mode, where each statement is a separate transaction. For a set of related operations to be grouped into a single transaction, auto-commit must be disabled. Spring does this automatically by setting connection.setAutoCommit(false) and then explicitly calling connection.commit().
Transaction Isolation Levels
Isolation levels define how transactions interact with each other. The TransactionDefinition interface defines five constants:
TransactionDefinition.ISOLATION_DEFAULT: Uses the default isolation level of the underlying database. For most databases, this isREAD_COMMITTED.TransactionDefinition.ISOLATION_READ_UNCOMMITTED: Allows a transaction to read uncommitted changes from other transactions. This level does not prevent dirty reads, non-repeatable reads, or phantom reads and is rarely used.TransactionDefinition.ISOLATION_READ_COMMITTED: Prevents dirty reads by only allowing a transaction to read committed changes from other transactions. This is the recommended level for most cases.TransactionDefinition.ISOLATION_REPEATABLE_READ: Ensures that if a transaction reads the same data multiple times, it will get the same result. This prevents dirty reads and non-repeatable reads.TransactionDefinition.ISOLATION_SERIALIZABLE: Transactions are executed sequentially, preventing dirty reads, non-repeatable reads, and phantom reads. However, this severely impacts performance.
Transaction Propagation Behaviors
Propagation behavior defines how a transactional method behaves when it is called within an existing transaction context. The TransactionDefinition interface defines the following constants:
TransactionDefinition.PROPAGATION_REQUIRED: Joins the current transaction if one exists; otherwise, creates a new one. This is the default.TransactionDefinition.PROPAGATION_REQUIRES_NEW: Creates a new transaction, suspending the current one if it exists.TransactionDefinition.PROPAGATION_SUPPORTS: Joins the current transaction if one exists; otherwise, runs non-transactionally.TransactionDefinition.PROPAGATION_NOT_SUPPORTED: Runs non-transactionally, suspending the current transaction if one exists.TransactionDefinition.PROPAGATION_NEVER: Runs non-transactionally and throws an exception if a transaction exists.TransactionDefinition.PROPAGATION_MANDATORY: Joins the current transacsion if one exists; throws an exception if there is no existing transaction.TransactionDefinition.PROPAGATION_NESTED: If a current transaction exists, runs within a nested transaction; otherwise, behaves likePROPAGATION_REQUIRED.
Transaction Rollback Rules
By default, the Spring transaction manager rolls back a transaction for unchecked exceptions (subclasses of RuntimeException and Error). Checked exceptions do not trigger a rollback. You can configure specific exceptions to roll back or not roll back.
Common Transaction Attributes
readOnly: Marks the transaction as read-only (true) or read-write (false). Default is false. Example:@Transactional(readOnly=true)rollbackFor: Array of exception classes that trigger a rollback. Example:@Transactional(rollbackFor=RuntimeException.class)or@Transactional(rollbackFor={RuntimeException.class, Exception.class})rollbackForClassName: Array of exception class names that trigger a rollback.noRollbackFor: Array of exception classes that do not trigger a rollback.noRollbackForClassName: Array of exception class names that do not trigger a rollback.propagation: Sets the propagation behavior. Example:@Transactional(propagation=Propagation.NOT_SUPPORTED, readOnly=true)isolation: Sets the isolation level. The default value is usually sufficient.timeout: Sets the transaction timeout in seconds. A value of -1 means never time out.
Important Considerations for Transactions
- Determine whether transactions are needed before writing code to avoid maintenance difficulties later.
- Always test transactions, as they may not work as expected.
- The
@Transactionalannotation should only be used on public methods. Using it on protected or private methods will not cause an error but will be silently ignored, and the transaction will not work. @Transactionaldoes not apply to sub-methods called within the annotated method. If you want a sub-method to participate in the transaction, either userollbackForon the sub-method or have the sub-method throw an exception that is handled by the calling method.- If a transaction method catches an exception, the transaction will not be rolled back unless you manually roll back by calling
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()or rethrow the exception (e.g.,throw new RuntimeException()).
Setup and Configuration
Environment Requirements
- JDK: 1.8
- Spring Boot: 1.5.17.RELEASE
Maven Dependencies (pom.xml)
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.17.RELEASE</version>
<relativePath/>
</parent>
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- DevTools -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
<!-- MyBatis Spring Boot Starter -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.2.0</version>
</dependency>
<!-- MySQL Connector -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.44</version>
</dependency>
<!-- Druid Connection Pool -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.8</version>
</dependency>
</dependencies>
Application Properties (application.properties)
banner.charset=UTF-8
server.tomcat.uri-encoding=UTF-8
spring.http.encoding.charset=UTF-8
spring.http.encoding.enabled=true
spring.http.encoding.force=true
spring.messages.encoding=UTF-8
spring.application.name=springboot-transactional
server.port=8182
spring.datasource.url=jdbc:mysql://localhost:3306/springBoot?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driverClassName=com.mysql.jdbc.Driver
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.initialSize=5
spring.datasource.minIdle=5
spring.datasource.maxActive=20
spring.datasource.maxWait=60000
spring.datasource.timeBetweenEvictionRunsMillis=60000
spring.datasource.minEvictableIdleTimeMillis=300000
spring.datasource.validationQuery=SELECT 1 FROM DUAL
spring.datasource.testWhileIdle=true
spring.datasource.testOnBorrow=false
spring.datasource.testOnReturn=false
spring.datasource.poolPreparedStatements=true
spring.datasource.maxPoolPreparedStatementPerConnectionSize=20
spring.datasource.filters=stat,wall,log4j
spring.datasource.connectionProperties=druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
logging.level.com.pancm.dao=debug
Code Implementation
To use transaction management in Spring Boot, add @EnableTransactionManagement to the main application class and @Transactional to the service layer's public methods.
Example 1: Basic Usage
The simplest way to use @Transactional is to add it to a public method and let it throw exceptions that Spring will handle.
@Transactional
public boolean test1(User user) throws Exception {
long id = user.getId();
System.out.println("First query result: " + userDao.findById(id));
// Insert the same user twice to cause a primary key conflict and test rollback
userDao.insert(user);
System.out.println("Second query result: " + userDao.findById(id));
userDao.insert(user);
return false;
}
Example 2: Manual Rollback
If you want to handle exceptions yourself, you can manually roll back the transaction by calling TransactionAspectSupport.currentTransactionStatus().setRollbackOnly() in the catch block. Ensure this is called immediately after the exception is caught.
@Transactional
public boolean test2(User user) {
long id = user.getId();
try {
System.out.println("First query result: " + userDao.findById(id));
userDao.insert(user);
System.out.println("Second query result: " + userDao.findById(id));
userDao.insert(user);
} catch (Exception e) {
System.out.println("An exception occurred, performing manual rollback!");
// Manually rollback the transaction
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
e.printStackTrace();
}
return false;
}
Example 3: Transaction with Sub-methods
To make a sub-method participate in the transaction, either use rollbackFor on the sub-method or have it throw an exception that the calling method handles. Note that the sub-method must also be public.
@Transactional
public boolean test3(User user) {
try {
System.out.println("First query result: " + userDao.findById(user.getId()));
deal1(user);
deal2(user);
deal3(user);
} catch (Exception e) {
System.out.println("An exception occurred, performing manual rollback!");
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
e.printStackTrace();
}
return false;
}
public void deal1(User user) throws SQLException {
userDao.insert(user);
System.out.println("Second query result: " + userDao.findById(user.getId()));
}
public void deal2(User user) throws SQLException {
if (user.getAge() < 20) {
// This will cause an SQL exception (duplicate primary key)
userDao.insert(user);
} else {
user.setAge(21);
userDao.update(user);
System.out.println("Third query result: " + userDao.findById(user.getId()));
}
}
@Transactional(rollbackFor = SQLException.class)
public void deal3(User user) {
if (user.getAge() > 20) {
// This will cause an SQL exception (duplicate primary key)
userDao.insert(user);
}
}
Example 4: Programmatic Transaction Management
If you prefer not to use @Transactional, you can manage transactions programmatically using DataSourceTransactionManager and TransactionDefinition. Be careful to only rollback when a transaction is active and not yet committed.
@Autowired
private DataSourceTransactionManager dataSourceTransactionManager;
@Autowired
private TransactionDefinition transactionDefinition;
public boolean test4(User user) {
TransactionStatus transactionStatus = null;
boolean committed = false;
try {
transactionStatus = dataSourceTransactionManager.getTransaction(transactionDefinition);
System.out.println("First query result: " + userDao.findById(user.getId()));
userDao.insert(user);
System.out.println("Second query result: " + userDao.findById(user.getId()));
if (user.getAge() < 20) {
user.setAge(user.getAge() + 2);
userDao.update(user);
System.out.println("Third query result: " + userDao.findById(user.getId()));
} else {
throw new Exception("Simulating an exception!");
}
dataSourceTransactionManager.commit(transactionStatus);
committed = true;
System.out.println("Transaction committed successfully!");
throw new Exception("Simulating a second exception!");
} catch (Exception e) {
if (!committed) {
System.out.println("An exception occurred, performing manual rollback!");
dataSourceTransactionManager.rollback(transactionStatus);
}
e.printStackTrace();
}
return false;
}
The examples above cover common use cases. Another approach is using savepoints:
Object savepoint = null;
try {
// Set a savepoint
savepoint = TransactionAspectSupport.currentTransactionStatus().createSavepoint();
} catch (Exception e) {
// Rollback to the savepoint
TransactionAspectSupport.currentTransactionStatus().rollbackToSavepoint(savepoint);
}
Main Components
Entity Class
public class User {
private Long id;
private String name;
private Integer age;
// Getters and setters omitted
}
RestController
@RestController
@RequestMapping(value = "/api/user")
public class UserRestController {
@Autowired
private UserService userService;
@Autowired
private UserDao userDao;
@PostMapping("/test1")
public boolean test1(@RequestBody User user) {
System.out.println("Request parameters: " + user);
try {
userService.test1(user);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("Final query result: " + userDao.findById(user.getId()));
return true;
}
@PostMapping("/test2")
public boolean test2(@RequestBody User user) {
System.out.println("Request parameters: " + user);
userService.test2(user);
System.out.println("Final query result: " + userDao.findById(user.getId()));
return true;
}
@PostMapping("/test3")
public boolean test3(@RequestBody User user) {
System.out.println("Request parameters: " + user);
userService.test3(user);
System.out.println("Final query result: " + userDao.findById(user.getId()));
return true;
}
@PostMapping("/test4")
public boolean test4(@RequestBody User user) {
System.out.println("Request parameters: " + user);
userService.test4(user);
System.out.println("Final query result: " + userDao.findById(user.getId()));
return true;
}
}
Main Application Class
@EnableTransactionManagement
@SpringBootApplication
public class TransactionalApp {
public static void main(String[] args) {
SpringApplication.run(TransactionalApp.class, args);
System.out.println("Transactional application is running...");
}
}
Testing
After starting the application, use Postman to test the endpoints. The examples below correspond to the code examples above.
Test 1: Basic @Transactional
With out @Transactional:
- POST to
http://localhost:8182/api/user/test1with body{"id": 1, "name": "xuwujing", "age": 18} - Console output shows that the data is inserted despite the exception:
Request parameters: User [id=1, name=xuwujing, age=18]
First query result: null
Second query result: User [id=1, name=xuwujing, age=18]
Duplicate entry '1' for key 'PRIMARY'
Final query result: User [id=1, name=xuwujing, age=18]
With @Transactional:
- After uncommenting
@Transactionaland deleting the previously inserted record, POST again. - Console output shows the data is rolled back:
Request parameters: User [id=1, name=xuwujing, age=18]
First query result: null
Second query result: User [id=1, name=xuwujing, age=18]
Duplicate entry '1' for key 'PRIMARY'
Final query result: null
Test 2: Manual Rollback
- POST to
http://localhost:8182/api/user/test2with body{"id": 1, "name": "xuwujing", "age": 18} - Console output shows the transaction is rolled back:
Request parameters: User [id=1, name=xuwujing, age=18]
First query result: null
Second query result: User [id=1, name=xuwujing, age=18]
An exception occurred, performing manual rollback!
Duplicate entry '1' for key 'PRIMARY'
Final query result: null
Test 3: Sub-method Transaction
Test with age=18:
- POST to
http://localhost:8182/api/user/test3with body{"id": 1, "name": "xuwujing", "age": 18} - Console output shows rollback:
Request parameters: User [id=1, name=xuwujing, age=18]
First query result: null
Second query result: User [id=1, name=xuwujing, age=18]
An exception occurred, performing manual rollback!
Duplicate entry '1' for key 'PRIMARY'
Final query result: null
Test with age=21:
- POST with body
{"id": 1, "name": "xuwujing", "age": 21} - Console output shows rollback after a successful update:
Request parameters: User [id=1, name=xuwujing, age=21]
First query result: null
Second query result: User [id=1, name=xuwujing, age=21]
Third query result: User [id=1, name=xuwujing2, age=21]
An exception occurred, performing manual rollback!
Duplicate entry '1' for key 'PRIMARY'
Final query result: null
Test 4: Programmatic Transaction
Test with age=18:
- Delete the record with id=1 first.
- POST to
http://localhost:8182/api/user/test4with body{"id": 1, "name": "xuwujing", "age": 18} - Console output shows commit followed by an exception:
Request parameters: User [id=1, name=xuwujing, age=18]
First query result: null
Second query result: User [id=1, name=xuwujing, age=18]
Third query result: User [id=1, name=xuwujing2, age=20]
Transaction committed successfully!
Simulating a second exception!
Final query result: User [id=1, name=xuwujing, age=20]
Test with age=21:
- Delete the record with id=1 again.
- POST with body
{"id": 1, "name": "xuwujing", "age": 21} - Console output shows rolback:
Request parameters: User [id=1, name=xuwujing, age=21]
First query result: null
Second query result: User [id=1, name=xuwujing, age=21]
An exception occurred, performing manual rollback!
Simulating an exception!
Final query result: null
These tests confirm that the transactional behavior works as expected.
Additional Resources
Project Repository
- Source code: springboot-transactional
- Collection of Spring Boot examples: springBoot-study