五分鐘教你手寫 SpringBoot 本地事務管理實現

白菜Java自習室 涵蓋核心知識

1. SpringBoot 事務

一直在用 SpringBoot 中的 @Transactional 來做事務管理,但是很少沒想過 SpringBoot 是如何實現事務管理的,今天從源碼入手,看看 @Transactional 是如何實現事務的,最後我們結合源碼的理解,自己動手寫一個類似的註解來實現事務管理,幫助我們加深理解。

1.1. 事務的隔離級別

事務為什麼需要隔離級別呢?這是因為在並發事務情況下,如果沒有隔離級別會導致如下問題:

  • 臟讀 (Dirty Read) :當A事務對數據進行修改,但是這種修改還沒有提交到數據庫中,B事務同時在訪問這個數據,由於沒有隔離,B獲取的數據有可能被A事務回滾,這就導致瞭數據不一致的問題。
  • 丟失修改 (Lost To Modify):當A事務訪問數據100,並且修改為100-1=99,同時B事務讀取數據也是100,修改數據100-1=99,最終兩個事務的修改結果為99,但是實際是98。事務A修改的數據被丟失瞭。
  • 不可重復讀 (Unrepeatable Read):指A事務在讀取數據X=100的時候,B事務把數據X=100修改為X=200,這個時候A事務第二次讀取數據X的時候,發現X=200瞭,導致瞭在整個A事務期間,兩次讀取數據X不一致瞭,這就是不可重復讀。
  • 幻讀 (Phantom Read):幻讀和不可重復讀類似。幻讀表現在,當A事務讀取表數據時候,隻有3條數據,這個時候B事務插入瞭2條數據,當A事務再次讀取的時候,發現有5條記錄瞭,平白無故多瞭2條記錄,就像幻覺一樣。

不可重復讀 VS 幻讀

不可重復讀的重點是修改 :同樣的條件 , 你讀取過的數據 , 再次讀取出來發現值不一樣瞭,重點在更新操作。
幻讀的重點在於新增或者刪除:同樣的條件 , 第 1 次和第 2 次讀出來的記錄數不一樣,重點在增刪操作。

所以,為瞭避免上述的問題,事務中就有瞭隔離級別的概念,在Spring中定義瞭五種表示隔離級別的常量 TransactionDefinition:

  • ISOLATION_DEFAULT:數據庫默認的隔離級別,MySQL默認采用的 REPEATABLE_READ 隔離級別。
  • ISOLATION_READ_UNCOMMITTED:最低的隔離級別,允許讀取未提交的數據變更,可能會導致臟讀、幻讀或不可重復讀。
  • ISOLATION_READ_COMMITTED:允許讀取並發事務已經提交的數據,可以阻止臟讀,但是幻讀或不可重復讀仍有可能發生。
  • ISOLATION_REPEATABLE_READ:對同一字段的多次讀取結果都是一致的,除非數據是被本身事務自己所修改,可以阻止臟讀和不可重復讀,但幻讀仍有可能發生。MySQL中通過MVCC解決瞭該隔離級別下出現幻讀的可能。
  • ISOLATION_SERIALIZABLE:串行化隔離級別,該級別可以防止臟讀、不可重復讀以及幻讀,但是串行化會影響性能。

1.2. Spring中事務的傳播機制

為什麼Spring中要搞一套事務的傳播機制呢?這是Spring給我們提供的事務增強工具,主要是解決方法之間調用,事務如何處理的問題。比如有方法A、方法B和方法C,在A中調用瞭方法B和方法C。偽代碼如下:

MethodA() {
 MethodB();
 MethodC();
}

假設三個方法中都開啟瞭自己的事務,那麼他們之間是什麼關系呢?MethodA的回滾會影響MethodB和MethodC嗎?Spring中的事務傳播機制就是解決這個問題的。
Spring中定義瞭七種事務傳播行為:

  • PROPAGATION_REQUIRED: 如果存在一個事務,則支持當前事務。如果沒有事務則開啟一個新的事務。
  • PROPAGATION_SUPPORTS: 如果存在一個事務,支持當前事務。如果沒有事務,則非事務的執行。但是對於事務同步的事務管理器,PROPAGATION_SUPPORTS與不使用事務有少許不同。
  • PROPAGATION_MANDATORY: 如果已經存在一個事務,支持當前事務。如果沒有一個活動的事務,則拋出異常。
  • PROPAGATION_REQUIRES_NEW: 總是開啟一個新的事務。如果一個事務已經存在,則將這個存在的事務掛起。
  • PROPAGATION_NOT_SUPPORTED: 總是非事務地執行,並掛起任何存在的事務。
  • PROPAGATION_NEVER: 總是非事務地執行,如果存在一個活動事務,則拋出異常。
  • PROPAGATION_NESTED: 如果一個活動的事務存在,則運行在一個嵌套的事務中。 如果沒有活動事務, 則按 TransactionDefinition.PROPAGATION_REQUIRED 屬性執行。

1.3. Spring中事務如何實現異常回滾的

回顧完瞭事務的相關知識,接下來我們正式來研究下 Spring Boot 中如何通過 @Transactional 來管理事務的,我們重點看看它是如何實現回滾的。
在 Spring 中 TransactionInterceptor 和 PlatformTransactionManager 這兩個類是整個事務模塊的核心,我們重點研究下這兩個類的源碼。

  • TransactionInterceptor 負責攔截方法執行,進行判斷是否需要提交或者回滾事務。
  • PlatformTransactionManager 是 Spring 中的事務管理接口,真正定義瞭事務如何回滾和提交。

TransactionInterceptor 類中的代碼有很多,我簡化一下邏輯,方便說明:

 // 以下代碼省略部分內容
 public Object invoke(MethodInvocation invocation) throws Throwable {
  // 獲取事務調用的目標方法
  Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);
  // 執行帶事務調用
  return invokeWithinTransaction(invocation.getMethod(), targetClass, invocation::proceed);
 }

invokeWithinTransaction 簡化邏輯如下:

 // 以下代碼省略部分內容
 protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass, final InvocationCallback invocation) throws Throwable {
  Object retVal;
  try {
   // 調用真正的方法體
   retVal = invocation.proceedWithInvocation();
  }
  catch (Throwable ex) {
   // 如果出現異常,執行事務異常處理
   completeTransactionAfterThrowing(txInfo, ex);
   throw ex;
  }
  finally {
   // 最後做一下清理工作,主要是緩存和狀態等
   cleanupTransactionInfo(txInfo);
  }
  // 如果沒有異常,直接提交事務
  commitTransactionAfterReturning(txInfo);
  return retVal;
 }

事務出現異常回滾的邏輯 completeTransactionAfterThrowing 如下:

 // 以下代碼省略部分內容
 protected void completeTransactionAfterThrowing(@Nullable TransactionInfo txInfo, Throwable ex) {
  // 判斷是否需要回滾,判斷的邏輯就是看有沒有聲明事務屬性,同時判斷是不是在目前的這個異常中執行回滾
  if (txInfo.transactionAttribute != null && txInfo.transactionAttribute.rollbackOn(ex)) {
   // 執行回滾
   txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());
  }
  else {
   // 否則不需要回滾,直接提交即可
   txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
  }
 }

上面的代碼已經把 Spring 的事務的基本原理說清楚瞭,如何進行判斷執行事務,如何回滾。下面到瞭真正執行回滾邏輯的代碼中 PlatformTransactionManager 接口的子類,我們以 JDBC 的事務為例,DataSourceTransactionManager 就是 jdbc 的事務管理類。跟蹤上面的代碼rollback(txInfo.getTransactionStatus()) 可以發現最終執行的代碼如下:

 @Override
 protected void doRollback(DefaultTransactionStatus status) {
  DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction();
  Connection con = txObject.getConnectionHolder().getConnection();
  if (status.isDebug()) {
   logger.debug("Rolling back JDBC transaction on Connection [" + con + "]");
  }
  try {
   // 調用jdbc的 rollback進行回滾事務
   con.rollback();
  }
  catch (SQLException ex) {
   throw new TransactionSystemException("Could not roll back JDBC transaction", ex);
  }
 }

這裡小結下 Spring 中事務的實現思路,Spring 主要依靠 TransactionInterceptor 來攔截執行方法體,判斷是否開啟事務,然後執行事務方法體,方法體中 catch 住異常,接著判斷是否需要回滾,如果需要回滾就委托真正的 TransactionManager 比如 JDBC 中的 DataSourceTransactionManager 來執行回滾邏輯。提交事務也是同樣的道理。
這裡用個流程圖展示下思路:

2. 手寫註解實現事務回滾

我們弄清楚瞭 Spring 的事務執行流程,那我們可以模仿著自己寫一個註解,實現遇到指定異常就回滾的功能。這裡持久層就以最簡單的 JDBC 為例。我們先梳理下需求,首先註解我們可以基於 Spring 的 AOP 來實現,接著既然是 JDBC,那麼我們需要一個類來幫我們管理連接,用來判斷異常是否回滾或者提交。

2.1. Maven 加入依賴

  <dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-aop</artifactId>
  </dependency>
  <dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-jdbc</artifactId>
  </dependency>

2.2. 新建一個註解

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface MyTransaction {
 // 指定異常回滾
 Class<? extends Throwable>[] rollbackFor() default {};
}

2.3. 新建連接管理器

該類幫助我們管理連接,該類的核心功能是把取出的連接對象綁定到線程上,方便在 AOP 處理中取出,進行提交或者回滾操作。

@Component
public class DataSourceConnectHolder {

 @Autowired
 private DataSource dataSource;
 /**
  * 線程綁定對象
  */
 ThreadLocal<Connection> resources = new NamedThreadLocal<>("Transactional resources");

 public Connection getConnection() {
  Connection con = resources.get();
  if (con != null) {
   return con;
  }
  try {
   con = dataSource.getConnection();
   // 為瞭體現事務,全部設置為手動提交事務
   con.setAutoCommit(false);
  } catch (SQLException e) {
   e.printStackTrace();
  }
  resources.set(con);
  return con;
 }

 public void cleanHolder() {
  Connection con = resources.get();
  if (con != null) {
   try {
    con.close();
   } catch (SQLException e) {
    e.printStackTrace();
   }
  }
  resources.remove();
 }
}

2.4. 新建一個切面

這部分是事務處理的核心,先獲取註解上的異常類,然後捕獲住執行的異常,判斷異常是不是註解上的異常或者其子類,如果是就回滾,否則就提交。

@Aspect
@Component
public class MyTransactionAopHandler {
 
 @Autowired
 private DataSourceConnectHolder connectHolder;
 
 Class<? extends Throwable>[] es;

 // 攔截所有MyTransaction註解的方法
 @org.aspectj.lang.annotation.Pointcut("@annotation(你的包路徑.MyTransaction)")
 public void Transaction() {

 }

 @Around("Transaction()")
 public Object TransactionProceed(ProceedingJoinPoint proceed) throws Throwable {
  Object result = null;
  Signature signature = proceed.getSignature();
  MethodSignature methodSignature = (MethodSignature) signature;
  Method method = methodSignature.getMethod();
  if (method == null) {
   return result;
  }
  MyTransaction transaction = method.getAnnotation(MyTransaction.class);
  if (transaction != null) {
   es = transaction.rollbackFor();
  }
  try {
   result = proceed.proceed();
  } catch (Throwable throwable) {
   // 異常處理
   completeTransactionAfterThrowing(throwable);
   throw throwable;
  }
  // 直接提交
  doCommit();
  return result;
 }

 /**
  * 執行回滾,最後關閉連接和清理線程綁定
  */
 private void doRollBack() {
  try {
   connectHolder.getConnection().rollback();
  } catch (SQLException e) {
   e.printStackTrace();
  } finally {
   connectHolder.cleanHolder();
  }

 }

 /**
  * 執行提交,最後關閉連接和清理線程綁定
  */
 private void doCommit() {
  try {
   connectHolder.getConnection().commit();
  } catch (SQLException e) {
   e.printStackTrace();
  } finally {
   connectHolder.cleanHolder();
  }
 }

 /**
  * 異常處理,捕獲的異常是目標異常或者其子類,就進行回滾,否則就提交事務。
  */
 private void completeTransactionAfterThrowing(Throwable throwable) {
  if (es != null && es.length > 0) {
   for (Class<? extends Throwable> e : es) {
    if (e.isAssignableFrom(throwable.getClass())) {
     doRollBack();
    }
   }
  }
  doCommit();
 }

}

2.4. 編寫一個 Service

saveTest 方法調用瞭2個插入語句,同時聲明瞭 @MyTransaction 事務註解,遇到 Exception 就進行回滾。

@Service
public class MyTransactionTest {

 @Autowired
 private DataSourceConnectHolder holder;

 // 一個事務中執行兩個sql插入
 @MyTransaction(rollbackFor = NullPointerException.class)
 public void saveTest(int id) {
  save(id, "白菜Java自習室");
  save(id + 10, "白菜Java自習室");
  throw new RuntimeException();
 }

 // 執行sql
 private void save(int id, String value) {
  String sql = "insert into test values(?,?)";
  Connection connection = holder.getConnection();
  PreparedStatement stmt = null;
  try {
   stmt = connection.prepareStatement(sql);
   stmt.setInt(1, id);
   stmt.setString(2, value);
   stmt.executeUpdate();
  } catch (SQLException e) {
   e.printStackTrace();
  }
 }

}

我們自己通過 JDBC 結合 Spring 的 AOP 自己寫瞭個 @MyTransactional 的註解,實現瞭遇到指定異常回滾的功能。

到此這篇關於五分鐘教你手寫 SpringBoot 本地事務管理實現的文章就介紹到這瞭,更多相關SpringBoot 本地事務管理內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: