spring transaction-2 (transaction propagation level combination description)

Posted by byrt on Tue, 22 Feb 2022 17:45:37 +0100

Transaction propagation level combination description

example

  1. Configuration class

    package transaction;
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.jdbc.core.JdbcTemplate;
    import org.springframework.jdbc.datasource.DataSourceTransactionManager;
    import org.springframework.jdbc.datasource.DriverManagerDataSource;
    import org.springframework.transaction.PlatformTransactionManager;
    import org.springframework.transaction.annotation.EnableTransactionManagement;
    
    import javax.sql.DataSource;
    
    @Configuration(proxyBeanMethods = false)
    @EnableTransactionManagement
    public class Config {
    	@Bean
    	public DriverManagerDataSource driverManagerDataSource() {
    		DriverManagerDataSource dataSource = new DriverManagerDataSource();
    		dataSource.setDriverClassName("com.mysql.jdbc.Driver");
    		dataSource.setUrl("jdbc:mysql://10.100.0.176:3306/mybatis");
    		dataSource.setUsername("root");
    		dataSource.setPassword("123456");
    		return dataSource;
    	}
    	@Bean
    	public TestTx testTx() {
    		return new TestTx();
    	}
    	@Bean
    	public JdbcTemplate jdbcTemplate(DataSource dataSource) {
    		JdbcTemplate jdbcTemplate = new JdbcTemplate();
    		jdbcTemplate.setDataSource(dataSource);
    		return jdbcTemplate;
    	}
    
    	@Bean
    	public PlatformTransactionManager txManager(DataSource dataSource) {
    		return new DataSourceTransactionManager(dataSource);
    	}
    }
    
    
  2. Transaction method

    package transaction;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.jdbc.core.BeanPropertyRowMapper;
    import org.springframework.jdbc.core.JdbcTemplate;
    import org.springframework.transaction.annotation.Propagation;
    import org.springframework.transaction.annotation.Transactional;
    import org.springframework.util.Assert;
    
    public class TestTx {
    	@Autowired
    	private JdbcTemplate jdbcTemplate;
    	private int count;
    	@Autowired
    	private TestTx testTxAgent;
    
    	@Transactional(propagation = Propagation.REQUIRED)
    	public void b(int id, String name) {
    		throw new RuntimeException("updateNameWithException1");
    	}
    
    	public void be(int id, String name) {
    		jdbcTemplate.update("update t_test set name=? where id=?", name, id);
    	}
    
    	@Transactional(propagation = Propagation.REQUIRED)
    	public TestBean a(int id) {
    		TestBean testBean = jdbcTemplate.queryForObject("select * from t_test where id=?", new BeanPropertyRowMapper<>(TestBean.class), id);
    		Assert.notNull(testBean, "testBean not be null");
    		try {
    			testTxAgent.b(id, testBean.getName() + "-" + count++);
    		} catch (Exception e) {
    			e.printStackTrace();
    		}
    		be(id,"be-name");
    		return testBean;
    	}
    
    }
    
    class TestBean {
    	private Integer id;
    	private String name;
    	private Double age;
    
    	public Integer getId() {
    		return id;
    	}
    
    	public void setId(Integer id) {
    		this.id = id;
    	}
    
    	public String getName() {
    		return name;
    	}
    
    	public void setName(String name) {
    		this.name = name;
    	}
    
    	public Double getAge() {
    		return age;
    	}
    
    	public void setAge(Double age) {
    		this.age = age;
    	}
    
    	@Override
    	public String toString() {
    		return "TestBean{" +
    				"id=" + id +
    				", name='" + name + '\'' +
    				", age=" + age +
    				'}';
    	}
    }
    
  3. DDL

    -- auto-generated definition
    create table t_student
    (
        id   int auto_increment,
        age  int         null,
        name varchar(50) null,
        constraint t_student_id_uindex
            unique (id)
    );
    
    alter table t_student
        add primary key (id);
    
    
    
  4. Main startup class

    package transaction;
    
    import org.springframework.context.annotation.AnnotationConfigApplicationContext;
    
    import java.sql.SQLException;
    
    public class MainTest
    {
    	public static void main(String[] args) throws SQLException {
    		try {
    			AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class);
    			TestTx testTx = context.getBean(TestTx.class);
    			TestBean testBeanById = testTx.a(1);
    			System.out.println(testBeanById);
    		}catch (Throwable e){
    			e.printStackTrace();;
    		}
    	}
    }
    

The following examples are all tested around the above code. If there is no additional explanation, it is a - > b, and @ Transactional in the above method will change. Not the same as in the above example.

Through the last article, we know that the final operation is Connection. The so-called Hang is to change the state of the previous transaction into an attribute of the current transaction. The propagation level of transactions has been made clear above. I want to make a general distinction here.

It is mainly divided into:

  1. A new transaction needs to be created.
  2. A transaction is required, and there is no need to create a new transaction. There is no need to use SavePoint (in fact, the second one cannot be promotion_nested).
  3. A transaction is required, and there is no need to create a new transaction. SavePoint is required (in fact, the second one is promotion_needed)
  4. No transaction is required, but it can be executed
  5. There can be no business. If there is, an error will be reported.

The following combination is also analyzed according to this idea. The following is only about abnormal conditions. There is nothing to say about normal conditions.

Another point is that you can specify exception rollback. If the current exception does not match the specified exception, RuntimeException and Error will still be found. The exceptions I throw here are subclasses of RuntimeException. Therefore, there is no need to specify exceptions here.

Now there are two ways. A and B.

A new transaction needs to be created

This requirement does not have many levels of communication that can be combined. a can be (promotion_required, promotion_requirements_new, promotion_new), b can only be (promotion_requirements_new).

Exception not handled

The transactions in b and a will be rolled back.

analysis:

In the previous analysis of the transaction implementation (TransactionInterceptor), the exception will be caught and rolled back, but this exception will continue to be thrown. With the same logic, this exception will be dropped by the TransactionInterceptor when a is called, and the transaction will also be rolled back. The current condition is that the exception needs to be rolled back. If you do not need to roll back, it will be submitted. Of course, it is important to note that the code after the b method is not executed in the a method. If submitted, it is also an operation before calling the b method in advance, and rollback is also.

  • Supplementary description (for example, if the exception is not satisfied, the operation before calling method b in a will be submitted)

Exception handled

a handled the exception with try catch when calling b.

b rollback, a commit

analysis:

A and b are two independent transactions. Exceptions are caught in the catch block of the TransactionInterceptor. After the transaction rollback is processed, the original exception will be thrown out. After calling b, after the transaction rollback is processed, the original exception will be thrown out. But at this time, it is caught by a in the internal catch. This exception is not thrown into the catch block in the TransactionInterceptor. So for a, everything is normal. So his affairs will be submitted.

A transaction is required, and there is no need to create a new transaction. There is no need to use SavePoint (in fact, the second one cannot be promotion_needed)

This means that a must have a transaction before B can reuse the transactions in a. a(PROPAGATION_REQUIRED,PROPAGATION_REQUIRES_NEW,PROPAGATION_NESTED),b(PROPAGATION_REQUIRED)

Exception not handled

Note: here is a transaction from beginning to end. b uses the transaction of A.

The operations of a and b are not effective, and the transaction is rolled back.

analysis:

In the same mode as above, the TransactionInterceptor catches the exception and does the rollback operation, and then throws the original exception. With the same logic, it does the rollback operation in a.

Points to note:

A and b use the same transaction, that is, the same Connection object. After the exception of b is called, the TransactionInterceptor does the rollback operation and will not really do the rollback (that is, it will not call the Connection.rollback() method). It just sets a flag bit of the ConnectionHolder (Rollback only is true), indicating that an exception has occurred in this transaction, Rollback is required. The real place to deal with rollback should be the Connection created by that method, which should do the real rollback operation.

However, when a calls b, it does not capture the catch block. The original exception can still be captured by the TransactionInterceptor to do the rollback operation. A really creates the connection, so the real rollback operation also needs to be done by it.

Exception handled

The operations of a and b do not take effect, and the transaction rolled back because it has been marked as rollback only

analysis:

Then, as mentioned above, the exception of b is caught by A. the corresponding TransactionInterceptor of a cannot get the exception information. The catch block cannot go, but can only go through the submission operation. When it is found that the Rollback attribute of the current ConnectionHolder is true, it will roll back, call the rollback() method of connection, and throw an unexpected rollbackexception.

Long transaction: multiple methods refer to the same transaction, and these methods do not have savePoint operation. This is a long transaction. Generally, by default, there is only one error in a long transaction, and the whole transaction will fail.

Supplementary notes

Looking at the code directly makes it clearer

  1. Rollback (AbstractPlatformTransactionManager#processRollback(DefaultTransactionStatus,boolean))

    private void processRollback(DefaultTransactionStatus status, boolean unexpected) {
    		try {
    			boolean unexpectedRollback = unexpected;
    			try {
    				triggerBeforeCompletion(status);
            // The order of judgment is the key to whether there is a savePoint. If b is nested, there will be no problem if an exception is handled in a. Because the rollback only property is not set
    				if (status.hasSavepoint()) {
    					if (status.isDebug()) {
    						logger.debug("Rolling back transaction to savepoint");
    					}
              // Rollback to savePoint and release to savePoint
    					status.rollbackToHeldSavepoint();
    				}
            // Whether it is a new transaction, that is, whether the transaction is created by this method. as
    				else if (status.isNewTransaction()) {
    					if (status.isDebug()) {
    						logger.debug("Initiating transaction rollback");
    					}
    					doRollback(status);// This rollback operation is very simple, that is, connection Rollback method
    				}
    				else {
      			// If the above conditions are not met, it is a long transaction.
    					if (status.hasTransaction()) {
                // You can directly change the corresponding value of TransactionStatus or the return value of the method in AbstractPlatformTransactionManager.
                // Basically, AbstractPlatformTransactionManager is true
    						if (status.isLocalRollbackOnly() || isGlobalRollbackOnParticipationFailure()) {
    							if (status.isDebug()) {
    								logger.debug("Participating transaction failed - marking existing transaction as rollback-only");
    							}
                  // Set rollback only for ConnectionHolder
    							doSetRollbackOnly(status);
    						}
    						else {
    							if (status.isDebug()) {
    								logger.debug("Participating transaction failed - letting transaction originator decide on rollback");
    							}
    						}
    					}
    					else {
    						logger.debug("Should roll back transaction but cannot - no transaction available");
    					}
    					// Unexpected rollback only matters here if we're asked to fail early
    					if (!isFailEarlyOnGlobalRollbackOnly()) {
    						unexpectedRollback = false;
    					}
    				}
    			}
    			catch (RuntimeException | Error ex) {
    				triggerAfterCompletion(status, TransactionSynchronization.STATUS_UNKNOWN);
    				throw ex;
    			}
    
    			triggerAfterCompletion(status, TransactionSynchronization.STATUS_ROLLED_BACK);
    
          // If this is true, an exception will be thrown directly,
    			if (unexpectedRollback) {
    				throw new UnexpectedRollbackException(
    						"Transaction rolled back because it has been marked as rollback-only");
    			}
    		}
    		finally {
    			cleanupAfterCompletion(status);
    		}
    	}
    

    There are many places to call this method. The value of unexpected rollback is true, which is mainly passed when the transaction is committed.

  2. Submit (abstractplatform transactionmanager #commit (transactionstatus))

    public final void commit(TransactionStatus status) throws TransactionException {
    		if (status.isCompleted()) {
    			throw new IllegalTransactionStateException(
    					"Transaction is already completed - do not call commit or rollback more than once per transaction");
    		}
    
    		DefaultTransactionStatus defStatus = (DefaultTransactionStatus) status;
      // First judge the rollback only of DefaultTransactionStatus
    		if (defStatus.isLocalRollbackOnly()) {
    			if (defStatus.isDebug()) {
    				logger.debug("Transactional code has requested rollback");
    			}
    			processRollback(defStatus, false);
    			return;
    		}
    		// true is passed here, which will determine the value of rollback only in connectionHolder.
      // shouldCommitOnGlobalRollbackOnly can also be overridden
    		if (!shouldCommitOnGlobalRollbackOnly() && defStatus.isGlobalRollbackOnly()) {
    			if (defStatus.isDebug()) {
    				logger.debug("Global transaction is marked as rollback-only but transactional code requested commit");
    			}
    			processRollback(defStatus, true);
    			return;
    		}
       // The submission operation will be done here.
    		processCommit(defStatus);
    	}
    

Compared with the code, the above example is relatively clear.

A transaction is required, and there is no need to create a new transaction. SavePoint is required (in fact, the second one is promotion_needed)

This means that a must have a transaction before B can reuse the transactions in a. Same as the previous one, but B can only be promotion_ NESTED. a(PROPAGATION_REQUIRED,PROPAGATION_REQUIRES_NEW,PROPAGATION_NESTED),b(PROPAGATION_NESTED)

Exception not handled

The operations of a and b are not effective, and the transaction is rolled back.

analysis

The reason for this is the same as the above. Without try catch, exceptions will be directly sent to a related TransactionInterceptor. Exceptions will be caught and rolled back.

Exception handled

Method b rolls back and method a executes successfully. Transaction commit, a and b use the same transaction.

analysis:

If there is a transaction, nested will execute in a nested manner. In fact, it uses savepoint to start a transaction and execute it to b. The TransactionInterceptor corresponding to b will save a savepoint in the Connection of the current transaction before calling the b method, and will release the savepoint after execution.

If b there is an exception, the rollback operation after the exception has been analyzed in the above example. As soon as it comes up, it will check whether there is a savePoint. If so, it will be rolled back to savePoint. Rollback only will not be set. For this example, the exception has been caught. The TransactionInterceptor corresponding to a does not catch the exception and does not need rollback only, so the transaction is submitted normally.

Here a and b use the same transaction. This is a nested transaction. The rollback of outer transactions will cause the rollback of inner transactions, and the rollback of memory transactions will not cause the rollback of outer transactions. This is actually wrong. There is no saying of internal and external affairs. It is a matter from the beginning to the end.

No transaction is required, but it can be executed

There are many combinations, such as method a has transactions and method b has no transactions. Such combinations include a (promotion_required, promotion_requirements_new, promotion_needed) and b (promotion_not_supported). It can also be that both a and b methods have no transactions. Such combinations include a (promotion_not_supported, promotion_never) and b (promotion_not_supported, promotion_never).

Method a has transactions and method b has no transactions

Exception not handled

b runs as if there is no transaction, and the transaction in a is rolled back

analysis:

b runs without transaction, so the sql operation of each row is transactional. Therefore, the modification in b succeeded.

However, this exception is thrown to a. the TransactionInterceptor corresponding to a will catch the exception and roll back the transaction. Therefore, the modification in a has no effect.

Exception handled

b runs as if there is no transaction, and the transaction in a is committed

analysis:

The exception has been handled in a, so the TransactionInterceptor corresponding to a has no exception and is submitted normally

Neither a nor B methods have transactions

Both a and b are executed in a non transactional manner, so there's nothing to say

Supplementary notes

Since there is no need for transactions, why not remove these two annotations directly?

These two annotations are just the absence of Connection related operations, but in Spring transactions, in addition to Connection, there are other resources, such as transaction synchronization, which will go through the process of transaction operations and support no operations in some places that really need to be rolled back.

There can be no business. If there is, an error will be reported.

This is actually promotion_ Never, if there is a transaction, it will directly report an error.

There must be a transaction, and an error must be reported.

This is actually promotion_ Mandatory. If there is no transaction, it will directly report an error, but it will not create a new transaction and will only reuse the previous transaction. Then this logic and a and b are both promotion_ Supports is almost there. The concept of long transaction.

About the blog, I take it as my notes. There are a lot of contents in it that reflect my thinking process. Because my thinking is limited, there are some differences in some contents. If there are any problems, please point them out. Discuss together. thank you.

Topics: Java MySQL Spring