Spring事務管理高級應用難點剖析
Spring最成功,最吸引人的地方莫過于輕量級的聲明式事務管理,僅此一點,它就宣告了重量級EJB容器的覆滅。Spring聲明式事務管理將開發者從繁復的事務管理代碼中解脫出來,專注于業務邏輯的開發上,這是一件可以被拿來頂禮膜拜的事情。
但是,世界并未從此消停,開發人員需要面對的是層出不窮的應用場景,這些場景往往逾越了普通Spring技術書籍的理想界定。因此,隨著應用開發的深入,在使用經過Spring層層封裝的聲明式事務時,開發人員越來越覺得自己墜入了迷霧,陷入了沼澤,體會不到外界所宣稱的那種暢快淋漓。本系列文章的目標旨在整理并剖析實際應用中種種讓我們迷茫的場景,讓陽光照進云遮霧障的山頭。
很少有使用Spring但不使用Spring事務管理器的應用,因此常常有人會問:是否用了Spring,就一定要用Spring事務管理器,否則就無法進行數據的持久化操作呢?事務管理器和DAO是什么關系呢?
也許是DAO和事務管理如影隨行的緣故吧,這個看似簡單的問題實實在在地存在著,從初學者心中涌出,縈繞在開發老手的腦際。答案當然是否定的!我們都知道:Spring事務管理是保證數據操作的事務性(即原子性、一致性、隔離性、持久性,也即所謂的ACID),脫離了事務性,DAO照樣可以順利地進行數據的操作。下面,我們來看一段使用SpringJDBC進行數據訪問的代碼:
清單1.UserJdbcWithoutTransManagerService.java
- packageuser.withouttm;
- importorg.springframework.beans.factory.annotation.Autowired;
- importorg.springframework.jdbc.core.JdbcTemplate;
- importorg.springframework.stereotype.Service;
- importorg.springframework.context.ApplicationContext;
- importorg.springframework.context.support.ClassPathXmlApplicationContext;
- importorg.apache.commons.dbcp.BasicDataSource;
- @Service("service1")
- publicclassUserJdbcWithoutTransManagerService{
- @Autowired
- privateJdbcTemplatejdbcTemplate;
- publicvoidaddScore(StringuserName,inttoAdd){
- Stringsql="UPDATEt_useruSETu.score=u.score+?WHEREuser_name=?";
- jdbcTemplate.update(sql,toAdd,userName);
- }
- publicstaticvoidmain(String[]args){
- ApplicationContextctx=
- newClassPathXmlApplicationContext("user/withouttm/jdbcWithoutTransManager.xml");
- UserJdbcWithoutTransManagerServiceservice=
- (UserJdbcWithoutTransManagerService)ctx.getBean("service1");
- JdbcTemplatejdbcTemplate=(JdbcTemplate)ctx.getBean("jdbcTemplate");
- BasicDataSourcebasicDataSource=(BasicDataSource)jdbcTemplate.getDataSource();
- //①.檢查數據源autoCommit的設置
- System.out.println("autoCommit:"+basicDataSource.getDefaultAutoCommit());
- //②.插入一條記錄,初始分數為10
- jdbcTemplate.execute(
- "INSERTINTOt_user(user_name,password,score)VALUES('tom','123456',10)");
- //③.調用工作在無事務環境下的服務類方法,將分數添加20分
- service.addScore("tom",20);
- //④.查看此時用戶的分數
- intscore=jdbcTemplate.queryForInt(
- "SELECTscoreFROMt_userWHEREuser_name='tom'");
- System.out.println("score:"+score);
- jdbcTemplate.execute("DELETEFROMt_userWHEREuser_name='tom'");
- }
- }
jdbcWithoutTransManager.xml的配置文件如下所示:
清單2.jdbcWithoutTransManager.xml
- xmlversionxmlversion="1.0"encoding="UTF-8"?>
- <beansxmlnsbeansxmlns="http://www.springframework.org/schema/beans"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xmlns:context="http://www.springframework.org/schema/context"
- xmlns:p="http://www.springframework.org/schema/p"
- xsi:schemaLocation="http://www.springframework.org/schema/beans
- http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
- http://www.springframework.org/schema/context
- http://www.springframework.org/schema/context/spring-context-3.0.xsd">
- <context:component-scanbase-packagecontext:component-scanbase-package="user.withouttm"/>
- <beanidbeanid="dataSource"
- class="org.apache.commons.dbcp.BasicDataSource"
- destroy-method="close"
- p:driverClassName="oracle.jdbc.driver.OracleDriver"
- p:url="jdbc:oracle:thin:@localhost:1521:orcl"
- p:username="test"
- p:password="test"/>
- <beanidbeanid="jdbcTemplate"
- class="org.springframework.jdbc.core.JdbcTemplate"
- p:dataSource-ref="dataSource"/>
- beans>
運行UserJdbcWithoutTransManagerService,在控制臺上打出如下的結果:
- defaultAutoCommit:true
- score:30
在jdbcWithoutTransManager.xml中,沒有配置任何事務管理器,但是數據已經成功持久化到數據庫中。在默認情況下,dataSource數據源的autoCommit被設置為true――這也意謂著所有通過JdbcTemplate執行的語句馬上提交,沒有事務。如果將dataSource的defaultAutoCommit設置為false,再次運行UserJdbcWithoutTransManagerService,將拋出錯誤,原因是新增及更改數據的操作都沒有提交到數據庫,所以④處的語句因無法從數據庫中查詢到匹配的記錄而引發異常。
對于強調讀速度的應用,數據庫本身可能就不支持事務,如使用MyISAM引擎的MySQL數據庫。這時,無須在Spring應用中配置事務管理器,因為即使配置了,也是沒有實際用處的。
不過,對于Hibernate來說,情況就有點復雜了。因為Hibernate的事務管理擁有其自身的意義,它和Hibernate一級緩存有密切的關系:當我們調用Session的save、update等方法時,Hibernate并不直接向數據庫發送SQL語句,而是在提交事務(commit)或flush一級緩存時才真正向數據庫發送SQL。所以,即使底層數據庫不支持事務,Hibernate的事務管理也是有一定好處的,不會對數據操作的效率造成負面影響。所以,如果是使用Hibernate數據訪問技術,沒有理由不配置HibernateTransactionManager事務管理器。但是,不使用Hibernate事務管理器,在Spring中,Hibernate照樣也可以工作,來看下面的例子: #p#
清單3.UserHibernateWithoutTransManagerService.java
- packageuser.withouttm;
- importorg.springframework.beans.factory.annotation.Autowired;
- importorg.springframework.jdbc.core.JdbcTemplate;
- importorg.springframework.stereotype.Service;
- importorg.springframework.context.ApplicationContext;
- importorg.springframework.context.support.ClassPathXmlApplicationContext;
- importorg.springframework.orm.hibernate3.HibernateTemplate;
- importorg.apache.commons.dbcp.BasicDataSource;
- importuser.User;
- @Service("service2")
- publicclassUserHibernateWithoutTransManagerService{
- @Autowired
- privateHibernateTemplatehibernateTemplate;
- publicvoidaddScore(StringuserName,inttoAdd){
- Useruser=(User)hibernateTemplate.get(User.class,userName);
- user.setScore(user.getScore()+toAdd);
- hibernateTemplate.update(user);
- }
- publicstaticvoidmain(String[]args){
- //參考UserJdbcWithoutTransManagerService相應代碼
- …
- }
- }
此時,采用hiberWithoutTransManager.xml的配置文件,其配置內容如下:
清單4.hiberWithoutTransManager.xml
- xmlversionxmlversion="1.0"encoding="UTF-8"?>
- <beansxmlnsbeansxmlns="http://www.springframework.org/schema/beans"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xmlns:context="http://www.springframework.org/schema/context"
- xmlns:p="http://www.springframework.org/schema/p"
- xsi:schemaLocation="http://www.springframework.org/schema/beans
- http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
- http://www.springframework.org/schema/context
- http://www.springframework.org/schema/context/spring-context-3.0.xsd">
- …
- <beanidbeanid="sessionFactory"
- class=
- "org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean"
- p:dataSource-ref="dataSource">
- <propertynamepropertyname="annotatedClasses">
- <list>
- <value>user.Uservalue>
- list>
- property>
- <propertynamepropertyname="hibernateProperties">
- <props>
- <propkeypropkey="hibernate.dialect">
- org.hibernate.dialect.Oracle10gDialect
- prop>
- <propkeypropkey="hibernate.show_sql">trueprop>
- props>
- property>
- bean>
- <beanidbeanid="hibernateTemplate"
- class="org.springframework.orm.hibernate3.HibernateTemplate"
- p:sessionFactory-ref="sessionFactory"/>
- beans>
運行UserHibernateWithoutTransManagerService,程序正確執行,并得到類似于UserJdbcWithoutTransManagerService的執行結果,這說明Hibernate在Spring中,在沒有事務管理器的情況下,依然可以正常地進行數據的訪問。
應用分層的迷惑
Web、Service及DAO三層劃分就像西方國家的立法、行政、司法三權分立一樣被奉為金科玉律,甚至有開發人員認為如果要使用Spring事務管理就一定先要進行三層的劃分。這個看似荒唐的論調在開發人員中頗有市場。更有甚者,認為每層必須先定義一個接口,然后再定義一個實現類。其結果是:一個很簡單的功能,也至少需要3個接口,3個類,再加上視圖層的JSP和JS等,打牌都可以轉上兩桌了,這種誤解貽害不淺。
對將“面向接口編程”奉為圭臬,認為放之四海而皆準的論調,筆者深不以為然。是的,“面向接口編程”是MartinFowler,RodJohnson這些大師提倡的行事原則。如果拿這條原則去開發架構,開發產品,怎么強調都不為過。但是,對于我們一般的開發人員來說,做的最多的是普通工程項目,往往最多的只是一些對數據庫增、刪、查、改的功能。此時,“面向接口編程”除了帶來更多的類文件外,看不到更多其它的好處。
Spring框架提供的所有附加的好處(AOP、注解增強、注解MVC等)唯一的前提就是讓POJO的類變成一個受Spring容器管理的Bean,除此以外沒有其它任何的要求。下面的實例用一個POJO完成所有的功能,既是Controller,又是Service,還是DAO:
清單5.MixLayerUserService.java
- packageuser.mixlayer;
- importorg.springframework.beans.factory.annotation.Autowired;
- importorg.springframework.jdbc.core.JdbcTemplate;
- importorg.springframework.stereotype.Controller;
- importorg.springframework.web.bind.annotation.RequestMapping;
- //①.將POJO類通過注解變成SpringMVC的Controller
- @Controller
- publicclassMixLayerUserService{
- //②.自動注入JdbcTemplate
- @Autowired
- privateJdbcTemplatejdbcTemplate;
- //③.通過SpringMVC注解映URL請求
- @RequestMapping("/logon.do")
- publicStringlogon(StringuserName,Stringpassword){
- if(isRightUser(userName,password)){
- Stringsql="UPDATEt_useruSETu.score=u.score+?WHEREuser_name=?";
- jdbcTemplate.update(sql,20,userName);
- return"success";
- }else{
- return"fail";
- }
- }
- privatebooleanisRightUser(StringuserName,Stringpassword){
- //dosth...
- returntrue;
- }
- }
通過@Controller注解將MixLayerUserService變成Web層的Controller,同時也是Service層的服務類。此外,由于直接使用JdbcTemplate訪問數據,所以MixLayerUserService還是一個DAO。來看一下對應的Spring配置文件:
清單6.applicationContext.xml
- xmlversionxmlversion="1.0"encoding="UTF-8"?>
- <beansxmlnsbeansxmlns="http://www.springframework.org/schema/beans"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xmlns:context="http://www.springframework.org/schema/context"
- xmlns:p="http://www.springframework.org/schema/p"
- xmlns:aop="http://www.springframework.org/schema/aop"
- xmlns:tx="http://www.springframework.org/schema/tx"
- xsi:schemaLocation="http://www.springframework.org/schema/beans
- http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
- http://www.springframework.org/schema/context
- http://www.springframework.org/schema/context/spring-context-3.0.xsd
- http://www.springframework.org/schema/aop
- http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
- http://www.springframework.org/schema/tx
- http://www.springframework.org/schema/tx/spring-tx-3.0.xsd">
- <context:component-scanbase-packagecontext:component-scanbase-package="user.mixlayer"/>
- <beanclassbeanclass="org.springframework.web.servlet.mvc.annotation
- .AnnotationMethodHandlerAdapter"/>
- <beanclassbeanclass="org.springframework.web.servlet.view
- .InternalResourceViewResolver"
- pp:prefix="/WEB-INF/jsp/"p:suffix=".jsp"/>
- <beanidbeanid="dataSource"
- class="org.apache.commons.dbcp.BasicDataSource"
- destroy-method="close"
- p:driverClassName="oracle.jdbc.driver.OracleDriver"
- p:url="jdbc:oracle:thin:@localhost:1521:orcl"
- p:username="test"
- p:password="test"/>
- <beanidbeanid="jdbcTemplate"
- class="org.springframework.jdbc.core.JdbcTemplate"
- p:dataSource-ref="dataSource"/>
- <beanidbeanid="jdbcManager"
- class="org.springframework.jdbc.datasource.DataSourceTransactionManager"
- p:dataSource-ref="dataSource"/>
- <aop:configproxy-target-classaop:configproxy-target-class="true">
- <aop:pointcutidaop:pointcutid="serviceJdbcMethod"
- expression="execution(public*user.mixlayer.MixLayerUserService.*(..))"/>
- <aop:advisorpointcut-refaop:advisorpointcut-ref="serviceJdbcMethod"
- advice-ref="jdbcAdvice"order="0"/>
- aop:config>
- <tx:adviceidtx:adviceid="jdbcAdvice"transaction-manager="jdbcManager">
- <tx:attributes>
- <tx:methodnametx:methodname="*"/>
- tx:attributes>
- tx:advice>
- beans>
在①處,我們定義配置了AnnotationMethodHandlerAdapter,以便啟用SpringMVC的注解驅動功能。而②和③處通過Spring的aop及tx命名空間,以及Aspject的切點表達式語法進行事務增強的定義,對MixLayerUserService的所有公有方法進行事務增強。要使程序能夠運行起來還必須進行web.xml的相關配置:#p#
清單7.web.xml
- xmlversionxmlversion="1.0"encoding="GB2312"?>
- <web-appversionweb-appversion="2.4"xmlns="http://java.sun.com/xml/ns/j2ee"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
- http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
- <context-param>
- <param-name>contextConfigLocationparam-name>
- <param-value>classpath*:user/mixlayer/applicationContext.xmlparam-value>
- context-param>
- <context-param>
- <param-name>log4jConfigLocationparam-name>
- <param-value>/WEB-INF/classes/log4j.propertiesparam-value>
- context-param>
- <listener>
- <listener-class>
- org.springframework.web.util.Log4jConfigListener
- listener-class>
- listener>
- <listener>
- <listener-class>
- org.springframework.web.context.ContextLoaderListener
- listener-class>
- listener>
- <servlet>
- <servlet-name>userservlet-name>
- <servlet-class>
- org.springframework.web.servlet.DispatcherServlet
- servlet-class>
- <init-param>
- <param-name>contextConfigLocationparam-name>
- <param-value>classpath:user/mixlayer/applicationContext.xmlparam-value>
- init-param>
- <load-on-startup>1load-on-startup>
- servlet>
- <servlet-mapping>
- <servlet-name>userservlet-name>
- <url-pattern>*.dourl-pattern>
- servlet-mapping>
- web-app>
這個配置文件很簡單,唯一需要注意的是DispatcherServlet的配置。默認情況下SpringMVC根據Servlet的名字查找WEB-INF下的
將org.springframework.jdbc及org.springframework.transaction的日志級別設置為DEBUG,啟動項目,并訪問http://localhost:8088/logon.do?userName=tom應用,MixLayerUserService#logon方法將作出響應,查看后臺輸出日志:
清單8執行日志
- 13:24:22,625DEBUG(AbstractPlatformTransactionManager.java:365)-
- Creatingnewtransactionwithname
- [user.mixlayer.MixLayerUserService.logon]:PROPAGATION_REQUIRED,ISOLATION_DEFAULT
- 13:24:22,906DEBUG(DataSourceTransactionManager.java:205)-
- AcquiredConnection[org.apache.commons.dbcp.PoolableConnection@6e1cbf]
- forJDBCtransaction
- 13:24:22,921DEBUG(DataSourceTransactionManager.java:222)-
- SwitchingJDBCConnection
- [org.apache.commons.dbcp.PoolableConnection@6e1cbf]tomanualcommit
- 13:24:22,921DEBUG(JdbcTemplate.java:785)-
- ExecutingpreparedSQLupdate
- 13:24:22,921DEBUG(JdbcTemplate.java:569)-
- ExecutingpreparedSQLstatement
- [UPDATEt_useruSETu.score=u.score+?WHEREuser_name=?]
- 13:24:23,140DEBUG(JdbcTemplate.java:794)-
- SQLupdateaffected0rows
- 13:24:23,140DEBUG(AbstractPlatformTransactionManager.java:752)-
- Initiatingtransactioncommit
- 13:24:23,140DEBUG(DataSourceTransactionManager.java:265)-
- CommittingJDBCtransactiononConnection
- [org.apache.commons.dbcp.PoolableConnection@6e1cbf]
- 13:24:23,140DEBUG(DataSourceTransactionManager.java:323)-
- ReleasingJDBCConnection[org.apache.commons.dbcp.PoolableConnection@6e1cbf]
- aftertransaction
- 13:24:23,156DEBUG(DataSourceUtils.java:312)-
- ReturningJDBCConnectiontoDataSource
日志中粗體部分說明了MixLayerUserService#logon方法已經正確運行在事務上下文中。Spring框架本身不應該是復雜化代碼的理由,使用Spring的開發者應該是無拘無束的:從實際應用出發,去除掉那些所謂原則性的接口,去除掉強制分層的束縛,簡單才是硬道理。
事務方法嵌套調用的迷茫
Spring事務一個被訛傳很廣說法是:一個事務方法不應該調用另一個事務方法,否則將產生兩個事務。結果造成開發人員在設計事務方法時束手束腳,生怕一不小心就踩到地雷。其實這種是不認識Spring事務傳播機制而造成的誤解,Spring對事務控制的支持統一在TransactionDefinition類中描述,該類有以下幾個重要的接口方法:
◆intgetPropagationBehavior():事務的傳播行為;
◆intgetIsolationLevel():事務的隔離級別;
◆intgetTimeout():事務的過期時間;
◆booleanisReadOnly():事務的讀寫特性。
很明顯,除了事務的傳播行為外,事務的其它特性Spring是借助底層資源的功能來完成的,Spring無非只充當個代理的角色。但是事務的傳播行為卻是Spring憑借自身的框架提供的功能,是Spring提供給開發者最珍貴的禮物,訛傳的說法玷污了Spring事務框架最美麗的光環。所謂事務傳播行為就是多個事務方法相互調用時,事務如何在這些方法間傳播。Spring支持7種事務傳播行為:
◆PROPAGATION_REQUIRED如果當前沒有事務,就新建一個事務,如果已經存在一個事務中,加入到這個事務中。這是最常見的選擇。
◆PROPAGATION_SUPPORTS支持當前事務,如果當前沒有事務,就以非事務方式執行。
◆PROPAGATION_MANDATORY使用當前的事務,如果當前沒有事務,就拋出異常。
◆PROPAGATION_REQUIRES_NEW新建事務,如果當前存在事務,把當前事務掛起。
◆PROPAGATION_NOT_SUPPORTED以非事務方式執行操作,如果當前存在事務,就把當前事務掛起。
◆PROPAGATION_NEVER以非事務方式執行,如果當前存在事務,則拋出異常。
◆PROPAGATION_NESTED如果當前存在事務,則在嵌套事務內執行。如果當前沒有事務,則執行與PROPAGATION_REQUIRED類似的操作。
Spring默認的事務傳播行為是PROPAGATION_REQUIRED,它適合于絕大多數的情況。假設ServiveX#methodX()都工作在事務環境下(即都被Spring事務增強了),假設程序中存在如下的調用鏈:Service1#method1()->Service2#method2()->Service3#method3(),那么這3個服務類的3個方法通過Spring的事務傳播機制都工作在同一個事務中。
下面,我們來看一下實例,UserService#logon()方法內部調用了UserService#updateLastLogonTime()和ScoreService#addScore()方法,這兩個類都繼承于BaseService。它們之間的類結構說明如下:
具體的代碼如下所示:
清單9UserService.java
- @Service("userService")
- publicclassUserServiceextendsBaseService{
- @Autowired
- privateJdbcTemplatejdbcTemplate;
- @Autowired
- privateScoreServicescoreService;
- publicvoidlogon(StringuserName){
- updateLastLogonTime(userName);
- scoreService.addScore(userName,20);
- }
- publicvoidupdateLastLogonTime(StringuserName){
- Stringsql="UPDATEt_useruSETu.last_logon_time=?WHEREuser_name=?";
- jdbcTemplate.update(sql,System.currentTimeMillis(),userName);
- }
- }
UserService中注入了ScoreService的Bean,ScoreService的代碼如下所示:
清單10ScoreService.java
- @Service("scoreUserService")
- publicclassScoreServiceextendsBaseService{
- @Autowired
- privateJdbcTemplatejdbcTemplate;
- publicvoidaddScore(StringuserName,inttoAdd){
- Stringsql="UPDATEt_useruSETu.score=u.score+?WHEREuser_name=?";
- jdbcTemplate.update(sql,toAdd,userName);
- }
- }
通過Spring的事務配置為ScoreService及UserService中所有公有方法都添加事務增強,讓這些方法都工作于事務環境下。下面是關鍵的配置代碼:#p#
清單11事務增強配置
- <aop:configproxy-target-classaop:configproxy-target-class="true">
- <aop:pointcutidaop:pointcutid="serviceJdbcMethod"
- expression="within(user.nestcall.BaseService+)"/>
- <aop:advisorpointcut-refaop:advisorpointcut-ref="serviceJdbcMethod"
- advice-ref="jdbcAdvice"order="0"/>
- aop:config>
- <tx:adviceidtx:adviceid="jdbcAdvice"transaction-manager="jdbcManager">
- <tx:attributes>
- <tx:methodnametx:methodname="*"/>
- tx:attributes>
- tx:advice>
將日志級別設置為DEBUG,啟動Spring容器并執行UserService#logon()的方法,仔細觀察如下的輸出日志:
清單12執行日志
- 16:25:04,765DEBUG(AbstractPlatformTransactionManager.java:365)-
- Creatingnewtransactionwithname[user.nestcall.UserService.logon]:
- PROPAGATION_REQUIRED,ISOLATION_DEFAULT①為UserService#logon方法啟動一個事務
- 16:25:04,765DEBUG(DataSourceTransactionManager.java:205)-
- AcquiredConnection[org.apache.commons.dbcp.PoolableConnection@32bd65]
- forJDBCtransaction
- logonmethod...
- updateLastLogonTime...②直接執行updateLastLogonTime方法
- 16:25:04,781DEBUG(JdbcTemplate.java:785)-ExecutingpreparedSQLupdate
- 16:25:04,781DEBUG(JdbcTemplate.java:569)-ExecutingpreparedSQLstatement
- [UPDATEt_useruSETu.last_logon_time=?WHEREuser_name=?]
- 16:25:04,828DEBUG(JdbcTemplate.java:794)-SQLupdateaffected0rows
- 16:25:04,828DEBUG(AbstractPlatformTransactionManager.java:470)-Participating
- inexistingtransaction③ScoreService#addScore方法加入到UserService#logon的事務中
- addScore...
- 16:25:04,828DEBUG(JdbcTemplate.java:785)-ExecutingpreparedSQLupdate
- 16:25:04,828DEBUG(JdbcTemplate.java:569)-ExecutingpreparedSQLstatement
- [UPDATEt_useruSETu.score=u.score+?WHEREuser_name=?]
- 16:25:04,828DEBUG(JdbcTemplate.java:794)-SQLupdateaffected0rows
- 16:25:04,828DEBUG(AbstractPlatformTransactionManager.java:752)-
- Initiatingtransactioncommit
- 16:25:04,828DEBUG(DataSourceTransactionManager.java:265)-CommittingJDBCtransaction
- onConnection[org.apache.commons.dbcp.PoolableConnection@32bd65]
- 16:25:04,828DEBUG(DataSourceTransactionManager.java:323)-ReleasingJDBCConnection
- [org.apache.commons.dbcp.PoolableConnection@32bd65]aftertransaction
- 16:25:04,828DEBUG(DataSourceUtils.java:312)-ReturningJDBCConnectiontoDataSource
從上面的輸入日志中,可以清楚地看到Spring為UserService#logon()方法啟動了一個新的事務,而UserSerive#updateLastLogonTime()和UserService#logon()是在相同的類中,沒有觀察到有事務傳播行為的發生,其代碼塊好像“直接合并”到UserService#logon()中。接著,當執行到ScoreService#addScore()方法時,我們就觀察到了發生了事務傳播的行為:Participatinginexistingtransaction,這說明ScoreService#addScore()添加到UserService#logon()的事務上下文中,兩者共享同一個事務。所以最終的結果是UserService的logon(),updateLastLogonTime()以及ScoreService的addScore都工作于同一事務中。
多線程的困惑
由于Spring事務管理器是通過線程相關的ThreadLocal來保存數據訪問基礎設施,再結合IOC和AOP實現高級聲明式事務的功能,所以Spring的事務天然地和線程有著千絲萬縷的聯系。
我們知道Web容器本身就是多線程的,Web容器為一個Http請求創建一個獨立的線程,所以由此請求所牽涉到的Spring容器中的Bean也是運行于多線程的環境下。在絕大多數情況下,Spring的Bean都是單實例的(singleton),單實例Bean的最大的好處是線程無關性,不存在多線程并發訪問的問題,也即是線程安全的。一個類能夠以單實例的方式運行的前提是“無狀態”:即一個類不能擁有狀態化的成員變量。我們知道,在傳統的編程中,DAO必須執有一個Connection,而Connection即是狀態化的對象。所以傳統的DAO不能做成單實例的,每次要用時都必須new一個新的實例。傳統的Service由于將有狀態的DAO作為成員變量,所以傳統的Service本身也是有狀態的。
但是在Spring中,DAO和Service都以單實例的方式存在。Spring是通過ThreadLocal將有狀態的變量(如Connection等)本地線程化,達到另一個層面上的“線程無關”,從而實現線程安全。Spring不遺余力地將狀態化的對象無狀態化,就是要達到單實例化Bean的目的。由于Spring已經通過ThreadLocal的設施將Bean無狀態化,所以Spring中單實例Bean對線程安全問題擁有了一種天生的免疫能力。不但單實例的Service可以成功運行于多線程環境中,Service本身還可以自由地啟動獨立線程以執行其它的Service。下面,通過一個實例對此進行描述:
清單13UserService.java在事務方法中啟動獨立線程運行另一個事務方法
- @Service("userService")
- publicclassUserServiceextendsBaseService{
- @Autowired
- privateJdbcTemplatejdbcTemplate;
- @Autowired
- privateScoreServicescoreService;
- //①在logon方法體中啟動一個獨立的線程,在該獨立的線程中執行ScoreService#addScore()方法
- publicvoidlogon(StringuserName){
- System.out.println("logonmethod...");
- updateLastLogonTime(userName);
- ThreadmyThread=newMyThread(this.scoreService,userName,20);
- myThread.start();
- }
- publicvoidupdateLastLogonTime(StringuserName){
- System.out.println("updateLastLogonTime...");
- Stringsql="UPDATEt_useruSETu.last_logon_time=?WHEREuser_name=?";
- jdbcTemplate.update(sql,System.currentTimeMillis(),userName);
- }
- //②封裝ScoreService#addScore()的線程
- privateclassMyThreadextendsThread{
- privateScoreServicescoreService;
- privateStringuserName;
- privateinttoAdd;
- privateMyThread(ScoreServicescoreService,StringuserName,inttoAdd){
- this.scoreService=scoreService;
- this.userName=userName;
- this.toAdd=toAdd;
- }
- publicvoidrun(){
- scoreService.addScore(userName,toAdd);
- }
- }
- }
將日志級別設置為DEBUG,執行UserService#logon()方法,觀察以下輸出的日志:
清單14執行日志
- [main](AbstractPlatformTransactionManager.java:365)-Creatingnewtransactionwithname
- [user.multithread.UserService.logon]:PROPAGATION_REQUIRED,ISOLATION_DEFAULT①
- [main](DataSourceTransactionManager.java:205)-AcquiredConnection
- [org.apache.commons.dbcp.PoolableConnection@1353249]forJDBCtransaction
- logonmethod...
- updateLastLogonTime...
- [main](JdbcTemplate.java:785)-ExecutingpreparedSQLupdate
- [main](JdbcTemplate.java:569)-ExecutingpreparedSQLstatement
- [UPDATEt_useruSETu.last_logon_time=?WHEREuser_name=?]
- [main](JdbcTemplate.java:794)-SQLupdateaffected0rows
- [main](AbstractPlatformTransactionManager.java:752)-Initiatingtransactioncommit
- [Thread-2](AbstractPlatformTransactionManager.java:365)-
- Creatingnewtransactionwithname[user.multithread.ScoreService.addScore]:
- PROPAGATION_REQUIRED,ISOLATION_DEFAULT②
- [main](DataSourceTransactionManager.java:265)-CommittingJDBCtransaction
- onConnection[org.apache.commons.dbcp.PoolableConnection@1353249]③
- [main](DataSourceTransactionManager.java:323)-ReleasingJDBCConnection
- [org.apache.commons.dbcp.PoolableConnection@1353249]aftertransaction
- [main](DataSourceUtils.java:312)-ReturningJDBCConnectiontoDataSource
- [Thread-2](DataSourceTransactionManager.java:205)-AcquiredConnection
- [org.apache.commons.dbcp.PoolableConnection@10dc656]forJDBCtransaction
- addScore...
- [main](JdbcTemplate.java:416)-ExecutingSQLstatement
- [DELETEFROMt_userWHEREuser_name='tom']
- [main](DataSourceUtils.java:112)-FetchingJDBCConnectionfromDataSource
- [Thread-2](JdbcTemplate.java:785)-ExecutingpreparedSQLupdate
- [Thread-2](JdbcTemplate.java:569)-ExecutingpreparedSQLstatement
- [UPDATEt_useruSETu.score=u.score+?WHEREuser_name=?]
- [main](DataSourceUtils.java:312)-ReturningJDBCConnectiontoDataSource
- [Thread-2](JdbcTemplate.java:794)-SQLupdateaffected0rows
- [Thread-2](AbstractPlatformTransactionManager.java:752)-Initiatingtransactioncommit
- [Thread-2](DataSourceTransactionManager.java:265)-CommittingJDBCtransaction
- onConnection[org.apache.commons.dbcp.PoolableConnection@10dc656]④
- [Thread-2](DataSourceTransactionManager.java:323)-ReleasingJDBCConnection
- [org.apache.commons.dbcp.PoolableConnection@10dc656]aftertransaction
在①處,在主線程(main)執行的UserService#logon()方法的事務啟動,在③處,其對應的事務提交,而在子線程(Thread-2)執行的ScoreService#addScore()方法的事務在②處啟動,在④處對應的事務提交。
所以,我們可以得出這樣的結論:在相同線程中進行相互嵌套調用的事務方法工作于相同的事務中。如果這些相互嵌套調用的方法工作在不同的線程中,不同線程下的事務方法工作在獨立的事務中。
小結
Spring聲明式事務是Spring最核心,最常用的功能。由于Spring通過IOC和AOP的功能非常透明地實現了聲明式事務的功能,一般的開發者基本上無須了解Spring聲明式事務的內部細節,僅需要懂得如何配置就可以了。
但是在實際應用開發過程中,Spring的這種透明的高階封裝在帶來便利的同時,也給我們帶來了迷惑。就像通過流言傳播的消息,最終聽眾已經不清楚事情的真相了,而這對于應用開發來說是很危險的。本系列文章通過剖析實際應用中給開發者造成迷惑的各種難點,通過分析Spring事務管理的內部運作機制將真相還原出來。在本文中,我們通過剖析了解到以下的真相:
◆在沒有事務管理的情況下,DAO照樣可以順利進行數據操作;
◆將應用分成Web,Service及DAO層只是一種參考的開發模式,并非是事務管理工作的前提條件;
◆Spring通過事務傳播機制可以很好地應對事務方法嵌套調用的情況,開發者無須為了事務管理而刻意改變服務方法的設計;
◆由于單實例的對象不存在線程安全問題,所以進行事務管理增強的Bean可以很好地工作在多線程環境下。
在下一篇文章中,筆者將繼續分析Spring事務管理的以下難點:
◆混合使用多種數據訪問技術(如SpringJDBC+Hibernate)的事務管理問題;
◆在通過Bean的方法通過SpringAOP增強存在哪些特殊的情況。
【編輯推薦】