【Java EE】深入Spring數據庫事務管理

深入Spring數據庫事務管理

數據庫事務是企業應用最爲重要的內容之一。首先討論Spring數據庫的事務應用,然後討論Spring中最著名的註解之一——@Transactional。

Spring數據庫事務管理器的設計

在Spring中數據庫事務是通過PlatformTransactionManager進行管理的,而能夠支持事務的是org.springframework.transaction.support.TransactionTemplate模板,它是Spring所提供的事務管理器的模板,源碼如下:

//事務管理器
private PlatformTransactionManager transactionManager;
......
@Override
public <T> T execute(TransactionCallback<T> action) throws TransactionException{
	// 使用自定義的事務管理器
	if(this.transactionManager instanceof CallbackPreferringPlatformTransactionManager){
		return ((CallbackPreferringPlatformTransactionManager)this.transactionManager).execute(this,action);
	}else{//系統默認管理器
		//獲取事務狀態
		TransactionStatus status = this.transactionManager.getTransaction(this);
		T result;
		try{
			//回調接口方法
			result = action.doInTransaction(status);
		}catch(RuntimeException ex){
			//Transactional code threw application exception -> rollback
			//回滾異常方法
			rollbackOnException(status, ex);
			//拋出異常
			throw ex;
		}catch(Error ex){
			//Transactional code threw error -> rollback
			//回滾異常方法
			rollbackOnException(status, err);
			//拋出錯誤
			throw err;
		}catch(Throwable ex){
			//Transactional code threw unexpected exception -> rollback
			//回滾異常方法
			rollbackOnException(status, ex);
			//拋出無法捕獲異常
			throw new UndeclaredThrowableException(ex, "TransactionCallback threw undeclared checked exception");
		}
		//提交事務
		this.transactionManager.commit(status);
		//返回結果
		return result;
	}
}

通過上面的源碼,可以看到:

  • 事務的創建、提交和回滾是通過PlatformTransactionManager接口來完成的。
  • 當事務產生異常時會回滾事務,在默認的實現中所有的異常都會回滾。可以通過配置去修改在某些異常發生時回滾或者不會滾事務。
  • 當無異常時,會提交事務。
    在Spring中,有多種事務管理器,它們的設計如下:
    Spring事務管理器的設計
    其中可以看到多個數據庫事務管理器,並且支持JTA事務,常用的是DataSourceTransactionManager,它集成抽象事務管理器AbstractPlatformTransactionManager,而AbstractPlatformTransactionManager又實現了PlatformTransactionManager。這樣Spring就可以如同源碼中那樣使用PlatformTransactionManager接口的方法,創建、提交或者回滾事務了。
    PlatformTransactionManager接口的源碼如下:
public interface PlatformTransactionManager{
 //獲取事務狀態
 TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException;
 // 提交事務
 void commit(TransactionStatus status) throws TransactionException;
 //回滾事務
 void rollback(TransactionStatus status) throws TransactionException;
}

配置事務管理器

對於MyBatis框架,用得最多的事務管理器是DataSourceTransactionManager(org.springframework.jdbc.datasource.DataSourceTransactionManager);如果是Hibernate框架,那麼要用到spring-orm包org.springframework.orm.hibernate4.HibernateTransactionManager了。它們大同小異,一般而言,在使用時,還會加入XML的事務命名空間。如下所示:

<?xml version='1.0' encoding='UTF-8' ?>
<!-- was: <?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:p="http://www.springframework.org/schema/p"
   xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
   xmlns:context="http://www.springframework.org/schema/context"
   xsi:schemaLocation="http://www.springframework.org/schema/beans 
      http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
       http://www.springframework.org/schema/aop 
       http://www.springframework.org/schema/aop/spring-aop-4.0.xsd
       http://www.springframework.org/schema/tx 
       http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
       http://www.springframework.org/schema/context 
       http://www.springframework.org/schema/context/spring-context-4.0.xsd">
<!-- 數據庫連接池 -->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
   <property name="driverClassName" value="com.mysql.jdbc.Driver" />
   <property name="url" value="jdbc:mysql://localhost:3306/chapter13"/>
   <property name="username" value="root" />
   <property name="password" value="123456" />
   <property name="maxActive" value="255" />
   <property name="maxIdle" value="5" />
   <property name="maxWait" value="10000" />
</bean>
<!-- 事務管理器配置數據源事務 -->
<bean id="transactionManager"
   class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
   <property name="dataSource" ref="dataSource" />
</bean>
</beans>

這裏先引入了XML的命名空間,然後定義了數據庫連接池,於是使用了DataSourceTransactionManager去定義數據庫事務管理器,並且注入了數據庫連接池。這樣Spring就知道已經將數據庫事務委託給事務管理器transactionManager管理了。
在Spring中可以使用聲明式事務或者編程式事務,編程式事務幾乎不用,因此主要介紹聲明式事務。聲明式事務又可以分爲XML配置和註解事務,但是XML方式也已經不用了。目前主流方法是註解@Transactional。

使用Java配置方式實現Spring數據庫事務

用Java配置的方式來實現Spring數據庫事務,需要在配置類中實現接口TransactionManagerConfigurer的annotationDrivenTransactionManager方法。Spring會把annotationDrivenTransactionManager方式返回的事務管理器作爲程序中的事務管理器。下面是使用Java配置方式實現Spring的數據庫事務配置代碼:

/********* import ************/
@Configuration
@ComponentScan("com.ssm.chapter13.*")
//使用事務驅動管理器
@EnableTransactionManagement
public class JavaConfig implements TransactionManagerConfigurer{
	//數據源
	private DataSource dataSource = null;
	/**
	*配置數據源
	*@return 數據源.
	*/
	@Bean(name="dataSource")
	public DataSource initDataSource(){
		if(dataSource != null){
			return dataSource;
		}
		Properties props = new Properties();
		props.setProperty("driverClassName","com.mysql.jdbc.Driver");
		props.setProperty("url","jdbc:mysql://lcalhost:3306/chapter15");
		props.setProperty("username","root");
		props.setProperty("password","123456");
		props.setProperty("maxActiv","200");
		props.setProperty("maxIdle","20");
		props.setProperty("maxWait","30000");
		try{
			dataSource = BasicDataSourceFactory.createDataSource(props);
		}catch(Exception e){
			e.printStackTrace();
		}
		return dataSource;
	}
	/**
	*配置jdbcTemplate
	*@return jdbcTemplate
	*/
	@Bean(name="jdbcTemplate")
	public JdbcTemplate initjdbcTemplate(){
		JdbcTemplate jdbcTemplate = new JdbcTemplate();
		jdbcTemlate.setDataSource(initDataSource());
		return jdbcTemplate;
	}
	/**
	*實現接口方法,使得返回數據庫事務管理器
	*/
	@Override
	@Bean(name = "transactionManager")
	public PlatformTransactionManager annotationDriverTransactionManager(){
		DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
		//設置事務管理器管理的數據源
		transactionManager.setDataSource(initDataSource());
		return transactionManager;
	}
}

注意,使用註解@EnableTransactionManagement後,在Spring上下文中使用事務註解@Transactional,Spring就會知道使用這個數據庫事務管理器管理事務了。

編程式事務

編程式事務以代碼的方式管理事務,需要使用一個事務定義接口——TransactionDefinition。只要使用默認的實現類——DefaultTransactionDefinition就可以了。示例代碼如下:

ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-cfg.xml");
JdbcTemplate jdbcTemplate = ctx.getBean(JdbcTemplate.clsss);
//事務定義類
TransactionDefinition def = new DefaultTransactionDefinition();
PlatformTransactionManager transactionManager = ctx.getBean(PlatformTransactionManager.class);
TransactionStatus status = transactionManager.getTransaction(def);
try{
	//執行SQL語句
	jdbcTemplate.update("insert into t_role(role_name, note) values('role_name_transactionManagert','note_transactionManager')");
	//提交事務
	transactionManager.commit(status);
	//回滾事務
	transactionManager.rollback(status);
}

所有的事務都是由開發者自己進行控制的,由於事務已交由事務管理器管理,所以JbdcTemplate本身的數據庫資源已經由事務管理器管理,因此當它執行完insert語句時不會自動提交事務,這個時候需要使用事務管理器的commit方法,回滾事務需要使用rollback方法。

聲明式事務

聲明式事務是一種約定型的事務,在大部分情況下,當使用數據庫事務時,大部分的場景是在代碼中發生了異常時,需要回滾事務,而不發生異常時則提交事務,從而保證數據庫數據的一致性。
首先聲明式事務允許自定義事務接口——TransactionDefinition,它可以由XML或者註解@Transactional進行配置。

Transactional的配置項

Transactional的源碼如下:

package org.springframework.transaction.annotation;

import java.lang.annotation.Documented;
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @Interface Transactional{
	@AliasFor("transactionManager");
	String value() default "";

	@AliasFor("value")
	String transactionManager() default "";

	Propagation propagation() default Propagation.REQUIRED;

	Isolation isolation() default Isolation.DEFAULT;

	int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;

	boolean readOnly() default false;
	
	Class<?extends Throwable>[] rollbackFor() default{};
	
	String[] rollbackForClassName() default{};
	
	Class<? extends Throwable>[] noRollbackFor() default{};

	String[] noRollbackForClassName() default {};
}

下面是它的配置項:

配置項 含義 備註
value 定義事務管理器 它是Spring IoC容器裏的一個Bean id,這個Bean需要實現接口PlatformTransactionManager
transactionManager 同上 同上
isolation 隔離級別 這是一個數據庫在多個事務同時存在時的概念,默認值取數據庫默認隔離級別
propagation 傳播行爲 傳播行爲是方法之間調用的問題,默認值爲Propagation.REQUIRED
timeout 超時時間 單位爲秒,當超時時,會引發異常,默認會導致事務回滾
readOnly 是否開啓只讀事務 默認值爲false
rollbackFor 回滾事務的異常類定義 也就是隻有當方法產生所定義異常時,纔回滾事務,否則就提交事務
rollbackForClassName 回覆事務的異常類名定義 同rollbackFor,只是使用類名稱定義
noRollbackFor 當產生哪些異常不回滾事務 當產生所定義異常時,Spring將繼續提交事務
noRollbackForClassName 同noRollbackFor 同noRollbackFor,只是使用類的名稱定義

isolation和propagation是最重要的內容。上述所有的屬性將被會Spring放到事務定義類TransactionDefinition中,事務聲明器的配置內容也是以這些爲主了。
注意:使用聲明式事務需要配置註解驅動,只需要在代碼中加入如下配置即可使用@Transactional配置事務了:

<tx:annotation-driven transaction-manager="transactionManager"/>

使用XML進行配置事務管理器

介紹一種通用的XML聲明式事務配置,它需要一個事務攔截器——TransactionInterceptor,可以把攔截器想象成AOP編程,首先配置它,代碼如下:

<bean id="transactionInterceptor" class="org.springframework.transaction.interceptor.TransactionInterceptor">
	<property name="transactionManager" ref="transactionManager" />
	<!--配置事務屬性-->
	<property name="transactionAttributes">
		<props>
		<!--key代表的是業務方法的正則式匹配,而其內容可以配置各類事務定義參數-->
			<prop key="insert*">PROPAGATION_REQUIRED,ISOLATION_READ_UNCOMMITTED</prop>
			<prop key="save*">PROPAGATION_REQUIRED, ISOLATION_READ_UNCOMMITTED</prop>
			<prop key="add*">PROPAGATION_REQUIRED,ISOLATION_READ_UNCOMMITTED</prop>
			<prop key="select*"PROPAGATION_REQUIRED,readOnly</prop>
			<prop key="get*">PROPAGATION_REQUIRED,readOnly</prop>
			<prop key="find*">PROPAGATION_REQUIRED,readOnly</prop>
			<prop key="del*">PROPAGATION_REQUIRED,ISOLATION_READ_UNCOMMITTED</prop>
			<prop key="remove*">PROPAGATION_REQUIRED,ISOLATION_READ_UNCOMMITTED</prop>
			<prop key="update*">PROPAGATION_REQUIRED,ISOLATION_READ_UNCOMMITTED</prop>
		</props>
	</property>
</bean>

配置transactionAttributes的內容是需要關注的重點,Spring IoC啓動時會解析這些內容,放到事務定義類TransactionDefinition中,再運行時會根據正則式的匹配度決定方法採取哪種策略。這也揭示了聲明式事務的底層原理——Spring AOP技術。
展示Spring方法採取的事務策略之外,還需要告訴Spring哪些類要使用事務攔截器進行攔截,爲此再配置一個類BeanNameAutoProxyCreator:

<bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">	
	<property name="beanNames">
		<list>
			<value>*ServiceImpl</value>
		</list>
	</property>
	<property name="interceptorNames">
		<list>
			<value>transactionInterceptor</value>
		</list>
	</property>
</bean>

BeanName屬性告訴Spring如何攔截類。然後interceptorName則是定義事務攔截器,這樣對應的類和方法就會被事務管理器所攔截了。

事務定義器

下面是事務定義器TransactionDefinition的源碼;

package org.springframework.transaction;
import java.sql.Connection;
public interface TransactionDefinition{
	//傳播行爲常量定義(7個)
	int PROPAGATION_REQUIRED = 0; //默認傳播行爲
	int PROPAGATION_SUPPORTS = 1;
	int PROPAGATION_MANDATORY = 2;
	int PROPAGATION_REQUIRES_NEW = 3;
	int PROPAGATION_NOT_SUPPORTED = 4;
	int PROPATATION_NEVER = 5;
	int PROPATATION_NESTED = 6;
	//隔離級別定義(5個)
	int ISOLATION_DEFAULT = -1;//默認隔離級別
	//隔離級別定義(5個)
	int ISOLATION_READ_UNCOMMITTED = Connection.TRANSACTION_READ_UNCOMMITTED;
	int ISOLATION_READ_COMMITTED = Connection.TRANSACTION_REPEATABLE_READ;
	int ISOLATION_SERIALIZABLE = Connection.TRANSACTION_SERIALIZABLE;
	int TIMEOUT_DEFAULT = -1//-1代表永不超時
	//獲取傳播行爲
	int getPropagationBehavior();
	//獲取隔離級別
	int getIsolationLevel();
	//事務超時時間
	int getTimeout();
	//是否只讀事務
	boolean isReadOnly();
	//獲取事務定義器名稱
	String getName();
}

以上就是關於事務定義器的內容,除了異常的定義,其他關於事務的定義都可以在這裏完成。而對於事務的回滾內容,會以RollbackRuleAttribute和NoRollbackRuleAttribute兩個類進行保存。

聲明式事務的約定流程

約定十分重要,首先要理解@Transaction註解或者XML配置。@Transaction註解可以使用在方法或者類上面,在Spring IoC容器初始化時,Spring會讀入這個註解或着XML配置的事務信息,並且保存到一個事務定義類裏面(TransactionDefinition接口的子類),以備將來使用。當運行時會讓Spring攔截註解標註的某一個方法或者類的所有方法,這就是AOP的原理。
首先Spring通過事務管理器(PlatformTransactionManager的子類)創建事務,與此同時會把事務定義中的隔離級別、超時時間等屬性根據配置內容往事務上配置。然後啓動開發者提供的業務代碼,即Spring通過反射的方式調度開發者的業務代碼,如果反射的結果是異常的,並且符合事務定義類回滾條件,Spring就會將數據庫事務回滾,否則將數據庫事務提交,這是Spring自己完成的。
聲明式事務的流程如下:
聲明式事務的流程圖
以插入角色代碼爲例:

@Autowired
private RoleDao roleDao = null;

@Transactional(propagation=Propagation.REQUIRED,isolation=Isolation.DEFAULT,timeout=3)
public int insertRole(Role role){
	return roleDao.insert(role);
}

這裏配置了Propagation.REQUIRED的傳播行爲,意味着當別的方法調度時,如果存在事務就沿用下來,如果不存在事務就開啓新的事務,而隔離級別採用默認的,並且設置了超時時間爲3秒。這就是Spring AOP技術的神奇之處,其底層的實現原理是動態代理,也就是隻有代理對象相互調用才能像AOP那麼神奇。

數據庫的相關知識

數據庫事務ACID特性

數據庫事務正確執行的4個基礎要素是原子性(Atomicity)、一致性(Consistency)、隔離性(Isolation)和持久性(Durability)。

  • 原子性:整個事務中的所有操作,要麼全部完成,要麼全部不完成,不可能停滯在中間某個環節。事務在執行過程中發生錯誤,會被回滾到事務開始前的狀態,就像這個事務從來沒有被執行過一樣。
  • 一致性:指一個事務可以改變封裝狀態(除非它是一個只讀的)。事務必須始終保持系統處於一致的狀態,不管在任何給定的時間併發事務有多少。
  • 隔離性:它是指兩個事務之間的隔離程度。
  • 持久性:在事務完成以後,該事務對數據庫所做的更改便持久保存在數據庫之中,並不會被回滾。

隔離性涉及多個事務併發的狀態,會產生數據庫丟失更新的問題,其次隔離性又分爲多個層級。

丟失更新

在互聯網中存在着搶購、秒殺等高併發場景、使得數據庫在一個多事務的環境中運行,多個事務的併發會產生一系列的問題,主要的問題之一就是丟失更新,一般而言存在兩類丟失更新。
第一類丟失更新:兩個事務併發,其中一個回滾,另一個提交成功導致不一致;
第二類丟失更新:兩個都提交了的事務,由於在不同的事務中,無法探知其他事務的操作,導致實際結果不符。

目前大部分數據庫基本已經消滅了第一類丟失更新,對於第二類丟失更新,主要目標是克服事務之間協助的一致性,方法是在數據庫標準規範中定義事務之間的隔離級別,來在不同程度上減少出現丟失更新的可能性。

隔離級別

隔離級別可以在不同程度上減少丟失更新。
按照SQL的標準規範,把隔離級別定義爲4層,分別是:髒讀(dirty read)讀/寫提交(read commit)可重複度(repeatable read)序列化(Serializable)

  • 髒讀是最低的隔離級別,其含義是允許一個事務去讀取另一個事務中未提交的數據
  • 讀/寫提交,就是說一個事務只能讀取另一個事務已經提交的數據
  • 可重複讀,是針對數據庫同一條記錄而言,換句話說,可重複讀會使得同一條數據庫記錄的讀/寫按照一個序列化進行操作,不會產生交叉情況,這樣就能保證同一條數據的一致性,進而解決不可重複讀(unrepeatable read)的問題
  • 序列化,它是一種讓SQL按照順序讀/寫的方式,能夠消除數據庫事務之間併發產生數據不一致的問題,即幻讀(phantom read)

選擇隔離級別和傳播行爲

選擇隔離級別的出發點在於兩點:性能和數據一致性

選擇隔離級別

一般而言,從髒讀到序列化,系統性能直線下降。因此設置高的級別,比如序列化,會嚴重壓制併發,從而引發大量的線程掛起,直到獲得鎖才能進一步操作,而恢復時又需要大量的等待時間。在大部分場景下,企業會選擇讀/寫提交的方式設置事務,這樣既有助於提高併發,又壓制了髒讀,但是對於數據一致性問題並沒有解決。對於一般的應用都可以使用@Transactional方法進行配置,示例如下:

@Autowired
private RoleDao roleDao = null;
//設置方法爲讀/寫提交的隔離級別
@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED)
public int insertRole(Role role) {
   return roleDao.insert(role);
}

當業務併發量不是很大或者根本不需要考慮的情況下,使用序列化隔離級別用以保證數據的一致性,也是合理的。
總之,隔離級別需要根據併發的大小和性能來做出決定,對於併發不大又需要保證數據安全性的可以使用序列化的隔離級別,這樣就能保證數據庫在多事務環境中的一致性。代碼如下:

@Autowired
private RoleDao roleDao = null;
// 設置方法爲序列化的隔離級別
@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.SERIALIZABLE)
public int insertRole(Role role) {
   return roleDao.insert(role);
}

注意,實際工作中,註解@Transactional隔離級別的默認值爲Isolation.DEFAULT,其含義是默認的,隨數據庫默認值的變化而變化。不同的數據庫隔離級別的支持是不一樣的,MySQL支持4種隔離級別,默認是可重複讀,而Oracle只支持讀/寫提交和序列化兩種隔離級別,默認爲讀/寫提交。

傳播行爲

傳播行爲是指方法之間的調用事務策略的問題。當一個方法調度另外一個方法時,可以對事務的特性進行傳播配置,稱之爲傳播行爲。
在Spring中傳播行爲的類型,是通過一個枚舉類型去定義的,這個枚舉類是org.springframework.transaction.annotation.Propagation,它定義了7種傳播行爲,如下表所示:

傳播行爲 含義 備註
REQUIRED 當方法調用時,如果不存在當前事務,那麼就創建事務;如果之前的方法已經存在事務了,那麼就沿用之前的事務 這是Spring默認的傳播行爲
SUPPORTS 當方法調用時,如果不存在當前事務,那麼不啓用事務;如果存在當前事務,那麼就沿用當前事務
MANDATORY 方法必須在事務內允許 如果不存在當前事務,那麼就拋出異常
REQUIRES_NEW 無論是否存在當前事務,方法都會在新的事務中允許 也就是事務管理器會打開新的事務運行該方法
NOT_SUPPORTED 不支持事務,如果不存在當前事務也不會創建事務;如果存在當前事務,則掛起它,直到該方法結束後才恢復當前事務 適用於那些不需要事務的SQL
NEVER 不支持事務,只有在沒有事務的環境中才能運行它 如果方法存在當前事務,則拋出異常
NESTED 嵌套事務,也就是調用方法如果拋出異常只回滾自己內部執行的SQL,而不回滾主方法的SQL 它的實現存在兩種情況,如果當前數據庫支持保存點(savepoint),那麼它就會在當前事務上使用保存點技術;如果發生異常則將方法內執行的SQL回滾到保存點上,而不是全部回滾,否則就等同於REQUIRES_NEW創建新的事務運行方法代碼

最常用的時REQUIRED,也是默認的傳播行爲。一般而言,企業比較關注的時REQUIRES_NEW和NESTED.

在Spring+MyBatis組合中使用事務

通過Spring和MyBatis的組合,給出一個較爲詳細的實例:
目錄圖如下:
在這裏插入圖片描述
文件作用表如下:
在這裏插入圖片描述
首先,搭建環境。環境配置的XML文件如下:

<?xml version='1.0' encoding='UTF-8' ?>
<!-- was: <?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:p="http://www.springframework.org/schema/p"
   xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
   xmlns:context="http://www.springframework.org/schema/context"
   xsi:schemaLocation="http://www.springframework.org/schema/beans 
      http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
       http://www.springframework.org/schema/aop 
       http://www.springframework.org/schema/aop/spring-aop-4.0.xsd
       http://www.springframework.org/schema/tx 
       http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
       http://www.springframework.org/schema/context 
       http://www.springframework.org/schema/context/spring-context-4.0.xsd">
   <!--啓用掃描機制,並指定掃描對應的包-->
   <context:annotation-config />
   <context:component-scan base-package="com.ssm.chapter13.*" />
   <!-- 數據庫連接池 -->
   <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
      <property name="driverClassName" value="com.mysql.jdbc.Driver" />
      <property name="url" value="jdbc:mysql://localhost:3306/chapter13"/>
      <property name="username" value="root" />
      <property name="password" value="123456" />
      <property name="maxActive" value="255" />
      <property name="maxIdle" value="5" />
      <property name="maxWait" value="10000" />
   </bean>

   <!-- 集成MyBatis -->
   <bean id="SqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
      <property name="dataSource" ref="dataSource" />
         <!--指定MyBatis配置文件-->
      <property name="configLocation" value="classpath:/mybatis/mybatis-config.xml" />
   </bean>

   <!-- 事務管理器配置數據源事務 -->
   <bean id="transactionManager"
      class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
      <property name="dataSource" ref="dataSource" />
   </bean>

   <!-- 使用註解定義事務 -->
   <tx:annotation-driven transaction-manager="transactionManager" />

   <!-- 採用自動掃描方式創建mapper bean -->
   <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
      <property name="basePackage" value="com.ssm.chapter13" />
      <property name="SqlSessionFactory" ref="SqlSessionFactory" />
      <property name="annotationClass" value="org.springframework.stereotype.Repository" />
   </bean>
   
</beans>

先給出數據庫表映射的POJO類:

public class Role {
   private Long id;
   private String roleName;
   private String note;
   
   public Long getId() {
      return id;
   }
   public void setId(Long id) {
      this.id = id;
   }
   public String getRoleName() {
      return roleName;
   }
   public void setRoleName(String roleName) {
      this.roleName = roleName;
   }
   public String getNote() {
      return note;
   }
   public void setNote(String note) {
      this.note = note;
   }
}

搭建MyBatis的映射文件,建立SQL和POJO的關係:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ssm.chapter13.mapper.RoleMapper">
    <insert id="insertRole" parameterType="com.ssm.chapter13.pojo.Role">
        insert into t_role (role_name, note) values(#{roleName}, #{note})
    </insert>
</mapper>

配置一個接口:

import com.ssm.chapter13.pojo.Role;
import org.springframework.stereotype.Repository;

@Repository
public interface RoleMapper {
   public int insertRole(Role role);
}

爲了引入這個映射器,需要配置一個MyBatis的配置文件:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
  PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <mappers>
        <mapper resource="com/ssm/chapter13/sqlMapper/RoleMapper.xml"/>
    </mappers>
</configuration>

到此MyBatis部分的內容已經配置完成,接着配置一些服務(Service)類。對於服務類,堅持“接口+實現類“的規則,先定義兩個接口:

import java.util.List;
import com.ssm.chapter13.pojo.Role;

public interface RoleService {
   public int insertRole(Role role);
}
import java.util.List;
import com.ssm.chapter13.pojo.Role;

public interface RoleListService {
   public int insertRoleList(List<Role> roleList);
}

RoleService接口的insertRole方法可以對單個角色進行插入,而RoleListService的insertRoleList方法可以對角色列表進行插入。注意insertRoleList方法會調用insertRole,這樣就可以測試各類的傳播行爲了。下面是兩個接口的實現類:

import java.util.List;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import com.ssm.chapter13.pojo.Role;
import com.ssm.chapter13.service.RoleListService;
import com.ssm.chapter13.service.RoleService;
@Service
public class RoleListServiceImpl implements RoleListService {
   @Autowired
   private RoleService roleService = null;
   Logger log = Logger.getLogger(RoleListServiceImpl.class);
   @Override
   @Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED)
   public int insertRoleList(List<Role> roleList) {
      int count = 0;
      for (Role role : roleList) {
         try {
            count += roleService.insertRole(role);
         } catch (Exception ex) {
            log.info(ex);
         }
      }
      return count;
   }
}
import java.util.List;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import com.ssm.chapter13.mapper.RoleMapper;
import com.ssm.chapter13.pojo.Role;
import com.ssm.chapter13.service.RoleService;

@Service
public class RoleServiceImpl implements RoleService, ApplicationContextAware {
   @Autowired
   private RoleMapper roleMapper = null;  
   @Override
   @Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.READ_COMMITTED)
   public int insertRole(Role role) {
      return roleMapper.insertRole(role);
   }
}

兩個服務實現類方法標註了@Transactinal註解,它們都會以對應的隔離級和傳播行爲中運行。
爲了更好地測試從而輸出對應的日誌,修改log4j的配置文件:

log4j.rootLogger=DEBUG , stdout 
log4j.logger.org.springframework=DEBUG 
log4j.appender.stdout=org.apache.log4j.ConsoleAppender   
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout   
log4j.appender.stdout.layout.ConversionPattern=%5p %d %C: %m%n 

測試代碼如下:

import java.util.ArrayList;
import java.util.List;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.ssm.chapter13.pojo.Role;
import com.ssm.chapter13.service.RoleListService;
public class Chapter13Main {
   public static void main(String [] args) {
      ApplicationContext ctx = new ClassPathXmlApplicationContext ("spring-cfg.xml");
      RoleListService roleListService = ctx.getBean(RoleListService. class);
      List<Role> roleList = new ArrayList<Role>();
        for (int i=1; i<=2; i++) {
            Role role = new Role();
            role.setRoleName("role_name_" + i);
            role.setNote("note_" + i);
            roleList.add(role);
        }
        int count = roleListService.insertRoleList(roleList);
      System.out.println(count);
   }
}

這裏插入了兩個角色,由於insertRoleList會調用insertRole,而insertRole標註了REQUIRES_NEW,所以每次調用會產生新的事務。

由於保存點技術並不是每一個數據庫都能支持的,所以當你把傳播行爲設置爲NESTED時,Spring會先去探測當前數據庫是否能夠支持保存點技術。如果數據庫不予支持,它就會和REQUIRES_NEW一樣創建新事務去運行代碼,以達到內部方法發生異常時並不回滾當前事務的目的。

@Transactional的自調用失效問題

有時候配置了註解@Transactional,但是它會失效,這裏要注意一些細節問題,以避免落入陷阱。

註解@Transaction的底層實現是Spring AOP技術,而Spring AOP技術使用的是動態代理。這就意味着對於靜態(static)方法和非public方法,註解@Transactional是失效的。還有一個更爲隱祕的,而且在使用過程中極其容易犯錯誤的——自調用
所謂自調用,就是一個類的一個方法去調用自身另外一個方法的過程。示例代碼如下:

import java.util.List;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import com.ssm.chapter13.mapper.RoleMapper;
import com.ssm.chapter13.pojo.Role;
import com.ssm.chapter13.service.RoleService;

@Service
public class RoleServiceImpl implements RoleService, ApplicationContextAware {
   
   @Autowired
   private RoleMapper roleMapper = null;
   @Override
@Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.READ_COMMITTED)
public int insertRole(Role role) {
   return roleMapper.insertRole(role);
}

   //自調用問題
@Override
@Transactional(propagation = Propagation.REQUIRED, isolation=Isolation.READ_COMMITTED)
public int insertRoleList(List<Role> roleList) {
   int count = 0;
   for (Role role : roleList) {
      try {
               //調用自身類的方法,產生自調用問題
         insertRole(role);
         count++;
      } catch (Exception ex) {
         ex.printStackTrace();
      }
   }
   return count;
}
}

在insertRoleList方法的實現中,它調用了自身類實現insertRole的方法,而insertRole聲明是REQUIRES_NEW的傳播行爲,也就是每次調用就會產生新的事務運行。

出現這個的問題根本原因在於AOP的實現原理。由於@Transactional的實現原理是AOP,而AOP的實現原理是動態代理,而自己調用自己的過程,並不存在代理對象的調用,這樣就不會產生AOP去爲我們設置@Transactional配置的參數,這樣就出現了自調用註解失效的問題
爲了克服這個問題,一方面可以使用兩個服務類Spring IoC容器中爲你生成了RoleService的代理對象,這樣就可以使用AOP,且不會出現自調用的問題。另外一方面,你也可以直接從容器中獲取Service的代理對象,從IoC容器中獲取RoleService代理對象。代碼如下:

//消除自調用問題
@Override
@Transactional(propagation = Propagation.REQUIRED, isolation= Isolation.READ_COMMITTED)
public int insertRoleList2(List<Role> roleList) {
   int count = 0;
   //從容器中獲取RoleService對象,實際是一個代理對象
   RoleService service = ctx.getBean(RoleService.class);
   for (Role role : roleList) {
      try {
         service.insertRole(role);
         count++;
      } catch (Exception ex) {
         ex.printStackTrace();
      }
   }
   return count;
}

從容器獲取代理對象的方法克服了自調用的過程,但是有一個弊端,就是從容器獲取代理對象的方法有侵入之嫌,你的類需要依賴於Spring IoC容器,而這個類可以使用另一個服務類去調用。

典型錯誤用法的剖析

數據事務是企業應用關注的核心內容,也是開發者最容易犯錯的問題,因此這裏列舉一些使用不良習慣,注意它們可以避免一些錯誤和性能的丟失。

錯誤使用Service

互聯網往往採用模型—視圖—控制器(Model View Controller,MVC)來搭建開發環境,因此在Controller中使用Service是十分常見的。

在Controller每調用一次帶事務的service都會創建數據庫事務。如果多次調用,則不在同一個事務中,這會造成不同時提交和回滾不一致的問題。每一個Java EE開發者都要注意這類問題,以避免一些不必要的錯誤。

過長時間佔用事務

在企業的生產系統中,數據庫事務資源是最寶貴的資源之一,使用了數據庫事務之後,要及時釋放數據庫事務。換言之,我們應該儘可能地使用數據庫事務資源去完成所需工作,但是在一些工作中需要使用到文件、對外連接等操作,而這些操作往往會佔用較長時間,針對這些,如果開發者不注意細節,就很容易出現系統宕機的問題。
一些系統之間的通信及一些可能需要花費較長時間的操作,都要注意這個問題,放在controller層等事務外進行處理,以避免長時間佔用數據庫事務,導致系統性能的低下。

錯誤捕捉異常

帶事務的service中,出現異常想要回滾時應拋出異常,而不是捕獲。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章