Spring事務失效事件(非常規原因)

????????近期遇到一個spring注解事務失效的問題,先上代碼(以下代碼經過簡化,只保留關鍵點)。

? ? ? ? 沒耐心的朋友可以直接看最下面結論。

第一部分:具體問題

接下來上一下代碼,大致介紹一下流程。

Controller

Controller里面使用@Autowired注入了一個serviceImpl,然后調用這個serviceImpl的業務方法。??

ServiceImpl的屬性

自動裝配了另一個service和一個dao

重點看serviceImpl的方法(經過簡化,只保留了關鍵點)

紅框是兩個insert數據庫操作,需要事務管理

最下面的拋出異常是為了測試事務。

其中紅框里面的super.addNew方法上有@Transactional(rollbackFor = Exception.class)注解。所以看上去并沒有什么問題,但是每次拋出異常,數據庫的操作都沒有回滾。

問題大致就是這樣,本來以為事情會很容易被解決,因為在網上看過很多事務失效的例子,各種原因被各種大神列舉了很多次了。

第二部分:常規問題解決


普通事務失效原因:

1.如使用mysql且引擎是MyISAM,則事務會不起作用,原因是MyISAM不支持事務,可以改成InnoDB

2.?如果使用了spring+mvc,則context:component-scan重復掃描問題可能會引起事務失敗。?(即父子容器)

3.?@Transactional 注解開啟配置,必須放到listener里加載,如果放到DispatcherServlet的配置里,事務也是不起作用的。?

4.?@Transactional 注解只能應用到 public 可見度的方法上。 如果你在 protected、private 或者 package-visible 的方法上使用 @Transactional 注解,它也不會報錯,事務也會失效。?

5.?Spring團隊建議在具體的類(或類的方法)上使用 @Transactional 注解,而不要使用在類所要實現的任何接口上。在接口上使用 @Transactional 注解,只能當你設置了基于接口的代理時它才生效。因為注解是 不能繼承 的,這就意味著如果正在使用基于類的代理時,那么事務的設置將不能被基于類的代理所識別,而且對象也將不會被事務代理所包裝。

以上五點摘抄自:Spring事務失效的原因


看到常規的原因,首先的解決思路如下

1.首先,數據庫使用的是oracle,所以排除1

2.項目確實使用的是spring+mvc不過已經分開掃描了,mvc只掃描Controller和RestController,spring掃描排除這兩個注解這樣可以排除2,

3.事務管理器配置確實是放在spring容器里,用Listener加載的

4.方法是public

5.是在類的方法上使用的,非接口上

所以,常規的問題被我巧妙地全避開了。

第三部分:解決思路

????????這個事務失效問題是第一次出現,并沒有廣泛存在我們的系統中。于是和其他的事務方法對比,發現其他的事務方法都是通過Service.getInstance()方法獲取到的實例,跟進去,發現getInstance方法是通過我們項目定義的工具類從xml里面getBean獲取到的。看上去貌似沒什么問題,getBean和使用@Autowired不都一樣嘛,都能獲取到bean。

? ? ? ? 打開日志debug級別,看能不能查到點什么細節。并且給方法入口和spring的事務管理器入口org.springframework.transaction.interceptor.TransactionInterceptor#invoke打上斷點,然后運行到該事務方法。

????????首先在方法入口的斷點處用evaluate expression執行AopUtils.isAopProxy這個方法來看一下注入的serviceImpl是否被代理了,結果是true。

????????繼續執行,進入到spring事務管理器里面,然后一步步跟代碼,獲取TransactionInfo等,并沒有發現什么可疑點,因為都已經進入到了spring事務管理器了,而spring幾乎不會出bug,進入到方法第一行后,查看了下debug日志,發現有Creating new transaction with name [XXX]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT; '',-java.lang.Exception,繼續執行,執行到super.addNew方法時,debug日志又出現了一行 Creating new transaction with name [XXX]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT; '',-java.lang.Exception,而且此XXX非剛才的XXX,意思是在第一個事務里面,又起了一個事務,但是我事務沒有顯示配置PROPAGATION,默認的是REQUIRED,為什么會又重新起一個事務呢,這里讓我百思不得其解。于是我回過頭又檢查了一遍父子容器問題,是不是還是因為包掃描重復導致的呢,雖然如果是子容器(即MVC)的話,肯定不會進入到事務管理器,但我還是抱著僥幸心理檢查了一遍,沒有任何問題。

????????沒找到問題所在,繼續執行下去,發現跑出的異常被事務管理局接管了,debug日志也顯示了transaction rollback。這就更奇怪了,既然事務都回滾了,數據是怎么插到數據庫里的呢。我繼續在插數據的那一行sql打上斷點,重復執行,發現執行完這條sql的時候,數據就已經在數據庫了,但是事務還沒提交。。這又是怎么回事。這里我想到了會不會是數據庫連接設置的autoCommit,于是重復執行,在spring的transaction源碼里面打斷點,發現事務管理器會自動把autoCommit關掉。

? ? ? ? 感覺這條追查的路已經堵死了。。反而產生了兩個其他的問題,1.既然是PROPAGATION_REQUIRED,為什么會在事務里又起一個事務,2.既然事務都還沒提交,你是怎么插入到數據庫里的。。

? ? ? ? 想到最一開始觀察到的,和其他事務起作用的方法差別在于,他們的成員變量是getInstance獲得的,而這個有問題的是@Autowired注入的,那么是不是這里有問題呢,于是我把Autowired的全換成了getInstance獲取在xml里面注冊的bean,果然,事務起作用了。可是,這tm到底咋回事。。。Autowired和getBean不是一樣么,為什么會出現兩種截然不同的結果。剛好最近剛看完SpringIoc的源碼,就決定追查到底。

第四部分:真相大白

? ? ? ? 理理思路,父容器先加載,子容器后加載,子容器在加載過程中會通過獲取到父容器。

? ? ? ? 請求到達應用后,Controller會先被初始化,然后他的私有屬性serviceImpl會被 inject,實質上是會被父容器inject,然后循環注入serviceImpl私有屬性的私有屬性。。一直循環直到所有的bean都被實例化。事務管理器也在父容器中定義,貌似沒有問題,事實也證明了,事務管理器確實代理了目標方法。

? ? ? ? 然后深入看了下getInstance方法,這個方法是通過項目的一個工具類的getBean方法工作的,工具類的getBean方法實際上是new了一個ClassPathXmlApplicationContext,然后調用他的getBean方法,那么看到這里好像一切都明朗了。。

? ? ? ? @Autowired自動裝配的是web.xml里面定義的Listener加載的父容器里的bean,getInstance是自己定義的第二個容器獲取到的bean,那么事務管理器也是一樣,實際上有兩個事物管理器,第一個是webxml加載父容器的,第二個是工具類加載的容器的。所以才會出現在一個事務中又起一個事務,因為容器2不知道方法已經在容器1起了一個事務,我所看到的事務沒提交也是看到的容器1起的事務沒提交。

? ? ? ? 如果我再仔細縷一遍全流程就會發現,dao里面還封裝了一層mybatis的sqlsession,這里使用的是工具類的getBean方法。

? ? ? ? 如果再仔細看一下debug日志的話,就會發現啟動應用的時候,每個xml都被加載了兩次,一次是webxml的父容器,一次是工具類new加載的。

終章:結論

遇到事務失效的時候,先看一下是否被事務管理器接管了,如果有,那么多半是容器的問題,這里并不一定是父子容器,也有可能是父父容器。。

借用《黑客與畫家》的一句話:如果自己就是潮水的一部分,又怎么能看見潮流的方向呢。

當遇到問題時,不要迷在某個局部,要跳出來,先把整個流程理清。

最后再說一句,開啟了上帝視角之后,感覺這問題實在是很簡單,不過解決過程中花費了我本人非常多時間(不少于十小時)。

希望這篇博客可以給有問題的朋友帶來一點思路。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。