大家好,我是樓仔!
下面我會簡單介紹一下 Spring 事務的基礎知識,以及使用方法,然后直接對源碼進行拆解。
不 BB,上文章目錄。

1. 項目準備
需要搭建環(huán)境的同學,代碼詳見:https://github.com/lml200701158/program_demo/tree/main/spring-transaction
下面是 DB 數(shù)據(jù)和 DB 操作接口:
// 提供的接口
public interface UserDao {
// select * from user_test where uid = "#{uid}"
public MyUser selectUserById(Integer uid);
// update user_test set uname =#{uname},usex = #{usex} where uid = #{uid}
public int updateUser(MyUser user);
}
基礎測試代碼,testSuccess() 是事務生效的情況:
@Service
public class Louzai {
@Autowired
private UserDao userDao;
public void update(Integer id){
MyUser user = new MyUser();
user.setUid(id);
user.setUname("張三-testing");
user.setUsex("女");
userDao.updateUser(user);
}
public MyUser query(Integer id){
MyUser user = userDao.selectUserById(id);
return user;
}
// 正常情況
@Transactional(rollbackFor = Exception.class)
public void testSuccess() throws Exception {
Integer id = 1;
MyUser user = query(id);
System.out.println("原記錄:" + user);
update(id);
throw new Exception("事務生效");
}
}
執(zhí)行入口:
public class SpringMyBatisTest {
public static void main(String[] args) throws Exception {
String xmlPath = "applicationContext.xml";
ApplicationContext applicationContext = new ClassPathXmlApplicationContext(xmlPath);
Louzai uc = (Louzai) applicationContext.getBean("louzai");
uc.testSuccess();
}
}
輸出:
16:44:38.267 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.transaction.interceptor.TransactionInterceptor#0'
16:44:38.363 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'txManager'
16:44:40.966 [main] DEBUG org.springframework.jdbc.datasource.DataSourceTransactionManager - Creating new transaction with name [com.mybatis.controller.Louzai.testSuccess]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT,-java.lang.Exception
16:44:40.968 [main] DEBUG org.springframework.jdbc.datasource.DriverManagerDataSource - Creating new JDBC DriverManager Connection to [jdbc:mysql://127.0.0.1:3306/java_study?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimeznotallow=Asia/Shanghai]
16:44:41.228 [main] DEBUG org.springframework.jdbc.datasource.DataSourceTransactionManager - Acquired Connection [com.mysql.cj.jdbc.ConnectionImpl@5b5caf08] for JDBC transaction
16:44:41.231 [main] DEBUG org.springframework.jdbc.datasource.DataSourceTransactionManager - Switching JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@5b5caf08] to manual commit
原記錄:MyUser(uid=1, uname=張三, usex=女)
16:42:59.345 [main] DEBUG org.springframework.jdbc.datasource.DataSourceTransactionManager - Initiating transaction rollback
16:42:59.346 [main] DEBUG org.springframework.jdbc.datasource.DataSourceTransactionManager - Rolling back JDBC transaction on Connection [com.mysql.cj.jdbc.ConnectionImpl@70807224]
16:42:59.354 [main] DEBUG org.springframework.jdbc.datasource.DataSourceTransactionManager - Releasing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@70807224] after transaction
Exception in thread "main" java.lang.Exception: 事務生效
at com.mybatis.controller.Louzai.testSuccess(Louzai.java:34)
// 異常日志省略...
2. Spring 事務工作流程
為了方便大家能更好看懂后面的源碼,我先整體介紹一下源碼的執(zhí)行流程,讓大家有一個整體的認識,否則容易被繞進去。
整個 Spring 事務源碼,其實分為 2 塊,我們會結合上面的示例,給大家進行講解。
第一塊是后置處理,我們在創(chuàng)建 Louzai Bean 的后置處理器中,里面會做兩件事情:
獲取 Louzai 的切面方法:首先會拿到所有的切面信息,和 Louzai 的所有方法進行匹配,然后找到 Louzai 所有需要進行事務處理的方法,匹配成功的方法,還需要將事務屬性保存到緩存 attributeCache 中。
創(chuàng)建 AOP 代理對象:結合 Louzai 需要進行 AOP 的方法,選擇 Cglib 或 JDK,創(chuàng)建 AOP 代理對象。

第二塊是事務執(zhí)行,整個邏輯比較復雜,我只選取 4 塊最核心的邏輯,分別為從緩存拿到事務屬性、創(chuàng)建并開啟事務、執(zhí)行業(yè)務邏輯、提交或者回滾事務。
3. 源碼解讀
注意:Spring 的版本是 5.2.15.RELEASE,否則和我的代碼不一樣!!!
上面的知識都不難,下面才是我們的重頭戲,讓你跟著樓仔,走一遍代碼流程。
3.1 代碼入口


這里需要多跑幾次,把前面的 beanName 跳過去,只看 louzai。


進入 doGetBean(),進入創(chuàng)建 Bean 的邏輯。

進入 createBean(),調(diào)用 doCreateBean()。

進入 doCreateBean(),調(diào)用 initializeBean()。




如果看過我前面幾期系列源碼的同學,對這個入口應該會非常熟悉,其實就是用來創(chuàng)建代理對象。
3.2 創(chuàng)建代理對象

這里是重點!敲黑板!!!
先獲取 louzai 類的所有切面列表;
創(chuàng)建一個 AOP 的代理對象。

3.2.1 獲取切面列表

這里有 2 個重要的方法,先執(zhí)行 findCandidateAdvisors(),待會我們還會再返回 findEligibleAdvisors()。




依次返回,重新來到 findEligibleAdvisors()。




進入 canApply(),開始匹配 louzai 的切面。

這里是重點!敲黑板!!!
這里只會匹配到 Louzai.testSuccess() 方法,我們直接進入匹配邏輯。

如果匹配成功,還會把事務的屬性配置信息放入 attributeCache 緩存。






我們依次返回到 getTransactionAttribute(),再看看放入緩存中的數(shù)據(jù)。

再回到該小節(jié)開頭,我們拿到 louzai 的切面信息,去創(chuàng)建 AOP 代理對象。

3.2.2 創(chuàng)建 AOP 代理對象
創(chuàng)建 AOP 代理對象的邏輯,在上一篇文章(Spring AOP)講解過,我是通過 Cglib 創(chuàng)建,感興趣的同學可以關注公眾號「樓仔」,翻一下樓仔的歷史文章。
3.3 事務執(zhí)行
回到業(yè)務邏輯,通過 louzai 的 AOP 代理對象,開始執(zhí)行主方法。

因為代理對象是 Cglib 方式創(chuàng)建,所以通過 Cglib 來執(zhí)行。




這里是重點!敲黑板!!!
下面的代碼是事務執(zhí)行的核心邏輯 invokeWithinTransaction()。

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 PlatformTransactionManager tm = determineTransactionManager(txAttr);
//從tx屬性對象中獲取出標注了@Transactionl的方法描述符
final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);
//處理聲明式事務
if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {
//有沒有必要創(chuàng)建事務
TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
Object retVal;
try {
//調(diào)用鉤子函數(shù)進行回調(diào)目標方法
retVal = invocation.proceedWithInvocation();
}
catch (Throwable ex) {
//拋出異常進行回滾處理
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
}
finally {
//清空我們的線程變量中transactionInfo的值
cleanupTransactionInfo(txInfo);
}
//提交事務
commitTransactionAfterReturning(txInfo);
return retVal;
}
//編程式事務
else {
// 這里不是我們的重點,省略...
}
}
3.3.1 獲取事務屬性
在 invokeWithinTransaction() 中,我們找到獲取事務屬性的入口。

從 attributeCache 獲取事務的緩存數(shù)據(jù),緩存數(shù)據(jù)是在 “2.2.1 獲取切面列表” 中保存的。

3.3.2 創(chuàng)建事務



通過 doGetTransaction() 獲取事務。
protected Object doGetTransaction(){
//創(chuàng)建一個數(shù)據(jù)源事務對象
DataSourceTransactionObject txObject = new DataSourceTransactionObject();
//是否允許當前事務設置保持點
txObject.setSavepointAllowed(isNestedTransactionAllowed());
/**
* TransactionSynchronizationManager 事務同步管理器對象(該類中都是局部線程變量)
* 用來保存當前事務的信息,我們第一次從這里去線程變量中獲取 事務連接持有器對象 通過數(shù)據(jù)源為key去獲取
* 由于第一次進來開始事務 我們的事務同步管理器中沒有被存放.所以此時獲取出來的conHolder為null
*/
ConnectionHolder conHolder =
(ConnectionHolder) TransactionSynchronizationManager.getResource(obtainDataSource());
txObject.setConnectionHolder(conHolder, false);
//返回事務對象
return txObject;
}
通過 startTransaction() 開啟事務。

下面是開啟事務的詳細邏輯,了解一下即可。
protected void doBegin(Object transaction, TransactionDefinition definition){
//強制轉(zhuǎn)化事務對象
DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
Connection con = null;
try {
//判斷事務對象沒有數(shù)據(jù)庫連接持有器
if (!txObject.hasConnectionHolder() ||
txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
//通過數(shù)據(jù)源獲取一個數(shù)據(jù)庫連接對象
Connection newCon = obtainDataSource().getConnection();
if (logger.isDebugEnabled()) {
logger.debug("Acquired Connection [" + newCon + "] for JDBC transaction");
}
//把我們的數(shù)據(jù)庫連接包裝成一個ConnectionHolder對象 然后設置到我們的txObject對象中去
txObject.setConnectionHolder(new ConnectionHolder(newCon), true);
}
//標記當前的連接是一個同步事務
txObject.getConnectionHolder().setSynchronizedWithTransaction(true);
con = txObject.getConnectionHolder().getConnection();
//為當前的事務設置隔離級別
Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition);
txObject.setPreviousIsolationLevel(previousIsolationLevel);
//關閉自動提交
if (con.getAutoCommit()) {
txObject.setMustRestoreAutoCommit(true);
if (logger.isDebugEnabled()) {
logger.debug("Switching JDBC Connection [" + con + "] to manual commit");
}
con.setAutoCommit(false);
}
//判斷事務為只讀事務
prepareTransactionalConnection(con, definition);
//設置事務激活
txObject.getConnectionHolder().setTransactionActive(true);
//設置事務超時時間
int timeout = determineTimeout(definition);
if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) {
txObject.getConnectionHolder().setTimeoutInSeconds(timeout);
}
// 綁定我們的數(shù)據(jù)源和連接到我們的同步管理器上 把數(shù)據(jù)源作為key,數(shù)據(jù)庫連接作為value 設置到線程變量中
if (txObject.isNewConnectionHolder()) {
TransactionSynchronizationManager.bindResource(obtainDataSource(), txObject.getConnectionHolder());
}
}
catch (Throwable ex) {
if (txObject.isNewConnectionHolder()) {
//釋放數(shù)據(jù)庫連接
DataSourceUtils.releaseConnection(con, obtainDataSource());
txObject.setConnectionHolder(null, false);
}
throw new CannotCreateTransactionException("Could not open JDBC Connection for transaction", ex);
}
}
最后返回到 invokeWithinTransaction(),得到 txInfo 對象。

3.3.3 執(zhí)行邏輯
還是在 invokeWithinTransaction() 中,開始執(zhí)行業(yè)務邏輯。





進入到真正的業(yè)務邏輯。

執(zhí)行完畢后拋出異常,依次返回,走后續(xù)的回滾事務邏輯。
3.3.4 回滾事務
還是在 invokeWithinTransaction() 中,進入回滾事務的邏輯。

。

執(zhí)行回滾邏輯很簡單,我們只看如何判斷是否回滾。



如果拋出的異常類型,和事務定義的異常類型匹配,證明該異常需要捕獲。
之所以用遞歸,不僅需要判斷拋出異常的本身,還需要判斷它繼承的父類異常,滿足任意一個即可捕獲。

到這里,所有的流程結束。
4. 結語
我們再小節(jié)一下,文章先介紹了事務的使用示例,以及事務的執(zhí)行流程。
之后再剖析了事務的源碼,分為 2 塊:
先匹配出 louzai 對象所有關于事務的切面列表,并將匹配成功的事務屬性保存到緩存;
從緩存取出事務屬性,然后創(chuàng)建、啟動事務,執(zhí)行業(yè)務邏輯,最后提交或者回滾事務。