深入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中,有多種事務管理器,它們的設計如下:
其中可以看到多個數據庫事務管理器,並且支持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中,出現異常想要回滾時應拋出異常,而不是捕獲。