At the forefront of

A production error occurred in Spring’s declarative transaction, and a rollback was determined. Without further ado, get right to the code

@Service
public class A {

    @Autowired
    private B b;

    @Autowired
    private C c;

    @Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.DEFAULT)
    public void operate(a) {
        try {
            b.insertB();
            c.insertC();
        }catch(Exception e) { e.printStackTrace(); }}}@Service
public class B {

    @Autowired
    private BM bm;

    @Transactional(propagation = Propagation.REQUIRED)
    public int insertB(a) {
        return bm.insert("B"); }}@Service
public class C {

    @Autowired
    private CM cm;

    @Transactional(propagation = Propagation.REQUIRED)
    public int insertC(a) {
        return cm.insert("C"); }}Copy the code

The problem this paper

Ok, so you can see the above code. In the normal case, we would insert one data into table B and one data into table C, but what happens when the code fails?

We now assume that B successfully inserted data, but C failed to insert data. The exception is thrown to A, which is caught by the try-cache of the operate method in A. Normally, B would insert A record, while C failed. In fact, no data is inserted into table B and no data is inserted into table C, meaning that the entire operation is rolled back by Spring

Pay attention to the point

If the code is slightly changed and try-cache is placed inside the insertC block, a record will be successfully inserted into B in the same scenario

Knowledge preconditions

Those who understand Spring’s propagation mechanism can skip it

First, we need to understand the role of REQUIRED in Spring

REQUIRED: Create a new transaction if there are no transactions currently available, and join the current transaction if one already exists

In other words, when the propagation mechanism is REQUIRED at the same time, the transactions of A, B, and C share the same transaction. A COMMIT or rollback operation is performed only when all the processes of A are completed. The commit or rollback operation is not performed during B or C

The problem tracking

Good, have certain knowledge reserve, we look at the source code together

We first find the TransactionInterceptor proxy entry for the Spring transaction. When we call the Operate method in class A, we call the TransactionInterceptor invoke method, which is the entry to the entire transaction. Let’s go straight to the invokeWithinTransaction method in the focus invoke

/ / get the transaction attribute class AnnotationTransactionAttributeSource
TransactionAttributeSource tas = getTransactionAttributeSource();
// Get transaction attributes
finalTransactionAttribute txAttr = (tas ! =null ? tas.getTransactionAttribute(method, targetClass) : null);
// Get the transaction manager
final TransactionManager tm = determineTransactionManager(txAttr);

PlatformTransactionManager ptm = asPlatformTransactionManager(tm);
/ / get the joinpoint
final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);
// The annotation transaction will go here
if (txAttr == null| |! (ptminstanceof CallbackPreferringPlatformTransactionManager)) {
	// Standard transaction demarcation with getTransaction and commit/rollback calls.
	// Start the transaction
	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
		// Transaction rollback
		completeTransactionAfterThrowing(txInfo, ex);
		throw ex;
	} finally {
		cleanupTransactionInfo(txInfo);
	}

	if(retVal ! =null && 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); }}// Transaction commit
	commitTransactionAfterReturning(txInfo);
	return retVal;
}
Copy the code

Not important code I have omitted, well let’s take a look at this process, the above code clearly reflect, when we are in the process of program execution to throw exceptions when they call to completeTransactionAfterThrowing rollback operations, If no exception occurs you will call transaction commit commitTransactionAfterReturning, we have to analyze

Normal is C an exception occurs, and execute the completeTransactionAfterThrowing transaction rollback, but because not the affairs of the newly created, but join transaction so will not trigger A rollback operation, and in A captured the exception, And eventually go commitTransactionAfterReturning transaction commit, the fact is that so?

In fact, that’s what happened. That’s weird. I submitted it, but it rolled back. Let’s move on, okay

public final TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException {
	// Use defaults if no transaction definition given.TransactionDefinition def = (definition ! =null ? definition : TransactionDefinition.withDefaults());

	// Focus on... DataSourceTransactionObject get object
	Object transaction = doGetTransaction();
	boolean debugEnabled = logger.isDebugEnabled();

	// The connectionHolder is empty the first time, so there is no transaction
	if (isExistingTransaction(transaction)) {
		// Existing transaction found -> check propagation behavior to find out how to behave.
		// If it is not the first time to enter, this logic will be used
		return handleExistingTransaction(def, transaction, debugEnabled);
	}

	// Check definition settings for new transaction.
	if (def.getTimeout() < TransactionDefinition.TIMEOUT_DEFAULT) {
		throw new InvalidTimeoutException("Invalid transaction timeout", def.getTimeout());
	}

	// No existing transaction found -> check propagation behavior to find out how to proceed.
	if (def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_MANDATORY) {
		throw new IllegalTransactionStateException(
				"No existing transaction found for transaction marked with propagation 'mandatory'");
	}
	// The first time the user enters the database, most of them will go here. (The propagation attribute is Required | New | Nested)
	else if (def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED ||
			def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW ||
			def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) {
		/ / hang up first
		SuspendedResourcesHolder suspendedResources = suspend(null);
		if (debugEnabled) {
			logger.debug("Creating new transaction with name [" + def.getName() + "]." + def);
		}
		try {
			// Start the transaction
			return startTransaction(def, transaction, debugEnabled, suspendedResources);
		} catch (RuntimeException | Error ex) {
			resume(null, suspendedResources);
			throwex; }}else {
		// Create "empty" transaction: no actual transaction, but potentially synchronization.
		if(def.getIsolationLevel() ! = TransactionDefinition.ISOLATION_DEFAULT && logger.isWarnEnabled()) { logger.warn("Custom isolation level specified but no actual transaction initiated; " +
					"isolation level will effectively be ignored: " + def);
		}
		boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS);
		return prepareTransactionStatus(def, null.true, newSynchronization, debugEnabled, null); }}Copy the code

So this is the code that starts the transaction, so let’s see, when we first walk in here, there’s no transaction, so the isExistingTransaction method doesn’t work, and then down here, because our propagation mechanism is REQUIRED, we’re going to go to the startTransaction method

private TransactionStatus startTransaction(TransactionDefinition definition, Object transaction, boolean debugEnabled, @Nullable SuspendedResourcesHolder suspendedResources) {
	booleannewSynchronization = (getTransactionSynchronization() ! = SYNCHRONIZATION_NEVER);// create a newTransaction state, noting that the newTransaction attribute is true
	DefaultTransactionStatus status = newTransactionStatus(
			definition, transaction, true, newSynchronization, debugEnabled, suspendedResources);
	// Start the transaction
	doBegin(transaction, definition);
	// Change the transaction state after the transaction is started
	prepareSynchronization(status, definition);
	return status;
}
Copy the code

Okay, so there’s only one thing we need to focus on here and that’s newTransaction, the third parameter of newTransactionStatus, which is only true when we create a newTransaction, and that’s an important property, and we’ll use it later

Okay, so at this point the first transaction is done, and then we’re going to call the business logic, and when we call insertB, we’re going to go to getTransaction, and we’re going to keep looking at it, and we’re going to get the value of isExistingTransaction, because A has already created the transaction for us, Will call to handleExistingTransaction method at this time

// If PROPAFGATION_REQUIRED comes in the second time, go here and newTransation is false
return prepareTransactionStatus(definition, transaction, false, newSynchronization, debugEnabled, null);
Copy the code

NewTransaction (false) is a transaction that has been added to a previous transaction, and is not a newTransaction created by the transaction itself. Let’s look at what happens in commit

// If there are rollback points
if (status.hasSavepoint()) {
	if (status.isDebug()) {
		logger.debug("Releasing transaction savepoint");
	}
	unexpectedRollback = status.isGlobalRollbackOnly();
	status.releaseHeldSavepoint();
}
// If it is a new transaction, the transaction is committed
else if (status.isNewTransaction()) {
	if (status.isDebug()) {
		logger.debug("Initiating transaction commit");
	}
	unexpectedRollback = status.isGlobalRollbackOnly();
	doCommit(status);
}
else if (isFailEarlyOnGlobalRollbackOnly()) {
	unexpectedRollback = status.isGlobalRollbackOnly();
}
Copy the code

It didn’t do anything. Why? Because our newTransaction is not true, our code will arrive here only after the operate method has fully executed

Ok, so let’s look at insertC, and we’re doing exactly the same thing, we’re looking at the rollback code

private void processRollback(DefaultTransactionStatus status, boolean unexpected) {
	try {
		boolean unexpectedRollback = unexpected;

		try {
			triggerBeforeCompletion(status);

			if (status.hasSavepoint()) {
				if (status.isDebug()) {
					logger.debug("Rolling back transaction to savepoint");
				}
				status.rollbackToHeldSavepoint();
			} else if (status.isNewTransaction()) {
        if (status.isDebug()) {
					logger.debug("Initiating transaction rollback");
				}
				doRollback(status);
			} else {
				// Participating in larger transaction
				if (status.hasTransaction()) {
					if (status.isLocalRollbackOnly() || isGlobalRollbackOnParticipationFailure()) {
						if (status.isDebug()) {
							logger.debug("Participating transaction failed - marking existing transaction as rollback-only");
						}
						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);

		// Raise UnexpectedRollbackException if we had a global rollback-only marker
		if (unexpectedRollback) {
			throw new UnexpectedRollbackException(
					"Transaction rolled back because it has been marked as rollback-only"); }}finally{ cleanupAfterCompletion(status); }}Copy the code

Our insertC method also doesn’t have true newTransaction, so we end up with doSetRollbackOnly, which is the most important method, and we end up calling this code

public void setRollbackOnly(a) {
	this.rollbackOnly = true;
}
Copy the code

We will then execute the Commit code to Operate in our key code A

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;
	if (defStatus.isLocalRollbackOnly()) {
		if (defStatus.isDebug()) {
			logger.debug("Transactional code has requested rollback");
		}
		processRollback(defStatus, false);
		return;
	}

	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;
	}

	// Perform the transaction commit
	processCommit(defStatus);
}
Copy the code

Ok, see the everyone see, in the commit, Spring will to judge defStatus. IsGlobalRollbackOnly ever thrown exception was intercepted by the Spring, if you have, so will not perform a commit operation, Instead, perform the processRollback rollback operation

conclusion

In Spring’s REQUIRED, whenever an exception has been caught by Spring, Spring eventually rolls back the entire transaction, even if it has been caught in the business

So back to the original code, if we wanted Spring not to roll back, we could just insert the try-cache method into the insertC method, since the exception thrown would not be intercepted by Spring