程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> Spring事務管理高級應用難點剖析,第1部分

Spring事務管理高級應用難點剖析,第1部分

編輯:關於JAVA

概述

Spring 最成功,最吸引人的地方莫過於輕量級的聲明式事務管理,僅此一點,它就宣告了 重量級 EJB 容器的覆滅。Spring 聲明式事務管理將開發者從繁復的事務管理代碼中解脫出來 ,專注於業務邏輯的開發上,這是一件可以被拿來頂禮膜拜的事情。但是,世界並未從此消停 ,開發人員需要面對的是層出不窮的應用場景,這些場景往往逾越了普通 Spring 技術書籍的 理想界定。因此,隨著應用開發的深入,在使用經過 Spring 層層封裝的聲明式事務時,開發 人員越來越覺得自己墜入了迷霧,陷入了沼澤,體會不到外界所宣稱的那種暢快淋漓。本系列 文章的目標旨在整理並剖析實際應用中種種讓我們迷茫的場景,讓陽光照進雲遮霧障的山頭。

DAO 和事務管理的牽絆

很少有使用 Spring 但不使用 Spring 事務管理器的應用,因此常常有人會問:是否用了 Spring,就一定要用 Spring 事務管理器,否則就無法進行數據的持久化操作呢?事務管理器 和 DAO 是什麼關系呢?

也許是 DAO 和事務管理如影隨行的緣故吧,這個看似簡單的問題實實在在地存在著,從初 學者心中湧出,萦繞在開發老手的腦際。答案當然是否定的!我們都知道:事務管理是保證數 據操作的事務性(即原子性、一致性、隔離性、持久性,也即所謂的 ACID),脫離了事務性, DAO 照樣可以順利地進行數據的操作。

下面,我們來看一段使用 Spring JDBC 進行數據訪問的代碼:

清單 1. UserJdbcWithoutTransManagerService.java

package user.withouttm;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.context.ApplicationContext;
import  org.springframework.context.support.ClassPathXmlApplicationContext;
import org.apache.commons.dbcp.BasicDataSource;

@Service("service1")
public class UserJdbcWithoutTransManagerService {
   @Autowired
   private JdbcTemplate jdbcTemplate;

   public void addScore(String userName,int toAdd){
     String sql = "UPDATE t_user u SET u.score = u.score + ?  WHERE user_name =?";
     jdbcTemplate.update(sql,toAdd,userName);
   }

   public static void main(String[] args) {
     ApplicationContext ctx =
     new ClassPathXmlApplicationContext ("user/withouttm/jdbcWithoutTransManager.xml");
     UserJdbcWithoutTransManagerService service =
       (UserJdbcWithoutTransManagerService)ctx.getBean("service1");
     JdbcTemplate jdbcTemplate = (JdbcTemplate)ctx.getBean ("jdbcTemplate");
     BasicDataSource basicDataSource = (BasicDataSource) jdbcTemplate.getDataSource();

     //①.檢查數據源autoCommit的設置
     System.out.println("autoCommit:"+  basicDataSource.getDefaultAutoCommit());

     //②.插入一條記錄,初始分數為10
     jdbcTemplate.execute(
     "INSERT INTO t_user(user_name,password,score) VALUES ('tom','123456',10)");

     //③.調用工作在無事務環境下的服務類方法,將分數添加20分
     service.addScore("tom",20);

      //④.查看此時用戶的分數
     int score = jdbcTemplate.queryForInt(
     "SELECT score FROM t_user WHERE user_name ='tom'");
     System.out.println("score:"+score);
     jdbcTemplate.execute("DELETE FROM t_user WHERE  user_name='tom'");
   }
}

jdbcWithoutTransManager.xml 的配置文件如下所示:

清單 2. jdbcWithoutTransManager.xml

<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="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-scan base-package="user.withouttm"/>

   <!-- 數據源默認將autoCommit設置為true -->
   <bean id="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"/>

   <bean id="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 照樣也可以工作,來看 下面的例子:

清單 3.UserHibernateWithoutTransManagerService.java

package user.withouttm;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.orm.hibernate3.HibernateTemplate;
import org.apache.commons.dbcp.BasicDataSource;
import user.User;

@Service("service2")
public class UserHibernateWithoutTransManagerService {
   @Autowired
   private HibernateTemplate hibernateTemplate;

   public void addScore(String userName,int toAdd){
     User user = (User)hibernateTemplate.get(User.class,userName);
     user.setScore(user.getScore()+toAdd);
     hibernateTemplate.update(user);
   }

   public static void main(String[] args) {
     //參考UserJdbcWithoutTransManagerService相應代碼
     …
   }
}

此時,采用 hiberWithoutTransManager.xml 的配置文件,其配置內容如下:

清單 4.hiberWithoutTransManager.xml

<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="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">

<!--省略掉包掃描,數據源,JdbcTemplate配置部分,參見jdbcWithoutTransManager.xml  -->
   …

   <bean id="sessionFactory"
     class=
       "org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean"
       p:dataSource-ref="dataSource">
     <property name="annotatedClasses">
       <list>
         <value>user.User</value>
       </list>
     </property>
     <property name="hibernateProperties">
       <props>
         <prop key="hibernate.dialect">
           org.hibernate.dialect.Oracle10gDialect
         </prop>
         <prop key="hibernate.show_sql">true</prop>
       </props>
     </property>
   </bean>

   <bean id="hibernateTemplate"
      class="org.springframework.orm.hibernate3.HibernateTemplate"
      p:sessionFactory-ref="sessionFactory"/>
</beans>

運行 UserHibernateWithoutTransManagerService,程序正確執行,並得到類似於 UserJdbcWithoutTransManagerService 的執行結果,這說明 Hibernate 在 Spring 中,在沒 有事務管理器的情況下,依然可以正常地進行數據的訪問。

應用分層的迷惑

Web、 Service 及 DAO 三層劃分就像西方國家的立法、行政、司法三權分立一樣被奉為金 科玉律,甚至有開發人員認為如果要使用 Spring 的事務管理就一定先要進行三層的劃分。這 個看似荒唐的論調在開發人員中頗有市場。更有甚者,認為每層必須先定義一個接口,然後再 定義一個實現類。其結果是:一個很簡單的功能,也至少需要 3 個接口,3 個類,再加上視圖 層的 JSP 和 JS 等,打牌都可以轉上兩桌了,這種誤解贻害不淺。

對將“面向接口編程”奉為圭臬,認為放之四海而皆准的論調,筆者深不以為然。是的,“ 面向接口編程”是 Martin Fowler,Rod Johnson 這些大師提倡的行事原則。如果拿這條原則 去開發架構,開發產品,怎麼強調都不為過。但是,對於我們一般的開發人員來說,做的最多 的是普通工程項目,往往最多的只是一些對數據庫增、刪、查、改的功能。此時,“面向接口 編程”除了帶來更多的類文件外,看不到更多其它的好處。

Spring 框架提供的所有附加的好處(AOP、注解增強、注解 MVC 等)唯一的前提就是讓 POJO 的類變成一個受 Spring 容器管理的 Bean,除此以外沒有其它任何的要求。下面的實例 用一個 POJO 完成所有的功能,既是 Controller,又是 Service,還是 DAO:

清單 5. MixLayerUserService.java

package user.mixlayer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
//①.將POJO類通過注解變成Spring MVC的Controller
@Controller
public class MixLayerUserService {

   //②.自動注入JdbcTemplate
   @Autowired
   private JdbcTemplate jdbcTemplate;

   //③.通過Spring MVC注解映URL請求
   @RequestMapping("/logon.do")
   public String logon(String userName,String password){
     if(isRightUser(userName,password)){
       String sql = "UPDATE t_user u SET u.score = u.score + ?  WHERE user_name =?";
       jdbcTemplate.update(sql,20,userName);
       return "success";
     }else{
       return "fail";
     }
   }
   private boolean isRightUser(String userName,String password){
     //do sth...
     return true;
   }
}

通過 @Controller 注解將 MixLayerUserService 變成 Web 層的 Controller,同時也是 Service 層的服務類。此外,由於直接使用 JdbcTemplate 訪問數據,所以 MixLayerUserService 還是一個 DAO。來看一下對應的 Spring 配置文件:

清單 6.applicationContext.xml

<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="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">
   <!--掃描Web類包,通過注釋生成Bean-->
   <context:component-scan base-package="user.mixlayer"/>
   <!--①.啟動Spring MVC的注解功能,完成請求和注解POJO的映射-->
   <bean class="org.springframework.web.servlet.mvc.annotation
    .AnnotationMethodHandlerAdapter"/>

   <!--模型視圖名稱的解析,即在模型視圖名稱添加前後綴 -->
   <bean class="org.springframework.web.servlet.view
    .InternalResourceViewResolver"
      p:prefix="/WEB-INF/jsp/" p:suffix=".jsp"/>

   <!--普通數據源 -->
   <bean id="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"/>

   <bean id="jdbcTemplate"
      class="org.springframework.jdbc.core.JdbcTemplate"
      p:dataSource-ref="dataSource"/>

   <!--事務管理器 -->
   <bean id="jdbcManager"
     class="org.springframework.jdbc.datasource.DataSourceTransactionManager"
     p:dataSource-ref="dataSource"/>

   <!--②使用aop和tx命名空間語法為MixLayerUserService所有公用方法添加事務增強 -- >
   <aop:config proxy-target-class="true">
     <aop:pointcut id="serviceJdbcMethod"
       expression="execution(public *  user.mixlayer.MixLayerUserService.*(..))"/>
     <aop:advisor pointcut-ref="serviceJdbcMethod"
       advice-ref="jdbcAdvice" order="0"/>
   </aop:config>
   <tx:advice id="jdbcAdvice" transaction-manager="jdbcManager">
     <tx:attributes>
       <tx:method name="*"/>
     </tx:attributes>
   </tx:advice>
</beans>

在 ① 處,我們定義配置了 AnnotationMethodHandlerAdapter,以便啟用 Spring MVC 的 注解驅動功能。而②和③處通過 Spring 的 aop 及 tx 命名空間,以及 Aspject 的切點表達 式語法進行事務增強的定義,對 MixLayerUserService 的所有公有方法進行事務增強。要使程 序能夠運行起來還必須進行 web.xml 的相關配置:

清單 7.web.xml

<?xml version="1.0" encoding="GB2312"?>
<web-app version="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>contextConfigLocation</param-name>
     <param-value>classpath*:user/mixlayer/applicationContext.xml</param- value>
   </context-param>
   <context-param>
     <param-name>log4jConfigLocation</param-name>
     <param-value>/WEB-INF/classes/log4j.properties</param-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>user</servlet-name>
     <servlet-class>
       org.springframework.web.servlet.DispatcherServlet
     </servlet-class>
     <!--①通過contextConfigLocation參數指定Spring配置文件的位置 -->
     <init-param>
       <param-name>contextConfigLocation</param-name>
       <param-value>classpath:user/mixlayer/applicationContext.xml</param- value>
     </init-param>
     <load-on-startup>1</load-on-startup>
   </servlet>
   <servlet-mapping>
     <servlet-name>user</servlet-name>
     <url-pattern>*.do</url-pattern>
   </servlet-mapping>
</web-app>

這個配置文件很簡單,唯一需要注意的是 DispatcherServlet 的配置。默認情況下 Spring MVC 根據 Servlet 的名字查找 WEB-INF 下的 <servletName>-servlet.xml 作為 Spring MVC 的配置文件,在此,我們通過 contextConfigLocation 參數顯式指定 Spring MVC 配置文件的確切位置。

將 org.springframework.jdbc 及 org.springframework.transaction 的日志級別設置為 DEBUG,啟動項目,並訪問 http://localhost:8088/logon.do?userName=tom 應用, MixLayerUserService#logon 方法將作出響應,查看後台輸出日志:

清單 8 執行日志

13:24:22,625 DEBUG (AbstractPlatformTransactionManager.java:365) -
   Creating new transaction with name
  [user.mixlayer.MixLayerUserService.logon]:  PROPAGATION_REQUIRED,ISOLATION_DEFAULT
13:24:22,906 DEBUG (DataSourceTransactionManager.java:205) -
   Acquired Connection [org.apache.commons.dbcp.PoolableConnection@6e1cbf]
  for JDBC transaction
13:24:22,921 DEBUG (DataSourceTransactionManager.java:222) -
   Switching JDBC Connection
  [org.apache.commons.dbcp.PoolableConnection@6e1cbf] to manual commit
13:24:22,921 DEBUG (JdbcTemplate.java:785) -
   Executing prepared SQL update
13:24:22,921 DEBUG (JdbcTemplate.java:569) -
   Executing prepared SQL statement
  [UPDATE t_user u SET u.score = u.score + ? WHERE user_name =?]
13:24:23,140 DEBUG (JdbcTemplate.java:794) -
   SQL update affected 0 rows
13:24:23,140 DEBUG (AbstractPlatformTransactionManager.java:752) -
   Initiating transaction commit
13:24:23,140 DEBUG (DataSourceTransactionManager.java:265) -
   Committing JDBC transaction on Connection
  [org.apache.commons.dbcp.PoolableConnection@6e1cbf]
13:24:23,140 DEBUG (DataSourceTransactionManager.java:323) -
   Releasing JDBC Connection  [org.apache.commons.dbcp.PoolableConnection@6e1cbf]
  after transaction
13:24:23,156 DEBUG (DataSourceUtils.java:312) -
   Returning JDBC Connection to DataSource

日志中粗體部分說明了 MixLayerUserService#logon 方法已經正確運行在事務上下文中。

Spring 框架本身不應該是復雜化代碼的理由,使用 Spring 的開發者應該是無拘無束的: 從實際應用出發,去除掉那些所謂原則性的接口,去除掉強制分層的束縛,簡單才是硬道理。

事務方法嵌套調用的迷茫

Spring 事務一個被訛傳很廣說法是:一個事務方法不應該調用另一個事務方法,否則將產 生兩個事務。結果造成開發人員在設計事務方法時束手束腳,生怕一不小心就踩到地雷。

其實這種是不認識 Spring 事務傳播機制而造成的誤解,Spring 對事務控制的支持統一在 TransactionDefinition 類中描述,該類有以下幾個重要的接口方法:

int getPropagationBehavior():事務的傳播行為

int getIsolationLevel():事務的隔離級別

int getTimeout():事務的過期時間

boolean isReadOnly():事務的讀寫特性。

很明顯,除了事務的傳播行為外,事務的其它特性 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。它們之間的類結構說明如下:

圖 1. UserService 和 ScoreService

具體的代碼如下所示:

清單 9 UserService.java

@Service("userService")
public class UserService extends BaseService {
   @Autowired
   private JdbcTemplate jdbcTemplate;
   @Autowired
   private ScoreService scoreService;

   public void logon(String userName) {
     updateLastLogonTime(userName);
     scoreService.addScore(userName, 20);
   }

   public void updateLastLogonTime(String userName) {
     String sql = "UPDATE t_user u SET u.last_logon_time = ? WHERE  user_name =?";
     jdbcTemplate.update(sql, System.currentTimeMillis(), userName);
   }
}

UserService 中注入了 ScoreService 的 Bean,ScoreService 的代碼如下所示:

清單 10 ScoreService.java

@Service("scoreUserService")
public class ScoreService extends BaseService{
   @Autowired
   private JdbcTemplate jdbcTemplate;
   public void addScore(String userName, int toAdd) {
     String sql = "UPDATE t_user u SET u.score = u.score + ?  WHERE user_name =?";
     jdbcTemplate.update(sql, toAdd, userName);
   }
}

通過 Spring 的事務配置為 ScoreService 及 UserService 中所有公有方法都添加事務增 強,讓這些方法都工作於事務環境下。下面是關鍵的配置代碼:

清單 11 事務增強配置

<!-- 添加Spring事務增強 -->
<aop:config proxy-target-class="true">
   <aop:pointcut id="serviceJdbcMethod"
     <!-- 所有繼承於BaseService類的子孫類的public方法都進行事務增強-->
     expression="within(user.nestcall.BaseService+)"/>
   <aop:advisor pointcut-ref="serviceJdbcMethod"
     advice-ref="jdbcAdvice" order="0"/>
</aop:config>
<tx:advice id="jdbcAdvice" transaction-manager="jdbcManager">
   <tx:attributes>
     <tx:method name="*"/>
   </tx:attributes>
</tx:advice>

將日志級別設置為 DEBUG,啟動 Spring 容器並執行 UserService#logon() 的方法,仔細 觀察如下的輸出日志:

清單 12 執行日志

16:25:04,765 DEBUG (AbstractPlatformTransactionManager.java:365) -
   Creating new transaction with name  [user.nestcall.UserService.logon]:
  PROPAGATION_REQUIRED,ISOLATION_DEFAULT ①為UserService#logon方法啟動一個事務

16:25:04,765 DEBUG (DataSourceTransactionManager.java:205) -
   Acquired Connection [org.apache.commons.dbcp.PoolableConnection@32bd65]
  for JDBC transaction

logon method...

updateLastLogonTime... ②直接執行updateLastLogonTime方法 

16:25:04,781 DEBUG (JdbcTemplate.java:785) - Executing prepared SQL  update

16:25:04,781 DEBUG (JdbcTemplate.java:569) - Executing prepared SQL  statement
   [UPDATE t_user u SET u.last_logon_time = ? WHERE user_name =?]

16:25:04,828 DEBUG (JdbcTemplate.java:794) - SQL update affected 0  rows

16:25:04,828 DEBUG (AbstractPlatformTransactionManager.java:470) -  Participating
   in existing transaction  ③ScoreService#addScore方法加入到 UserService#logon的事務中

addScore...

16:25:04,828 DEBUG (JdbcTemplate.java:785) - Executing prepared SQL  update

16:25:04,828 DEBUG (JdbcTemplate.java:569) - Executing prepared SQL  statement
   [UPDATE t_user u SET u.score = u.score + ? WHERE user_name =?]

16:25:04,828 DEBUG (JdbcTemplate.java:794) - SQL update affected 0  rows

16:25:04,828 DEBUG (AbstractPlatformTransactionManager.java:752) -
   Initiating transaction commit

④提交事務

16:25:04,828 DEBUG (DataSourceTransactionManager.java:265) - Committing JDBC  transaction
   on Connection [org.apache.commons.dbcp.PoolableConnection@32bd65]

16:25:04,828 DEBUG (DataSourceTransactionManager.java:323) - Releasing JDBC  Connection
   [org.apache.commons.dbcp.PoolableConnection@32bd65] after  transaction

16:25:04,828 DEBUG (DataSourceUtils.java:312) - Returning JDBC Connection  to DataSource

從上面的輸入日志中,可以清楚地看到 Spring 為 UserService#logon() 方法啟動了一個 新的事務,而 UserSerive#updateLastLogonTime() 和 UserService#logon() 是在相同的類中 ,沒有觀察到有事務傳播行為的發生,其代碼塊好像“直接合並”到 UserService#logon() 中 。接著,當執行到 ScoreService#addScore() 方法時,我們就觀察到了發生了事務傳播的行為 :Participating in existing transaction,這說明 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。下面,通過一 個實例對此進行描述:

清單 13 UserService.java 在事務方法中啟動獨立線程運行另一個事務方法

@Service("userService")
public class UserService extends BaseService {
   @Autowired
   private JdbcTemplate jdbcTemplate;

   @Autowired
   private ScoreService scoreService;
   //① 在logon方法體中啟動一個獨立的線程,在該獨立的線程中執行 ScoreService#addScore()方法
   public void logon(String userName) {
     System.out.println("logon method...");
     updateLastLogonTime(userName);
     Thread myThread = new MyThread(this.scoreService,userName,20);
     myThread.start();
   }

   public void updateLastLogonTime(String userName) {
     System.out.println("updateLastLogonTime...");
     String sql = "UPDATE t_user u SET u.last_logon_time = ? WHERE  user_name =?";
     jdbcTemplate.update(sql, System.currentTimeMillis(), userName);
   }
   //② 封裝ScoreService#addScore()的線程 
   private class MyThread extends Thread{
     private ScoreService scoreService;
     private String userName;
     private int toAdd;
     private MyThread(ScoreService scoreService,String userName,int  toAdd) {
       this.scoreService = scoreService;
       this.userName = userName;
       this.toAdd = toAdd;
     }
     public void run() {
       scoreService.addScore(userName,toAdd);
     }
   }
}

將日志級別設置為 DEBUG,執行 UserService#logon() 方法,觀察以下輸出的日志:

清單 14 執行日志

[main] (AbstractPlatformTransactionManager.java:365) - Creating new  transaction with name
   [user.multithread.UserService.logon]:  PROPAGATION_REQUIRED,ISOLATION_DEFAULT ① 

[main] (DataSourceTransactionManager.java:205) - Acquired Connection
   [org.apache.commons.dbcp.PoolableConnection@1353249] for JDBC  transaction

logon method...

updateLastLogonTime...

[main] (JdbcTemplate.java:785) - Executing prepared SQL update

[main] (JdbcTemplate.java:569) - Executing prepared SQL statement
   [UPDATE t_user u SET u.last_logon_time = ? WHERE user_name =?]

[main] (JdbcTemplate.java:794) - SQL update affected 0 rows

[main] (AbstractPlatformTransactionManager.java:752) - Initiating transaction  commit

[Thread-2](AbstractPlatformTransactionManager.java:365) -
   Creating new transaction with name  [user.multithread.ScoreService.addScore]:
  PROPAGATION_REQUIRED,ISOLATION_DEFAULT ②

[main] (DataSourceTransactionManager.java:265) - Committing JDBC  transaction
   on Connection [org.apache.commons.dbcp.PoolableConnection@1353249] ③

[main] (DataSourceTransactionManager.java:323) - Releasing JDBC  Connection
   [org.apache.commons.dbcp.PoolableConnection@1353249] after  transaction

[main] (DataSourceUtils.java:312) - Returning JDBC Connection to  DataSource

[Thread-2] (DataSourceTransactionManager.java:205) - Acquired Connection
   [org.apache.commons.dbcp.PoolableConnection@10dc656] for JDBC  transaction

addScore...

[main] (JdbcTemplate.java:416) - Executing SQL statement
   [DELETE FROM t_user WHERE user_name='tom']

[main] (DataSourceUtils.java:112) - Fetching JDBC Connection from  DataSource

[Thread-2] (JdbcTemplate.java:785) - Executing prepared SQL update

[Thread-2] (JdbcTemplate.java:569) - Executing prepared SQL statement
   [UPDATE t_user u SET u.score = u.score + ? WHERE user_name =?]

[main] (DataSourceUtils.java:312) - Returning JDBC Connection to  DataSource

[Thread-2] (JdbcTemplate.java:794) - SQL update affected 0 rows

[Thread-2] (AbstractPlatformTransactionManager.java:752) - Initiating  transaction commit

[Thread-2] (DataSourceTransactionManager.java:265) - Committing JDBC  transaction
   on Connection [org.apache.commons.dbcp.PoolableConnection@10dc656] ④

[Thread-2] (DataSourceTransactionManager.java:323) - Releasing JDBC  Connection
   [org.apache.commons.dbcp.PoolableConnection@10dc656] after  transaction

在 ① 處,在主線程(main)執行的 UserService#logon() 方法的事務啟動,在 ③ 處, 其對應的事務提交,而在子線程(Thread-2)執行的 ScoreService#addScore() 方法的事務在 ② 處啟動,在 ④ 處對應的事務提交。

所以,我們可以得出這樣的結論:在 相同線程中進行相互嵌套調用的事務方法工作於相同 的事務中。如果這些相互嵌套調用的方法工作在不同的線程中,不同線程下的事務方法工作在 獨立的事務中。

小結

Spring 聲明式事務是 Spring 最核心,最常用的功能。由於 Spring 通過 IOC 和 AOP 的 功能非常透明地實現了聲明式事務的功能,一般的開發者基本上無須了解 Spring 聲明式事務 的內部細節,僅需要懂得如何配置就可以了。

但是在實際應用開發過程中,Spring 的這種透明的高階封裝在帶來便利的同時,也給我們 帶來了迷惑。就像通過流言傳播的消息,最終聽眾已經不清楚事情的真相了,而這對於應用開 發來說是很危險的。本系列文章通過剖析實際應用中給開發者造成迷惑的各種難點,通過分析 Spring 事務管理的內部運作機制將真相還原出來。

在本文中,我們通過剖析了解到以下的真相:

在沒有事務管理的情況下,DAO 照樣可以順利進行數據操作;

將應用分成 Web,Service 及 DAO 層只是一種參考的開發模式,並非是事務管理工作的前 提條件;

Spring 通過事務傳播機制可以很好地應對事務方法嵌套調用的情況,開發者無須為了事務 管理而刻意改變服務方法的設計;

由於單實例的對象不存在線程安全問題,所以進行事務管理增強的 Bean 可以很好地工作在 多線程環境下。

在下一篇文章中,筆者將繼續分析 Spring 事務管理的以下難點:

混合使用多種數據訪問技術(如 Spring JDBC+Hibernate)的事務管理問題;

在通過 Bean 的方法通過 Spring AOP 增強存在哪些特殊的情況。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved