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

概述html

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

DAO 和事務管理的牽絆spring

不多有使用 Spring 但不使用 Spring 事務管理器的應用,所以經常有人會問:是否用了 Spring,就必定要用 Spring 事務管理器,不然就沒法進行數據的持久化操做呢?事務管理器和 DAO 是什麼關係呢?sql

也許是 DAO 和事務管理形影不離的緣故吧,這個看似簡單的問題實實在在地存在着,從初學者心中涌出,縈繞在開發老手的腦際。答案固然是否認的!咱們都知道:事務管理是 保證數據操做的事務性(即原子性、一致性、隔離性、持久性,也即所謂的 ACID),脫離了事務性,DAO 照樣能夠順利地進行數據的操做。數據庫

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


清單 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 的配置文件以下所示:apache


清單 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
圖 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 能夠很好地工做在多線程環境下。
相關文章
相關標籤/搜索