歡迎光臨
每天分享高質量文章

Spring 事務管理

(給ImportNew加星標,提高Java技能)

作者:小謝

fdx321.github.io/2016/09/18/Spring%E4%BA%8B%E5%8A%A1%E7%AE%A1%E7%90%86//

 

1. 關鍵類

 

public interface PlatformTransactionManager {
    TransactionStatus getTransaction(
            TransactionDefinition definition) throws TransactionException;
    void commit(TransactionStatus status) throws TransactionException;
    void rollback(TransactionStatus status) throws TransactionException;
}

 

事務真正的開始、提交、回滾都是透過PlatformTransactionManager這個介面來實現的,例如,我們常用的org.springframework.jdbc.datasource.DataSourceTransactionManager。

 

TransactionDefinition用於獲取事務的一些屬性,Isolation, Propagation,Timeout,Read-only,還定義了事務隔離級別,傳播屬性等常量。

 

TransactionStatus用於設定和查詢事務的狀態,如是否是新事務,是否有儲存點,設定和查詢RollbackOnly等。

 

2. 宣告式事務

 

所謂宣告式事務,就是透過配置的方式省去很多程式碼,從而讓Spring來幫你管理事務。本質上就是配置一個Around方式的AOP,在執行方法之前,用TransactionInterceptor擷取,然後呼叫PlatformTransactionManager的某個實現做一些事務開始前的事情,然後在方法執行後,呼叫PlatformTransactionManager的某個實現做commit或rollback. 如圖:

 

 

宣告式事務可以透過XML配置,也可以透過Annotation的方式來配置,還可以兩種結合。平時專案中看到比較多的是兩種結合的方式,在XML中配置資料源,事務管理器,然後AOP相關的透過@Transactional(該註解可以註在Class,Method上)來配置。(個人感覺,AOP相關的配置用XML配置挺繁瑣的,還是註解好)例如:

 


"dataSource"

class=“org.apache.commons.dbcp.BasicDataSource”>
“driverClassName” value=“com.mysql.jdbc.Driver”>
“url” value=“jdbc:mysql://localhost:3306/test”>
“username” value=“root”>
“password” value=“ali88”>


“txManager”/>
“txManager” class=“org.springframework.jdbc.datasource.DataSourceTransactionManager”>
“dataSource” ref=“dataSource”/>


“jdbcTemplate” class=“org.springframework.jdbc.core.JdbcTemplate”>
“dataSource”>
“dataSource” />

 

@Transactional(readOnly = true)
public class DefaultFooService{
    public Foo getFoo(String fooName) {
        // do something
    }

    @Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW)
    public void updateFoo(Foo foo) {
        // do something
    }
}

 

3. 事務屬性

 

取用官方檔案的表格

 

 

  • value,在有多個事務管理器存在的情況下,用於標識使用哪個事務管理器
  • isolation,事務的隔離級別,預設是Isolation.DEFAULT,這個DEFAULT是和具體使用的資料庫相關的。關於隔離級別,可以參考MySQL事務學習總結
  • readOnly, 是否只讀,如果配置了true,但是方法裡使用了update,insert陳述句,會報錯。對於只讀的事務,配置為true有助於提高效能。
  • rollbackFor, noRollbackFor. Spring的宣告式事務的預設行為是如果方法丟擲RuntimeException或者Error,則事務會回滾,對於其他的checked型別的異常,不會回滾。如果想改變這種預設行為,可以透過這幾個屬性來配置。
  • propagation, 後面會具體講。

 

4. 事務的傳播機制

 

型別 說明
PROPAGATION_REQUIRED 如果當前沒有事務,就新建一個事務,如果已經存在一個事務中,加入到這個事務中。這是 最常見的選擇。
PROPAGATION_SUPPORTS 支援當前事務,如果當前沒有事務,就以非事務方式執行
PROPAGATION_MANDATOR 使用當前的事務,如果當前沒有事務,就丟擲異常
PROPAGATION_REQUIRES_NEW 新建事務,如果當前存在事務,把當前事務掛起
PROPAGATION_NOT_SUPPORTED 以非事務方式執行操作,如果當前存在事務,就把當前事務掛起
PROPAGATION_NEVER 以非事務方式執行,如果當前存在事務,則丟擲異常
PROPAGATION_NESTED 如果當前存在事務,則在巢狀事務內執行。如果當前沒有事務,則執行與 PROPAGATION_REQUIRED 類似的操作

 

其他的都還好理解,後面結合例子重點介紹下PROPAGATION_REQUIRED,PROPAGATION_REQUIRES_NEW,PROPAGATION_NESTED三種傳播級別。

表結構和原始資料

 

mysql> select * from test;
+----+-------+
| id | money |
+----+-------+
|  3 |   500 |
|  5 |   500 |
|  7 |   600 |
+----+-------+
3 rows in set (0.00 sec)

 

  • PROPAGATION_REQUIRED

 

@Service
public class MysqlTest01 {
    @Autowired
    private JdbcTemplate jdbcTemplate;
    @Autowired
    private MysqlTest02 mysqlTest02;

    @Transactional
    public void test() {
        jdbcTemplate.execute("update test set money = '501' where id = 3");
        try {
            mysqlTest02.test();
        } catch (Exception e) {
            System.out.println("第二個事務異常");
        }
    }
}

@Service
class MysqlTest02 {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Transactional(propagation = Propagation.REQUIRED)
    public void test() {
        jdbcTemplate.execute("update test set money = '502' where id = 3");
        throw new RuntimeException();
    }
}

 

執行完之後,test表的資料沒有任何變化。

 

由於MysqlTest02中的事務傳播型別是Propagation.REQUIRED,邏輯上有兩個事務,但底層是共用一個物理事務的,第二個事務的丟擲RuntimeExcetion導致事務回滾,對於這種傳播型別,內層事務的回滾會導致外層事務回滾。所以資料庫中的資料沒有任何變化。

 

  • PROPAGATION_REQUIRES_NEW

 

@Service
public class MysqlTest01 {
    @Autowired
    private JdbcTemplate jdbcTemplate;
    @Autowired
    private MysqlTest02 mysqlTest02;

    @Transactional
    public void test() {
        jdbcTemplate.execute("update test set money = '501' where id = 3");
        try {
            mysqlTest02.test();
        } catch (Exception e) {
            System.out.println("第二個事務異常");
        }
    }
}

@Service
class MysqlTest02 {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void test() {
        jdbcTemplate.execute("update test set money = '502' where id = 3");
        throw new RuntimeException();
    }
}

 

同樣的程式碼,唯一的區別就是第二個事務的傳播屬性改成了REQUIRES_NEW,執行結果是啥?不好意思,第二個事務執行不了。

 

對於REQUIRES_NEW,邏輯上有兩個事務,底層物理上也有兩個事務,由於第一個事務和第二個事務更新的是同一條記錄,對於Mysql預設的隔離級別REPEATABLE-READ來說,第一個事務會對該記錄加排他鎖,所以第二個事務就一直卡住了。

 

OK,我們把第二個事務的執行的SQL陳述句換成。

 

update test set money = '501' where id = 5"

 

執行結果如下,可以看到只有第二個事務回滾了。

 

mysql> select * from test;
+----+-------+
| id | money |
+----+-------+
|  5 |   500 |
|  3 |   501 |
|  7 |   600 |
+----+-------+
3 rows in set (0.00 sec)

 

  • PROPAGATION_NESTED

 

對於這種傳播型別,物理上只有一個事務,不過可以有多個savePoint用來回滾。當然是用這種傳播型別,需要資料庫支援savePoint,使用jdbc的也是要3.0版本以上(這個不太確定)。

 

@Service
public class MysqlTest01 {
    @Autowired
    private JdbcTemplate jdbcTemplate;
    @Autowired
    private MysqlTest02 mysqlTest02;
    @Autowired
    private MysqlTest03 mysqlTest03;

    @Transactional
    public void test() {
        jdbcTemplate.execute("update test set money = '501' where id = 3");
        try {
            mysqlTest02.test();
        } catch (Exception e) {
            System.out.println("第二個事務異常");
        }
        mysqlTest03.test();
    }
}

@Service
class MysqlTest02 {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Transactional(propagation = Propagation.NESTED)
    public void test() {
        jdbcTemplate.execute("update test set money = '502' where id = 3");
        throw new RuntimeException();
    }
}

@Service
class MysqlTest03 {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Transactional(propagation = Propagation.NESTED)
    public void test() {
        jdbcTemplate.execute("update test set money = '503' where id = 3");
    }
}

 

執行結果是如下,可以看到第一個事務和第三個事務提交成功了,第二個事務回滾了。物理上它們是在一個事務裡的,只不過用到了儲存點的技術。

 

mysql> select * from test;
+----+-------+
| id | money |
+----+-------+
|  5 |   500 |
|  3 |   501 |
|  7 |   601 |
+----+-------+
3 rows in set (0.01 sec)

 

5. 其他

 

在寫測試程式碼的時候遇到了一個關於AOP的問題,可以看到我的測試程式碼,每個事務都是在一個新的class中寫的。為什麼不像下麵這樣寫呢?

 

@Service
public class MysqlTest01 {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Transactional
    public void test01() {
        jdbcTemplate.execute("update test set money = '501' where id = 3");
        test02();
    }
    @Transactional
    public void test02() {
        jdbcTemplate.execute("update test set money = '501' where id = 5");
    }
}

 

這是因為在Spring的AOP中,test01呼叫test02, test02是不會被AOP截獲的,所以也不會被Spring進行事務管理。原因是Spring AOP的實現本質是透過動態代理的方式去執行真正的方法,然後在代理類裡面做一些額外的事情。當透過別的類呼叫MysqlTest01中的test01方法時,因為使用了Spring的DI,註入的其實是一個MysqlTest01的一個代理類,而透過內部方法呼叫test02時,則不是。

 

6. Reference

 

Spring Framework Reference Documentation

贊(0)

分享創造快樂