這篇文章以一個問題開始,若是你知道答案的話就能夠跳過不看啦@(o・ェ・)@數據庫
Q:在一個批量任務執行的過程當中,調用多個子任務時,若是有一些子任務發生異常,只是回滾那些出現異常的任務,而不是整個批量任務,請問在Spring中事務須要如何配置才能實現這一功能呢?markdown
隔離性(Isolation)做爲事務特性的一個關鍵特性,它要求每一個讀寫事務的對象對其餘事務的操做對象能相互分離,即該事務提交前對其餘事務都不可見,在數據庫層面都是使用鎖來實現。併發
事務的隔離級別從低到高有如下四種:app
時間 | 事務1 | 事務2 | 備註 |
---|---|---|---|
1 |
讀取庫存爲100 | --- |
--- |
2 |
扣減庫層50 |
--- |
剩餘50 |
3 |
扣減庫層50 | --- |
|
4 |
提交事務 |
庫存保存爲0 | |
5 |
回滾事務 |
--- |
事務回滾爲0 |
髒讀對於要求數據一致性的應用來講是致命的,目前主流的數據庫的隔離級別都不會設置成READ UNCOMMITTED。不過髒讀雖然看起來毫無用處,可是它主要優勢是併發能力高,適合那些對數據一致性沒有要求而追求高併發的場景。ide
時間 | 事務1 |
事務2 |
備註 |
---|---|---|---|
1 |
讀取庫存爲1 | ||
2 |
扣減庫存 |
事務未提交 |
|
3 |
讀取庫存爲1 | ||
4 |
提交事務 |
庫存變成0 |
|
5 |
扣減庫存 |
庫存爲0,沒法扣減 |
不可重複讀和髒讀的區別是:髒讀讀取到的是未提交的數據,而不可重複讀讀到的確實已經提交的數據,可是違反了數據庫事務一致性的要求。高併發
通常來講,不可重複讀的問題是能夠接受的,由於其讀到的是已經提交的數據,自己並不會帶來很大的問題。所以,不少數據庫如(ORACLE,SQL SERVER)將其默認隔離級別設置爲READ COMMITTED,容許不可重複讀的現象。this
時間 | 事務1 |
事務2 |
備註 |
---|---|---|---|
1 |
讀取庫存爲1 | ||
2 |
扣減庫存 |
事務未提交 |
|
3 |
讀取庫存 | 不容許讀取,等待事務1提交 | |
4 |
提交事務 |
庫存變成0 |
|
5 |
讀取庫存 | 庫存爲0,沒法扣減 |
REPEATABLE READ雖然解決了不可重複讀問題,可是他又會帶來幻讀問題,幻讀是指,在一個事務中,第一次查詢某條記錄,發現沒有,可是,當試圖更新這條不存在的記錄時,居然能成功,而且,再次讀取同一條記錄,它就神奇地出現了。spa
事務B在第2步第一次讀取id=99
的記錄時,讀到的記錄爲空,說明不存在id=99
的記錄。隨後,事務A在第3步插入了一條id=99
的記錄並提交。事務B在第5步再次讀取id=99
的記錄時,讀到的記錄仍然爲空,可是,事務B在第6步試圖更新這條不存在的記錄時,居然成功了,而且,事務B在第8步再次讀取id=99
的記錄時,記錄出現了。代理
在Spring項目中配置隔離級別只須要作以下操做code
@Transactional(isolation = Isolation.SERIALIZABLE)
public int insertUser(User user){
return userDao.insertUser(user);
}複製代碼
上面的代碼中咱們使用了串行化的隔離級別來包住數據的一致性,這使它將阻塞其餘的事務進行併發,因此它只能運用在那些低併發而又須要保證數據一致性的場景下。
隔離級別字典:
DEFAULT(-1), ## 數據庫默認級別
READ_UNCOMMITTED(1),
READ_COMMITTED(2),
REPEATABLE_READ(4),
SERIALIZABLE(8);複製代碼
在Spring中,當一個方法調用另一個方法時,可讓事務採起不一樣的策略工做,如新建事務或者掛起當前事務等,這即是事務的傳播行爲。
在Spring的事務機制中對數據庫存在7種傳播行爲,經過枚舉類Propagation
定義。
public enum Propagation {
/**
* 須要事務,默認傳播性行爲。
* 若是當前存在事務,就沿用當前事務,不然新建一個事務運行子方法
*/
REQUIRED(0),
/**
* 支持事務,若是當前存在事務,就沿用當前事務,
* 若是不存在,則繼續採用無事務的方式運行子方法
*/
SUPPORTS(1),
/**
* 必須使用事務,若是當前沒有事務,拋出異常
* 若是存在當前事務,就沿用當前事務
*/
MANDATORY(2),
/**
* 不管當前事務是否存在,都會建立新事務容許方法
* 這樣新事務就能夠擁有新的鎖和隔離級別等特性,與當前事務相互獨立
*/
REQUIRES_NEW(3),
/**
* 不支持事務,當前存在事務時,將掛起事務,運行方法
*/
NOT_SUPPORTED(4),
/**
* 不支持事務,若是當前方法存在事務,將拋出異常,不然繼續使用無事務機制運行
*/
NEVER(5),
/**
* 在當前方法調用子方法時,若是子方法發生異常
* 只回滾子方法執行過的SQL,而不回滾當前方法的事務
*/
NESTED(6);
......
}複製代碼
平常開發中基本只會使用到REQUIRED(0)
,REQUIRES_NEW(3)
,NESTED(6)
三種。
NESTED
和REQUIRES_NEW
是有區別的。NESTED
傳播行爲會沿用當前事務的隔離級別和鎖等特性,而REQUIRES_NEW
則能夠擁有本身獨立的隔離級別和鎖等特性。
NESTED
的實現主要依賴於數據庫的保存點(SAVEPOINT)技術,SAVEPOINT記錄了一個保存點,能夠經過ROLLBACK TO SAVEPOINT
來回滾到某個保存點。若是數據庫支持保存點技術時就啓用保存點技術;若是不支持就會新建一個事務去執行代碼,也就至關於REQUIRES_NEW
。
若是一個類中自身方法的調用,咱們稱之爲自調用。如一個訂單業務實現類OrderServiceImpl中有methodA方法調用了自身類的methodB方法就是自調用,如:
@Transactional
public void methodA(){
for (int i = 0; i < 10; i++) {
methodB();
}
}
@Transactional(isolation = Isolation.READ_COMMITTED,propagation = Propagation.REQUIRES_NEW)
public int methodB(){
......
}複製代碼
在上面方法中無論methodB如何設置隔離級別和傳播行爲都是不生效的。即自調用失效。
這主要是因爲@Transactional的底層實現原理是基於AOP實現,而AOP的原理是動態代理,在自調用的過程當中是類自身的調用,而不是代理對象去調用,那麼就不會產生AOP,因而就發生了自調用失敗的現象。
要克服這個問題,有2種方法:
public class OrderServiceImpl implements OrderService,ApplicationContextAware {
private ApplicationContext applicationContext = null;
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
@Transactional
public void methodA(){
OrderService orderService = applicationContext.getBean(OrderService.class);
for (int i = 0; i < 10; i++) {
orderService.methodB();
}
}
@Transactional(isolation = Isolation.READ_COMMITTED,propagation = Propagation.REQUIRES_NEW)
public int methodB(){
......
}
}複製代碼
上面代碼中咱們先實現了ApplicationContextAware
接口,而後經過applicationContext.getBean()
獲取了OrderService
的接口對象。這個時候獲取到的是一個代理對象,也就能正常使用AOP的動態代理了。
回到最開始的那個問題,看完這篇文章是否是有答案了呢?