天天看點

【09】Spring筆記--聲明式事務

【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 事務隔離級别

  1. 讀未送出:允許讀取到其他事務尚未送出的資料變更,可能導緻髒讀,丢失修改,不可重複讀,幻讀
  2. 讀已送出:隻允許讀取到已經送出的資料變更,防止了髒讀,不可重複讀,幻讀仍存在
  3. 可重複讀:在一個事務中,對同一資料的多次讀取結果是一緻的。除非資料被事務本身所修改
  4. 串行化:所有的事務依次逐個執行,這樣事務之間就完全不可能産生幹擾。最進階的隔離級别,可以防止幻讀

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.項目結構:

【09】Spring筆記--聲明式事務

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.檢視輸出日志

【09】Spring筆記--聲明式事務

從中可以觀察出,當調用子方法 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);
    }
}
           
【09】Spring筆記--聲明式事務

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);
    }
}
           
【09】Spring筆記--聲明式事務

從日志中可以看出,當調用 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];
    }
}
           
【09】Spring筆記--聲明式事務

從日志可以看出,調用 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];
    }
}

           
【09】Spring筆記--聲明式事務
參考:《深入淺出Spring Boot 2.x》