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

handawei_5發表於2010-06-10

概述

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)的事務管理問題;
  • 進行 Spring AOP 增強的 Bean 存在哪些特殊的情況。

相關文章