今天是二零二四年二月十八,农历正月初九。同时今天也是农历新年假期后的第一个工作日。我的内心既兴奋,又担忧,更急躁。兴奋是因为假期后的第一个工作日工作轻松;担忧是因为经过了这么长时间,我依旧没搞明白Spring事务,总觉得有些东西没搞懂,却又不知道是哪里,所以只能在这里徘徊不前;急躁是因为许多事情都要用钱去解决,自己那微薄的收入根本就是杯水车薪,而自己又不知道用哪种方式去筹集足够的资金来解决问题,所以只能整日急不可耐,而问题依旧。到底该怎么办呢?∙∙∙∙∙∙
针对第二种心理,我觉得没有其他办法,只能继续梳理,但也不能因此而放弃后面知识点的梳理。今天就让我们来聊一下Spring事务的几个面试题吧!我想围绕下面这样几个问题展开:
- 《Spring事务原理总结四》这篇文章中提到的将TransactionInfo绑定到当前线程的意义是什么呢?
- Spring是如何解决事务嵌套的?
首先回忆一下前面几篇文章的主要内容:《Spring事务原理总结一》主要梳理了事务的基本概念及特征,同时也梳理了Spring事务的基本用法;《Spring事务原理总结二》则主要梳理了Spring框架解析及注册事务代理的流程;《Spring事务原理总结三》则主要梳理了Spring事务的一些核心组件及其继承结构;《Spring事务原理总结四》梳理了Spring事务的执行流程;《Spring事务原理总结五》主要梳理并回答了前一篇文章中遗留的几个问题;《Spring事务原理总结六》主要梳理了Spring事务的异常回滚流程。接着让我们试着利用这些文章梳理的内容来回答这两个问题。
首先看第一个问题,将TransactionInfo对象绑定到当前线程的操作,在《Spring事务原理总结四》这篇文章中有提到,如下图所示:
当时只是说了这些代码位于TransactionAspectSupport类中,不过并没有深究其意义。现在让我们细化一下这个说法,然后在梳理完第二个问题后来回答这个问题。TransactionInfos是TransactionAspectSupport类中的最终静态内部类(该类内部定义了bindToThread()方法和restoreThreadLocalStatus()方法。所以截图中说前者位于TransactionAspectSupport类的说法也是对的,因为其所在的类位于TransactionAspectSupport类中),其源码如下所示:
protected static final class TransactionInfo {
@Nullable
private final PlatformTransactionManager transactionManager;
@Nullable
private final TransactionAttribute transactionAttribute;
private final String joinpointIdentification;
@Nullable
private TransactionStatus transactionStatus;
@Nullable
private TransactionInfo oldTransactionInfo;
public TransactionInfo(@Nullable PlatformTransactionManager transactionManager,
@Nullable TransactionAttribute transactionAttribute, String joinpointIdentification) {
this.transactionManager = transactionManager;
this.transactionAttribute = transactionAttribute;
this.joinpointIdentification = joinpointIdentification;
}
public PlatformTransactionManager getTransactionManager() {
Assert.state(this.transactionManager != null, "No PlatformTransactionManager set");
return this.transactionManager;
}
@Nullable
public TransactionAttribute getTransactionAttribute() {
return this.transactionAttribute;
}
/**
* Return a String representation of this joinpoint (usually a Method call)
* for use in logging.
*/
public String getJoinpointIdentification() {
return this.joinpointIdentification;
}
public void newTransactionStatus(@Nullable TransactionStatus status) {
this.transactionStatus = status;
}
@Nullable
public TransactionStatus getTransactionStatus() {
return this.transactionStatus;
}
/**
* Return whether a transaction was created by this aspect,
* or whether we just have a placeholder to keep ThreadLocal stack integrity.
*/
public boolean hasTransaction() {
return (this.transactionStatus != null);
}
private void bindToThread() {
// Expose current TransactionStatus, preserving any existing TransactionStatus
// for restoration after this transaction is complete.
this.oldTransactionInfo = transactionInfoHolder.get();
transactionInfoHolder.set(this);
}
private void restoreThreadLocalStatus() {
// Use stack to restore old transaction TransactionInfo.
// Will be null if none was set.
transactionInfoHolder.set(this.oldTransactionInfo);
}
@Override
public String toString() {
return (this.transactionAttribute != null ? this.transactionAttribute.toString() : "No transaction");
}
}
这个类上有bindToThread()方法和restoreThreadLocalStatus()两个方法,它们的主要作用就是更改当前线程持有的ThreadLocal上的TransactionInfo值。
下面让我们来看一下第二个问题,要回答这个问题,需要从TransactionInterceptor的invoke(MethodInvocation)方法看起,这个方法的代码如下所示:
@Nullable
protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
final InvocationCallback invocation) throws Throwable {
// If the transaction attribute is null, the method is non-transactional.
TransactionAttributeSource tas = getTransactionAttributeSource();
final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
final TransactionManager tm = determineTransactionManager(txAttr);
// ……
// 这里删除了一些无用代码
PlatformTransactionManager ptm = asPlatformTransactionManager(tm);
final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);
if (txAttr == null || !(ptm instanceof CallbackPreferringPlatformTransactionManager cpptm)) {
// 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 && txAttr != null) {
TransactionStatus status = txInfo.getTransactionStatus();
if (status != null) {
if (retVal instanceof Future<?> future && future.isDone()) {
try {
future.get();
}
catch (ExecutionException ex) {
if (txAttr.rollbackOn(ex.getCause())) {
status.setRollbackOnly();
}
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
else if (vavrPresent && VavrDelegate.isVavrTry(retVal)) {
// Set rollback-only in case of Vavr failure matching our rollback rules...
retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status);
}
}
}
commitTransactionAfterReturning(txInfo);
return retVal;
}
else {
Object result;
final ThrowableHolder throwableHolder = new ThrowableHolder();
// It's a CallbackPreferringPlatformTransactionManager: pass a TransactionCallback in.
try {
result = cpptm.execute(txAttr, status -> {
TransactionInfo txInfo = prepareTransactionInfo(ptm, txAttr, joinpointIdentification, status);
try {
Object retVal = invocation.proceedWithInvocation();
if (retVal != null && vavrPresent && VavrDelegate.isVavrTry(retVal)) {
// Set rollback-only in case of Vavr failure matching our rollback rules...
retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status);
}
return retVal;
}
catch (Throwable ex) {
if (txAttr.rollbackOn(ex)) {
// A RuntimeException: will lead to a rollback.
if (ex instanceof RuntimeException runtimeException) {
throw runtimeException;
}
else {
throw new ThrowableHolderException(ex);
}
}
else {
// A normal return value: will lead to a commit.
throwableHolder.throwable = ex;
return null;
}
}
finally {
cleanupTransactionInfo(txInfo);
}
});
}
catch (ThrowableHolderException ex) {
throw ex.getCause();
}
catch (TransactionSystemException ex2) {
if (throwableHolder.throwable != null) {
logger.error("Application exception overridden by commit exception", throwableHolder.throwable);
ex2.initApplicationException(throwableHolder.throwable);
}
throw ex2;
}
catch (Throwable ex2) {
if (throwableHolder.throwable != null) {
logger.error("Application exception overridden by commit exception", throwableHolder.throwable);
}
throw ex2;
}
// Check result state: It might indicate a Throwable to rethrow.
if (throwableHolder.throwable != null) {
throw throwableHolder.throwable;
}
return result;
}
}
当调用事务代理对象时,程序会经过判断后,走进if (txAttr == null || !(ptm instanceof CallbackPreferringPlatformTransactionManager cpptm))分支。首先让我们考虑一下非事务嵌套的操作场景,if分支中的retVal = invocation.proceedWithInvocation();就表示业务代码执行完成了,后面就是释放资源(即调用cleanupTransactionInfo(txInfo)方法)、提交事务(则是指调用commitTransactionAfterReturning(txInfo)方法)等一系列常规操作。这样整个事务和业务处理逻辑就执行完成了。接着再来考虑一下事务嵌套的处理场景(即需要事务的业务处理代码中调用了另外一个需要事务的业务代码),这个时候第一次进入if分支的程序执行完retVal = invocation.proceedWithInvocation();后,会再次进入到if (txAttr == null || !(ptm instanceof CallbackPreferringPlatformTransactionManager cpptm))分支,这个时候我们需要关注if分支中的TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification)这句,先来看一下这个被调用的方法(createTransactionIfNecessary())及其关联方法(prepareTransactionInfo())的源码:
protected TransactionInfo createTransactionIfNecessary(@Nullable PlatformTransactionManager tm,
@Nullable TransactionAttribute txAttr, final String joinpointIdentification) {
// If no name specified, apply method identification as transaction name.
if (txAttr != null && txAttr.getName() == null) {
txAttr = new DelegatingTransactionAttribute(txAttr) {
@Override
public String getName() {
return joinpointIdentification;
}
};
}
TransactionStatus status = null;
if (txAttr != null) {
if (tm != null) {
status = tm.getTransaction(txAttr);
}
else {
if (logger.isDebugEnabled()) {
logger.debug("Skipping transactional joinpoint [" + joinpointIdentification +
"] because no transaction manager has been configured");
}
}
}
return prepareTransactionInfo(tm, txAttr, joinpointIdentification, status);
}
//
protected TransactionInfo prepareTransactionInfo(@Nullable PlatformTransactionManager tm,
@Nullable TransactionAttribute txAttr, String joinpointIdentification,
@Nullable TransactionStatus status) {
TransactionInfo txInfo = new TransactionInfo(tm, txAttr, joinpointIdentification);
if (txAttr != null) {
// We need a transaction for this method...
if (logger.isTraceEnabled()) {
logger.trace("Getting transaction for [" + txInfo.getJoinpointIdentification() + "]");
}
// The transaction manager will flag an error if an incompatible tx already exists.
txInfo.newTransactionStatus(status);
}
else {
// The TransactionInfo.hasTransaction() method will return false. We created it only
// to preserve the integrity of the ThreadLocal stack maintained in this class.
if (logger.isTraceEnabled()) {
logger.trace("No need to create transaction for [" + joinpointIdentification +
"]: This method is not transactional.");
}
}
// We always bind the TransactionInfo to the thread, even if we didn't create
// a new transaction here. This guarantees that the TransactionInfo stack
// will be managed correctly even if no transaction was created by this aspect.
txInfo.bindToThread();
return txInfo;
}
这时我们主要关注prepareTransactionInfo()方法中的txInfo.bindToThread(),关于这个被调用的方法的详情可以看参看前面的源码。这里再贴一下图片:
从图中不难看出,这个方法会先从当前线程的ThreadLocal中拿出一个TransactionInfo对象,并将其赋值给TransactionInfo的oldTransactionInfo属性,然后将新的TransactionInfo对象重新赋值到当前线程的ThreadLocal中。接着就是就是继续调用retVal = invocation.proceedWithInvocation(),然后调用cleanupTransactionInfo(txInfo)方法,最后再调用commitTransactionAfterReturning(txInfo)方法。这里我们再啰嗦一下cleanupTransactionInfo ()方法,其源码如下图所示:
protected void cleanupTransactionInfo(@Nullable TransactionInfo txInfo) {
if (txInfo != null) {
txInfo.restoreThreadLocalStatus();
}
}
该方法最终调用的是TransactionInfo类中的restoreThreadLocalStatus()方法,其源码如下图所示:
说白了就是将当前TransactionInfo对象中的oldTransactionInfo对象重新赋值到当前线程的ThreadLocal对象中,这个对象就是嵌套事务的上一层事务。至此我们就可以用自己的语言来回答前面的两个问题了!
总的来看,将TransactionInfo绑定到当前线程的主要目的就是解决嵌套事务的。Spring解决嵌套的主要思路就是先将当前的TransactionInfo对象绑定到当前线程,当当前TransactionInfo对应的业务处理代码调用其他事务代理时,会将当前线程中保存的TransactionInfo对象赋值给新的TransactionInfo对象的oldTransactionInfo属性,然后将新的TransactionInfo对象重新绑定到当前线程的ThreadLocal上,这样就实现了前后两个事务的关联,并完成对事务传播行为的处理。