【09】Spring筆記–聲明式事務
一、聲明式事務的使用
對于聲明式事務,使用@Transactional 注解 進行标注即可,可以放在類或方法上,Spring就會産生AOP的功能,這是Spring事務的底層實作
- 放在類上,該類的所有公共非靜态方法都将啟用事務功能
- 放在方法上,就代表這個方法啟用事務
在 @Transactional 中,可配置許多屬性,比如事務的隔離級别和傳播行為,或者復原政策
當啟動事務時,就會根據事務定義器内的配置去設定事務,首先根據傳播行為去确定事務的政策,然後是隔離級别、逾時時間、隻讀等内容設定
檢視 @Transactional 源碼
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
// 通過Bean name指定事務管理器
@AliasFor("transactionManager")
String value() default "";
// 同value屬性
@AliasFor("value")
String transactionManager() default "";
// 傳播行為設定
Propagation propagation() default Propagation.REQUIRED;
// 隔離級别設定
Isolation isolation() default Isolation.DEFAULT;
// 逾時時間 機關秒
int timeout() default -1;
// 是否隻讀事務
boolean readOnly() default false;
// 指定異常復原,預設所有異常都復原
Class<? extends Throwable>[] rollbackFor() default {};
// 指定異常名稱復原,~
String[] rollbackForClassName() default {};
// 指定發送哪些異常不復原,~
Class<? extends Throwable>[] noRollbackFor() default {};
String[] noRollbackForClassName() default {};
}
1.1 Spring 事務管理器
在Spring中,事務管理器的頂層接口是PlatformTransactionManager,當我們在Spring boot裡面引入MyBatis時,就會自動建立一個 DataSourceTransactionManager 對象,作為事務管理器
檢視PlatformTransactionManager源碼
public interface PlatformTransactionManager extends TransactionManager {
// 擷取事務,還可以設定資料屬性
TransactionStatus getTransaction(@Nullable TransactionDefinition var1) throws TransactionException;
// 送出事務
void commit(TransactionStatus var1) throws TransactionException;
// 復原事務
void rollback(TransactionStatus var1) throws TransactionException;
}
Spring 在事務管理時,就會将這些方法按照約定織入對應的流程中。TransactionDefinition 是一個事務定義器,依賴于 @Transactional 的配置項生成,通過它可以設定事務的屬性
二、隔離級别
隔離級别是資料庫的概念。場景:對于商品庫存,時刻都是多個線程共享的資料,這樣就會在多線程環境下扣減商品庫存。對于資料庫而已,則出現多個事務同時通路同一記錄的情況,就會引起資料出現不一緻的情況,便是資料庫的丢失更新問題
2.1 資料庫事務的4個特性
- 原子性(Atomicity):事務是最小的執行機關,不可分割。事務中的一系列操作要麼都成功,要麼都失敗。
- 一緻性(Consistency):事務前後,資料保持一緻。比如,轉賬業務中,2個人的總額在事務前後保持不變
- 隔離性(Isolation):并發事務中,一個事務不會影響到其他事務的執行,不會互相幹擾
- 持久性(Durability):事務對資料的修改是永久的,不會因為資料庫的故障而改變
2.2 事務隔離級别
- 讀未送出:允許讀取到其他事務尚未送出的資料變更,可能導緻髒讀,丢失修改,不可重複讀,幻讀
- 讀已送出:隻允許讀取到已經送出的資料變更,防止了髒讀,不可重複讀,幻讀仍存在
- 可重複讀:在一個事務中,對同一資料的多次讀取結果是一緻的。除非資料被事務本身所修改
- 串行化:所有的事務依次逐個執行,這樣事務之間就完全不可能産生幹擾。最進階的隔離級别,可以防止幻讀
2.3 使用隔離級别
檢視隔離級别源碼
package org.springframework.transaction.annotation;
import org.springframework.transaction.TransactionDefinition;
public enum Isolation {
// 預設隔離級别(使用基礎資料存儲的預設隔離級别。 所有其他級别對應于JDBC隔離級别。
DEFAULT(TransactionDefinition.ISOLATION_DEFAULT),
// 讀未送出
READ_UNCOMMITTED(TransactionDefinition.ISOLATION_READ_UNCOMMITTED),
// 讀已送出
READ_COMMITTED(TransactionDefinition.ISOLATION_READ_COMMITTED),
// 可重複讀
REPEATABLE_READ(TransactionDefinition.ISOLATION_REPEATABLE_READ),
// 串行化
SERIALIZABLE(TransactionDefinition.ISOLATION_SERIALIZABLE);
private final int value;
Isolation(int value) {
this.value = value;
}
public int value() {
return this.value;
}
}
三、傳播行為
傳播行為是方法之間調用事務采取的政策問題。
大多數情況下,我們會認為資料庫事務要麼全部成功,要麼都失敗。但也有特殊情況,比如,執行一個批量程式,它會處理很多的交易,絕大多數交易可以順序完成,但極少數交易可能出現問題,我們不能因為這幾個異常交易而復原整個批量任務。而是隻復原那些出現異常的交易
3.1 傳播行為的定義
事務行為 | 說明 |
---|---|
REQUIRED | 預設傳播行為,如果目前存在事務,就沿用目前事務。否則建立一個事務運作子方法 |
SUPPORTS | 支援目前事務,如果目前沒有事務,則将繼續采用無事務方式運作子方法 |
MANDATORY | 必須使用事務,如果目前沒有事務,就抛出異常 |
REQUIRES_NEW | 無論目前是否存在事務,都建立事務運作方法。如果目前存在事務,把目前事務挂起 |
NOT_SUPPORTED | 不支援事務,如果目前存在事務,就挂起事務,運作方法 |
NEVER | 不支援事務,如果目前存在事務,則抛出異常 |
NESTED | 如果目前存在事務,則在嵌套事務内執行。如果目前沒有事務,則執行與REQUIRED類似的操作。 |
Spring中,是通過枚舉類 Propagation定義的,
public enum Propagation {
REQUIRED(TransactionDefinition.PROPAGATION_REQUIRED),
SUPPORTS(TransactionDefinition.PROPAGATION_SUPPORTS),
MANDATORY(TransactionDefinition.PROPAGATION_MANDATORY),
REQUIRES_NEW(TransactionDefinition.PROPAGATION_REQUIRES_NEW),
NOT_SUPPORTED(TransactionDefinition.PROPAGATION_NOT_SUPPORTED),
NEVER(TransactionDefinition.PROPAGATION_NEVER),
NESTED(TransactionDefinition.PROPAGATION_NESTED);
private final int value;
Propagation(int value) {
this.value = value;
}
public int value() {
return this.value;
}
}
3.2 測試傳播行為-REQUIRED
REQUIRED:預設傳播行為,如果目前存在事務,就沿用目前事務,否則建立一個事務運作子方法
示例:批量插入使用者
1.項目結構:
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsIiclRnblN2XjlGcjAzNfRHLGZkRGZkRfJ3bs92YsYTMfVmepNHLwsmaNhXQ65UeRpHW4Z0MMBjVtJWd0ckW65UbM5WOHJWa5kHT20ESjBjUIF2X0hXZ0xCMx81dvRWYoNHLrdEZwZ1Rh5WNXp1bwNjW1ZUba9VZwlHdssmch1mclRXY39CXldWYtlWPzNXZj9mcw1ycz9WL49zZwpmL1cTO5QDO1YTM4ITMxAjMwIzLc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.jpg)
2.UserService接口,和批量操作接口 UserBatchService
public interface UserService {
int insertUser(User user);
}
// 批量操作接口
public interface UserBatchService {
int insertUsers(List<User> users);
}
3.對應實作類
// UserService
@Service
public class UserServieImpl implements UserService {
@Autowired
private UserMapper userMapper;
// 開啟事務,其他預設
@Transactional
@Override
public int insertUser(User user) {
return userMapper.insertUser(user);
}
}
// UserBatchService實作類
@Service
public class UserBatchServieImpl implements UserBatchService {
@Autowired
private UserService userService;
// 開啟事務,隔離級别=讀已送出、傳播行為=沿用目前事務(目前事務存在的話
@Transactional(isolation = Isolation.READ_COMMITTED,propagation = Propagation.REQUIRED,rollbackFor = Exception.class)
@Override
public int insertUsers(List<User> users) {
final int[] count = {0};
users.forEach(user-> {
count[0] += userService.insertUser(user);
});
return count[0];
}
}
4.測試方法
@Test
public void testBatchUser(){
User user = new User();
user.setUsername("Tom2");
user.setAddress("北京");
User user2 = new User();
user2.setUsername("Cat2");
user2.setAddress("上海");
List<User> list = new ArrayList<>();
list.add(user);
list.add(user2);
userBatchService.insertUsers(list);
}
5.檢視輸出日志
從中可以觀察出,當調用子方法 insertUser() 時,會加入已經存在的事務。這就是 Propagation.REQUIRED 隔離級别。
3.2 測試傳播行為-REQUIRES_NEW
REQUIRES_NEW:無論目前事務是否存在,都會建立新事務運作方法
1.改變 UserServiceImpl的事務設定
@Service
public class UserServieImpl implements UserService {
@Autowired
private UserMapper userMapper;
// 設定隔離級别=讀已送出 、傳播行為=REQUIRES_NEW
@Transactional(isolation = Isolation.READ_COMMITTED,propagation = Propagation.REQUIRES_NEW)
@Override
public int insertUser(User user) {
return userMapper.insertUser(user);
}
}
2.從日志可以看出,對于子方法 insertUser() 調用時,會開啟一個新的事務,獨立送出,完全脫離原有事務的管控,每一個事務都有自己獨立的隔離級别和鎖
3.3 測試傳播行為-NESTED
NESTED:在目前方法調用子方法時,如果子方法發生異常,隻復原子方法執行過的SQL,而不復原目前方法的事務
1.改變 UserSeviceImpl 的事務設定
@Service
public class UserServieImpl implements UserService {
@Autowired
private UserMapper userMapper;
// 設定隔離級别=讀已送出 、傳播行為=REQUIRES_NEW
@Transactional(isolation = Isolation.READ_COMMITTED,propagation = Propagation.NESTED)
@Override
public int insertUser(User user) {
return userMapper.insertUser(user);
}
}
從日志中可以看出,當調用 insertUser() 時,就會建立 nested類型事務
3.4 NESTED傳播行為和REQUIRES_NEW差別
NESTED 傳播行為會沿用目前事務的隔離級别和鎖等特性,而REQUIRES_NEW 則可以擁有自己獨立的隔離級别和鎖等特性。
四、@Transactional 自調用失效問題
4.1 什麼是自調用失效問題
之前測試傳播行為時,使用的是 UserBatchServiceImpl 去調用 UserService的方法去完成批量導入使用者,如果我們不使用 UserBatchService,而把 insertUsers() 也放在 UserServie裡面。讓它調用本類和 insertUser() 會怎麼呢?就像這樣
@Service
public class UserServieImpl implements UserService {
@Autowired
private UserMapper userMapper;
// 設定隔離級别=讀已送出 、傳播行為=REQUIRES_NEW
@Transactional(isolation = Isolation.READ_COMMITTED,propagation = Propagation.REQUIRES_NEW)
@Override
public int insertUser(User user) {
return userMapper.insertUser(user);
}
// 批量插入接口
@Transactional(isolation = Isolation.READ_COMMITTED,propagation = Propagation.REQUIRED,rollbackFor = Exception.class)
@Override
public int insertUsers(List<User> users) {
final int[] count = {0};
users.forEach(user-> {
count[0] += this.insertUser(user);
});
return count[0];
}
}
從日志可以看出,調用 insertUser() 時,沒有開啟事務。是以該方法上的 @Transactional 注解失效了
4.2 失效的原因
因為 Spring資料庫事務,底層實作是 AOP,AOP的原理又是動态代理。
在自調用過程中,是類的自身調用,而不是代理對象去調用,就沒有使用 AOP ,是以注解失效。
4.3 如何解決?
1.第一種:使用2個 Service,像上面一樣。UserBatchService 去調用 UserService。
2.第二種:從容器裡面去擷取代理對象去啟用AOP,就像下面這樣
@Service
public class UserServieImpl implements UserService , ApplicationContextAware {
@Autowired
private UserMapper userMapper;
private ApplicationContext applicationContext;
// 實作生命周期方法,設定IOC容器
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
// 單條插入
@Transactional(isolation = Isolation.READ_COMMITTED,propagation = Propagation.REQUIRES_NEW)
@Override
public int insertUser(User user) {
return userMapper.insertUser(user);
}
// 批量插入
@Transactional(isolation = Isolation.READ_COMMITTED,propagation = Propagation.REQUIRED,rollbackFor = Exception.class)
@Override
public int insertUsers(List<User> users) {
// 從容器中擷取代理對象
UserService userService = applicationContext.getBean(UserService.class);
final int[] count = {0};
users.forEach(user-> {
// 使用代理對象插入使用者,AOP生效。事務開啟成功
count[0] += userService.insertUser(user);
});
return count[0];
}
}
參考:《深入淺出Spring Boot 2.x》