前言
對於一個應用而言,事務的使用基本是不可避免的。雖然Spring給我們提供了開箱即用的事務功能——@Transactional
。
但是,自帶的事務功能卻也存在控制粒度不夠的缺點。更糟糕的是,@Transactional
在某些情況下就失效了。可能一些讀者baidu/google一下解決辦法後,失效的問題確實解決了。但是由於不瞭解底層的原理,這樣的問題可能在今後的工作中往復出現。
本文就為大家揭開@Transactional
下的祕密。
原生的事務管理
在沒有Spring存在的時候,事務就已經誕生了。其實框架依賴的還是底層提供的能力,只不過它對這一過程的抽象和複用。
這裡我們用底層的API來了解下事務管理的過程(JDBC為例):
// 獲取mysql資料庫連線
Connection conn = DriverManager.getConnection("xxxx");
conn.setAutoCommit(false);
statement = conn.createStatement();
// 執行sql,返回結果集
resultSet = statement.executeQuery("xxxx");
conn.commit(); //提交
//conn.rollback();//回滾
上面是一個原生操作事務的一個例子,這些過程也是Spring事務逃不開的,只不過在為了程式設計的效率讓這一過程自動化或是透明化的你無法感知罷了。
而我們之後做的就是逐步還原這一自動化的過程。
Spring提供的事務API
Spring提供了很多關於事務的API。但是最為基本的就是PlatformTransactionManager
、TransactionDefintion
和TransactionStatus
。
事務管理器——PlatformTransactionManager
PlatformTransactionManager
是事務管理器的頂層介面。事務的管理是受限於具體的資料來源的(例如,JDBC對應的事務管理器就是DatasourceTransactionManager
),因此PlatformTransactionManager
只規定了事務的基本操作:建立事務,提交事物和回滾事務。
public interface PlatformTransactionManager extends TransactionManager {
/**
* 開啟事務
*/
TransactionStatus getTransaction(@Nullable TransactionDefinition definition)
throws TransactionException;
/**
* 提交事務
*/
void commit(TransactionStatus status) throws TransactionException;
/**
* 回滾事務
*/
void rollback(TransactionStatus status) throws TransactionException;
}
同時為了簡化事務管理器的實現,Spring提供了一個抽象類AbstractPlatformTransactionManager
,規定了事務管理器的基本框架,僅將依賴於具體平臺的特性作為抽象方法留給子類實現。
事務狀態——TransactionStatus
事務狀態是我對TransactionStatus
這個類的直譯。其實我覺得這個類可以直接當作事務的超集來看(包含了事務物件,並且儲存了事務的狀態)。PlatformTransactionManager.getTransaction()
時建立的也正是這個物件。
這個物件的方法都和事務狀態相關:
public interface TransactionStatus extends TransactionExecution, SavepointManager, Flushable {
/**
* 是否有Savepoint Savepoint是當事務回滾時需要恢復的狀態
*/
boolean hasSavepoint();
/**
* flush()操作和底層資料來源有關,並非強制所有資料來源都要支援
*/
@Override
void flush();
}
此外,TransactionStatus
還從父介面中繼承了其他方法,都歸總在下方:
/**
* 是否是新事務(或是其他事務的一部分)
*/
boolean isNewTransaction();
/**
* 設定rollback-only 表示之後需要回滾
*/
void setRollbackOnly();
/**
* 是否rollback-only
*/
boolean isRollbackOnly();
/**
* 判斷該事務已經完成
*/
boolean isCompleted();
/**
* 建立一個Savepoint
*/
Object createSavepoint() throws TransactionException;
/**
* 回滾到指定Savepoint
*/
void rollbackToSavepoint(Object savepoint) throws TransactionException;
/**
* 釋放Savepoint 當事務完成後,事務管理器基本上自動釋放該事務所有的savepoint
*/
void releaseSavepoint(Object savepoint) throws TransactionException;
事務屬性的定義——TransactionDefinition
TransactionDefinition
表示一個事務的定義,將根據它規定的特性去開啟事務。
事務的傳播等級和隔離級別的常量同樣定義在這個介面中。
/**
* 返回事務的傳播級別
*/
default int getPropagationBehavior() {
return PROPAGATION_REQUIRED;
}
/**
* 返回事務的隔離級別
*/
default int getIsolationLevel() {
return ISOLATION_DEFAULT;
}
/**
* 事務超時時間
*/
default int getTimeout() {
return TIMEOUT_DEFAULT;
}
/**
* 是否為只讀事務(只讀事務在處理上能有一些優化)
*/
default boolean isReadOnly() {
return false;
}
/**
* 返回事務的名稱
*/
@Nullable
default String getName() {
return null;
}
/**
* 預設的事務配置
*/
static TransactionDefinition withDefaults() {
return StaticTransactionDefinition.INSTANCE;
}
程式設計式使用Spring事務
有了上述這些API,就已經可以通過程式設計的方式實現Spring的事務控制了。
但是Spring官方建議不要直接使用PlatformTransactionManager
這一偏低層的API來程式設計,而是使用TransactionTemplate
和TransactionCallback
這兩個偏向使用者層的介面。
示例程式碼如下:
//設定事務的各種屬性;可以猜測TransactionTemplate應該是實現了TransactionDefinition
transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
transactionTemplate.setTimeout(30000);
//執行事務 將業務邏輯封裝在TransactionCallback中
transactionTemplate.execute(new TransactionCallback<Object>() {
@Override
public Object doInTransaction(TransactionStatus transactionStatus) {
//.... 業務程式碼
}
});
以上就是Spring事務最基本的原理。但是為什麼這些過程對我們似乎都不可見呢?那是因為這些過程都通過AOP的方式被織入了我們的業務邏輯中。
所以,像要深入瞭解Spring事務原理,還需要了解AOP的原理。
AOP的原理
AOP的實現機制有兩種:Proxy-based和Weaving-based。
前者是依賴動態代理的方式達到對代理類增強的目的。後者應該是通過位元組碼增強的方式達到增強的目的。
在Spring中,一般預設使用前者。之後也僅是針對前者進行分析。
而Spring宣告AOP的方式也有兩種,一種是通過宣告Aspect,另一種是通過宣告Advisor。
無論是哪種方式,都需要表達清楚你要進行增強的邏輯 (what)和你要增強的地方(where)。即,需要告訴Spring你要增強什麼邏輯,並且對哪些Bean/哪些方法增強。
這裡的what和where換成AOP中的概念分別就是對應Advice
和Pointcut
。
因為事務是通過Advisor宣告AOP的,因此本文也只針對Advisor的實現展開分析。
動態代理
既然是動態代理,那麼必然存在被代理類(Target),代理類(Proxy),以及類被代理的過程(因為對使用者而言,並不知道類被代理了)。
被代理的類
被代理類是最容易知道的,就是那些被Advisor的Pointcut匹配(classFliter匹配或是methodMatches)到的類。
代理的類
而代理類是在執行時直接建立的。通常有兩種方式:
- JDK的動態代理
- CGLIB的動態代理
二者的區別是JDK動態代理是通過實現介面的方式(代理的物件為介面),因此只能代理介面中的方法。
而CGLIB動態代理是通過繼承的方式,因此可以對物件中的方法進行代理,但是由於是繼承關係,無法代理final的類和方法(無法繼承),或是private的方法(對子類不可見)。
建立代理及取代目標類的過程
建立代理及取代目標類主要是應用了Spring容器在獲取Bean時留下的一個擴充點。
Spring在getBean
的時候,如果Bean還不存在會分三步去建立Bean:
- 例項化
- 填充屬性
- 初始化
例項化通常是通過反射建立Bean物件的例項,此時得到的 Bean還只是一個空白物件。
填充屬性主要是為這個Bean注入其他的Bean,實現自動裝配。
而初始化則是讓使用者可以控制Bean的建立過程。
為Bean建立代理,並取代原有的Bean就是發生在初始化這一步,更具體的是在BeanPostProcessor.postProcessorAfterInitialization()
中。
動態代理也有可能在例項化之前直接建立代理,這種情況發生在
InstantiationAwareBeanPostProcessor.postProcessBeforeInstantiation()
中,此時的例項化過程不再是我們上文介紹的通過簡單反射建立物件。
在眾多的BeanPostProcessor
中有一類後置處理器就是專門用於建立代理的。例如,我們要介紹的AbstractAdvisorAutoProxyCreator
。
看一下AbstractAutoProxyCreator
建立代理的流程:
- 先確認是否已經建立過代理物件(
earlyProxyReferences
,避免對代理物件在進行代理) - 如果沒有,則考慮是否需要進行代理(通過
wrapIfNecessary
) - 如果是特殊的Bean 或者之前判斷過不用建立代理的Bean則不建立代理
- 否則看是否有匹配的Advise(匹配方式就是上文介紹的通過PointCut或者IntroducationAdvisor可以直接匹配類)
- 如果找到了Advisor,說明需要建立代理,進入
createProxy
- 首先會建立
ProxyFactory
,這個工廠是用來建立AopProxy的,而AopProxy
才是用來建立代理物件的。因為底層代理方式有兩種(JDK動態代理和CGLIB,對應到AopProxy
的實現就是JdkDynamicAopProxy
和ObjenesisCglibAopProxy
),所以這裡使用了一個簡單工廠的設計。ProxyFactory
會設定此次代理的屬性,然後根據這些屬性選擇合適的代理方式,建立代理物件。 - 建立的物件會替換掉被代理物件(Target),被儲存在
BeanFactory.singletonObjects
,因此當有其他Bean希望注入Target時,其實已經被注入了Proxy。
以上就是Spring實現動態代理的過程。
Spring註解式事務
上文中,我們從程式設計式事務瞭解了Spring事務API的基本使用方式,又瞭解了Spring Advisor的原理。現在,我們在回到Spring註解式事務中,驗證下註解式事務是否就是通過以上這些方式隱藏了具體的事務控制邏輯。
從@EnableTransactionManagement說起
@EnableTransactionManagement
是開啟註解式事務的事務。如果註解式事務真的有玄機,那麼@EnableTransactionManagement
就是我們揭開祕密的突破口。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(TransactionManagementConfigurationSelector.class)
public @interface EnableTransactionManagement {
/**
* 用來表示預設使用JDK Dynamic Proxy還是CGLIB Proxy
*/
boolean proxyTargetClass() default false;
/**
* 表示以Proxy-based方式實現AOP還是以Weaving-based方式實現AOP
*/
AdviceMode mode() default AdviceMode.PROXY;
/**
* 順序
*/
int order() default Ordered.LOWEST_PRECEDENCE;
}
@EnableTransactionManagement
註解看起來並沒有特別之處,都是一些屬性的配置。但它卻通過@Import
引入了另一個配置TransactionManagentConfigurationSelector
。
TransactionManangementConfigurationSelector
在Spring中,Selector
通常都是用來選擇一些Bean,向容器註冊BeanDefinition的(嚴格意義上Selector僅時選擇過程,註冊的具體過程是在ConfigurationClasspathPostProcessor
解析時,呼叫ConfigurationClassParser
觸發)。
主要的邏輯就是根據代理模式,註冊不同的BeanDefinition。
對Proxy的模式而言,注入的有兩個:
- AutoProxyRegistrar
- ProxyTransactionManagementConfiguration
AutoProxyRegistrar
Registrar同樣也是用來向容器註冊Bean的,在Proxy的模式下,它會呼叫AopConfigUtils.registerAutoProxyCreatorIfNecessary(registry);
向容器中註冊InfrastructureAdvisorAutoProxyCreator
。而這個類就是我們上文提到的AbstractAdvisorAutoProxyCreator
的子類。
從而,我們完成了我們的第一個條件——AOP代理。
ProxyTransactionManagementConfiguration
ProxyTransactionManagementConfiguration
是一個配置類,如果算上其繼承的父類,一共是宣告瞭四個類:
- TransactionalEventListenerFactory
- BeanFactoryTransactionAttributeSourceAdvisor
- TransactionAttributeSource
- TransactionInterceptor
後三個類相對比較重要,我們一一分析。
BeanFactoryTransactionAttributeSourceAdvisor
從名字看就知道這是一個Advisor,那麼它身上應該有Pointcut和Advise。
其中的Pointcut是TransactionAttributeSourcePointcut
,主要是一些filter和matches之類的方法,用來匹配被代理類。
而Adivise就是我們之後要介紹的TransactionInterceptor
。
TransactionAttributeSource
TransactionAttributeSource
只是一個介面,擴充套件了TransactionDefinition
,增加了isCandidateClass()
的方法(可以用來幫助Pointcut匹配)。
這裡使用的具體實現是AnnotationTransactionAttributeSource
。因為註解式事務候選類(即要被代理的類)是通過@Transactional
註解標識的,並且所有的事務屬性也都來自@Transactional
註解。
TransactionInterceptor
剛才我們說了,TransactionInterceptor
就是我們找的Advise。
這個類稍微複雜一點,首先根據事務處理相關的邏輯都放在了其父類TransactionAspectSupport
中。此外,為了適配動態代理的反射呼叫(兩種代理方式),實現了MethodInterceptor
介面。
也就是說,反射發起的入口是MethodInterceptor.invoke()
,而反射邏輯在TransactionAspectSupport.invokeWithinTransaction()
中。
我們可以簡單看invokeWithTransaction()
方法中的部分程式碼:
@Nullable
protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
final InvocationCallback invocation) throws Throwable {
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)) {
// 開啟事務(內部就是getTransactionStatus的過程)
TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);
Object retVal;
try {
// 執行業務邏輯 invocation.proceedWithInvocation();
}
catch (Throwable ex) {
// 異常回滾
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
}
finally {
cleanupTransactionInfo(txInfo);
}
//省略部分程式碼
//提交事物
commitTransactionAfterReturning(txInfo);
return retVal;
}
雖然程式碼比我們之前的複雜,但是其主體結構依然是我們程式設計式事務的常見那幾步。
行文至此,隱藏在Spring自動事務下的邏輯都分析的差不多了。未避免枯燥,本文並沒有對程式碼一行行的分析,而是希望能夠幫助讀者把握大概的原理。
事務失效的常見情況及其背後的原因
資料庫儲存引擎不支援
常見的像mySQL的myISAM儲存引擎就不支援事務功能。
這很好理解,說到底事務是資料庫的功能,如果資料庫本身就沒有這個功能,那上層再怎麼五花八門也是沒用的。
未指定RollbackOn,且丟擲的異常並非RuntimeException
這個背後的原因我們可以從DefualtTransactionAttribute
中來找。
//可見預設觸發回滾的異常是RuntimeException和Error
@Override
public boolean rollbackOn(Throwable ex) {
return (ex instanceof RuntimeException || ex instanceof Error);
}
因此阿里巴巴程式碼規範倡議是顯示指定rollbackOn為Exception
同一個類中呼叫事務方法
這是在Proxy模式下才會失效的。
根據上文我們瞭解了Spring事務是機遇動態代理的,而當在類當中呼叫事務的方法時,動態代理是無法生效的,因為此時你拿到的this指向的已經是被代理類(Target),而非代理類(Proxy)。
非公開方法上的事務
如果你將@Transactional
註解應用在一個non-public的方法上(即便是protected和defualt的方法),你會發現事務同樣不生效(也是在Proxy模式下)。
有讀者可能會疑問,GCLIB的侷限應該是在private或是final的方法上,private方法代理失效還能理解,為什麼protected和defualt方法也不行呢?
其實,non-public方法失效的真正原因不是動態代理的限制,而是Spring有意為之。
之前我們介紹了TransactionAttributeSource
會幫助Pointcut匹配類和方法,而在AnnotationTransactionAttributeSource
中,有一個屬性final boolean publicMethodsOnly
表示是否只允許公有方法。這個屬性在預設的建構函式中被設定了true。因此代理只會對public方法生效。
網上找了下Spring這麼設計的目的,有說業務類就是應該基於介面方法呼叫的,因此總為public。也有說這是為了r讓CGLIB和JDK dynamic Proxy保持一致。
Emm...我覺得Duck不必。
不過Spring也沒有把著屬性限制死,如果你真想在non-public的方法上使用自動事務,使點手段修改這個變數即可(例如搞個高優先順序的BeanPostProcessor,在通過反射修改這個變數)。但是儘量還是按照規範來吧。