【Spring系列】- Spring事務底層原理
Spring事務底層原理
😄生命不息,寫作不止
🔥 繼續踏上學習之路,學之分享筆記
👊 總有一天我也能像各位大佬一樣
🏆 一個有夢有戲的人 @怒放吧德德
🌝分享學習心得,歡迎指正,大家一起學習成長!
前言
昨天學習了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;
}
}
準備數據表
本次實驗使用學生表,就簡單幾個字段。
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());
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')");
}
}
可是,最後的結果卻不是預期結果。
失效原理
可見最後還是將兩條數據插入了,顯然這個事務是不回滾的,那麼這是為什麼呢?從上面說事務的底層原理就可以知道,當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();
}
}
然後把數據表清空,在此執行
這回就報錯了,而且也是我們所期望的錯誤,在看一下數據表,發現數據沒有保存進去。
方案二
那如果我們還是想要在本類中去執行這個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();
}
也能得到我們預期的實現效果,數據庫中也沒有相關數據。
@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是不同,那麼最後得到的連接也就不同。
👍創作不易,如有錯誤請指正,感謝觀看!記得點贊哦!👍