[Spring] In-depth understanding of Spring transactions and their propagation mechanisms


1. What is a Spring transaction

In the Spring framework, a transaction (Transaction) is a mechanism for managing database operations, designed to ensure data integrity一致性、可靠性和完整性 . A transaction can treat a set of database operations (such as insert, update, delete, etc.) as a single unit of execution, either all executed successfully, or all rolled back . This ensures that the database remains in a consistent state at all times, maintaining data integrity even in the event of failures or errors.

The Spring Framework makes it easier for developers to manage transaction boundaries by providing transaction management capabilities. Spring mainly provides two main transaction management methods:

  1. Programmatic transaction management : Explicitly manage transaction start, commit, and rollback operations by writing code. This approach provides greater flexibility, but also requires more code maintenance.

  2. Declarative transaction management : By declaring the behavior of the transaction in the configuration, the Spring framework automatically handles the boundary of the transaction, reducing the workload of the developer and improving the maintainability of the code.

Second, the implementation method of transactions in Spring

2.1 Spring programmatic transactions (manual)

2.1.1 Demonstration of the use of programmatic transactions

In Spring, programmatic transaction management is a way to manually control transaction boundaries, similar to how MySQL operates transactions, and it involves three important steps:

  1. Start a transaction (get transaction)DataSourceTransactionManager : First you need to start a new transaction by getting a transaction manager (for example ) to get a transaction. The transaction manager is the core component used to manage transactions.

  2. Committing a transaction : Once a set of database operations has been successfully performed and you want to persist those changes to the database, you can call the transaction object's commit method. This will cause all operations in the transaction to be applied to the database.

  3. Rollback transaction : If an error occurs during transaction processing or a certain condition is not met, the rollback method of the transaction object can be called to undo all operations in the transaction and return to the state before the transaction started.

In Spring Boot, you can take advantage of the built-in transaction manager DataSourceTransactionManagerto acquire transactions, commit or rollback transactions. In addition, TransactionDefinitionit is used to define the attributes of the transaction. When obtaining the transaction, it needs to be TransactionDefinitionpassed in DataSourceTransactionManagerto obtain a transaction status TransactionStatus.

For example, the following code demonstrates programmatic transactions:

@RestController
@RequestMapping("/user")
public class UserController {
    
    

    // 编程式事务
    @Autowired
    private DataSourceTransactionManager dataSourceTransactionManager;

    @Autowired
    private TransactionDefinition transactionDefinition;

    @Autowired
    private UserService userService;

    @RequestMapping("/del")
    public int delById(@RequestParam("id") Integer id) {
    
    
        if (id == null || id < 0) return 0;
        // 1. 开启事务
        TransactionStatus transactionStatus = null;
        int res = 0;
        try {
    
    
            transactionStatus = dataSourceTransactionManager.getTransaction(transactionDefinition);

            // 2. 业务操作 —— 删除用户
            res = userService.delById(id);
            System.out.println("删除: " + res);

            // 3. 提交、回滚事务
            // 提交事务
            dataSourceTransactionManager.commit(transactionStatus);
        } catch (Exception e) {
    
    
            e.printStackTrace();

            // 回滚事务
            if (transactionStatus != null) {
    
    
                dataSourceTransactionManager.rollback(transactionStatus);
            }
        }
        return res;
    }
}

This code shows how to handle user deletion in Spring Boot through programmatic transaction management. Programmatic transactions allow us to explicitly control transaction boundaries in code, and to manually commit or rollback transactions when needed.

2.1.2 Problems with programmatic transactions

From the sample code above, it can be found that although programmatic transactions provide greater flexibility, there are also some problems and challenges:

  1. Code redundancy and poor readability: Programmatic transactions need to explicitly add transaction management logic to the code, making the code redundant and difficult to maintain. Every time a transaction needs to be used, the code for opening, committing and rolling back the transaction needs to be repeatedly written, which reduces the readability of the code.

  2. Complex transaction boundary control: developers need to manually manage transaction boundaries to ensure that transactions start, commit, and rollback are in the correct position. This may lead to missing transaction management code, which affects data consistency.

  3. Transaction propagation and nesting issues: In scenarios involving multiple method calls, manually controlling transaction propagation and nesting relationships can become complicated. Developers need to ensure that transactions are correctly propagated between methods, and at the same time handle the problem of nested transactions.

  4. Exception handling is cumbersome: Programmatic transactions need to be manually rolled back during exception handling. If exceptions are not handled properly, transactions may not be rolled back correctly, resulting in data inconsistencies.

  5. Poor maintainability: With the development of the project, the business logic may become more complex, and the transaction management code may need to be modified frequently. This will increase the difficulty of code maintenance and may lead to the introduction of errors.

  6. Not conducive to horizontal expansion: Programmatic transactions are difficult to support horizontal expansion, because the code of transaction management is tightly coupled in the business logic, and a large amount of code may need to be modified when expanding.

In contrast, declarative transaction management separates transaction management from business logic by adding annotations to methods or declaring them in configuration files, providing better code organization and maintainability. Declarative transactions can automatically handle the start, commit, and rollback of transactions in aspects, thereby reducing the workload of developers.

Therefore, in most cases, it is recommended to use declarative transaction management to handle transactions, especially in simplifying transaction logic and improving code readability.

2.2 Spring declarative transaction (automatic)

The implementation of declarative transactions is very simple, you only need to add annotations to the required methods @Transactional, and you don't need to manually open or commit transactions.

  • When entering the annotated method, Spring will automatically open a transaction.
  • After the method is executed, if no uncaught exception is thrown, the transaction will be automatically committed to ensure data consistency.
  • However, if an unhandled exception occurs during method execution, the transaction is automatically rolled back to ensure database integrity and consistency.

This method greatly simplifies the coding of transaction management, reduces the cumbersome operations of manual transaction processing, and improves the readability and maintainability of the code. For example, the following code implementation:

@RestController
@RequestMapping("/user")
public class UserController {
    
    
    @Autowired
    private UserService userService;

    // 声明式事务
    @RequestMapping("/del")
    @Transactional
    public int delById(Integer id) {
    
    
        if (id == null || id < 0) return 0;
        int result = userService.delById(id);
        return result;
    }
}

In this example, delByIdthe method is @Transactionalannotated to indicate that the method needs to be managed by a declarative transaction. Inside this method, the incoming value is first checked id, and if it is negative, the result is returned directly. Then, the method is called userService.delById(id)to delete the specified user. At the end of the method, the transaction is automatically committed.

At the same time, if an unhandled exception occurs during execution, the transaction will be rolled back automatically to maintain the consistency of the database. This approach simplifies transaction management and improves code readability and maintainability.

2.2.1 @Transactional Scope

@TransactionalAnnotations can be used to decorate methods or classes:

  • When modifying methods : Note that it can only be applied to publicmethods with access modifiers, otherwise the annotation will not take effect. Usually recommended at the method level @Transactional.

  • When modifying a class : Indicates that the annotation will take effect for all publicmethods in the class. If added at the class level @Transactional, all public methods in that class will automatically have transaction management applied.

In general, it is recommended to @Transactionalapply annotations at the method level in order to more precisely control the scope of transactions, thereby avoiding unnecessary transaction overhead. Applying annotations at the class level is a more convenient option if all methods in the class require transaction management.

2.2.2 @Transactional parameter description

By looking at @Transactionalthe source code, you can find that it supports multiple parameters to configure the behavior of the transaction.

The following is a description of the parameters:

parameter name type Defaults describe
value String “” The name of the transaction manager, transactionManagerequivalent to .
transactionManager String “” The name of the transaction manager, valueequivalent to .
label String[] empty array Transaction label, no specific use for now.
propagation Propagation Propagation.REQUIRED Transaction propagation behavior, defaults to REQUIRED.
isolation Isolation Isolation.DEFAULT The isolation level of the transaction, the default is the database default isolation level.
timeout int -1 Transaction timeout, in seconds. -1 means no timeout limit.
timeoutString String “” A string representation of the transaction timeout, timeoutequivalent to .
readOnly boolean false Whether to set the transaction as read-only, the default is false.
rollbackFor Class<? extends Throwable>[] empty array The exception type that triggered the rollback.
rollbackForClassName String[] empty array The class name string of the exception type that triggered the rollback.
noRollbackFor Class<? extends Throwable>[] empty array Exception types that do not trigger a rollback.
noRollbackForClassName String[] empty array The class name string of the exception type that does not trigger rollback.

These parameters provide flexible configuration of transaction behavior, and can adjust transaction propagation, isolation, timeout and rollback strategies according to specific business needs.

2.2.3 Rollback invalidation problem when @Transactional catches an exception

For the above example code, now simulate an exception in the middle of the code, and observe what happens:

@RequestMapping("/del")
@Transactional
public int delById(Integer id) {
    
    
    if (id == null || id < 0) return 0;
    int result = userService.delById(id);
    System.out.println(result);
    try {
    
    
        int num = 10 / 0;
    } catch (Exception e) {
    
    
        // 如果直接处理异常,则不会回滚
      	e.printStackTrace();
    }
    return result;
}

Through browser access, it was found that the server successfully caught the exception:

but the transaction was not rolled back, and the corresponding user data was still deleted:

the reason is:

In the exception handling, the exception is directly caught and processed, which causes the transaction rollback to fail . By default, the annotation will trigger a transaction rollback when an exception of and its subclasses @Transactionalare thrown within the method . RuntimeExceptionHowever, when catchthe exception is caught and handled within the block, Spring cannot perceive the exception and thus cannot trigger a transaction rollback.

Solution:

The solution to this problem can be roughly divided into two types:

  1. Throw the caught exception again:
e.printStackTrace();
throw e;

This approach enables Spring to catch the exception and trigger a transaction rollback by rethrowing the exception. After an exception occurs, the transaction will be rolled back to ensure that the previous database operations will not take effect, thereby maintaining data consistency.

  1. Roll back the transaction manually using TransactionAspectSupport:
e.printStackTrace();
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();

This approach makes use of the classes provided by Spring TransactionAspectSupportto manually set the transaction rollback status. After catching the exception, TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()the current transaction can be set to the rollback state by calling, so as to achieve the effect of rolling back the transaction. This method is more flexible and can manually control the rollback of transactions when needed.

No matter which method you choose, you can trigger transaction rollback when an exception occurs to ensure data integrity and consistency. Which method to choose depends on the specific code logic and requirements.

2.4.4 How @Transactional works

@TransactionalThe working principle of annotations is based on Spring AOP (Aspect Oriented Programming) and transaction manager. It utilizes the proxy mechanism of the Spring framework to implement transaction management.

When an @Transactionalannotated method is called, Spring creates a proxy object to wrap the method. Proxy objects add transaction management logic before and after method execution to ensure transaction start, commit, and rollback. This process is realized through AOP technology.

Specifically, @Transactionalthe workflow for annotations is as follows:

  1. Creation of transaction proxy: Spring will create a proxy object for each annotated class at runtime @Transactional. This proxy object will contain the transaction management logic.

  2. Method call: When calling a @Transactionalmethod decorated with annotations, it is actually called through the proxy object.

  3. Triggering of the transaction aspect: In the proxy object, the transaction aspect will be triggered before and after method execution. Before the method is executed, the aspect will start a transaction; after the method is executed, the aspect will decide whether to commit the transaction or roll back the transaction according to the execution of the method.

  4. Use of the transaction manager: Aspects control transactions through the transaction manager. The transaction manager is responsible for the actual transaction management operations, such as opening, committing, and rolling back transactions.

  5. Transaction control: If the method is executed normally, the aspect will notify the transaction manager to commit the transaction. If the method throws an exception during execution, the aspect will notify the transaction manager to roll back the transaction.

In general, @Transactionalthe working principle of annotations is to implement transaction management through agents and aspects, and separate transaction control from business logic, making the code more modular and maintainable. This is also one of the core mechanisms of declarative transaction management.

2.3 Spring transaction failure scenario

Under certain circumstances, transactions in Spring can be invalidated, causing transactions to not take effect or to perform as expected. The following are some scenarios that can lead to transaction failure:

  1. Non- publicmodified methods: By default, @Transactionalannotations only publicwork on methods with access modifiers. If you publicadd @Transactionalannotations on non-methods, transactions may not take effect.

  2. timeoutTimeout: If the transaction execution time exceeds the set timeoutvalue, the transaction may be forced to roll back. This can cause transactions to not perform as expected, especially if the transaction requires long-running operations.

  3. In the code try/catch: if the exception is caught and handled inside the method, Spring will not be able to perceive the exception and thus cannot trigger the transaction rollback. This may result in transactions not being rolled back when an exception occurs.

  4. Call @Transactionala method inside a class with : When a method inside a class is called, its @Transactionalannotations may not take effect. This is because Spring uses proxy-based transaction management by default, and calling methods directly inside the class will not go through the proxy, so transaction management may not take effect.

@RestController
@RequestMapping("/user")
public class UserController {
    
    
    @Autowired
    private UserService userService;

    public int del(Integer id){
    
    
        return delById(id);
    }
    
    // 声明式事务
    @RequestMapping("/del")
    @Transactional
    public int delById(Integer id) {
    
    
        if (id == null || id < 0) return 0;
        int result = userService.delById(id);
        return result;
    }
}
  1. Database does not support transactions: If your database does not support transactions, such as using some special database engine, transactions may not work properly. In this case, you should make sure to use a database engine that supports transactions.

3. Transaction isolation level

3.1 Review of transaction characteristics

In a database, transactions have the following four important properties, often referred to as ACID properties:

  1. Atomicity: A transaction is regarded as an indivisible unit of operation, either all executed successfully, or all failed and rolled back.

  2. Consistency: Transactions transform the database from one consistent state to another, ensuring data integrity and consistency.

  3. Isolation: Concurrently executed transactions should not affect each other, and each transaction feels that it is operating data independently.

  4. Durability: Once a transaction is committed, its modifications to the database should be permanent and should not be lost even if a system crash occurs.

3.2 MySQL transaction isolation level

MySQL supports the following four transaction isolation levels to control the degree of interaction between multiple transactions:

  1. Read Uncommitted (Read Uncommitted): Allows a transaction to read data that has not yet been committed by another transaction. This is the lowest isolation level and may cause problems with dirty reads, non-repeatable reads, and phantom reads.

  2. Read Committed (Read Committed): Allows a transaction to only read data that has been committed by another transaction. This avoids dirty reads, but may cause problems with non-repeatable reads and phantom reads.

  3. Repeatable Read (Repeatable Read): Ensure that the results of reading the same record multiple times in the same transaction are consistent, even if other transactions modify the record. This avoids dirty reads and non-repeatable reads, but phantom reads are possible.

  4. Serializable: The highest isolation level, ensuring that each transaction runs completely independently, avoiding dirty reads, non-repeatable reads, and phantom reads, but may affect concurrency performance.

The following are the dirty reads, non-repeatable reads, and phantom reads corresponding to the four isolation levels of transactions:

isolation level dirty read non-repeatable read Phantom reading
read uncommitted
read committed ×
repeatable read × ×
Serialization × × ×
  • √ indicates that the problem may occur.
  • × indicates that the problem does not occur.

3.3 Isolation level of Spring transaction

Spring supports different transaction isolation levels through parameters @Transactionalin annotations . The source code is as follows:isolationIsolation

The isolation level can be set using these enumeration values:

  • Isolation.DEFAULT: Use the default isolation level of the database.
  • Isolation.READ_UNCOMMITTED: read uncommitted.
  • Isolation.READ_COMMITTED: read committed.
  • Isolation.REPEATABLE_READ: Repeatable read.
  • Isolation.SERIALIZABLE: serialization.

For example, specify the isolation level for Spring transactions as DEFAULT:

@RequestMapping("/del")
@Transactional(isolation = Isolation.DEFAULT)
public int delById(Integer id) {
    
    
    if (id == null || id < 0) return 0;
    int result = userService.delById(id);
    return result;
}

By choosing an appropriate transaction isolation level, the degree of mutual influence between transactions can be controlled in a concurrent environment, thereby avoiding the problem of data inconsistency. Different isolation levels have different trade-offs in terms of performance and data consistency, and developers need to choose an appropriate isolation level based on specific business needs.

Fourth, the propagation mechanism of Spring transactions

4.1 Why do we need a transaction propagation mechanism

In complex application scenarios, a transaction operation may call multiple methods or services. These approaches may require transaction management independently, but need to work together to maintain data consistency and integrity. At this time, a transaction propagation mechanism needs to be introduced.

The transaction propagation mechanism defines how multiple transaction methods work together, how to share the same transaction, and how to isolate and commit in nested transactions. Through the transaction propagation mechanism, it can ensure that multiple transaction methods can be coordinated according to certain rules during execution to avoid the problem of data inconsistency.

4.2 Classification of Transaction Propagation Mechanisms

Spring defines seven transaction propagation behaviors for controlling the interaction between multiple transaction methods. These propagation behaviors can be set in the parameter @Transactionalin the annotation . propagationHere are the spreading behaviors:

  1. REQUIRED (default): If there is a current transaction, join the current transaction; if there is no transaction, create a new transaction. This is the most commonly used propagation behavior.

  2. SUPPORTS: If there is a transaction currently, it will be added to the current transaction; if there is no transaction, it will be executed in a non-transactional manner.

  3. MANDATORY: If there is a transaction currently, it will be added to the current transaction; if there is no transaction, an exception will be thrown.

  4. REQUIRES_NEW: Whether or not there is a current transaction, create a new transaction. If a transaction currently exists, suspend the current transaction.

  5. NOT_SUPPORTED: Execute in a non-transactional manner, if there is a current transaction, suspend the current transaction.

  6. NEVER: Execute in a non-transactional manner, and throw an exception if there is a current transaction.

  7. NESTED: If there is currently a transaction, it will be executed in a nested transaction; if there is no transaction, it will be the same as REQUIRED.

The above 7 propagation behaviors can be divided into the following 3 categories according to whether they support the current transaction:

4.3 Use Cases of Spring Transaction Propagation Mechanism

Transaction demonstration of the REQUIRED and NESTED propagation mechanisms:

Control layer Controller's UserController :

@RestController
@RequestMapping("/user")
public class UserController {
    
    

    @Autowired
    private UserService userService;

    @RequestMapping("/add") // /add?username=lisi&password=123456
    @Transactional(propagation = Propagation.NESTED)
    // Transactional(propagation = Propagation.REQUIRED)
    //@Transactional(propagation = Propagation.REQUIRES_NEW)
    public int add(@RequestParam("username") String username, @RequestParam("password") String password) {
    
    
        if (null == username || null == password || "".equals(username) || "".equals(password)) {
    
    
            return 0;
        }

        int result = 0;

        // 用户添加操作
        UserInfo user = new UserInfo();
        user.setUsername(username);
        user.setPassword(password);

        result = userService.add(user);


        try {
    
    
            int num = 10 / 0; // 加入事务:外部事务回滚,内部事务也会回滚
        } catch (Exception e) {
    
    
            e.printStackTrace();
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        }

        return result;
    }
}

ServiceAt the service layer UserService:

@Service
public class UserService {
    
    
    @Autowired
    private UserMapper userMapper;
    @Autowired
    private LogService logService;

    public int delById(Integer id){
    
    
        return userMapper.delById(id);
    }

    @Transactional(propagation = Propagation.NESTED)
    // Transactional(propagation = Propagation.REQUIRED)
    //@Transactional(propagation = Propagation.REQUIRES_NEW)
    public int add(UserInfo user){
    
    
        // 添加用户信息
        int addUserResult = userMapper.add(user);
        System.out.println("添加用户结果:" + addUserResult);

        //添加日志信息
        Log log = new Log();
        log.setMessage("添加用户信息");
        logService.add(log);

        return addUserResult;
    }
}

ServiceAt the service layer LogService :

@Service
public class LogService {
    
    
    @Autowired
    private LogMapper logMapper;

    @Transactional(propagation = Propagation.NESTED)
    // Transactional(propagation = Propagation.REQUIRED)
    //@Transactional(propagation = Propagation.REQUIRES_NEW)
    public int add(Log log){
    
    
        int result =  logMapper.add(log);
        System.out.println("添加日志结果:" + result);
        // 模拟异常情况
        try {
    
    
            int num = 10 / 0;
        } catch (Exception e) {
    
    
            // 加入事务:内部事务回滚,外部事务也会回滚,并且会抛异常
            // 嵌套事务:内部事务回滚,不影响外部事务
            e.printStackTrace();
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        }
        return result;
    }
}

In the transaction propagation mechanism, REQUIREDand NESTEDare two different propagation behaviors, they are different in transaction nesting, rollback and impact on external transactions. Through the demonstration of the above code, it can be concluded that the main difference between REQUIREDand is as follows:NESTED

  1. Nested properties:

    • REQUIRED: The internal method and the external method share the same transaction, and the transaction operation of the internal method is part of the transaction of the external method.
    • NESTED: The inner method creates a nested transaction, which is a subtransaction of the outer transaction and has an independent transaction state, and the rollback of the inner transaction will not affect the outer transaction.
  2. Rollback behavior:

    • REQUIRED: If the internal method throws an exception or sets a rollback, it will cause the entire external transaction to be rolled back, including the operations of the internal method and the external method.
    • NESTED: If the internal method throws an exception or sets a rollback, only the internal transaction will be rolled back, while the external transaction can still continue to execute.
  3. Affect external affairs:

    • REQUIRED: The transaction operation of the internal method will affect the state of the external transaction, and the rollback of the internal method will cause the rollback of the external transaction.
    • NESTED: The transaction operation of the internal method will not affect the state of the external transaction, and the rollback of the internal method will not affect the commit or rollback of the external transaction.
  4. Supportive:

    • REQUIRED: More commonly used, it is suitable for the situation where the operations of multiple methods are managed as a whole.
    • NESTED: It is not supported in some databases, and the database needs to support the savepoint (Savepoint) function.

In general, REQUIREDit is suitable for the situation where the operations of multiple methods need to be managed as a whole transaction, and it NESTEDis suitable for the situation where nested transactions need to be created in internal methods, keeping the independence of internal transactions without affecting external transactions. The choice of which propagation behavior to use depends on business requirements and database support.

Guess you like

Origin blog.csdn.net/qq_61635026/article/details/132216894