1、開篇陳述mysql
1.1 寫文原因 spring
最近在系統學習spring框架IoC、AOP、Transaction相關的知識點,準備寫三篇隨筆記錄學習過程當中的感悟。這是第一篇,記錄spring Transaction的使用及部分原理。spring的學習過程應該是從IoC到AOP再到Transaction,這裏寫隨筆的順序沒有按照學習路線。sql
1.2 預備技能數據庫
學習spring事務使用最好具有基礎的Java知識,掌握spring IoC和spring AOP,並對各種數據庫事務的概念有所瞭解。固然,這些都不是必須的,若是你相信你的理解力。筆者在學習spring 事務以前有不少疑問:如爲何數據庫在被事務中的操做改變以後,事務還能夠進行回滾,數據庫能夠像沒有操做過同樣?事務進行過程當中,其它讀寫數據庫的操做看到怎樣的結果,會不會看到事務中非完整的操做結果?帶着這些問題,開始學習spring事務的使用吧。express
2、基本概念和主要接口編程
2.1 基本概念框架
爲何須要事務:應用中須要保證用戶的操做的可靠性和完整性,有些操做必須做爲一組原子操做(如轉帳、下單減庫存等)提交到數據庫,若是其中的一個操做失敗,其它操做也不該該生效,這就是數據庫事務的概念(下單過程當中減了庫存,隨後的下單記錄添加失敗,那麼庫存就不該該減,因此應該將這個步驟做爲一個事務提交到數據庫,以保證數據完整性)。ide
事務的一些屬性:事務有一些屬性來描述,其中最重要的有事務的隔離級別、事務的傳播屬性、事務的超時時間和事務的只讀屬性。學習
隔離級別:隔離級別是指多個事務同時執行時,各個事務之間的影響相互隔離的程度。主要有以下幾個級別:測試
ISOLATION_DEFAULT(底層數據庫默認隔離級別,一般爲ISOLATION_READ_COMMITTED,這個級別的事務只能看到其它事務已經提交的修改,沒有提交的修改都不能被看到,如減庫存操做操做完成,下單還沒完成,整個事務沒有提交,那麼這種隔離級別的其它事務是無法看到減庫存成功的操做結果的,只有整個事務提交以後才能看到,這也是咱們經常使用的默認隔離級別)
ISOLATION_READ_UNCOMMITTED(能夠讀取另外一個事務修改但尚未提交的數據,會致使髒讀和不可重複讀,不多使用;好比減庫存操做完成,其它事務就能看到庫存被減了,若是這時候庫存正好被減爲0,其它用戶可能就下單失敗,可是若是這個事務最後失敗了,庫存被回滾,又有可能被其它用戶購買)
ISOLATION_READ_COMMITTED(只能讀取已提交的數據,能夠防止髒讀,仍是存在不可重複讀)
ISOLATION_REPEATABLE_READ(能夠屢次重複執行某個查詢,而且每次返回的記錄都相同。有新增數據知足查詢也會被忽略,防止髒讀和不可重複讀。當庫存減小時,已經在執行的其它事務看不到這個減小嗎?這個太奇怪了。暫時還沒搞清楚實現原理)
ISOLATION_SERIALIZABLE(事務依次逐個執行)
讀一致性:上面的隔離級別中咱們關心的都是讀數據的返回,由於寫確定是要互斥且順序執行的,寫不存在並行。下面研究一下讀數據的一致性。
髒讀(一個事務訪問並修改了數據,修改還沒提交,另外一個事務也訪問並使用這個數據;庫存被減了,可是還沒查下單記錄,事務未提交,另外一個事務讀庫存,發現庫存爲0,因而下單失敗,這個時候若是事務回滾,其實庫存仍是有的,讀到了髒數據)
不可重複讀(一個事務內屢次讀同一數據,在這之間,另外一個事務修改了數據,致使一個事務內兩次讀到的數據是不同的;兩次讀庫存,中間庫存被修改)
幻讀(事務不是獨立執行,在第一個事務對錶數據進行修改後,第二個事務也修改了表數據,而後第一個事務發現表中的數據跟預想的不一致;如第一個事務減庫存失敗,第二個事務增長了庫存,第一個事務發現莫名其妙的庫存變化,要防止幻讀只能用串行執行的隔離級別)
傳播屬性:當事務開始時,一個事務上下文已經存在,此時能夠指定一個事務性方法的執行行爲。
PROPAGATION_REQUIRED(有則加入,無則新建)
PROPAGATION_REQUIRES_NEW(新建事務,掛起以前的事務)
PROPAGATION_SUPPORTS(有則加入,沒有則以非事務方式運行)
PROPAGATION_NOT_SUPPORTED(有則掛起當前事務)
PROPAGATION_NEVER(有則拋異常)
PROPAGATION_MANDATORY(有則加入,沒有則拋異常)
PROPAGATION_NESTED(有則以嵌套事務的方式執行,外部事務提交纔會觸發內部事務提交,外部事務回滾會觸發內部事務回滾)
2.2 主要接口
事務最主要的API:
TransactionDefinition(事務規則:設置事務的一些屬性,上文提到的,使用DefaultTransactionDefinition默認實現通常能夠知足要求,或者能夠擴展接口,實現本身的定義)
PlatformTransactionManager(事務管理:spring沒有直接管理事務,而是將事務管理的責任委託給JTA或持久化機制的某個特定平臺的事務實現。spring的事務管理器充當了特定平臺事務的代理,以下圖所示)
TransactionStatus(事務狀態:表明一個新的或已經存在的事務,控制事務執行和查詢事務狀態)
3、編程式事務和聲明式事務
所謂編程式事務就是在業務代碼中顯示編寫事務邏輯,而聲明式事務則是在配置文件中聲明事務,基本不在代碼中影響bean的工做方式。
3.1 編程式事務
1)基於底層API的編程式事務管理
這種方式直接使用PlatformTransactionManager、TransactionDefinition、TransactionStatus三個核心接口編程實現事務。示例代碼如清單一、2所示:
清單1:業務邏輯
1 @Service("testDao") 2 3 public class TestDaoImpl implements TestDao { 4 5 6 7 @Resource(name="dataSource") 8 9 private DataSource dataSource; 10 11 12 13 @Resource(name="txDefinition") 14 15 private TransactionDefinition txDefinition; 16 17 18 19 @Resource(name="txManager") 20 21 private PlatformTransactionManager txManager; 22 23 24 25 public void insert(String key, Object value) { 26 27 TransactionStatus txStatus = txManager.getTransaction(txDefinition); 28 29 System.out.println("trans status: " + txStatus.isNewTransaction() + txStatus.isRollbackOnly() + txStatus.isCompleted()); 30 31 try { 32 33 JdbcTemplate jt = new JdbcTemplate(dataSource); 34 35 int ret1 = jt.update("insert into kv (k, v)" 36 37 + " values('" + key + "', '" + value + "')"); 38 39 System.out.println("insert first time. ret1 = " + ret1); 40 41 int ret2 = jt.update("insert into kv (k, v)" 42 43 + " values('" + key + "', '" + value + "')"); 44 45 System.out.println("insert second time.ret2 = " + ret2); 46 47 48 49 txManager.commit(txStatus); 50 51 System.out.println("is completed: " + txStatus.isCompleted()); 52 53 } catch (Exception e) { 54 55 txManager.rollback(txStatus); 56 57 System.out.println("is rollback: " + txStatus.isRollbackOnly()); 58 59 System.out.println("caught exception: --------"); 60 61 e.printStackTrace(); 62 63 64 65 } 66 67 } 68 69 }
清單2:主程序
1 public class App 2 3 { 4 5 private static ApplicationContext context = new ClassPathXmlApplicationContext("spring/spring.xml"); 6 7 8 9 public static void main( String[] args ) 10 11 { 12 13 TestDao testDao = (TestDao)context.getBean("testDao"); 14 15 try { 16 17 testDao.insert("i am JUNE", "June is excellent!"); 18 19 } catch (Exception e) { 20 21 System.out.println("out side caughter -----"); 22 23 e.printStackTrace(); 24 25 26 27 } 28 29 System.out.println( "Hello World!" ); 30 31 } 32 33 }
清單3:配置文件
1 <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> 2 <property name="driverClassName" value="com.mysql.jdbc.Driver" /> 3 <property name="url" value="jdbc:mysql://10.13.49.201:3306/database" /> 4 <property name="username" value="dmp" /> 5 <property name="password" value="test" /> 6 </bean> 7 <bean id="txDefinition" class="org.springframework.transaction.support.DefaultTransactionDefinition"> 8 <property name="propagationBehaviorName" value="PROPAGATION_REQUIRED"></property> 9 <property name="timeout" value="10000"></property> 10 </bean> 11 <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> 12 <property name="dataSource" ref="dataSource" /> 13 </bean>
如上清單1所示,示例中配置了三個bean(數據源dataSource、事務定義txDefinition以及事務管理器txManager),編程式事務開始於txManager.getTransaction,終止於txManager.commit(事務過程當中沒有異常,事務被提交)或txManager.rollback(事務過程當中拋出異常,事務被回滾)。示例中用於測試的事務向數據庫kv表中插入兩條相同的數據,因爲kv表中對key字段作了惟一性約束,因此在插入第二條數據的時候會拋出異常,若是沒有事務保證,數據庫會被插入一條數據,而第二條數據不能插入,示例將兩次插入放入事務中,當第二次插入時會拋出異常,事務被回滾,第一條數據也不會真正插入到數據庫。編寫這類事務須要注意在合適的地方進行事務提交或回滾。
2)基於TransactionTemplate的編程式事務管理
從上面的例子能夠看出,那種方式的事務管理存在不少樣板代碼,如事務開始、捕獲異常、事務提交和事務回滾,這些代碼嚴重破壞了業務代碼的結構,spring提供一個改進的方式編程實現事務。示例代碼如清單4所示:
清單4:基於TransactionTemplate實現的事務邏輯
1 @Service("testDaoTemplate") 2 3 public class TestDaoTemplateImpl implements TestDao{ 4 5 6 7 @Resource 8 9 private DataSource dataSource; 10 11 12 13 @Resource 14 15 private TransactionTemplate txTemplate; 16 17 18 19 public void insert(final String key, final Object value) { 20 21 txTemplate.execute(new TransactionCallback() { 22 23 24 25 public Object doInTransaction(TransactionStatus status) { 26 27 System.out.println("trans status: " 28 29 + status.isNewTransaction() 30 31 + status.isRollbackOnly() 32 33 + status.isCompleted()); 34 35 try { 36 37 JdbcTemplate jt = new JdbcTemplate(dataSource); 38 39 int ret1 = jt.update("insert into kv (k, v)" 40 41 + " values('" + key + "', '" + value + "')"); 42 43 System.out.println("insert first time. ret1 = " + ret1); 44 45 int ret2 = jt.update("insert into kv (k, v)" 46 47 + " values('" + key + "', '" + value + "')"); 48 49 System.out.println("insert second time.ret2 = " + ret2); 50 51 52 53 System.out.println("is completed: " + status.isCompleted()); 54 55 } catch (Exception e) { 56 57 status.setRollbackOnly(); 58 59 System.out.println("is rollback: " + status.isRollbackOnly()); 60 61 62 63 System.out.println("caught exception: --------"); 64 65 e.printStackTrace(); 66 67 68 69 } 70 71 72 73 return null; 74 75 } 76 77 78 79 }); 80 81 } 82 83 }
清單5:配置文件
1 <bean id="txTemplate" class="org.springframework.transaction.support.TransactionTemplate"> 2 3 <property name="transactionManager" ref="txManager"></property> 4 5 </bean>
這種方式只是將這些模板代碼封裝到TransactionTemplate中,其業務邏輯寫在一個TransactionCallback內部類中,做爲txTemplate的參數傳遞進去。默認規則是執行回調方法的過程當中拋出unchecked異常或顯示調用setRollbackOnly方法,事務將被回滾,不然(未拋異常或拋出異常被捕獲而沒有顯示調用setRollbackOnly)提交事務。這類方式比底層API的方式稍微簡便些,可是仍然破壞了業務代碼的結構。
3.2 聲明式事務
聲明式事務創建在AOP(事務管理自己就是一個典型的橫切邏輯)的基礎之上,本質是對方法進行攔截,方法開始前加入事務,方法執行完後根據狀況提交或回滾事務。聲明式事務最大的優勢就是不須要在業務邏輯中摻瑣事務管理的代碼,只在配置文件中作相關的事務規則聲明(大型項目中嚴重建議使用聲明式事務)。聲明式事務的缺點就是事務的最細粒度只能做用到方法級別。
1) 基於TransactionInterceptor類實現的聲明式事務
清單6: 基於TransactionInterceptor的事務業務邏輯
1 @Service("testDaoInterceptor") 2 3 public class TestDaoInterceptor implements TestDao{ 4 5 6 7 @Resource 8 9 private DataSource dataSource; 10 11 12 13 public void insert(String key, Object value) throws Exception { 14 15 try { 16 17 JdbcTemplate jt = new JdbcTemplate(dataSource); 18 19 int ret1 = jt.update("insert into kv (k, v)" 20 21 + " values('" + key + "', '" + value + "')"); 22 23 System.out.println("insert first time. ret1 = " + ret1); 24 25 int ret2 = jt.update("insert into kv (k, v)" 26 27 + " values('" + key + "', '" + value + "')"); 28 29 System.out.println("insert second time.ret2 = " + ret2); 30 31 32 33 } catch (Exception e) { 34 35 36 37 System.out.println("caught exception: --------"); 38 39 e.printStackTrace(); 40 41 throw e; 42 43 } 44 45 } 46 47 }
清單7:配置文件
1 <bean id="txInterceptor" class="org.springframework.transaction.interceptor.TransactionInterceptor"> 2 3 <property name="transactionManager" ref="txManager"></property> 4 5 <property name="transactionAttributes"> 6 7 <props> 8 9 <prop key="insert">PROPAGATION_REQUIRED</prop> 10 11 </props> 12 13 </property> 14 15 </bean> 16 17 <bean id="testDaoInterceptorBean" class="org.springframework.aop.framework.ProxyFactoryBean"> 18 19 <property name="target" ref="testDaoInterceptor"/> 20 21 <property name="interceptorNames"> 22 23 <list> 24 25 <idref bean="txInterceptor"/> 26 27 </list> 28 29 </property> 30 31 </bean>
使用這種方式配置聲明式事務,須要先配置一個TransactionInterceptor來定義相關的事務規則。它主要包括了兩個屬性,一個是transactionManager,指定一個事務管理器,transactionInterceptor將攔截到的事務相關操做委託給它。另外一個是Properties類型的transactionAttributes屬性,主要用來定義事務規則, 這個屬性的具體配置不在這裏詳述。前面已經說過這種配置事務的方式是基於spring AOP的,從TransactionInterceptor的類繼承關係中能夠看到,這個類是繼承自Advice接口的,熟悉spring AOP的同窗應該知道AOP除了須要配置Advice以外,還須要一個ProxyFactoryBean來組裝target 和 advice,經過spring工廠獲取proxyFactoryBean實例時,其實返回的是proxyFactoryBean實例getObject返回的對象,也就是織入了事務管理邏輯後的目標類的代理類實例。這種方式的事務實現沒有對業務代碼進行任何操做,全部設置均在配置文件中完成。可是這種方式也存在一個煩人的問題:配置文件太長。須要爲每一個目標對象配置一個proxyFactoryBean和一個transactionInterceptor,至關於每一個業務類須要配置3個bean,隨着業務類增多,配置文件會愈來愈龐大,管理變得複雜。
2) 基於TransactionProxyFactoryBean的聲明式事務管理
爲了緩解ProxyFactoryBean實現聲明式事務配置繁雜的問題,spring提供了TransactionProxyFactoryBean,將ProxyFactoryBean和TransactionInterceptor的配置打包成一個,其它的東西根本沒有改變。示例配置以下:
清單8:基於TransactionProxyFactoryBean的配置文件
1 <bean id="testDaoTxBean" class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean"> 2 3 <property name="target" ref="testDaoInterceptor"/> 4 5 <property name="transactionManager" ref="txManager"></property> 6 7 <property name="transactionAttributes"> 8 9 <props> 10 11 <prop key="*">PROPAGATION_REQUIRED</prop> 12 13 </props> 14 15 </property> 16 17 </bean>
這種配置被稱爲spring經典的聲明式事務管理,雖然這種方式比起使用ProxyFactoryBean並無什麼大的改進,其實這兩種方式對於配置聲明式事務已經足夠簡單。
3) 基於<tx>命名空間的聲明式事務管理
前面的兩種方式已經很好的使用了AOP來實現事務管理,此外,spring還提供了一種引入<tx>命名空間,結合使用<aop>命名空間,帶給開發人員配置聲明式事務的全新體驗(這個太扯蛋了,沒什麼意思嘛)。
清單9:基於<tx>命名空間的事務管理配置文件
1 <tx:advice id="daoAdvice" transaction-manager="txManager"> 2 3 <tx:attributes> 4 5 <tx:method name="insert" propagation="REQUIRED"></tx:method> 6 7 </tx:attributes> 8 9 </tx:advice> 10 11 <aop:config> 12 13 <aop:pointcut expression="execution(* *.insert(..))" id="daoPointcut"/> 14 15 <aop:advisor advice-ref="daoAdvice" pointcut-ref="daoPointcut"/> 16 17 </aop:config>
這種方式有點好處,就是不須要指定具體的被代理的業務類,這樣就只須要合理的配置切點的表達式,而後只要知足條件的業務類都將被代理。
4) 基於@Transactional註解的聲明式事務管理
Spring中最簡便的配置方式固然要屬註解方式,聲明式事務管理也不例外,spring使用@Transactional註解做用在接口、接口方法、類和類方法上,通常使用@Transactional在業務類public方法上,這種方式簡單明瞭,沒有學習成本。
總的來講,這四種聲明式事務管理只是使用形式不一樣,其後臺的實現方式是相同的。
4、結篇總結
4.1 遇到問題
使用ProxyFactoryBean和TransactionProxyFactoryBean實現聲明式事務管理類時,發現事務不生效。屢次試驗後發現示例中的業務邏輯代碼把異常都捕獲並處理,相對於事務來講,業務沒有拋出異常,因此事務會被提交而插入了一條數據。使用這種方式處理事務時切記要將異常捕獲放開,讓事務檢測到異常並針對性的處理事務。
4.2 知識總結
1)編程式事務使用的三個接口。
2)兩種編程式事務實現和四種聲明式事務實現。