@Transactional annotation in Spring

Preface

During the development last week, I encountered transaction-related problems. The test environment was normal but the deployment to the formal environment throws an exception. I worked overtime for several days to solve the problem. Now I will review the problem and review the previous knowledge points. . If there are any errors, please correct me.

What is a transaction

The transaction of the database is a mechanism, a sequence of operations, including database operation commands. The transaction submits or cancels the operation request to the system together with all the commands as a whole, that is, this group of commands either succeeds or fails.

4 characteristics of transactions (ACID):

  1. Atomicity

A transaction is a complete operation. The elements in the transaction are indivisible. The elements in the transaction must be committed or rolled back as a whole. If any element in the transaction fails, the entire transaction will fail.
2. Consistency

Before the transaction begins, the data must be in a consistent state; after the transaction ends, the state of the data must also remain consistent. Modifications made to the data through transactions cannot damage the data.
3. Isolation

The execution of the transaction is not interfered by other transactions, and the intermediate results of the execution of the transaction must be transparent to other transactions.
4. Persistence

For committed transactions, the system must ensure that the changes made by the transaction to the database are not lost, even if the database fails.

How to implement transactions

In the current business development process, Spring framework is the mainstay. Spring supports two ways of transaction management 编程式事务and声明式事务

Programmatic transaction

The so-called programmatic transaction is to manually complete the commit of the transaction in the code, and roll back when an exception occurs.

Inject in the implementation classPlatformTransactionManager

    @Autowired
    private PlatformTransactionManager transactionManager;

    @Override
    public Map<String, Object> saveResource(MultipartFile file) {

        TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
        try {
            // 相关业务
            
            // 手动提交
            transactionManager.commit(status);
        } catch (Exception e) {
            log.error("Exception:{}", ExceptionUtil.stacktraceToString(e));
            // 发生异常时进行回滚
            transactionManager.rollback(status);
        }
    }

Declarative transaction

The so-called declarative transaction is the use of @Transactionalannotations to open the transaction. The annotation can be placed on the class and method. When placed on the class, all public methods of the class will open the transaction; when placed on the method, it means that the current method supports transactions.

	@Transactional
	@Override
    public Map<String, Object> saveResource(MultipartFile file) {
            // 相关业务
    }

@Transactionalannotation

The method marked with this annotation completes the rollback and commit of the transaction through AOP

Insert picture description here

First DispatcherServletfind the right Controller in, and then add interceptors through CgLib. In the class ReflectiveMethodInvocation, there are a variety of interceptor implementations, such as: parameter verification, AOP pre-interception, post-interception, exception-throwing interception, surround interception, etc., There is also a TransactionInterceptortransaction interceptor

Insert picture description here

Call invoke()method

Insert picture description here

The general execution logic of the transaction

protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,final InvocationCallback invocation) throws Throwable {
		...
		PlatformTransactionManager ptm = asPlatformTransactionManager(tm);
		// 切点
		final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);

		if (txAttr == null || !(ptm instanceof CallbackPreferringPlatformTransactionManager)) {
			// Standard transaction demarcation with getTransaction and commit/rollback calls.
			// 创建事务
			TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);

			// 环绕切点执行业务逻辑
			Object retVal;
			try {
				// This is an around advice: Invoke the next interceptor in the chain.
				// This will normally result in a target object being invoked.
				retVal = invocation.proceedWithInvocation();
			}
			catch (Throwable ex) {
				// target invocation exception
				// 执行过程中发生异常,执行回滚
				completeTransactionAfterThrowing(txInfo, ex);
				throw ex;
			}
			finally {
				cleanupTransactionInfo(txInfo);
			}

			if (vavrPresent && VavrDelegate.isVavrTry(retVal)) {
				// Set rollback-only in case of Vavr failure matching our rollback rules...
				TransactionStatus status = txInfo.getTransactionStatus();
				if (status != null && txAttr != null) {
					retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status);
				}
			}
			// 正常执行,提交事务
			commitTransactionAfterReturning(txInfo);
			return retVal;
		}
		...
}

completeTransactionAfterThrowing()method

	protected void completeTransactionAfterThrowing(@Nullable TransactionInfo txInfo, Throwable ex) {
		// 判断事务状态不为空的情况下
		if (txInfo != null && txInfo.getTransactionStatus() != null) {
		// 输出debug日志
			if (logger.isTraceEnabled()) {
				logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() +
						"] after exception: " + ex);
			}
			// 在指定异常下回滚
			if (txInfo.transactionAttribute != null && txInfo.transactionAttribute.rollbackOn(ex)) {
				try {
					// 开始回滚
					txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());
				}
				catch (TransactionSystemException ex2) {
					logger.error("Application exception overridden by rollback exception", ex);
					ex2.initApplicationException(ex);
					throw ex2;
				}
				catch (RuntimeException | Error ex2) {
					logger.error("Application exception overridden by rollback exception", ex);
					throw ex2;
				}
			}
			// 不是指定的异常,任然提交
			else {
				// We don't roll back on this exception.
				// Will still roll back if TransactionStatus.isRollbackOnly() is true.
				try {
					txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
				}
				catch (TransactionSystemException ex2) {
					logger.error("Application exception overridden by commit exception", ex);
					ex2.initApplicationException(ex);
					throw ex2;
				}
				catch (RuntimeException | Error ex2) {
					logger.error("Application exception overridden by commit exception", ex);
					throw ex2;
				}
			}
		}
	}

commitTransactionAfterReturning()method

// 结束执行,且没有抛出异常,就执行提交
protected void commitTransactionAfterReturning(@Nullable TransactionInfo txInfo) {
		if (txInfo != null && txInfo.getTransactionStatus() != null) {
			if (logger.isTraceEnabled()) {
				logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() + "]");
			}
			txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
		}
	}

Common attribute configuration

propagation

Configure the propagation characteristics of the transaction, the default is: required

Spreadingdescription
requiredExecute in a transaction state, if not, create a new transaction
required_newsCreate a new transaction, and suspend the current transaction if there is a current transaction
supportsIf there is a transaction, it will run under the current transaction, and if there is no transaction, it will run in a no-transaction state.
not_supportedRun in a transaction-free state, if there is a transaction, suspend the current transaction
mandatoryThere must be a transaction, if there is no transaction, an exception is thrownIllegalTransactionStateException
neverDoes not support transactions, throws an exception if there is a transaction
nestedIf there is a current transaction, start another transaction in the current transaction

isolation

Configure transaction isolation level, the default is the default isolation level of the current database (MySQL is REPEATABLE-READ)

View the isolation level of the data

Insert picture description here
Isolationdescription
READ UNCOMMITTED (uncommitted degree)Read uncommitted content, all transactions can see the results of other uncommitted transactions, seldom actually used (dirty reads will occur)
READ COMMITTED (commit to read)A transaction can only read the modified data of the committed transaction of another transaction, and every time other transactions modify and commit the data, the transaction can query the latest value
REPEATABLE READ (repeatable read)One is that the transaction reads the modified data that has been committed by the transaction. When a record is read for the first time, even when other transactions modify the record and commit, it is still the first time to read this record later. Instead of reading different data each time
SERIALIZABLE (Serialization)The transaction is executed serially, without trampling, avoiding dirty reads, phantom reads, and non-repeatability, but the efficiency is low

timeout

Configure the transaction timeout time, the default is: -1

readOnly

When the method only needs to query data, configure to true

rollbackFor

The configuration needs to roll back data in those abnormal situations. By default, it only rolls back RuntimeExceptionand Error. The best configuration in development isException

Problems encountered during development

Large transaction causes transaction timeout

There was a function last week, including FTP file upload and database operation. I don't know if it is due to the network. It takes a long time for FTP to transfer files to the other party's server, which makes subsequent write database operations impossible. Later, by splitting the function, file uploading is not executed inside the transaction, and just moved to the outer layer.

Transaction failure

  1. Non-public methods fail

In the source code, it returns null for non-public execution, and does not support non-public transaction support

Insert picture description here
  1. rollbackFor is configured as default

After configuring rollbackFor as the default value, when an unchecked exception is thrown, the transaction cannot be rolled back

  1. cacheExceptions are caught in the method summary marked with annotations , and there is no basis for throwing
        Resource resource = new Resource();
        resource.setCreateUser("admin");
        try {
            resourceMapper.insertUseGeneratedKeys(resource);
        } catch (Exception e) {
            // 在日志中输出堆栈信息,并抛出异常
            log.error("Exception:{}", ExceptionUtil.stacktraceToString(e));
            throw new RuntimeException("系统错误");
        }
  1. An engine that does not support transactions is used

The engine that supports transactions in MySQL isinnodb

Reference article

  1. JavaGuide (gitee.io)
  2. Say 6 in one breath, @Transactional annotated failure scenarios (juejin.cn)
  3. Concepts and characteristics of database transactions (biancheng.net)
  4. MySQL :: MySQL 5.7 Reference Manual :: 14.7.2.1 Transaction Isolation Levels

Read the original