【Spring系列】- Spring事務底層原理

Spring事務底層原理

😄生命不息,寫作不止
🔥 繼續踏上學習之路,學之分享筆記
👊 總有一天我也能像各位大佬一樣
🏆 一個有夢有戲的人 @怒放吧德德
🌝分享學習心得,歡迎指正,大家一起學習成長!

事務.jpg

前言

昨天學習了bean生命周期底層原理,今天就來接著簡單學習spring事務的底層理解。

實驗準備

配置文件

首先在配置文件中配置jdbcTemplate和事務管理器,並且需要開啟事務的註解@EnableTransactionManagement以及@Configuration註解

@ComponentScan("com.lyd")
@EnableTransactionManagement
@Configuration
public class ApplicationConfig {
    @Bean
    public JdbcTemplate jdbcTemplate() {
        return new JdbcTemplate(dataSource());
    }
    @Bean
    public PlatformTransactionManager transactionManager() {
        DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
        transactionManager.setDataSource(dataSource());
        return transactionManager;
    }
    @Bean
    public DataSource dataSource() {
        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setUrl("jdbc:mysql://127.0.0.1:3306/eladmin?serverTimezone=Asia/Shanghai&characterEncoding=utf8&useSSL=false&useOldAliasMetadataBehavior=true");
        dataSource.setUsername("root");
        dataSource.setPassword("12356");
        return dataSource;
    }
}

準備數據表

本次實驗使用學生表,就簡單幾個欄位。
image.png

Spring事務的底層原理

       我們在需要加上事務的方法上添加@Transactional註解,然後在此方法中使用jdbcTemplate去執行SQL語句,再來執行此方法,觀察可以看到,事務真的回滾了。

@Component
public class UserService {
    @Autowired
    private JdbcTemplate jdbcTemplate;
    @Transactional
    public void test(){
        jdbcTemplate.execute("insert into student values (1, 'lyd', 18, '20183033210')");
       throw new NullPointerException();
    }
}

原理

       首先spring會調用代理對象,對於事務,代理對象會通過執行事務的切面邏輯。在這個切面邏輯,Spring會去判斷是否含有@Transactional事務註解,如果有才會去開啟事務。spring的事務管理器會新建一個資料庫連接conn,緊接著會把conn.autocommit 設置為 false ,autocommit(自動提交),每次執行完SQL後就會立馬提交,因此這裡需要設置為false。spring默認是開啟了自動提交,當SQL執行結束之後就會提交,當遇到異常的時候,由於前面的事務都已經提升,因此就沒法回滾了,所以需要把自動提交給關閉了。最後在通過第一次創建的對象(Spring個生命周期中通過構造方法創造的對象)去執行test方法。接著會去執行SQL語句,在此SQL執行完之後是不會進行提交的,在執行SQL語句之前,jdbcTemplate會去拿到事務管理器創建的這個資料庫連接conn。當執行完test方法後,Spring事務會去判斷是否有異常,沒有異常就會提交事務(conn.commit()),否者就會事務回滾(conn.rollback());
image.png

Spring事務失效

       接下來實現一個案例,在test方法中調用本類的另一個方法Add,兩個方法都是執行了插入的SQL語句。兩個方法都加上了@Transactional註解,只是在第二個方法中的註解標上一個策略:propagation = Propagation.NEVER,這個的意思是:總是非事務地執行,如果存在一個活動事務,則拋出異常。按道理來說以下程式碼執行將會出現異常,並且會回滾事務。

@Component
public class UserService {
    @Autowired
    private JdbcTemplate jdbcTemplate;
    @Transactional
    public void test(){
        jdbcTemplate.execute("insert into student values (1, 'lyd', 18, '20183033210')");
        doAdd();
    }
    @Transactional(propagation = Propagation.NEVER)
    public void doAdd() {
        jdbcTemplate.execute("insert into student values (2, 'lyy', 18, '20183033211')");
    }
}

可是,最後的結果卻不是預期結果。
image.png

失效原理

       可見最後還是將兩條數據插入了,顯然這個事務是不回滾的,那麼這是為什麼呢?從上面說事務的底層原理就可以知道,當spring創建了代理對象,在代理對象內部的test方法中的切面邏輯,會去創建資料庫連接等等,最後由普通對象(UserService.class通過構造方法去創建的對象)去執行test,也就是相當於是使用了普通對象去執行doAdd方法,普通對象就只是構造方法實例化的一個對象,執行doAdd並不會去檢測這個@Transactional註解,因此這個事務就不會被執行到,也就不會回滾。然而spring應該是在執行代理類的test方法時候回去判斷@Transactional註解,會有額外的邏輯去判斷事務,也就是doAdd應該也要由代理對象去執行。

解決方案

那麼有辦法解決嗎?辦法肯定過有,接下來介紹一種解決方法

方案一

將add方法抽到另一個bean類裡面

@Component
public class UserBaseService {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Transactional(propagation = Propagation.NEVER)
    public void doAdd() {
        jdbcTemplate.execute("insert into student values (2, 'lyy', 18, '20183033211')");
    }
}

再來通過bean對象來執行這個doAdd方法

@Component
public class UserService {
    private JdbcTemplate jdbcTemplate;
    private UserBaseService userBaseService;
        jdbcTemplate.execute("insert into student values (1, 'lyd', 18, '20183033210')");
        userBaseService.doAdd();
    }
}

然後把數據表清空,在此執行
image.png

這回就報錯了,而且也是我們所期望的錯誤,在看一下數據表,發現數據沒有保存進去。

方案二

       那如果我們還是想要在本類中去執行這個doAdd方法呢?其實也是可以,就是我們通過自己調用自己的方式,在本類中引用本類的bean對象,此時他就是一個代理對象,這樣事務的策略也就能夠實現了。

@Autowired
private UserService userService;

@Transactional
public void test(){
    jdbcTemplate.execute("insert into student values (1, 'lyd', 18, '20183033210')");
    throw new NullPointerException();
    userService.doAdd();
}

也能得到我們預期的實現效果,資料庫中也沒有相關數據。
image.png

@Configuration底層原理

在上面我們提到了jdbcTemplate獲得資料庫連接,那麼這個又是如何得到呢?我們來看一下一開始的配置。

@Bean
public JdbcTemplate jdbcTemplate() {
    return new JdbcTemplate(dataSource());
}

@Bean
public PlatformTransactionManager transactionManager() {
    DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
    transactionManager.setDataSource(dataSource());
    return transactionManager;
}

@Bean
public DataSource dataSource() {
    DriverManagerDataSource dataSource = new DriverManagerDataSource();
    dataSource.setUrl("jdbc:mysql://127.0.0.1:3306/eladmin?serverTimezone=Asia/Shanghai&characterEncoding=utf8&useSSL=false&useOldAliasMetadataBehavior=true");
    dataSource.setUsername("root");
    dataSource.setPassword("12356");
    return dataSource;
}

貫穿邏輯

       在創建 jdbcTemplate 的bean對象的時候,會去調用 dataSource ,在創建事務管理PlatformTransactionManager也會去調用一次dataSource方法,在這個方法中會創建新的dataSource,那麼,所獲得的事務不就不一樣了嗎,顯然這是不行的。那麼在spring中,他是如何實現單例的呢?就是通過 @Configuration 註解來實現。
       其實spring會通過ThreadLocal(執行緒變數,是一個以ThreadLocal對象為鍵、任意對象為值的存儲結構),在spring中ThreadLocal<Map<DataSource, conn>>是根據 DataSource來存儲連接conn,如果沒有 @Configuration註解來實現,兩次使用的DataSource就是不同的。 jdbcTemplate在獲取 DataSource 對象的時候,會去ThreadLocal的map根據jdbcTemplate自己的DataSource去找連接,然而這時候DataSource對象不同,他就找不到,就會自己從新生成連接,當執行完SQL語句之後,就會去提交,這時候接下來再拋異常已經沒用了。

@Configuration原理

       然而,@Configuration能夠實現單例DataSource對象呢?這就是因為@Configuration也是採用了動態代理會創建ApplicationConfig的代理對象。spring會創建ApplicationConfig的代理對象,這個代理對象會去調用jdbcTemplate方法,而代理對象會執行super.jdbcTemplate(),在這個方法中需要執行dataSource()方法,他會首先去容器中找是否有dataSource的bean對象,如果有直接返回,沒有就會創建,創建之後會將單例進行保存。接著transactionManager方法也是由代理對象去執行的,在他需要dataSource對象的時候,也是現去容器查找,這就能夠實現他們兩個的dataSource是一樣的了。這樣事務拿到的資料庫連接就是相同了,如果是在使用dataSource這個bean對象的時候,使用的beanName是不同,那麼最後得到的連接也就不同。

👍創作不易,如有錯誤請指正,感謝觀看!記得點贊哦!👍