1. 程式人生 > 其它 >spring成神之路第四十五篇:帶你吃透 Spring 事務 7 種傳播行為

spring成神之路第四十五篇:帶你吃透 Spring 事務 7 種傳播行為

本文詳解Spring事務中的7種傳播行為,還是比較重要的。

環境

  1. jdk1.8
  2. Spring 5.2.3.RELEASE
  3. mysql5.7

什麼是事務傳播行為?

事務的傳播行為用來描述:系統中的一些方法交由spring來管理事務,當這些方法之間出現巢狀呼叫的時候,事務所表現出來的行為是什麼樣的?

比如下面2個類,Service1中的m1方法和Service2中的m2方法上面都有@Transactional註解,說明這2個方法由spring來控制事務。

但是注意m1中2行程式碼,先執行了一個insert,然後呼叫service2中的m2方法,service2中的m2方法也執行了一個insert。

那麼大家覺得這2個insert會在一個事務中執行麼?也就是說此時事務的表現行為是什麼樣的呢?這個就是spring事務的傳播行為來控制的事情,不同的傳播行為,表現會不一樣,可能他們會在一個事務中執行,也可能不會在一個事務中執行,這就需要看傳播行為的配置了。

@Component
publicclassService1{
@Autowired
privateService2service2;

@Autowired
privateJdbcTemplatejdbcTemplate;

@Transactional
publicvoidm1(){
this.jdbcTemplate.update("INSERTintot1values('m1')"
);
this.service2.m2();
}
}

@Component
publicclassService2{
@Autowired
privateJdbcTemplatejdbcTemplate;

@Transactional
publicvoidm2(){
this.jdbcTemplate.update("INSERTintot1values('m2')");
}
}

如何配置事務傳播行為?

通過@Transactional註解中的propagation屬性來指定事務的傳播行為

Propagationpropagation()defaultPropagation.REQUIRED;

Propagation是個列舉,有7種值,如下:

事務傳播行為型別說明
REQUIRED 如果當前事務管理器中沒有事務,就新建一個事務,如果已經存在一個事務中,加入到這個事務中。這是最常見的選擇,是預設的傳播行為。
SUPPORTS 支援當前事務,如果當前事務管理器中沒有事務,就以非事務方式執行
MANDATORY 使用當前的事務,如果當前事務管理器中沒有事務,就丟擲異常。
REQUIRES_NEW 新建事務,如果當前事務管理器中存在事務,把當前事務掛起,然後會新建一個事務。
NOT_SUPPORTED 以非事務方式執行操作,如果當前事務管理器中存在事務,就把當前事務掛起。
NEVER 以非事務方式執行,如果當前事務管理器中存在事務,則丟擲異常。
NESTED 如果當前事務管理器中存在事務,則在巢狀事務內執行;如果當前事務管理器中沒有事務,則執行與PROPAGATION_REQUIRED類似的操作。

注意:這7種傳播行為有個前提,他們的事務管理器是同一個的時候,才會有上面描述中的表現行為。

下面通過案例對7中表現行為來做說明,在看案例之前,先來回顧幾個知識點

1、Spring宣告式事務處理事務的過程

spring宣告式事務是通過事務攔截器TransactionInterceptor攔截目標方法,來實現事務管理的功能的,事務管理器處理過程大致如下:

1、獲取事務管理器
2、通過事務管理器開啟事務
try{
3、呼叫業務方法執行db操作
4、提交事務
}catch(RuntimeException|Error){
5、回滾事務
}

2、何時事務會回滾?

預設情況下,目標方法丟擲RuntimeException或者Error的時候,事務會被回滾

3、Spring事務管理器中的Connection和業務中操作db的Connection如何使用同一個的?

以DataSourceTransactionManager為事務管理器,操作db使用JdbcTemplate來說明一下。

建立DataSourceTransactionManager和JdbcTemplate的時候都需要指定dataSource,需要將他倆的dataSource指定為同一個物件。

當事務管理器開啟事務的時候,會通過dataSource.getConnection()方法獲取一個db連線connection,然後會將dataSource->connection丟到一個Map中,然後將map放到ThreadLocal中。

當JdbcTemplate執行sql的時候,以JdbcTemplate.dataSource去上面的ThreadLocal中查詢,是否有可用的連線,如果有,就直接拿來用了,否則呼叫JdbcTemplate.dataSource.getConnection()方法獲取一個連線來用。

所以spring中可以確保事務管理器中的Connection和JdbcTemplate中操作db的Connection是同一個,這樣才能確保spring可以控制事務。

程式碼驗證

準備db

DROPDATABASEIFEXISTSjavacode2018;
CREATEDATABASEifNOTEXISTSjavacode2018;

USEjavacode2018;
DROPTABLEIFEXISTSuser1;
CREATETABLEuser1(
idintPRIMARYKEYAUTO_INCREMENT,
namevarchar(64)NOTNULLDEFAULT''COMMENT'姓名'
);

DROPTABLEIFEXISTSuser2;
CREATETABLEuser2(
idintPRIMARYKEYAUTO_INCREMENT,
namevarchar(64)NOTNULLDEFAULT''COMMENT'姓名'
);

spring配置類MainConfig6

準備JdbcTemplate和事務管理器。

packagecom.javacode2018.tx.demo6;

importorg.springframework.context.annotation.Bean;
importorg.springframework.context.annotation.ComponentScan;
importorg.springframework.context.annotation.Configuration;
importorg.springframework.jdbc.core.JdbcTemplate;
importorg.springframework.jdbc.datasource.DataSourceTransactionManager;
importorg.springframework.transaction.PlatformTransactionManager;
importorg.springframework.transaction.annotation.EnableTransactionManagement;

importjavax.sql.DataSource;

@EnableTransactionManagement//開啟spring事務管理功能
@Configuration//指定當前類是一個spring配置類
@ComponentScan//開啟bean掃描註冊
publicclassMainConfig6{
//定義一個數據源
@Bean
publicDataSourcedataSource(){
org.apache.tomcat.jdbc.pool.DataSourcedataSource=neworg.apache.tomcat.jdbc.pool.DataSource();
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/javacode2018?characterEncoding=UTF-8");
dataSource.setUsername("root");
dataSource.setPassword("root123");
dataSource.setInitialSize(5);
returndataSource;
}

//定義一個JdbcTemplate,用來執行db操作
@Bean
publicJdbcTemplatejdbcTemplate(DataSourcedataSource){
returnnewJdbcTemplate(dataSource);
}

//定義我一個事務管理器
@Bean
publicPlatformTransactionManagertransactionManager(DataSourcedataSource){
returnnewDataSourceTransactionManager(dataSource);
}
}

來3個service

後面的案例中會在這3個service中使用spring的事務來演示效果。

User1Service

packagecom.javacode2018.tx.demo6;

importorg.springframework.beans.factory.annotation.Autowired;
importorg.springframework.jdbc.core.JdbcTemplate;
importorg.springframework.stereotype.Component;

@Component
publicclassUser1Service{
@Autowired
privateJdbcTemplatejdbcTemplate;
}

User2Service

packagecom.javacode2018.tx.demo6;

importorg.springframework.beans.factory.annotation.Autowired;
importorg.springframework.jdbc.core.JdbcTemplate;
importorg.springframework.stereotype.Component;

@Component
publicclassUser2Service{
@Autowired
privateJdbcTemplatejdbcTemplate;
}

TxService

packagecom.javacode2018.tx.demo6;

importorg.springframework.beans.factory.annotation.Autowired;
importorg.springframework.stereotype.Component;

@Component
publicclassTxService{
@Autowired
privateUser1Serviceuser1Service;
@Autowired
privateUser2Serviceuser2Service;
}

測試用例Demo6Test

before方法會在每個@Test標註的方法之前執行一次,這個方法主要用來做一些準備工作:啟動spring容器、清理2個表中的資料;after方法會在每個@Test標註的方法執行完畢之後執行一次,我們在這個裡面輸出2個表的資料;方便檢視的測試用例效果。

packagecom.javacode2018.tx.demo6;

importorg.junit.Before;
importorg.springframework.context.annotation.AnnotationConfigApplicationContext;

publicclassDemo6Test{

privateTxServicetxService;
privateJdbcTemplatejdbcTemplate;

//每個@Test用例執行之前先啟動一下spring容器,並清理一下user1、user2中的資料
@Before
publicvoidbefore(){
AnnotationConfigApplicationContextcontext=newAnnotationConfigApplicationContext(MainConfig6.class);
txService=context.getBean(TxService.class);
jdbcTemplate=context.getBean(JdbcTemplate.class);
jdbcTemplate.update("truncatetableuser1");
jdbcTemplate.update("truncatetableuser2");
}

@After
publicvoidafter(){
System.out.println("user1表資料:"+jdbcTemplate.queryForList("SELECT*fromuser1"));
System.out.println("user2表資料:"+jdbcTemplate.queryForList("SELECT*fromuser2"));
}

}

1、REQUIRED

User1Service

新增1個方法,事務傳播行為:REQUIRED

@Transactional(propagation=Propagation.REQUIRED)
publicvoidrequired(Stringname){
this.jdbcTemplate.update("insertintouser1(name)VALUES(?)",name);
}

User2Service

新增2個方法,事務傳播行為:REQUIRED,注意第2個方法內部最後一行會丟擲一個異常

@Transactional(propagation=Propagation.REQUIRED)
publicvoidrequired(Stringname){
this.jdbcTemplate.update("insertintouser1(name)VALUES(?)",name);
}

@Transactional(propagation=Propagation.REQUIRED)
publicvoidrequired_exception(Stringname){
this.jdbcTemplate.update("insertintouser1(name)VALUES(?)",name);
thrownewRuntimeException();
}

場景1(1-1)

外圍方法沒有事務,外圍方法內部呼叫2個REQUIRED級別的事務方法。

案例中都是在TxService的方法中去呼叫另外2個service,所以TxService中的方法統稱外圍方法,另外2個service中的方法稱內部方法

驗證方法1

TxService新增
publicvoidnotransaction_exception_required_required(){
this.user1Service.required("張三");
this.user2Service.required("李四");
thrownewRuntimeException();
}
測試用例,Demo6Test中新增
@Test
publicvoidnotransaction_exception_required_required(){
txService.notransaction_exception_required_required();
}
執行輸出
user1表資料:[{id=1,name=張三}]
user2表資料:[{id=1,name=李四}]

驗證方法2

TxService新增
publicvoidnotransaction_required_required_exception(){
this.user1Service.required("張三");
this.user2Service.required_exception("李四");
}
測試用例,Demo6Test中新增
@Test
publicvoidnotransaction_required_required_exception(){
txService.notransaction_required_required_exception();
}
執行輸出
user1表資料:[{id=1,name=張三}]
user2表資料:[]

結果分析

驗證方法序號資料庫結果結果分析
1 “張三”、“李四”均插入。 外圍方法未開啟事務,插入“張三”、“李四”方法在自己的事務中獨立執行,外圍方法異常不影響內部插入“張三”、“李四”方法獨立的事務。
2 “張三”插入,“李四”未插入。 外圍方法沒有事務,插入“張三”、“李四”方法都在自己的事務中獨立執行,所以插入“李四”方法丟擲異常只會回滾插入“李四”方法,插入“張三”方法不受影響。

結論

通過這兩個方法我們證明了在外圍方法未開啟事務的情況下Propagation.REQUIRED修飾的內部方法會新開啟自己的事務,且開啟的事務相互獨立,互不干擾。

場景2(1-2)

外圍方法開啟事務(Propagation.REQUIRED),這個使用頻率特別高。

驗證方法1

TxService新增
@Transactional(propagation=Propagation.REQUIRED)
publicvoidtransaction_exception_required_required(){
user1Service.required("張三");
user2Service.required("李四");
thrownewRuntimeException();
}
測試用例,Demo6Test中新增
@Test
publicvoidtransaction_exception_required_required(){
txService.transaction_exception_required_required();
}
執行輸出
user1表資料:[]
user2表資料:[]

驗證方法2

TxService新增
@Transactional(propagation=Propagation.REQUIRED)
publicvoidtransaction_required_required_exception(){
user1Service.required("張三");
user2Service.required_exception("李四");
}
測試用例,Demo6Test中新增
@Test
publicvoidtransaction_required_required_exception(){
txService.transaction_required_required_exception();
}
執行輸出
user1表資料:[]
user2表資料:[]

驗證方法3

TxService新增
@Transactional(propagation=Propagation.REQUIRED)
publicvoidtransaction_required_required_exception_try(){
user1Service.required("張三");
try{
user2Service.required_exception("李四");
}catch(Exceptione){
System.out.println("方法回滾");
}
}
測試用例,Demo6Test中新增
@Test
publicvoidtransaction_required_required_exception_try(){
txService.transaction_required_required_exception_try();
}
執行輸出
方法回滾
user1表資料:[]
user2表資料:[]

結果分析

驗證方法序號資料庫結果結果分析
1 “張三”、“李四”均未插入。 外圍方法開啟事務,內部方法加入外圍方法事務,外圍方法回滾,內部方法也要回滾
2 “張三”、“李四”均未插入。 外圍方法開啟事務,內部方法加入外圍方法事務,內部方法丟擲異常回滾,外圍方法感知異常致使整體事務回滾
3 “張三”、“李四”均未插入。 外圍方法開啟事務,內部方法加入外圍方法事務,內部方法丟擲異常回滾,即使方法被catch不被外圍方法感知,整個事務依然回滾

結論

以上試驗結果我們證明在外圍方法開啟事務的情況下Propagation.REQUIRED修飾的內部方法會加入到外圍方法的事務中,所有Propagation.REQUIRED修飾的內部方法和外圍方法均屬於同一事務,只要一個方法回滾整個事務均回滾

2、PROPAGATION_REQUIRES_NEW

User1Service

新增1個方法,事務傳播行為:REQUIRES_NEW

@Transactional(propagation=Propagation.REQUIRES_NEW)
publicvoidrequires_new(Stringname){
this.jdbcTemplate.update("insertintouser1(name)VALUES(?)",name);
}

User2Service

新增2個方法,事務傳播行為:REQUIRES_NEW,注意第2個方法內部最後一行會丟擲一個異常

@Transactional(propagation=Propagation.REQUIRES_NEW)
publicvoidrequires_new(Stringname){
this.jdbcTemplate.update("insertintouser2(name)VALUES(?)",name);
}

@Transactional(propagation=Propagation.REQUIRES_NEW)
publicvoidrequires_new_exception(Stringname){
this.jdbcTemplate.update("insertintouser2(name)VALUES(?)",name);
thrownewRuntimeException();
}

場景1(2-1)

外圍方法沒有事務

驗證方法1

TxService新增
publicvoidnotransaction_exception_requiresNew_requiresNew(){
user1Service.requires_new("張三");
user2Service.requires_new("李四");
thrownewRuntimeException();
}
Demo6Test中新增
@Test
publicvoidnotransaction_exception_requiresNew_requiresNew(){
txService.notransaction_exception_requiresNew_requiresNew();
}
執行輸出
user1表資料:[{id=1,name=張三}]
user2表資料:[{id=1,name=李四}]

驗證方法2

TxService新增
publicvoidnotransaction_requiresNew_requiresNew_exception(){
user1Service.requires_new("張三");
user2Service.requires_new_exception("李四");
}
測試用例,Demo6Test中新增
@Test
publicvoidnotransaction_requiresNew_requiresNew_exception(){
txService.notransaction_requiresNew_requiresNew_exception();
}
執行輸出
user1表資料:[{id=1,name=張三}]
user2表資料:[]

結果分析

驗證方法序號資料庫結果結果分析
1 “張三”插入,“李四”插入。 外圍方法沒有事務,插入“張三”、“李四”方法都在自己的事務中獨立執行,外圍方法丟擲異常回滾不會影響內部方法。
2 “張三”插入,“李四”未插入 外圍方法沒有開啟事務,插入“張三”方法和插入“李四”方法分別開啟自己的事務,插入“李四”方法丟擲異常回滾,其他事務不受影響。

結論

通過這兩個方法我們證明了在外圍方法未開啟事務的情況下Propagation.REQUIRES_NEW修飾的內部方法會新開啟自己的事務,且開啟的事務相互獨立,互不干擾

場景2(2-2)

外圍方法開啟事務

驗證方法1

TxService新增
@Transactional(propagation=Propagation.REQUIRED)
publicvoidtransaction_exception_required_requiresNew_requiresNew(){
user1Service.required("張三");

user2Service.requires_new("李四");

user2Service.requires_new("王五");
thrownewRuntimeException();
}
測試用例,Demo6Test中新增
@Test
publicvoidtransaction_exception_required_requiresNew_requiresNew(){
txService.transaction_exception_required_requiresNew_requiresNew();
}
執行輸出
user1表資料:[]
user2表資料:[{id=1,name=李四},{id=2,name=王五}]

驗證方法2

TxService新增
@Transactional(propagation=Propagation.REQUIRED)
publicvoidtransaction_required_requiresNew_requiresNew_exception(){
user1Service.required("張三");

user2Service.requires_new("李四");

user2Service.requires_new_exception("王五");
}
Demo6Test中新增
@Test
publicvoidtransaction_required_requiresNew_requiresNew_exception(){
txService.transaction_required_requiresNew_requiresNew_exception();
}
執行輸出
user1表資料:[]
user2表資料:[{id=1,name=李四}]

驗證方法3

TxService新增
@Transactional(propagation=Propagation.REQUIRED)
publicvoidtransaction_required_requiresNew_requiresNew_exception_try(){
user1Service.required("張三");

user2Service.requires_new("李四");

try{
user2Service.requires_new_exception("王五");
}catch(Exceptione){
System.out.println("回滾");
}
}
Demo6Test中新增
@Test
publicvoidtransaction_required_requiresNew_requiresNew_exception_try(){
txService.transaction_required_requiresNew_requiresNew_exception_try();
}
執行輸出
回滾
user1表資料:[{id=1,name=張三}]
user2表資料:[{id=1,name=李四}]

結果分析

驗證方法序號資料庫結果結果分析
1 “張三”未插入,“李四”插入,“王五”插入。 外圍方法開啟事務,插入“張三”方法和外圍方法一個事務,插入“李四”方法、插入“王五”方法分別在獨立的新建事務中,外圍方法丟擲異常只回滾和外圍方法同一事務的方法,故插入“張三”的方法回滾。
2 “張三”未插入,“李四”插入,“王五”未插入。 外圍方法開啟事務,插入“張三”方法和外圍方法一個事務,插入“李四”方法、插入“王五”方法分別在獨立的新建事務中。插入“王五”方法丟擲異常,首先插入 “王五”方法的事務被回滾,異常繼續丟擲被外圍方法感知,外圍方法事務亦被回滾,故插入“張三”方法也被回滾。
3 “張三”插入,“李四”插入,“王五”未插入。 外圍方法開啟事務,插入“張三”方法和外圍方法一個事務,插入“李四”方法、插入“王五”方法分別在獨立的新建事務中。插入“王五”方法丟擲異常,首先插入“王五”方法的事務被回滾,異常被catch不會被外圍方法感知,外圍方法事務不回滾,故插入“張三”方法插入成功。

結論

外圍方法開啟事務的情況下Propagation.REQUIRES_NEW修飾的內部方法依然會單獨開啟獨立事務,且與外部方法事務也獨立,內部方法之間、內部方法和外部方法事務均相互獨立,互不干擾。

3、PROPAGATION_NESTED

User1Service

新增1個方法,事務傳播行為:NESTED

@Transactional(propagation=Propagation.NESTED)
publicvoidnested(Stringname){
this.jdbcTemplate.update("insertintouser1(name)VALUES(?)",name);
}

User2Service

新增2個方法,事務傳播行為:NESTED,注意第2個方法內部最後一行會丟擲一個異常。

@Transactional(propagation=Propagation.NESTED)
publicvoidnested(Stringname){
this.jdbcTemplate.update("insertintouser2(name)VALUES(?)",name);
}

@Transactional(propagation=Propagation.NESTED)
publicvoidnested_exception(Stringname){
this.jdbcTemplate.update("insertintouser2(name)VALUES(?)",name);
thrownewRuntimeException();
}

場景1(3-1)

外圍方法沒有事務

驗證方法1

TxService新增
publicvoidnotransaction_exception_nested_nested(){
user1Service.nested("張三");
user2Service.nested("李四");
thrownewRuntimeException();
}
Demo6Test中新增
@Test
publicvoidnotransaction_exception_nested_nested(){
txService.notransaction_exception_nested_nested();
}
執行輸出
user1表資料:[{id=1,name=張三}]
user2表資料:[{id=1,name=李四}]

驗證方法2

TxService新增
publicvoidnotransaction_nested_nested_exception(){
user1Service.nested("張三");
user2Service.nested_exception("李四");
}
測試用例,Demo6Test中新增
@Test
publicvoidnotransaction_nested_nested_exception(){
txService.notransaction_nested_nested_exception();
}
執行輸出
user1表資料:[{id=1,name=張三}]
user2表資料:[]

結果分析

驗證方法序號資料庫結果結果分析
1 “張三”、“李四”均插入。 外圍方法未開啟事務,插入“張三”、“李四”方法在自己的事務中獨立執行,外圍方法異常不影響內部插入“張三”、“李四”方法獨立的事務。
2 “張三”插入,“李四”未插入。 外圍方法沒有事務,插入“張三”、“李四”方法都在自己的事務中獨立執行,所以插入“李四”方法丟擲異常只會回滾插入“李四”方法,插入“張三”方法不受影響。

結論

通過這兩個方法我們證明了在外圍方法未開啟事務的情況下Propagation.NESTEDPropagation.REQUIRED作用相同,修飾的內部方法都會新開啟自己的事務,且開啟的事務相互獨立,互不干擾

場景2(3-1)

外圍方法開啟事務

驗證方法1

TxService新增
@Transactional
publicvoidtransaction_exception_nested_nested(){
user1Service.nested("張三");
user2Service.nested("李四");
thrownewRuntimeException();
}
測試用例,Demo6Test中新增
@Test
publicvoidtransaction_exception_nested_nested(){
txService.transaction_exception_nested_nested();
}
執行輸出
user1表資料:[]
user2表資料:[]

驗證方法2

TxService新增
@Transactional
publicvoidtransaction_nested_nested_exception(){
user1Service.nested("張三");
user2Service.nested_exception("李四");
}
Demo6Test中新增
@Test
publicvoidtransaction_nested_nested_exception(){
txService.transaction_nested_nested_exception();
}
執行輸出
user1表資料:[]
user2表資料:[]

驗證方法3

TxService新增
@Transactional
publicvoidtransaction_nested_nested_exception_try(){
user1Service.nested("張三");
try{
user2Service.nested_exception("李四");
}catch(Exceptione){
System.out.println("方法回滾");
}
}
Demo6Test中新增
@Test
publicvoidtransaction_nested_nested_exception_try(){
txService.transaction_nested_nested_exception_try();
}
執行輸出
方法回滾
user1表資料:[{id=1,name=張三}]
user2表資料:[]

結果分析

驗證方法序號資料庫結果結果分析
1 “張三”、“李四”均未插入。 外圍方法開啟事務,內部事務為外圍事務的子事務,外圍方法回滾,內部方法也要回滾。
2 “張三”、“李四”均未插入。 外圍方法開啟事務,內部事務為外圍事務的子事務,內部方法丟擲異常回滾,且外圍方法感知異常致使整體事務回滾。
3 “張三”插入、“李四”未插入。 外圍方法開啟事務,內部事務為外圍事務的子事務,插入“李四”內部方法丟擲異常,可以單獨對子事務回滾。

結論

以上試驗結果我們證明在外圍方法開啟事務的情況下Propagation.NESTED修飾的內部方法屬於外部事務的子事務,外圍主事務回滾,子事務一定回滾,而內部子事務可以單獨回滾而不影響外圍主事務和其他子事務

內部事務原理

以mysql為例,mysql中有個savepoint的功能,NESTED內部事務就是通過這個實現的。

REQUIRED,REQUIRES_NEW,NESTED比較

由“場景2(1-2)”和“場景2(3-2)”對比,我們可知:

REQUIRED和NESTED修飾的內部方法都屬於外圍方法事務,如果外圍方法丟擲異常,這兩種方法的事務都會被回滾。但是REQUIRED是加入外圍方法事務,所以和外圍事務同屬於一個事務,一旦REQUIRED事務丟擲異常被回滾,外圍方法事務也將被回滾。而NESTED是外圍方法的子事務,有單獨的儲存點,所以NESTED方法丟擲異常被回滾,不會影響到外圍方法的事務。

由“場景2(2-2)”和“場景2(3-2)”對比,我們可知: