Spring Boot中的事務是如何實現的


本文首發於微信公眾號【猿燈塔】,轉載引用請說明出處

 

今天呢!燈塔君跟大家講:

Spring Boot中的事務是如何實現的

 

1. 概述

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

 

2. 事務的相關知識

開始看源碼之前,我們先回顧下事務的相關知識。

開始看源碼之前,我們先回顧下事務的相關知識。

 

2.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隔離級別

TransactionDefinition.ISOLATION_READ_UNCOMMITTED

最低的隔離級別,允許讀取未提交的數據變更,可能會導致臟讀、幻讀或不可重復讀

TransactionDefinition.ISOLATION_READ_COMMITTED

允許讀取並發事務已經提交的數據,可以阻止臟讀,但是幻讀或不可重復讀仍有可能發生

TransactionDefinition.ISOLATION_REPEATABLE_READ

對同一字段的多次讀取結果都是一致的,除非數據是被本身事務自己所修改,**可以阻止臟讀和不可重復讀,但幻讀仍有可能發生。**MySQL中通過MVCC解決了該隔離級別下出現幻讀的可能。

TransactionDefinition.ISOLATION_SERIALIZABLE

串行化隔離級別,該級別可以防止臟讀、不可重復讀以及幻讀,但是串行化會影響性能。

 

2.2 Spring中事務的傳播機制

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

MethodA{

MethodB;

MethodC;

}

MethodB{

 

}

MethodC{

 

}

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

Spring中定義了七種事務傳播行為:

類型

說明

PROPAGATION_REQUIRED

如果當前沒有事務,就新建一個事務,如果已經存在一個事務中,加入到這個事務中。這是最常見的選擇

PROPAGATION_SUPPORTS

支持當前事務,如果當前沒有事務,就以非事務方式執行。

PROPAGATION_MANDATORY

使用當前的事務,如果當前沒有事務,就拋出異常。

PROPAGATION_REQUIRES_NEW

新建事務,如果當前存在事務,把當前事務掛起。

PROPAGATION_NOT_SUPPORTED

以非事務方式執行操作,如果當前存在事務,就把當前事務掛起。

PROPAGATION_NEVER

以非事務方式執行,如果當前存在事務,則拋出異常。

PROPAGATION_NESTED

如果當前存在事務,則在嵌套事務內執行。如果當前沒有事務,則執行與PROPAGATION_REQUIRED類似的操作。

這七種傳播機制是如何影響事務的,感興趣的同學可以閱讀這篇文章

 

3. 如何實現異常回滾的

回顧完了事務的相關知識,接下來我們正式來研究下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 簡化邏輯如下:

//TransactionAspectSupport.class

//省略了部分代碼

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

}

}

3.1 小結

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

 

4. 手寫一個注解實現事務回滾

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

4.1 首先加入依賴

 <dependency>

            <groupId>org.springframework.boot</groupId>

            <artifactId>spring-boot-starter-jdbc</artifactId>

        </dependency>

        <dependency>

            <groupId>org.springframework.boot</groupId>

            <artifactId>spring-boot-starter-aop</artifactId>

        </dependency>

4.2 新增一個注解

/**

 * @description:

 * @author: luozhou

 * @create: 2020-03-29 17:05

 **/@Target({ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)@Inherited@Documentedpublic @interface MyTransaction {

    //指定異常回滾

    Class<? extends Throwable>[] rollbackFor() default {};

}

4.3 新增連接管理器

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

/**

 * @description:

 * @author: luozhou

 * @create: 2020-03-29 21:14

 **/@Componentpublic class DataSourceConnectHolder {

    @Autowired

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

    }

}

4.4 新增一個切面

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

/**

 * @description:

 * @author: luozhou

 * @create: 2020-03-29 17:08

 **/@Aspect@Componentpublic class MyTransactionAopHandler {

    @Autowired

    DataSourceConnectHolder connectHolder;

    Class<? extends Throwable>[] es;

 

    //攔截所有MyTransaction注解的方法

    @org.aspectj.lang.annotation.Pointcut("@annotation(luozhou.top.annotion.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();

    }

}

4.5 測試驗證

創建一個tb_test表,表結構如下:

SET NAMES utf8mb4;SET FOREIGN_KEY_CHECKS = 0;

-- ------------------------------ Table structure for tb_test-- ----------------------------DROP TABLE IF EXISTS `tb_test`;CREATE TABLE `tb_test` (

  `id` int(11) NOT NULL,

  `email` varchar(255) DEFAULT NULL,

  PRIMARY KEY (`id`)

) ENGINE=InnoDB DEFAULT CHARSET=latin1;

SET FOREIGN_KEY_CHECKS = 1;

4.5.1 編寫一個Service

saveTest方法調用了2個插入語句,同時聲明了@MyTransaction事務注解,遇到NullPointerException就進行回滾,最后我們執行了除以0操作,會拋出ArithmeticException。我們用單元測試看看數據是否會回滾。

/**

 * @description:

 * @author: luozhou kinglaw1204@gmail.com

 * @create: 2020-03-29 22:05

 **/@Servicepublic class MyTransactionTest implements TestService {

    @Autowired

    DataSourceConnectHolder holder;

//一個事務中執行兩個sql插入

   @MyTransaction(rollbackFor = NullPointerException.class)

    @Override

    public void saveTest(int id) {

        saveWitharamters(id, "luozhou@gmail.com");

        saveWitharamters(id + 10, "luozhou@gmail.com");

        int aa = id / 0;

    }

//執行sql

   private void saveWitharamters(int id, String email) {

        String sql = "insert into tb_test values(?,?)";

        Connection connection = holder.getConnection();

        PreparedStatement stmt = null;

        try {

            stmt = connection.prepareStatement(sql);

            stmt.setInt(1, id);

            stmt.setString(2, email);

            stmt.executeUpdate();

        } catch (SQLException e) {

            e.printStackTrace();

        }

    }

    

}

4.5.2 單元測試

@SpringBootTest@RunWith(SpringRunner.class)class SpringTransactionApplicationTests {

    @Autowired

    private TestService service;

 

    @Test

    void contextLoads() throws SQLException {

        service.saveTest(1);

    }

 

}

 

 

 

 

 

上圖代碼聲明了事務對NullPointerException異常進行回滾,運行中遇到了ArithmeticException異常,所以是不會回滾的,我們在右邊的數據庫中刷新發現數據正常插入成功了,說明並沒有回滾。

 

 

 

 

我們把回滾的異常類改為ArithmeticException,把原數據清空再執行一次,出現了ArithmeticException異常,這個時候查看數據庫是沒有記錄新增成功了,這說明事物進行回滾了,表明我們的注解起作用了。

5. 總結

本文最開始回顧了事務的相關知識,並發事務會導致臟讀丟失修改不可重復讀幻讀,為了解決這些問題,數據庫中就引入了事務的隔離級別,隔離級別包括:讀未提交讀提交可重復讀串行化

Spring中增強了事務的概念,為了解決方法A、方法B和方法C之間的事務關系,引入了事務傳播機制的概念。

Spring中的@Transactional注解的事務實現主要通過TransactionInterceptor攔截器來進行實現的,攔截目標方法,然后判斷異常是不是目標異常,如果是目標異常就行進行回滾,否則就進行事務提交。

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

 

365天干貨不斷,可以微信搜索「 猿燈塔」第一時間閱讀,回復【資料】【面試】【簡歷】有我准備的一線大廠面試資料和簡歷模板

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM