Spring事務(wù)管理器的應(yīng)對(duì) 



   Spring抽象的DAO體系兼容多種數(shù)據(jù)訪問(wèn)技術(shù),它們各有特色,各有千秋。像Hibernate是非常優(yōu)秀的ORM實(shí)現(xiàn)方案,但對(duì)底層SQL的控制不太方便;而iBatis則通過(guò)模板化技術(shù)讓你方便地控制SQL,但沒(méi)有Hibernate那樣高的開(kāi)發(fā)效率;自由度最高的當(dāng)然是直接使用Spring JDBC了,但它也是底層的,靈活的代價(jià)是代碼的繁復(fù)。很難說(shuō)哪種數(shù)據(jù)訪問(wèn)技術(shù)是最優(yōu)秀的,只有在某種特定的場(chǎng)景下才能給出答案。所以在一個(gè)應(yīng)用中,往往采用多個(gè)數(shù)據(jù)訪問(wèn)技術(shù):一般是兩種,一種采用ORM技術(shù)框架,而另一種采用偏JDBC的底層技術(shù),兩者珠聯(lián)璧合,形成聯(lián)合軍種,共同御敵。 
   但是,這種聯(lián)合軍種如何應(yīng)對(duì)事務(wù)管理的問(wèn)題呢?我們知道Spring為每種數(shù)據(jù)訪問(wèn)技術(shù)提供了相應(yīng)的事務(wù)管理器,難道需要分別為它們配置對(duì)應(yīng)的事務(wù)管理器嗎?它們到底是如何協(xié)作和工作的呢?這些層出不窮的問(wèn)題往往壓制了開(kāi)發(fā)人員使用聯(lián)合軍種的想法。 
   其實(shí),在這個(gè)問(wèn)題上,我們低估了Spring事務(wù)管理的能力。如果你采用了一個(gè)高端ORM技術(shù)(Hibernate、JPA、JDO),同時(shí)采用一個(gè)JDBC技術(shù)(Spring JDBC、iBatis),由于前者的會(huì)話(Session)是對(duì)后者連接(Connection)的封裝,Spring會(huì)“足夠智能地”在同一個(gè)事務(wù)線程讓前者的會(huì)話封裝后者的連接。所以,我們只要直接采用前者的事務(wù)管理器就可以了。表10-1給出了混合數(shù)據(jù)訪問(wèn)技術(shù)框架所對(duì)應(yīng)的事務(wù)管理器。 
序    號(hào)混合數(shù)據(jù)訪問(wèn)技術(shù)框架事務(wù)管理器
1Hibernate+ Spring JDBC或iBatisorg.springframework.orm.hibernate3.HibernateTransactionManager
2JPA+Spring JDBC或iBatisorg.springframework.orm.jpa.JpaTransactionManager
3JDO+Spring JDBC或iBatisorg.springframework.orm.jdo.JdoTransactionManager


Hibernate+Spring JDBC混合框架的事務(wù)管理 


   由于一般不會(huì)出現(xiàn)同時(shí)使用多個(gè)ORM框架的情況(如Hibernate+JPA),我們不擬對(duì)此命題展開(kāi)論述,只重點(diǎn)研究ORM框架+JDBC框架的情況。Hibernate+Spring JDBC可能是被使用得最多的組合,本節(jié)我們通過(guò)實(shí)例觀察事務(wù)管理的運(yùn)作情況。 
Java代碼  收藏代碼
  1. package com.baobaotao.mixdao;  
  2.   
  3. …  
  4.   
  5. @Service("userService")  
  6. public class UserService extends BaseService {  
  7.     @Autowired  
  8.     private HibernateTemplate hibernateTemplate;  
  9.   
  10.     @Autowired  
  11.     private ScoreService scoreService;  
  12.   
  13.     public void logon(String userName) {  
  14.          
  15.         //①通過(guò)Hibernate技術(shù)訪問(wèn)數(shù)據(jù)  
  16.         System.out.println("before updateLastLogonTime()..");  
  17.         updateLastLogonTime(userName);  
  18.         System.out.println("end updateLastLogonTime()..");  
  19.           
  20.         //②通過(guò)JDBC技術(shù)訪問(wèn)數(shù)據(jù)  
  21.         System.out.println("before scoreService.addScore()..");  
  22.         scoreService.addScore(userName, 20);  
  23.         System.out.println("end scoreService.addScore()..");  
  24.     }  
  25.       
  26.     public void updateLastLogonTime(String userName) {  
  27.         User user = hibernateTemplate.get(User.class,userName);  
  28.         user.setLastLogonTime(System.currentTimeMillis());  
  29.         hibernateTemplate.update(user);  
  30.   
  31.         //③這句很重要,請(qǐng)看下文的分析  
  32.         hibernateTemplate.flush();  
  33.     }  
  34. }  

  在①處,使用Hibernate操作數(shù)據(jù),而在②處調(diào)用ScoreService#addScore(),該方法內(nèi)部使用Spring JDBC操作數(shù)據(jù)。 
  在③處,我們顯式調(diào)用了flush()方法,將Session中的緩存同步到數(shù)據(jù)庫(kù)中(即馬上向數(shù)據(jù)庫(kù)發(fā)送一條更新記錄的SQL語(yǔ)句)。之所以要顯式執(zhí)行flush()方法,原因是在默認(rèn)情況下,Hibernate對(duì)數(shù)據(jù)的更改只是記錄在一級(jí)緩存中,要等到事務(wù)提交或顯式調(diào)用flush()方法時(shí)才會(huì)將一級(jí)緩存中的數(shù)據(jù)同步到數(shù)據(jù)庫(kù)中,而提交事務(wù)的操作發(fā)生在   logon()方法返回前。如果所有針對(duì)數(shù)據(jù)庫(kù)的更改操作都使用Hibernate,這種數(shù)據(jù)同步的延遲機(jī)制并不會(huì)產(chǎn)生任何問(wèn)題。但是,我們?cè)趌ogon()方法中同時(shí)采用了Hibernate和Spring JDBC混合數(shù)據(jù)訪問(wèn)技術(shù),Spring JDBC無(wú)法自動(dòng)感知Hibernate一級(jí)緩存,所以如果不及時(shí)調(diào)用flush()方法將記錄數(shù)據(jù)更改的一級(jí)緩存同步到數(shù)據(jù)庫(kù)中,則②處通過(guò)Spring JDBC進(jìn)行數(shù)據(jù)更改的結(jié)果將被Hibernate一級(jí)緩存中的更改覆蓋掉,因?yàn)镠ibernate一級(jí)緩存要等到logon()方法返回前才同步到數(shù)據(jù)庫(kù)! 
   ScoreService使用Spring JDBC數(shù)據(jù)訪問(wèn)技術(shù),其代碼如下所示: 
Java代碼  收藏代碼
  1. package com.baobaotao.mixdao;  
  2.   
  3. import org.springframework.beans.factory.annotation.Autowired;  
  4. import org.springframework.jdbc.core.JdbcTemplate;  
  5. import org.springframework.stereotype.Service;  
  6. import org.apache.commons.dbcp.BasicDataSource;  
  7.   
  8. @Service("scoreService")  
  9. public class ScoreService extends BaseService{  
  10.   
  11.     @Autowired  
  12.     private JdbcTemplate jdbcTemplate;  
  13.   
  14.     public void addScore(String userName, int toAdd) {  
  15.         String sql = "UPDATE t_user u SET u.score = u.score + ? WHERE user_name =?";  
  16.         jdbcTemplate.update(sql, toAdd, userName);  
  17.         BasicDataSource basicDataSource = (BasicDataSource) jdbcTemplate.getDataSource();  
  18.         //①查看此處數(shù)據(jù)庫(kù)激活的連接數(shù)量  
  19.         System.out.println("[scoreUserService.addScore]激活連接數(shù)量:"  
  20.                          +basicDataSource.getNumActive());  
  21.     }  
  22. }  

   Spring關(guān)鍵的配置文件代碼如下所示: 
Xml代碼  收藏代碼
  1. …  
  2. <!--①使用Hibernate事務(wù)管理器 -->  
  3. <bean id="hiberManager"  
  4.       class="org.springframework.orm.hibernate3.HibernateTransactionManager"  
  5.       p:sessionFactory-ref="sessionFactory"/>  
  6.   
  7. <!--②使UserService及ScoreService的公用方法都擁有事務(wù) -->  
  8. <aop:config proxy-target-class="true">  
  9.     <aop:pointcut id="serviceJdbcMethod"  
  10.                   expression="within(com.baobaotao.mixdao.BaseService+)"/>  
  11.     <aop:advisor pointcut-ref="serviceJdbcMethod"  
  12.                  advice-ref="hiberAdvice"/>  
  13. </aop:config>  
  14. <tx:advice id="hiberAdvice" transaction-manager="hiberManager">  
  15.     <tx:attributes>  
  16.         <tx:method name="*"/>  
  17.     </tx:attributes>  
  18. </tx:advice>  
  19.   
  20. /beans>  

啟動(dòng)Spring容器,執(zhí)行UserService#logon()方法,可以查看到如下的執(zhí)行日志: 
引用
before userService.logon().. 

①在執(zhí)行userService.logon()后,Spring開(kāi)啟一個(gè)事務(wù) 
Creating new transaction with name [com.baobaotao.mixdao.UserService.logon]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT 
opened session at timestamp: 13009379637 
Opened new Session [org.hibernate.impl.SessionImpl@c5f468] for Hibernate transaction 
… 
Exposing Hibernate transaction as JDBC transaction [jdbc:mysql://localhost:3306/sampledb, UserName=root@localhost, MySQL-AB JDBC Driver] 
before userService.updateLastLogonTime().. 

②userService.updateLastLogonTime()執(zhí)行時(shí)自動(dòng)綁定到①處開(kāi)啟的Session中 
Found thread-bound Session for HibernateTemplate 
loading entity: [com.baobaotao.User#tom] 
about to open PreparedStatement (open PreparedStatements: 0, globally: 0) 
… 
about to close PreparedStatement (open PreparedStatements: 1, globally: 1) 
Not closing pre-bound Hibernate Session after HibernateTemplate 
end updateLastLogonTime().. 

before scoreService.addScore().. 

③scoreService.addScore()執(zhí)行時(shí)綁定到①處開(kāi)啟的Session中,并加入其所對(duì)應(yīng)的事務(wù)中 
Found thread-bound Session [org.hibernate.impl.SessionImpl@c5f468] for Hibernate 
transaction 
Participating in existing transaction 
… 
SQL update affected 1 rows 

④此時(shí)數(shù)據(jù)源只打開(kāi)了一個(gè)連接 
[scoreUserService.addScore]激活連接數(shù)量:1 
end scoreService.addScore().. 
Initiating transaction commit 

⑤提交Hibernate的事務(wù),它將觸發(fā)一級(jí)緩存到數(shù)據(jù)庫(kù)的同步 
Committing Hibernate transaction on Session [org.hibernate.impl.SessionImpl@c5f468] 
commit 
processing flush-time cascades 
dirty checking collections 
Flushed: 0 insertions, 0 updates, 0 deletions to 1 objects 
Flushed: 0 (re)creations, 0 updates, 0 removals to 0 collections 
listing entities: 
com.baobaotao.User{lastLogonTime=1300937963882, score=10, userName=tom, password=123456} 
re-enabling autocommit 

⑥提效Session底層所綁定的JDBC Connection所對(duì)應(yīng)的事務(wù) 
committed JDBC Connection 
transaction completed on session with on_close connection release mode; be sure to close the session to release JDBC resources! 
Closing Hibernate Session [org.hibernate.impl.SessionImpl@c5f468] after transaction 
Closing Hibernate Session 
releasing JDBC connection [ (open PreparedStatements: 0, globally: 0) (open ResultSets: 0, globally: 0)] 
transaction completed on session with on_close connection release mode; be sure to close the session to release JDBC resources! 
after userService.logon().. 


   仔細(xì)觀察這段輸出日志,在①處UserService#logon()開(kāi)啟一個(gè)新的事務(wù)。②處的UserService# updateLastLogonTime() 綁定到事務(wù)上下文的Session中。③處ScoreService#addScore()方法加入到①處開(kāi)啟的事務(wù)上下文中。④處的輸出是ScoreService #addScore()方法內(nèi)部的輸出信息,匯報(bào)此時(shí)數(shù)據(jù)源激活的連接數(shù)為1,這清楚地告訴我們Hibernate和JDBC這兩種數(shù)據(jù)訪問(wèn)技術(shù)在同一事務(wù)上下文中“共用”一個(gè)連接。在⑤處,提交Hibernate事務(wù),接著在⑥處觸發(fā)調(diào)用底層的Connection提交事務(wù)。 
   從以上的運(yùn)行結(jié)果,我們可以得出這樣的結(jié)論:使用Hibernate事務(wù)管理器后,可以混合使用Hibernate和Spring JDBC數(shù)據(jù)訪問(wèn)技術(shù),它們將工作于同一事務(wù)上下文中。但是使用Spring JDBC訪問(wèn)數(shù)據(jù)時(shí),Hibernate的一級(jí)或二級(jí)緩存得不到同步,此外,一級(jí)緩存延遲數(shù)據(jù)同步機(jī)制可能會(huì)覆蓋Spring JDBC數(shù)據(jù)更改的結(jié)果。 
   由于混合數(shù)據(jù)訪問(wèn)技術(shù)方案存在“事務(wù)同步而緩存不同步”的情況,所以最好用Hibernate進(jìn)行讀寫操作,而只用Spring JDBC進(jìn)行讀操作。如用Spring JDBC進(jìn)行簡(jiǎn)要列表的查詢,而用Hibernate對(duì)查詢出的數(shù)據(jù)進(jìn)行維護(hù)。 
   如果確實(shí)要同時(shí)使用Hibernate和Spring JDBC讀寫數(shù)據(jù),則必須充分考慮到Hibernate緩存機(jī)制引發(fā)的問(wèn)題:必須整體分析數(shù)據(jù)維護(hù)邏輯,根據(jù)需要及時(shí)調(diào)用Hibernate的flush()方法,以免覆蓋Spring JDBC的更改,在Spring JDBC更改數(shù)據(jù)庫(kù)時(shí),維護(hù)Hibernate的緩存。由于方法調(diào)用順序的不同都可能影響數(shù)據(jù)的同步性,因此很容易發(fā)生問(wèn)題,這會(huì)極大提高數(shù)據(jù)訪問(wèn)程序的復(fù)雜性。所以筆者鄭重建議不要同時(shí)使用Spring JDBC和Hibernate對(duì)數(shù)據(jù)進(jìn)行寫操作。 
   可以將以上結(jié)論推廣到其他混合數(shù)據(jù)訪問(wèn)技術(shù)的方案中,如Hibernate+iBatis、JPA+Spring JDBC、JDO+Spring JDBC等。