引入spring事務管理
1.1 為什麼用spring事務管理?
在沒有spring事務管理器之前,java開發者通常有兩種事務管理器可供選擇:本地事務和全域性事務。本地事務針對的是某一個具體資源(例如JDBC),使用起來也比較方便,但當程式需要處理多個事務資源時,本地事務則無法勝任了。全域性事務解決了這個問題,一般都會採用JTA(JAVA transaction API)來實現全域性事務。但全域性事務的限制也很多,如必須使用JNDI資料來源、需要支援XA協議的資料庫、依賴於容器、效能低下、只支援單應用多資料來源,不支援多應用多資料來源等。
針對不同環境、不同事務策略,spring提供了一個事務管理器的抽象層,開發者通過這個抽象事務管理器進行管理事務,之後配置一個具體的事務實現層,就可以實現程式碼寫一次,可以適用於所有的具體事務管理器API。
1.2 理解spring抽象事務管理器
spring抽象事務管理器採用的是策略模式,理解策略模式的關鍵點就是看它的策略。org.springframework.transaction.PlatformTransactionManager介面定義了相關方法
public interface PlatformTransactionManager { TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException; void commit(TransactionStatus status) throws TransactionException; void rollback(TransactionStatus status) throws TransactionException; }
PlatformTransactionManager提供了3個方法,getTransaction根據TransactionDefinition中定義的相關資訊建立或者返回一個已經存在的事務。返回的TransactionStatus代表著一個事務。commit和rollback分別表示提交事務和回滾事務。TransactionDefinition細節如下:
public interface TransactionDefinition { int getPropagationBehavior(); int getIsolationLevel(); int getTimeout(); boolean isReadOnly(); String getName(); }
getPropagationBehavior獲取事務的傳播特性,可選值如下:
傳播特性 | 值 | 解釋 |
---|---|---|
PROPAGATION_REQUIRED | 0 | 支援當前事務環境,如果沒有則建立一個 |
PROPAGATION_SUPPORTS | 1 | 支援當前事務,沒有不建立事務 |
PROPAGATION_MANDATORY | 2 | 支援當前事務,沒有則報錯 |
PROPAGATION_REQUIRES_NEW | 3 | 掛起當前事務,新建一個事務 |
PROPAGATION_NOT_SUPPORTED | 4 | 如果存在事務則掛起 |
PROPAGATION_NEVER | 5 | 如果存在事務則報錯 |
PROPAGATION_NESTED | 6 | 開啟一個巢狀事務,很多資料庫都不支援 |
事務的七種傳播特性中,需要注意的一個點:用了事務管理器之後連線將由事務管理器管理了,例如用了PROPAGATION_NOT_SUPPORTED之後,第一次操作會獲取一個新的connection,但之後的操作都是基於已獲取的connection,如果用的是AbstractRoutingDataSource,在NOT_SUPPORTED傳播特性的方法內切換資料來源是無效的,因為選擇資料來源是在getConnection的時候確定的。
-- 切換資料來源後,只有獲取連線才會呼叫determineTargetDataSource使切換後的資料來源生效 public Connection getConnection(String username, String password) throws SQLException { return determineTargetDataSource().getConnection(username, password); }
getIsolationLevel獲取事務的隔離級別,可選值如下:
隔離級別 | 值 | 解釋 |
---|---|---|
ISOLATION_DEFAULT | -1 | 使用資料來源提供的隔離級別 |
ISOLATION_READ_UNCOMMITTED | 1 | 允許一個事務讀取另一個事務還沒提交的資料 |
ISOLATION_READ_COMMITTED | 2 | 禁止髒讀 |
ISOLATION_REPEATABLE_READ | 4 | 禁止不可重讀 |
ISOLATION_SERIALIZABLE | 8 | 禁止幻讀 |
一般資料庫的預設級別都會設定成READ_COMMITTED,即禁止髒讀。不可重讀的意思是當一個事務內兩次查詢同一條資料,在第二次查詢資料之前有個事務更改了這條資料並提交了事務,則第二次查詢的資料是更改之後的資料,與第一次查詢的不一致,即為不可重複讀。設定成REPEATABLE_READ之後,更新那條資料的事務會等待查詢那條資料的事務提交後才可以提交。幻讀的意思是當 一個事務兩次查詢一個範圍的資料, 在第二次查詢之前有個事務插入了一條符合查詢條件的事務,則會造成兩次查詢結果不一致。設定成禁止幻讀之後,插入符合第一個查詢條件的事務必須等查詢事務提交之後,才可以被提交。可以看到,禁止不可重讀和禁止幻讀會對效能帶來極大影響,一般資料庫預設隔離級別都設定成禁止髒讀。
getTimeout方法獲取事務超時值,超過這個時間之後將會被回滾。isReadOnly標明這個事務是不是隻讀事務,如果有寫操作會丟擲異常。getName返回事務名字,便於在開發事務管理後臺的時候區分事務,預設會設定成類全路徑.方法名。
TransactionStatus像其它事務介面一樣,提供了一些簡單方式來控制事務執行和查詢事務狀態,詳細如下:
public interface TransactionStatus extends SavepointManager, Flushable { boolean isNewTransaction(); boolean hasSavepoint(); void setRollbackOnly(); boolean isRollbackOnly(); void flush(); boolean isCompleted(); }
1.3 宣告spring事務管理器
無論使用宣告式事務還是程式設計式事務,都需要指定具體的事務管理器實現類,一般我們採用依賴注入來指定具體的事務管理器。通常根據我們專案工作的環境來選擇具體的事務管理器。
使用JDBC資料來源的步驟:
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="driverClassName" value="${jdbc.driverClassName}" /> <property name="url" value="${jdbc.url}" /> <property name="username" value="${jdbc.username}" /> <property name="password" value="${jdbc.password}" /> </bean> <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean>
使用JTA資料來源的步驟:
<?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:jee="http://www.springframework.org/schema/jee" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee.xsd"> <jee:jndi-lookup id="dataSource" jndi-name="jdbc/jpetstore"/> <bean id="txManager" class="org.springframework.transaction.jta.JtaTransactionManager" /> </beans>
使用hibernate資料來源
<bean id="sessionFactory" class="org.springframework.orm.hibernate5.LocalSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="mappingResources"> <list> <value>org/springframework/samples/petclinic/hibernate/petclinic.hbm.xml</value> </list> </property> <property name="hibernateProperties"> <value> hibernate.dialect=${hibernate.dialect} </value> </property> </bean> <bean id="txManager" class="org.springframework.orm.hibernate5.HibernateTransactionManager"> <property name="sessionFactory" ref="sessionFactory"/> </bean>
1.4 使用spring事務管理器
宣告式事務需要與AOP相結合才可以使用,AOP會給要進行事務管理的方法生成代理,在TransactionInterceptor的invoke方法中,選擇具體的PlatformTransactionManager 。可以在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: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.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <aop:config> <aop:pointcut id="fooServiceOperation" expression="execution(* x.y.service.*.*(..))"/> <aop:advisor advice-ref="txAdvice" pointcut-ref="fooServiceOperation"/> </aop:config> <tx:advice id="txAdvice" transaction-manager="txManager"> <tx:attributes> <!-- all methods starting with 'get' are read-only --> <tx:method name="get*" read-only="true"/> <!-- other methods use the default transaction settings --> <tx:method name="*"/> </tx:attributes> </tx:advice> </beans>
XML配置的一般是業務方法的預設事務行為。通常對於特殊方法,會採用@Transactional註解來配置事務的屬性,需要使用tx:annotation-driven來啟用事務註解支援。
<?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: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.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <!-- enable the configuration of transactional behavior based on annotations --> <tx:annotation-driven transaction-manager="txManager"/><!-- a PlatformTransactionManager is still required --> <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean> </beans>
屬性名 | 預設值 | 解析 |
---|---|---|
transaction-manager | transactionManager | 事務管理器的名字 |
mode | proxy | 代理模式,還可以是aspectj |
proxy-target-class | false | 僅在mode="proxy"時生效,即預設使用jdk介面代理,true的話則使用類代理 |
order | Ordered.LOWEST_PRECEDENCE | 事務委託器的優先順序 |
預設的代理模式是執行時織入,所以如果你用註解的方法不是public的,或者你不是通過代理物件而是直接方法內部呼叫,及時你用了@Transaction註解,也是不會生效的。Transaction註解有個比較重要的屬性value,當有多個事務管理器時,可以通過設定這個屬性選擇具體的事務管理器
public class TransactionalService { @Transactional("order") public void setSomething(String name) { ... } @Transactional("account") public void doSomething() { ... } } <tx:annotation-driven/> <bean id="transactionManager1" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> ... <qualifier value="order"/> </bean> <bean id="transactionManager2" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> ... <qualifier value="account"/> </bean>
1.5 手動獲取connection連線
如果某些特殊場景下,你需要用connection直接執行些sql,可以採取下面的方法。如果當前是在一個事務環境,則會返回對應的connection,否則會為你建立一個connection。針對JPA和Hibernate使用的是EntityManagerFactoryUtils和SessionFactoryUtils兩個類。
Connection conn = DataSourceUtils.getConnection(dataSource);
1.6 編碼方式使用事務管理器
如果你的應用只有幾個方法需要用到事務,為了避免使用代理帶來了效能損耗,可以採用編碼方式來管理事務
DefaultTransactionDefinition def = new DefaultTransactionDefinition(); def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); TransactionStatus status = txManager.getTransaction(def); try { // execute your business logic here } catch (MyException ex) { txManager.rollback(status); throw ex; } txManager.commit(status);