一文講透Spring Boot中的事務(wù)是如何實(shí)現(xiàn)的

概述

一直在用SpringBoot中的@Transactional來做事務(wù)管理茵典,但是很少?zèng)]想過SpringBoot是如何實(shí)現(xiàn)事務(wù)管理的屏积,今天從源碼入手医窿,看看@Transactional是如何實(shí)現(xiàn)事務(wù)的,最后我們結(jié)合源碼的理解炊林,自己動(dòng)手寫一個(gè)類似的注解來實(shí)現(xiàn)事務(wù)管理姥卢,幫助我們加深理解。

閱讀說明:本文假設(shè)你具備Java基礎(chǔ),同時(shí)對(duì)事務(wù)有基本的了解和使用独榴。

事務(wù)的相關(guān)知識(shí)

開始看源碼之前僧叉,我們先回顧下事務(wù)的相關(guān)知識(shí)。

1棺榔、事務(wù)的隔離級(jí)別

事務(wù)為什么需要隔離級(jí)別呢瓶堕?這是因?yàn)樵诓l(fā)事務(wù)情況下,如果沒有隔離級(jí)別會(huì)導(dǎo)致如下問題:

臟讀(Dirty Read) :當(dāng)A事務(wù)對(duì)數(shù)據(jù)進(jìn)行修改症歇,但是這種修改還沒有提交到數(shù)據(jù)庫中郎笆,B事務(wù)同時(shí)在訪問這個(gè)數(shù)據(jù),由于沒有隔離忘晤,B獲取的數(shù)據(jù)有可能被A事務(wù)回滾宛蚓,這就導(dǎo)致了數(shù)據(jù)不一致的問題。

丟失修改(Lost To Modify):當(dāng)A事務(wù)訪問數(shù)據(jù)100设塔,并且修改為100-1=99苍息,同時(shí)B事務(wù)讀取數(shù)據(jù)也是100,修改數(shù)據(jù)100-1=99壹置,最終兩個(gè)事務(wù)的修改結(jié)果為99,但是實(shí)際是98表谊。事務(wù)A修改的數(shù)據(jù)被丟失了钞护。

不可重復(fù)讀(Unrepeatable Read):指A事務(wù)在讀取數(shù)據(jù)X=100的時(shí)候,B事務(wù)把數(shù)據(jù)X=100修改為X=200,這個(gè)時(shí)候A事務(wù)第二次讀取數(shù)據(jù)X的時(shí)候爆办,發(fā)現(xiàn)X=200了难咕,導(dǎo)致了在整個(gè)A事務(wù)期間,兩次讀取數(shù)據(jù)X不一致了距辆,這就是不可重復(fù)讀余佃。

幻讀(Phantom Read):幻讀和不可重復(fù)讀類似】缢悖幻讀表現(xiàn)在爆土,當(dāng)A事務(wù)讀取表數(shù)據(jù)時(shí)候,只有3條數(shù)據(jù)诸蚕,這個(gè)時(shí)候B事務(wù)插入了2條數(shù)據(jù)步势,當(dāng)A事務(wù)再次讀取的時(shí)候,發(fā)現(xiàn)有5條記錄了背犯,平白無故多了2條記錄坏瘩,就像幻覺一樣。

不可重復(fù)讀 VS 幻讀

不可重復(fù)讀的重點(diǎn)是修改 : 同樣的條件 , 你讀取過的數(shù)據(jù) , 再次讀取出來發(fā)現(xiàn)值不一樣了漠魏,重點(diǎn)在更新操作倔矾。

幻讀的重點(diǎn)在于新增或者刪除:同樣的條件 , 第 1 次和第 2 次讀出來的記錄數(shù)不一樣,重點(diǎn)在增刪操作。

所以哪自,為了避免上述的問題丰包,事務(wù)中就有了隔離級(jí)別的概念,在Spring中定義了五種表示隔離級(jí)別的常量:

file

2提陶、 Spring中事務(wù)的傳播機(jī)制

為什么Spring中要搞一套事務(wù)的傳播機(jī)制呢烫沙?這是Spring給我們提供的事務(wù)增強(qiáng)工具,主要是解決方法之間調(diào)用隙笆,事務(wù)如何處理的問題锌蓄。比如有方法A、方法B和方法C撑柔,在A中調(diào)用了方法B和方法C瘸爽。

偽代碼如下:

MethodA{
    MethodB;
    MethodC;
}
MethodB{

}
MethodC{

}

假設(shè)三個(gè)方法中都開啟了自己的事務(wù)铅忿,那么他們之間是什么關(guān)系呢剪决?MethodA的回滾會(huì)影響MethodB和MethodC嗎?Spring中的事務(wù)傳播機(jī)制就是解決這個(gè)問題的檀训。

Spring中定義了七種事務(wù)傳播行為:

file

如何實(shí)現(xiàn)異掣塘剩回滾的

回顧完了事務(wù)的相關(guān)知識(shí),接下來我們正式來研究下Spring Boot中如何通過@Transactional來管理事務(wù)的峻凫,我們重點(diǎn)看看它是如何實(shí)現(xiàn)回滾的渗鬼。

在Spring中TransactionInterceptorPlatformTransactionManager這兩個(gè)類是整個(gè)事務(wù)模塊的核心,TransactionInterceptor負(fù)責(zé)攔截方法執(zhí)行荧琼,進(jìn)行判斷是否需要提交或者回滾事務(wù)譬胎。

PlatformTransactionManager是Spring 中的事務(wù)管理接口,真正定義了事務(wù)如何回滾和提交命锄。我們重點(diǎn)研究下這兩個(gè)類的源碼堰乔。

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

    //以下代碼省略部分內(nèi)容
    public Object invoke(MethodInvocation invocation) throws Throwable {
    //獲取事務(wù)調(diào)用的目標(biāo)方法
        Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);
    //執(zhí)行帶事務(wù)調(diào)用
        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 {
            //調(diào)用真正的方法體
                retVal = invocation.proceedWithInvocation();
            }
            catch (Throwable ex) {
                // 如果出現(xiàn)異常镐侯,執(zhí)行事務(wù)異常處理
                completeTransactionAfterThrowing(txInfo, ex);
                throw ex;
            }
            finally {
            //最后做一下清理工作,主要是緩存和狀態(tài)等
                cleanupTransactionInfo(txInfo);
            }
            //如果沒有異常驶冒,直接提交事務(wù)析孽。
            commitTransactionAfterReturning(txInfo);
            return retVal;

    }

事務(wù)出現(xiàn)異常回滾的邏輯completeTransactionAfterThrowing 如下:

//省略部分代碼
protected void completeTransactionAfterThrowing(@Nullable TransactionInfo txInfo, Throwable ex) {
                //判斷是否需要回滾只怎,判斷的邏輯就是看有沒有聲明事務(wù)屬性袜瞬,同時(shí)判斷是不是在目前的這個(gè)異常中執(zhí)行回滾。
            if (txInfo.transactionAttribute != null && txInfo.transactionAttribute.rollbackOn(ex)) {
                //執(zhí)行回滾
                    txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());
            } 
            else {
                        //否則不需要回滾身堡,直接提交即可邓尤。
                    txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());

            }
        }
    }

上面的代碼已經(jīng)把Spring的事務(wù)的基本原理說清楚了,如何進(jìn)行判斷執(zhí)行事務(wù),如何回滾汞扎。

下面到了真正執(zhí)行回滾邏輯的代碼中PlatformTransactionManager接口的子類季稳,我們以JDBC的事務(wù)為例,DataSourceTransactionManager就是jdbc的事務(wù)管理類澈魄。跟蹤上面的代碼rollback(txInfo.getTransactionStatus())可以發(fā)現(xiàn)最終執(zhí)行的代碼如下:

@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 {
        //調(diào)用jdbc的 rollback進(jìn)行回滾事務(wù)景鼠。
            con.rollback();
        }
        catch (SQLException ex) {
            throw new TransactionSystemException("Could not roll back JDBC transaction", ex);
        }
    }

小結(jié)

這里小結(jié)下Spring 中事務(wù)的實(shí)現(xiàn)思路,Spring 主要依靠 TransactionInterceptor 來攔截執(zhí)行方法體痹扇,判斷是否開啟事務(wù)铛漓,然后執(zhí)行事務(wù)方法體,方法體中catch住異常,接著判斷是否需要回滾鲫构,如果需要回滾就委托真正的TransactionManager 比如JDBC中的DataSourceTransactionManager來執(zhí)行回滾邏輯浓恶。提交事務(wù)也是同樣的道理。

這里用個(gè)流程圖展示下思路:

file

手寫一個(gè)注解實(shí)現(xiàn)事務(wù)回滾

我們弄清楚了Spring的事務(wù)執(zhí)行流程结笨,那我們可以模仿著自己寫一個(gè)注解包晰,實(shí)現(xiàn)遇到指定異常就回滾的功能。這里持久層就以最簡單的JDBC為例炕吸。

我們先梳理下需求伐憾,首先注解我們可以基于Spring 的AOP來實(shí)現(xiàn),接著既然是JDBC,那么我們需要一個(gè)類來幫我們管理連接赫模,用來判斷異常是否回滾或者提交树肃。梳理完就開干吧。

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>

2、新增一個(gè)注解

/**
 * @description:
 * @author: luozhou 
 * @create: 2020-03-29 17:05
 **/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface MyTransaction {
    //指定異忱蚋疲回滾
    Class<? extends Throwable>[] rollbackFor() default {};
}

3廓脆、新增連接管理器

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

/**
 * @description:
 * @author: luozhou 
 * @create: 2020-03-29 21:14
 **/
@Component
public class DataSourceConnectHolder {
    @Autowired
    DataSource dataSource;
    /**
     * 線程綁定對(duì)象
     */
    ThreadLocal<Connection> resources = new NamedThreadLocal<>("Transactional resources");

    public Connection getConnection() {
        Connection con = resources.get();
        if (con != null) {
            return con;
        }
        try {
            con = dataSource.getConnection();
            //為了體現(xiàn)事務(wù)蚊伞,全部設(shè)置為手動(dòng)提交事務(wù)
            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席赂、新增一個(gè)切面

這部分是事務(wù)處理的核心,先獲取注解上的異常類时迫,然后捕獲住執(zhí)行的異常颅停,判斷異常是不是注解上的異常或者其子類掠拳,如果是就回滾癞揉,否則就提交。

/**
 * @description:
 * @author: luozhou 
 * @create: 2020-03-29 17:08
 **/
@Aspect
@Component
public 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;
    }
        /**
        * 執(zhí)行回滾,最后關(guān)閉連接和清理線程綁定
        */
    private void doRollBack() {
        try {
            connectHolder.getConnection().rollback();
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            connectHolder.cleanHolder();
        }

    }
        /**
        *執(zhí)行提交喊熟,最后關(guān)閉連接和清理線程綁定
        */
    private void doCommit() {
        try {
            connectHolder.getConnection().commit();
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            connectHolder.cleanHolder();
        }
    }
        /**
        *異常處理柏肪,捕獲的異常是目標(biāo)異常或者其子類芥牌,就進(jìn)行回滾烦味,否則就提交事務(wù)。
        */
    private void completeTransactionAfterThrowing(Throwable throwable) {
        if (es != null && es.length > 0) {
            for (Class<? extends Throwable> e : es) {
                if (e.isAssignableFrom(throwable.getClass())) {
                    doRollBack();
                }
            }
        }
        doCommit();
    }
}

5壁拉、測試驗(yàn)證

創(chuàng)建一個(gè)tb_test表谬俄,表結(jié)構(gòu)如下:

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;

1)編寫一個(gè)Service

saveTest方法調(diào)用了2個(gè)插入語句,同時(shí)聲明了@MyTransaction事務(wù)注解扇商,遇到NullPointerException就進(jìn)行回滾凤瘦,最后我們執(zhí)行了除以0操作,會(huì)拋出ArithmeticException案铺。我們用單元測試看看數(shù)據(jù)是否會(huì)回滾蔬芥。

/**
 * @description:
 * @author: luozhou kinglaw1204@gmail.com
 * @create: 2020-03-29 22:05
 **/
@Service
public class MyTransactionTest implements TestService {
    @Autowired
    DataSourceConnectHolder holder;
        //一個(gè)事務(wù)中執(zhí)行兩個(gè)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;
    }
        //執(zhí)行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();
        }
    }

}

2)單元測試

@SpringBootTest
@RunWith(SpringRunner.class)
class SpringTransactionApplicationTests {
    @Autowired
    private TestService service;

    @Test
    void contextLoads() throws SQLException {
        service.saveTest(1);
    }

}
圖片描述

上圖代碼聲明了事務(wù)對(duì)NullPointerException異常進(jìn)行回滾,運(yùn)行中遇到了ArithmeticException異常控汉,所以是不會(huì)回滾的笔诵,我們?cè)谟疫叺臄?shù)據(jù)庫中刷新發(fā)現(xiàn)數(shù)據(jù)正常插入成功了,說明并沒有回滾姑子。

圖片描述

我們把回滾的異常類改為ArithmeticException,把原數(shù)據(jù)清空再執(zhí)行一次乎婿,出現(xiàn)了ArithmeticException異常,這個(gè)時(shí)候查看數(shù)據(jù)庫是沒有記錄新增成功了街佑,這說明事物進(jìn)行回滾了谢翎,表明我們的注解起作用了。

總結(jié)

本文最開始回顧了事務(wù)的相關(guān)知識(shí)沐旨,并發(fā)事務(wù)會(huì)導(dǎo)致臟讀森逮、丟失修改、不可重復(fù)讀磁携、幻讀褒侧,為了解決這些問題,數(shù)據(jù)庫中就引入了事務(wù)的隔離級(jí)別谊迄,隔離級(jí)別包括:讀未提交闷供、讀提交、可重復(fù)讀和串行化统诺。

Spring中增強(qiáng)了事務(wù)的概念歪脏,為了解決方法A、方法B和方法C之間的事務(wù)關(guān)系粮呢,引入了事務(wù)傳播機(jī)制的概念唾糯。

Spring中的@Transactional注解的事務(wù)實(shí)現(xiàn)主要通過TransactionInterceptor攔截器來進(jìn)行實(shí)現(xiàn)的怠硼,攔截目標(biāo)方法,然后判斷異常是不是目標(biāo)異常移怯,如果是目標(biāo)異常就行進(jìn)行回滾香璃,否則就進(jìn)行事務(wù)提交。

最后我們自己通過JDBC結(jié)合Spring的AOP自己寫了個(gè)@MyTransactional的注解舟误,實(shí)現(xiàn)了遇到指定異称厦耄回滾的功能。

作者:木木匠

原文鏈接:https://juejin.im/post/5e7ef0bae51d4546f16bb3fb

文源網(wǎng)絡(luò)嵌溢,僅供學(xué)習(xí)之用眯牧,如有侵權(quán)請(qǐng)聯(lián)系刪除。

我將面試題和答案都整理成了PDF文檔赖草,還有一套學(xué)習(xí)資料学少,涵蓋Java虛擬機(jī)、spring框架秧骑、Java線程版确、數(shù)據(jù)結(jié)構(gòu)、設(shè)計(jì)模式等等乎折,但不僅限于此绒疗。

關(guān)注公眾號(hào)【java圈子】獲取資料,還有優(yōu)質(zhì)文章每日送達(dá)骂澄。

file
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末吓蘑,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子坟冲,更是在濱河造成了極大的恐慌磨镶,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,311評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件健提,死亡現(xiàn)場離奇詭異琳猫,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)矩桂,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門沸移,熙熙樓的掌柜王于貴愁眉苦臉地迎上來痪伦,“玉大人侄榴,你說我怎么就攤上這事⊥矗” “怎么了癞蚕?”我有些...
    開封第一講書人閱讀 152,671評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長辉哥。 經(jīng)常有香客問我桦山,道長攒射,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,252評(píng)論 1 279
  • 正文 為了忘掉前任恒水,我火速辦了婚禮会放,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘钉凌。我一直安慰自己咧最,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評(píng)論 5 371
  • 文/花漫 我一把揭開白布御雕。 她就那樣靜靜地躺著矢沿,像睡著了一般。 火紅的嫁衣襯著肌膚如雪酸纲。 梳的紋絲不亂的頭發(fā)上捣鲸,一...
    開封第一講書人閱讀 49,031評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音闽坡,去河邊找鬼栽惶。 笑死,一個(gè)胖子當(dāng)著我的面吹牛无午,可吹牛的內(nèi)容都是我干的媒役。 我是一名探鬼主播,決...
    沈念sama閱讀 38,340評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼宪迟,長吁一口氣:“原來是場噩夢啊……” “哼酣衷!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起次泽,我...
    開封第一講書人閱讀 36,973評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤穿仪,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后意荤,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體啊片,經(jīng)...
    沈念sama閱讀 43,466評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評(píng)論 2 323
  • 正文 我和宋清朗相戀三年玖像,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了紫谷。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,039評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡捐寥,死狀恐怖狂票,靈堂內(nèi)的尸體忽然破棺而出赴邻,到底是詐尸還是另有隱情舅柜,我是刑警寧澤完丽,帶...
    沈念sama閱讀 33,701評(píng)論 4 323
  • 正文 年R本政府宣布,位于F島的核電站乡洼,受9級(jí)特大地震影響崇裁,放射性物質(zhì)發(fā)生泄漏匕坯。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評(píng)論 3 307
  • 文/蒙蒙 一拔稳、第九天 我趴在偏房一處隱蔽的房頂上張望葛峻。 院中可真熱鬧,春花似錦巴比、人聲如沸泞歉。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽腰耙。三九已至,卻和暖如春铲球,著一層夾襖步出監(jiān)牢的瞬間挺庞,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來泰國打工稼病, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留选侨,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,497評(píng)論 2 354
  • 正文 我出身青樓然走,卻偏偏與公主長得像援制,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子芍瑞,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評(píng)論 2 345