Транзакция Spring по-прежнему откатывается после исключения исключения

задняя часть Spring
Транзакция Spring по-прежнему откатывается после исключения исключения

передовой

Авария на производстве наводит на размышления. После ручного перехвата исключений в декларативной транзакции Spring фактически определено, что она подлежит откату. Что это за операция? Нечего сказать, сразу к коду

@Service
public class A {

    @Autowired
    private B b;

    @Autowired
    private C c;

    @Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.DEFAULT)
    public void operate() {
        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() {
        return bm.insert("B");
    }
}

@Service
public class C {

    @Autowired
    private CM cm;

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

постановка задачи

Ну, код выше все видели.В нормальных условиях мы вставляем часть данных в таблицу B и таблицу C, так что же происходит, когда код ненормальный?

Теперь мы предполагаем, что B успешно вставляет данные, но C не может вставить данные. В это время исключение будет выброшено для A, которое будет вставлено в A.operateЗахваченный методом try-cache метода, обычно в это время B может вставить запись в базу данных, но таблица C не вставляется, что мы и ожидаем, но на самом деле это не так, фактическая ситуация такова, что B таблица не вставляет данные, таблица C Данные не были вставлены, что означает, что Spring откатил всю операцию.

будь осторожен

Если код немного изменится, поместите try-cache вinsertCВ блоке кода по тому же сценарию запись будет успешно вставлена ​​в B

Состояние фронта точки знаний

Если вы понимаете механизм распространения Spring, вы можете пропустить его напрямую.

Сначала нам нужно выяснить, что находится в SpringREQUIREDроль

ТРЕБУЕТСЯ: Если текущей транзакции нет, создать новую транзакцию, если есть текущая транзакция, присоединиться к текущей транзакции

То есть, когда наш механизм распространения одновременноREQUIREDКогда транзакция A, B и C является общей, операция фиксации или отката будет выполняться только после завершения процесса A, а фиксация и откат не будут выполняться во время выполнения B или C.

Отслеживание проблем

Что ж, с некоторым запасом знаний, давайте посмотрим исходники вместе

Сначала мы находим запись прокси для транзакций SpringTransactionInterceptor, когда мы вызываемoperateметод будет вызыватьсяTransactionInterceptorизinvokeметод, это точка входа всей сделки, давайте посмотрим непосредственно на ключевые точкиinvokeсерединаinvokeWithinTransactionметод

//获取事务属性类 AnnotationTransactionAttributeSource
TransactionAttributeSource tas = getTransactionAttributeSource();
//获取事务属性
final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
//获取事务管理器
final TransactionManager tm = determineTransactionManager(txAttr);

PlatformTransactionManager ptm = asPlatformTransactionManager(tm);
//获取joinpoint
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 (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);
		}
	}

	//事务提交
	commitTransactionAfterReturning(txInfo);
	return retVal;
}

Я опустил неважный код, поэтому давайте посмотрим на процесс.Вышеприведенный код явно отражен.Когда во время выполнения нашей программы будет выброшено исключение, оно будет вызваноcompleteTransactionAfterThrowingОперация отката, если не возникает исключения, в конечном итоге вызовет фиксацию транзакции.commitTransactionAfterReturning, давайте проанализируем

img

Нормальная ситуация, когда в C происходит исключение, и выполнение прибываетcompleteTransactionAfterThrowingТранзакция откатывается, но поскольку это не вновь созданная транзакция, а присоединенная транзакция, операция отката не будет запущена, и исключение будет перехвачено в A, и в конечном итоге оно перейдет кcommitTransactionAfterReturningТранзакция совершается, это правда?

На самом деле так оно и есть,тогда странно.Я явно представил,но почему он откатился?Давайте дальше смотреть вниз.

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

	//重点看.. DataSourceTransactionObject拿到对象
	Object transaction = doGetTransaction();
	boolean debugEnabled = logger.isDebugEnabled();

	//第一次进来connectionHolder为空的, 所以不存在事务
	if (isExistingTransaction(transaction)) {
		// Existing transaction found -> check propagation behavior to find out how to behave.
		//如果不是第一次进来, 则会走这个逻辑
		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'");
	}
	//第一次进来大部分会走这里(传播属性是 Required | Requested New | Nested)
	else if (def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED ||
			def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW ||
			def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) {
		//先挂起
		SuspendedResourcesHolder suspendedResources = suspend(null);
		if (debugEnabled) {
			logger.debug("Creating new transaction with name [" + def.getName() + "]: " + def);
		}
		try {
			//开启事务
			return startTransaction(def, transaction, debugEnabled, suspendedResources);
		} catch (RuntimeException | Error ex) {
			resume(null, suspendedResources);
			throw ex;
		}
	} 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);
	}
}

Этот код является кодом для запуска транзакции. Давайте посмотрим, когда наш A впервые пришел, в это время не было транзакции, поэтомуisExistingTransactionМетод не установлен, идем вниз, потому что наш механизм распространения НЕОБХОДИМ, поэтому мы перейдем кstartTransactionметод

private TransactionStatus startTransaction(TransactionDefinition definition, Object transaction, boolean debugEnabled, @Nullable SuspendedResourcesHolder suspendedResources) {
	boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);
	//创建一个新的事务状态, 注意这里的newTransaction 属性为true
	DefaultTransactionStatus status = newTransactionStatus(
			definition, transaction, true, newSynchronization, debugEnabled, suspendedResources);
	//开启事务
	doBegin(transaction, definition);
	//开启事务后, 改变事务状态
	prepareSynchronization(status, definition);
	return status;
}

Ну, здесь нам просто нужно сосредоточиться на одном моменте, и этоnewTransactionStatusтретий параметрnewTransaction, только когда мы создаем новую транзакциюtrue,Это свойство очень важно, мы будем использовать его позже

Ну вот первая транзакция завершена, а дальше будем вызывать бизнес-логику.При вызове insertB мы перейдем кgetTransaction, продолжаем смотреть на него, на этот разisExistingTransactionВы можете получить значение, потому что A уже создал транзакцию для нас, и она будет вызвана в это время.handleExistingTransactionметод

//如果第二次进来还是PROPAFGATION_REQUIRED, 走这里, newTransation为false
return prepareTransactionStatus(definition, transaction, false, newSynchronization, debugEnabled, null);

Полезным кодом для REQUIRED является это предложение, а все остальные читать не нужно.Так же мы видим третий параметр newTransaction, который здесь false, указывая на то, что предыдущая транзакция добавлена, а не заново создана вами, а затем бизнес-код выполнен, наконец, переходим к коммиту, посмотрим, что делается в коммите

//如果有回滚点
if (status.hasSavepoint()) {
	if (status.isDebug()) {
		logger.debug("Releasing transaction savepoint");
	}
	unexpectedRollback = status.isGlobalRollbackOnly();
	status.releaseHeldSavepoint();
}
//如果是新事务, 则提交事务
else if (status.isNewTransaction()) {
	if (status.isDebug()) {
		logger.debug("Initiating transaction commit");
	}
	unexpectedRollback = status.isGlobalRollbackOnly();
	doCommit(status);
}
else if (isFailEarlyOnGlobalRollbackOnly()) {
	unexpectedRollback = status.isGlobalRollbackOnly();
}

Ничего не делает, почему? потому что нашnewTransactionневерно, поэтому, когда наш код находится вoperateОн придет сюда после выполнения всех методов.

Хорошо, посмотримinsertC, предыдущий процесс точно такой же, мы прямо видим код отката

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

нашinsertCТот же метод, который он не соответствует правду, это в конечном итоге придетdoSetRollbackOnly,Сюдавысший приоритет, и, наконец, вызовите этот фрагмент кода

public void setRollbackOnly() {
	this.rollbackOnly = true;
}

Затем мы выполним код нашего ключа Aoperateотправил код

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

	//执行事务提交
	processCommit(defStatus);
}

Ну, все понимают, когда вы видите это, в коммите, Spring рассудитdefStatus.isGlobalRollbackOnlyБыло ли выброшено исключение, которое было перехвачено Spring, если это так, то операция фиксации не будет выполнена, а вместо этого будет выполнено выполнениеprocessRollbackоперация отката

Суммировать

В Spring REQUIRED, пока Spring перехватывает исключение, Spring в конечном итоге откатит всю транзакцию, даже если она была перехвачена в бизнесе.

Итак, вернемся к исходному коду.Если мы хотим, чтобы Spring не откатывался, то нам нужно всего лишь поставить метод try-cache в метод insertC, потому что выброшенное в это время исключение не будет перехвачено Spring.