爲什麼需要事務管理器
如果沒有事務管理器的話,我們的程序可能是這樣:
Connection connection = acquireConnection();
try{
int updated = connection.prepareStatement().executeUpdate();
connection.commit();
}catch (Exception e){
rollback(connection);
}finally {
releaseConnection(connection);
}
也有可能是這樣"優雅的事務":
execute(new TxCallback() {
@Override
public Object doInTx(Connection var1) {
//do something...
return null;
}
});
public void execute(TxCallback txCallback){
Connection connection = acquireConnection();
try{
txCallback.doInTx(connection);
connection.commit();
}catch (Exception e){
rollback(connection);
}finally {
releaseConnection(connection);
}
}
# lambda版
execute(connection -> {
//do something...
return null;
});
但是以上兩種方式,針對一些複雜的場景是很不方便的。在實際的業務場景中,往往有比較複雜的業務邏輯,代碼冗長,邏輯關聯複雜,如果一個大操作中有全是這種代碼的話我想開發人員可能會瘋把。更不用提定製化的隔離級別,以及嵌套/獨立事務的處理了。
Spring 事務管理器簡介
Spring作爲Java最強框架,事務管理也是其核心功能之一。Spring爲事務管理提供了統一的抽象,有以下優點:
- 跨不同事務API(例如Java事務API(JTA),JDBC,Hibernate,Java持久性API(JPA)和Java數據對象(JDO))的一致編程模型。
- 支持聲明式事務管理(註解形式)
- 與JTA之類的複雜事務API相比, 用於程序化事務管理的API更簡單
- 和Spring的Data層抽象集成方便(比如Spring - Hibernate/Jdbc/Mybatis/Jpa...)
使用方式
事務,自然是控制業務的,在一個業務流程內,往往希望保證原子性,要麼全成功要麼全失敗。
所以事務一般是加載@Service
層,一個Service內調用了多個操作數據庫的操作(比如Dao),在Service結束後事務自動提交,如有異常拋出則事務回滾。
這也是Spring事務管理的基本使用原則。
下面貼出具體的使用代碼:
註解
在被Spring管理的類頭上增加@Transactional註解,即可對該類下的所有方法開啓事務管理。事務開啓後,方法內的操作無需手動開啓/提交/回滾事務,一切交給Spring管理即可。
@Service
@Transactional
public class TxTestService{
@Autowired
private OrderRepo orderRepo;
public void submit(Order order){
orderRepo.save(order);
}
}
也可以只在方法上配置,方法配置的優先級是大於類的
@Service
public class TxTestService{
@Autowired
private OrderRepo orderRepo;
@Transactional
public void submit(Order order){
orderRepo.save(order);
}
}
XML
XML的配置方式較爲古老,此處就不貼代碼了,如有需要自行搜索
隔離級別
事務隔離級別是數據庫最重要的特性之一,他保證了髒讀/幻讀等問題不會發生。作爲一個事務管理框架自然也是支持此配置的,在@Transactional註解中有一個isolation配置,可以很方便的配置各個事務的隔離級別,等同於connection.setTransactionIsolation()
Isolation {
DEFAULT(-1),
READ_UNCOMMITTED(1),
READ_COMMITTED(2),
REPEATABLE_READ(4),
SERIALIZABLE(8);
}
傳播行爲
可能沒有接觸過Spring的人聽到傳播行爲會奇怪,這是個什麼東西。
其實這個傳播行爲和數據庫功能無關,只是事務管理器爲了處理複雜業務而設計的一個機制。
比如現在有這樣一個調用場景,A Service -> B Service -> C Service
,但是希望A/B在一個事務內,C是一個獨立的事務,同時C如果出錯,不影響AB所在的事務。
此時,就可以通過傳播行爲來處理;將C Service的事務配置爲@Transactional(propagation = Propagation.REQUIRES_NEW)
即可
Spring支持以下幾種傳播行爲:
REQUIRED
默認策略,優先使用當前事務(及當前線程綁定的事務資源),如果不存在事務,則開啓新事務
SUPPORTS
優先使用當前的事務(及當前線程綁定的事務資源),如果不存在事務,則以無事務方式運行
MANDATORY
優先使用當前的事務,如果不存在事務,則拋出異常
REQUIRES_NEW
創建一個新事務,如果存在當前事務,則掛起(Suspend)
NOT_SUPPORTED
以非事務方式執行,如果當前事務存在,則掛起當前事務。
NEVER
以非事務方式執行,如果當前事務存在,則拋出異常
回滾策略
@Transactional中有4個配置回滾策略的屬性,分爲Rollback策略,和NoRollback策略
默認情況下,RuntimeException和Error這兩種異常會導致事務回滾,普通的Exception(需要Catch的)異常不會回滾。
Rollback
配置需要回滾的異常類
# 異常類Class
Class<? extends Throwable>[] rollbackFor() default {};
# 異常類ClassName,可以是FullName/SimpleName
String[] rollbackForClassName() default {};
NoRollback
針對一些要特殊處理的業務邏輯,比如插一些日誌表,或者不重要的業務流程,希望就算出錯也不影響事務的提交。
可以通過配置NoRollbackFor來實現,讓某些異常不影響事務的狀態。
# 異常類Class
Class<? extends Throwable>[] noRollbackFor() default {};
# 異常類ClassName,可以是FullName/SimpleName
String[] noRollbackForClassName() default {};
只讀控制
設置當時事務的只讀標示,等同於connection.setReadOnly()
常見問題
事務沒生效
有下列代碼,入口爲test方法,在testTx方法中配置了@Transactional註解,同時在插入數據後拋出RuntimeException異常,但是方法執行後插入的數據並沒有回滾,竟然插入成功了
public void test(){
testTx();
}
@Transactional
public void testTx(){
UrlMappingEntity urlMappingEntity = new UrlMappingEntity();
urlMappingEntity.setUrl("http://www.baidu.com");
urlMappingEntity.setExpireIn(777l);
urlMappingEntity.setCreateTime(new Date());
urlMappingRepository.save(urlMappingEntity);
if(true){
throw new RuntimeException();
}
}
這裏不生效的原因是因爲入口的方法/類沒有增加@Transaction註解,由於Spring的事務管理器也是基於AOP實現的,不管是Cglib(ASM)還是Jdk的動態代理,本質上也都是子類機制;在同類之間的方法調用會直接調用本類代碼,不會執行動態代理曾的代碼;所以在這個例子中,由於入口方法test
沒有增加代理註解,所以textTx
方法上增加的事務註解並不會生效
異步後事務失效
比如在一個事務方法中,開啓了子線程操作庫,那麼此時子線程的事務和主線程事務是不同的。
因爲在Spring的事務管理器中,事務相關的資源(連接,session,事務狀態之類)都是存放在TransactionSynchronizationManager中的,通過ThreadLocal存放,如果跨線程的話就無法保證一個事務了
# TransactionSynchronizationManager.java
private static final ThreadLocal<Map<Object, Object>> resources =
new NamedThreadLocal<>("Transactional resources");
private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations =
new NamedThreadLocal<>("Transaction synchronizations");
private static final ThreadLocal<String> currentTransactionName =
new NamedThreadLocal<>("Current transaction name");
private static final ThreadLocal<Boolean> currentTransactionReadOnly =
new NamedThreadLocal<>("Current transaction read-only status");
private static final ThreadLocal<Integer> currentTransactionIsolationLevel =
new NamedThreadLocal<>("Current transaction isolation level");
private static final ThreadLocal<Boolean> actualTransactionActive =
new NamedThreadLocal<>("Actual transaction active");
事務提交失敗
org.springframework.transaction.UnexpectedRollbackException:
Transaction silently rolled back because it has been marked as rollback-only
這個異常是由於在同一個事務內,多個事務方法之間調用,子方法拋出異常,但又被父方法忽略了導致的。
因爲子方法拋出了異常,Spring事務管理器會將當前事務標爲失敗狀態,準備進行回滾,可是當子方法執行完畢出棧後,父方法又忽略了此異常,待方法執行完畢後正常提交時,事務管理器會檢查回滾狀態,若有回滾標示則拋出此異常。具體可以參考org.springframework.transaction.support.AbstractPlatformTransactionManager#processCommit
示例代碼:
A -> B
# A Service(@Transactional):
public void testTx(){
urlMappingRepo.deleteById(98l);
try{
txSubService.testSubTx();
}catch (Exception e){
e.printStackTrace();
}
}
# B Service(@Transactional)
public void testSubTx(){
if(true){
throw new RuntimeException();
}
}