In database operations, a transaction is treated as a single unit of work. To ensure data integrity, transactions adhere to four key principles, often abbreviated as ACID:
- Atomicity: Ensures that all operations within the transaction succeed. If any step fails, the entire transaction is aborted, and no changes are applied.
- Consistency: Maintains the database in a valid state before and after the transaction. For example, in a fund transfer, the total amount of money remains constant; money is neither lost nor created.
- Isolation: Ensures that concurrent transactions do not interfere with one another. The result of concurrent transactions should be the same as if they were executed sequentially.
- Durability: Guarantees that once a transaction is committed successfully, the changes persist in the database even in the event of a system failure.
Implementing a Fund Transfer Scenario
To illustrate transaction management, we will implement a banking use case where one user transfers funds to another. We will begin by setting up the environment without transactions to highlight potential data inconsistencies.
1. Database Setup
Create a table to store account information and insert initial records.
CREATE TABLE wallet_table (
id INT AUTO_INCREMENT PRIMARY KEY,
user_name VARCHAR(50) NULL,
balance INT NULL
);
-- Initial data setup
INSERT INTO wallet_table (user_name, balance) VALUES ('Alice', 1000);
INSERT INTO wallet_table (user_name, balance) VALUES ('Bob', 1000);
2. Project Configuration and Dependencies
We need to configure the data source and JdbcTemplate in the Spring configuration file. The structure involves a Service layer handling business logic and a DAO layer handling database access.
XML Configuration (spring-config.xml):
<context:component-scan base-package="com.example.demo"/>
<bean id="sourceData" class="com.alibaba.druid.pool.DruidDataSource">
<property name="url" value="jdbc:mysql://localhost:3306/bank_db"/>
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
<property name="username" value="root"/>
<property name="password" value="password"/>
</bean>
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="sourceData"/>
</bean>
3. DAO Layer Implementation
The DAO layer provides methods to credit and debit accounts.
Interface:
package com.example.demo.dao;
public interface BankDao {
void increaseBalance(String name, int amount);
void decreaseBalance(String name, int amount);
}
Implementation:
package com.example.demo.dao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
@Repository
public class BankDaoImpl implements BankDao {
@Autowired
private JdbcTemplate jdbcTemplate;
@Override
public void increaseBalance(String name, int amount) {
String sql = "UPDATE wallet_table SET balance = balance + ? WHERE user_name = ?";
jdbcTemplate.update(sql, amount, name);
}
@Override
public void decreaseBalance(String name, int amount) {
String sql = "UPDATE wallet_table SET balance = balance - ? WHERE user_name = ?";
jdbcTemplate.update(sql, amount, name);
}
}
4. Service Layer Implementation
The Service layer orchestrates the transfer by calling the DAO methods.
package com.example.demo.service;
import com.example.demo.dao.BankDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class TransferService {
@Autowired
private BankDao bankDao;
public void executeTransfer() {
// Alice sends 100
bankDao.decreaseBalance("Alice", 100);
// Simulating a network failure or system crash
int result = 50 / 0;
// Bob receives 100
bankDao.increaseBalance("Bob", 100);
}
}
5. Testing the Scenario
package com.example.demo.test;
import com.example.demo.service.TransferService;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class AppTest {
@Test
public void testTransfer() {
ApplicationContext context = new ClassPathXmlApplicationContext("spring-config.xml");
TransferService service = context.getBean(TransferService.class);
service.executeTransfer();
}
}
6. Analyzing the Failure
When the test runs, the arithmetic exception occurs after Alice's balance is deducted but before Bob's balance is updated. As a result, the database shows an inconsistent state: Alice has 900, but Bob still has 1000. The 100 units are effectively lost.
Resolving Inconsistency with Spring Transactions
To fix this, we must ensure that the database operations are atomic. The standard approach involves: 1. Opening a transaction. 2. Executing business logic. 3. Committing the transaction if successful. 4. Rolling back the transaction if an exception occurs.
Spring simplifies this through Declarative Transaction Management, which typically uses AOP (Aspect-Oriented Programming).
Step 1: Configure Transaction Manager
We must define a transaction manager bean and enable annotation-driven transaction management in the XML configuration.
<!-- Transaction Manager Configuration -->
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="sourceData"/>
</bean>
<!-- Enable Transaction Annotations -->
<tx:annotation-driven transaction-manager="txManager"/>
Step 2: Apply @Transactional Annotation
Add the @Transactional annotation to the Service class or specific methods. This instructs Spring to manage the transaction boundary automatically.
import org.springframework.transaction.annotation.Transactional;
@Service
@Transactional
public class TransferService {
// ... dependencies and methods remain the same
public void executeTransfer() {
bankDao.decreaseBalance("Alice", 100);
// Simulating failure
int result = 50 / 0;
bankDao.increaseBalance("Bob", 100);
}
}
Now, reset the balances to 1000 for both users and run the test again. Although the exception is still thrown, Spring intercepts the exception and triggers a rollback. Both accounts will retain their original 1000 balance, ensuring data consistency.
@Transactional Configuration Parameters
The @Transactional annotation supports several parameters to fine-tune transaction behavior:
@Transactional(
propagation = Propagation.REQUIRED,
isolation = Isolation.REPEATABLE_READ,
timeout = 30,
readOnly = false
)
1. Propagation (Transaction Propagation Behavior)
Defines how transacsions relate to each other when multiple transactional methods call one another.
- REQUIRED: Default. Joins the existing transaction if one exists; creates a new one otherwise.
- REQUIRES_NEW: Always creates a new transaction, suspending any existing transaction.
- SUPPORTS: Runs within the existing transaction if available; executes non-transactionally otherwise.
- NOT_SUPPORTED: Executes non-transactionally, suspending any existing transaction.
- MANDATORY: Must run within an existing transaction; throws an exception if none exists.
- NEVER: Must not run within a transaction; throws a exception if a transaction exists.
- NESTED: Runs within a nested transaction if a transaction exists; creates a new one otherwise.
2. Isolation (Transaction Isolation Levels)
Isolation levels prevent issues caused by concurrent transactions, such as dirty reads, non-repeatable reads, and phantom reads.
- Dirty Read: Reading data written by an uncommitted transaction.
- Non-Repeatable Read: Reading different data when a row is modified and committed by another transaction between two reads.
- Phantom Read: Seeing new rows when a transaction adds data and commits between two reads of the same range.
Isolation Levels Matrix:
| Isolation Level | Dirty Read | Non-Repeatable Read | Phantom Read |
|---|---|---|---|
| READ UNCOMMITTED | Yes | Yes | Yes |
| READ COMMITTED | No | Yes | Yes |
| REPEATABLE READ | No | No | Yes |
| SERIALIZABLE | No | No | No |
3. Timeout
Specifies the time in seconds the transaction is allowed to run before being rolled back automatically. The default is -1 (no timeout).
4. ReadOnly
Optimizes the transaction for read-only operations. Set to true for queries to prevent potential dirty writes. Default is false.
5. Rollback Rules
- rollbackFor: Defines an array of exception classes that should trigger a rollback. By default, unchecked exceptions (RuntimeException) trigger rollback.
- noRollbackFor: Defines an array of exception classes that should not trigger a rollback.