引言
對於追求資料強一致性的系統,事務扮演者十分重要的角色.最近在專案中遇到一個事務失效的問題,在此分享給大家。
情景回放
### Cause: com.mysql.jdbc.exceptions.jdbc4.MySQLTransactionRollbackException: Lock wait timeout exceeded; try restarting transaction
; SQL []; Lock wait timeout exceeded; try restarting transaction; nested exception is com.mysql.jdbc.exceptions.jdbc4.MySQLTransactionRollbackException: Lock wait timeout exceeded; try restarting transaction
at org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator.doTranslate(SQLErrorCodeSQLExceptionTranslator.java:259)
at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:73)
at org.mybatis.spring.MyBatisExceptionTranslator.translateExceptionIfPossible(MyBatisExceptionTranslator.java:73)
at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:446)
at com.sun.proxy.$Proxy121.update(Unknown Source)
at org.mybatis.spring.SqlSessionTemplate.update(SqlSessionTemplate.java:294)
at org.apache.ibatis.binding.MapperMethod.execute(MapperMethod.java:62)
at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:59)
問題分析
初步分析這是事務獲取鎖超時導致的錯誤,奇怪的是丟擲異常但是事務沒有回滾。或許你們說MySQLTransactionRollbackException是檢查性異常(@Transactional預設只捕獲非檢查性異常),但是專案新增了註解: @Transactional(rollbackFor = Exception.class)
。唯一的解釋是——事務失效了。
ProductService.java
/**********************************************************************/
public interface ProductService{
Integer getPrice(ProductInfo p);
Integer compute(ProductInfo p);
}
/**********************************************************************/
ProductServiceImpl.java
/**********************************************************************/
@Service
public class ProductServiceImpl implements ProductService{
public Integer getPrice(ProductInfo p){
...
compute(p);
...
}
@Transactional(rollbackFor = Exception.class)
public Integer compute(ProductInfo p){ //TestService的普通方法
try{
...
}catch(Exception e){
e.printStackTrace();
return -1;
}
}
}
/**********************************************************************/
初看這段程式碼,沒啥毛病啊。噢,不對,compute 方法內部catch了異常,spring aop無法捕獲異常。如果需要捕獲異常,需要手動回滾,於是compute方法修改如下:
@Transactional(rollbackFor = Exception.class)
public Integer compute(ProductInfo p){ //TestService的普通方法
try{
...
}catch(Exception e){
e.printStackTrace();
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();//手動回滾事務
return 0;
}
}
繼續執行,結果發現事務還是未生效。通過查詢資料發現,service方法直接呼叫了本類的一個方法(沒有通過介面呼叫),該方法上的事務將不會生效。
解決方案
想啟用本類的普通方法的事務,通過介面來呼叫該方法即可生效。如果先在方法內部catch異常,需要新增TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()
;否則可以在外面捕獲這個異常。下面是在方法內部捕獲異常的示例:
ProductService.java
/**********************************************************************/
public interface ProductService{
Integer getPrice(ProductInfo p);
Integer compute(ProductInfo p);
}
/**********************************************************************/
ProductServiceImpl.java
/**********************************************************************/
@Service
public class ProductServiceImpl implements ProductService{
@Autowired
private ProductService productService;
public Integer getPrice(ProductInfo p){
productService.compute(p);
}
@Transactional(rollbackFor = Exception.class)
public Integer compute(ProductInfo p){
try{
...
}catch(Exception e){
e.printStackTrace();
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
return 0;
}
}
}
/**********************************************************************/
總結
Spring Transactional一直是RD的事務神器,但是如果用不好,反會傷了自己。下面總結@Transactional經常遇到的幾個場景:
@Transactional 加於private方法, 無效
@Transactional 加於未加入介面的public方法, 再通過普通介面方法呼叫, 無效
@Transactional 加於介面方法, 無論下面呼叫的是private或public方法, 都有效
@Transactional 加於介面方法後, 被本類普通介面方法直接呼叫, 無效
@Transactional 加於介面方法後, 被本類普通介面方法通過介面呼叫, 有效
@Transactional 加於介面方法後, 被它類的介面方法呼叫, 有效
@Transactional 加於介面方法後, 被它類的私有方法呼叫後, 有效
Transactional是否生效, 僅取決於是否載入於介面方法, 並且是否通過介面方法呼叫(而不是本類呼叫)。
如果大家有更好的方法,歡迎加入討論!