(給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
朋友會在“發現-看一看”看到你“在看”的內容