一、 事務
事務管理對於企業應用而言是非常重要的,事務的存在保證了用戶的每一次操作都是可靠的,當用戶操作出現異常時也不至於破壞了后台的數據。例如銀行的自動取款機,萬一你在轉賬的時候出現了異常,事務機制會保證你后台的數據還是出異常操作之前的數據,也就是是你出異常的這些操作失效。
事務就是一組由於邏輯上緊密關聯而合並成一個整體(工作單元)的多個數據庫操作,這些操作要么都執行,要么都不執行。
銀行轉賬操作:開啟事務,就是保證轉賬的操作要么都執行,要么都不執行。
如果在你的賬戶減去轉賬金額后出現異常,不能只是你的賬戶扣錢,對方賬戶加錢,這不就賠大了嗎;如果此處出現異常,因為該事務的操作並沒有全執行完,事務就會回退到轉賬操作前,也就是“自己賬戶減去 轉賬金額”的這個操作失效,不會執行了,這就保證了你賬戶的數據不被破壞,等系統好了你就可以再次執行轉賬操作了。
從上面的例子我們看出:事務就必須確保出故障前對賬戶的操作不生效,就像用戶剛才完全沒有使用過取款機一樣,以保證用戶和銀行的利益都不受損失。
二、事務的四個關鍵屬性(ACID)
原子性(atomicity):事務的原子性表現為一個事務所涉及到的多個操作是捆綁在一起的,要求事務中的所有操作要么都執行,要么都不執行,就像上面例子中的轉賬操作。
一致性(consistency):一個事務中不管涉及到多少個操作,都必須保證事務執行之前數據是正確的,事務執行之后數據也是正確,如果在事務執行過程中,存在操作失敗的操作則其他所有操作都必須撤銷,將數據恢復到事務執行之前的狀態,這個過程叫“回滾”。
隔離性(isolation):要求多個事務在並發執行過程中不會互相干擾。在應用程序實際執行的過程中,事務往往是兵法執行的,所以很可能許多事務會同時處理相同的數據,因此每個事務都應該與其他事務分開執行,不然同一個數據同時在多個事務中都被修改,那這個數據不就亂了嗎?
持久性(durability):持久性是事務執行完成后,他對數據的修改是永久保存的,不會因為時間長,或者出現故障而受影響。也就是說並不能你執行轉賬操作后過了一段時間后你轉出去的金額自己又回到你的賬戶,也就想想吧。
三、JDBC中的事務:
當在一個事務中執行多個操作時,要么所有的事務都被提交(commit),要么整個事務回滾(rollback)到最初狀態
在JDBC中,事務默認是自動提交的,每次執行一個 SQL 語句時,如果執行成功,就會向數據庫自動提交,而不能回滾
為了讓多個 SQL 語句作為一個事務執行:
調用 Connection 對象的 setAutoCommit(false); 以取消自動提交事務
在所有的 SQL 語句都成功執行后,調用 commit(); 方法提交事務
在出現異常時,調用 rollback(); 方法回滾事務
可以通過Connection的getAutoCommit()方法來獲得當前事務的提交方式
package cn.itcast.cd; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import org.junit.Test; import cn.itcast.cd.utils.DbUtils; public class TestTransaction { /* * 需求:張三給李四轉賬1000元 * * 轉賬成功的條件: * 1、判斷張三的賬號余額是否>=1000 * 2、從張三的賬號中轉出1000. * 3、給李四的賬號中轉入1000. */ @Test public void testTrans(){ Connection connection = DbUtils.getConnection(); try { //開啟事務,默認的情況下是true, 只要執行sql就開啟事務物,執行完sql后就提交事務connection.setAutoCommit(false);
PreparedStatement preparedStatement = null; ResultSet resultSet = null; String sql = ""; try { sql = "select * from accounts where name=?"; preparedStatement = connection.prepareStatement(sql); preparedStatement.setString(1, "張三"); resultSet = preparedStatement.executeQuery(); if (resultSet.next()){ Double sum = resultSet.getDouble("sum"); //判斷余額 if (sum < 1000){ System.out.println("余額不足,操作失敗!"); return; } //轉出 sql = "update accounts set sum=sum-? where name=?"; preparedStatement = connection.prepareStatement(sql); preparedStatement.setInt(1, 1000); preparedStatement.setString(2, "張三"); preparedStatement.executeUpdate(); //創建異常,沒有事務則轉出了1000,發生異常,轉入失敗,整個轉賬操作失敗! int a = 2/0; //轉入 sql = "update accounts set sum=sum+? where name=?"; preparedStatement = connection.prepareStatement(sql); preparedStatement.setInt(1, 1000); preparedStatement.setString(2, "李四"); preparedStatement.executeUpdate(); //提交事務,表明整個轉賬操作成功,將修改的數據更新到數據庫中, 事務就又變成了自動提交. connection.commit(); } } catch (SQLException e) { e.printStackTrace(); } finally{ DbUtils.close(connection, preparedStatement, resultSet); } } catch (Exception e) { e.printStackTrace(); //發生異常,事務回滾到原始狀態. try { connection.rollback(); } catch (SQLException e1) { e1.printStackTrace(); } } } }
四、spring事務
主要有兩種方式實現,一種是在代碼中編寫(編程式事務管理),一種是聲明式事務(又可分為AOP式事務管理和注解式事務管理)。在代碼中編寫要更加細粒度,而很多時候我們只需要簡單的事務處理,那就可以用聲明式事務。
從圖中可以看出,spring並不直接管理事務,而是提供了一個事務接口,實現事務的具體方法是由底層自己本身決定,如果管理JDBC DataSource事務就使用DataSourceTransactionManager事務管理器,如果管理Hibernate事務,則使用HibernateTransactionManager事務管理器;也就是說Spring管理機制是一種策略模式,他只提供接口,用哪種具體的事務管理器由持久層的實現框架決定。
五、編程式事務 :
編程式事務就是用編碼的方式實現事務,比較類似於JDBC編程實現事務。
下面實例使用DataSourceTransactionManager來管理JDBC事務。
spring實現編程式事務的方式方式中提供了一種模板類,能夠幫助我們實現DataSourceTransactionManager管理事務,這中模板類似於JDBCTemplate模板,將數據操作封裝在模板類中。
實例:實現賬號
1)創建數據庫
2)配置好spring環境
<!-- 引入外部屬性文件 --> <context:property-placeholder location="classpath:jdbc.properties"/> <!-- 配置C3p0 --> <bean id="comboPooledDataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"> <property name="user" value="${jdbc.username}"></property> <property name="password" value="${jdbc.password}"></property> <property name="driverClass" value="${jdbc.driver}"></property> <property name="jdbcUrl" value="${jdbc.url}"></property> </bean> <!-- 配置業務層類 --> <bean id="accountService" class="com.neuedu.spring.demo1.AccountService"> <property name="accountDao" ref="accountDao"></property> <!-- 注入事務管理的模板類 --> <property name="atTemplate" ref="transactionTemplate"></property> </bean> <!-- 配置DAO類 --> <bean id="accountDao" class="com.neuedu.spring.demo1.AccountDaoImpl"> <!-- 注入連接池 --> <property name="dataSource" ref="comboPooledDataSource"></property> </bean> <!-- 配置事務管理器 --> <bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="comboPooledDataSource"></property> </bean> <!-- 配置事務管理的模板:spring為了簡化事務管理代碼提供的模板 --> <bean id="transactionTemplate" class="org.springframework.transaction.support.TransactionTemplate"> <property name="transactionManager" ref="dataSourceTransactionManager"></property> </bean>
3)DAO層實現類
package com.neuedu.spring.demo1; import org.springframework.jdbc.core.support.JdbcDaoSupport; /* * 項目名稱:spring-BankAccount * @author:wzc * @date 創建時間:2017年9月12日 下午5:48:06 * @Description:實現轉賬DAO * @parameter * */ public class AccountDaoImpl extends JdbcDaoSupport implements AccountDao { /*Method:outMoney *Description:實現從數據庫中將賬戶轉賬金額扣除 *param:out,轉賬賬戶 *param:money,轉賬金額 */ @Override public void outMoney(String out, Double money) { String sql="update t_account set salary=salary-? where name=?"; this.getJdbcTemplate().update(sql, money,out); } /*Method:inMoney *Description:實現從數據庫中向賬戶轉入轉賬金額 *param:in,收款賬戶 *param:money,轉賬金額 */ @Override public void inMoney(String in, Double money) { String sql="update t_account set salary=salary+? where name=?"; this.getJdbcTemplate().update(sql, money,in); } }
4)業務層實現類
package com.neuedu.spring.demo1; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.TransactionCallbackWithoutResult; import org.springframework.transaction.support.TransactionTemplate; /* * 項目名稱:spring-BankAccount * @author:wzc * @date 創建時間:2017年9月12日 下午5:39:04 * @Description:轉賬業務層實現 * @parameter * */ public class AccountService implements AccountServiceIn{ //注入accountDao private AccountDaoImpl accountDao; public void setAccountDao(AccountDaoImpl accountDao) { this.accountDao = accountDao; } //注入事務管理的模板 private TransactionTemplate atTemplate; public void setAtTemplate(TransactionTemplate atTemplate) { this.atTemplate = atTemplate; } /*Method:transfer *Description:實現轉賬操作 *param:out ,轉出賬戶 *param:in,轉入賬戶 *param:money 轉賬金額 */ @Override public void transfer(String out, String in, Double money) { atTemplate.execute(new TransactionCallbackWithoutResult() { @Override protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) { //調用DAO層實現具體轉賬操作 accountDao.outMoney(out, 200d); //人為制造Debug //int i=10/0; accountDao.inMoney(in, 200d); } }); } }
測試:
@Test public void test() { ApplicationContext ioc=new ClassPathXmlApplicationContext("spring.xml"); AccountService aService = ioc.getBean(AccountService.class); aService.transfer("孫悟空", "豬八戒", 200d); }
結果:
現在給轉賬操作人為制作bug,在測試一下事務是否正常。
六、spring事務的概念
(一)基本原理:AOP
[1]前置通知:開啟事務
[2]返回通知:提交事務
[3]異常通知:回滾事務
[4]后置通知:釋放資源
(二)事務的傳播行為
當事務方法被另一個事務方法調用時,必須指定事務應該如何傳播。例如:方法可能繼續在現有事務中運行,也可能開啟一個新事務,並在自己的事務中運行。事務的傳播行為可以由傳播屬性指定,在TransactionDefinition定義了7種類傳播行為。
(三)事務的隔離級別
數據庫事務並發存在的問題
假設現在有兩個事務:Transaction01和Transaction02並發執行。
①臟讀
[1]Transaction01將某條記錄的AGE值從20修改為30。
[2]Transaction02讀取了Transaction01更新后的值:30。
[3]Transaction01回滾,AGE值恢復到了20。
[4]Transaction02讀取到的30就是一個無效的值。
②不可重復讀
[1]Transaction01讀取了AGE值為20。
[2]Transaction02將AGE值修改為30。
[3]Transaction01再次讀取AGE值為30,和第一次讀取不一致。
③幻讀
[1]Transaction01讀取了STUDENT表中的一部分數據。
[2]Transaction02向STUDENT表中插入了新的行。
[3]Transaction01讀取了STUDENT表時,多出了一些行。
數據庫系統必須具有隔離並發運行各個事務的能力,使它們不會相互影響,避免各種並發問題。一個事務與其他事務隔離的程度稱為隔離級別。SQL標准中規定了多種事務隔離級別,不同隔離級別對應不同的干擾程度,隔離級別越高,數據一致性就越好,但並發性越弱。
TransactionDefinition 接口中定義了五個表示隔離級別的常量,其中DEFAULT:這是默認值,表示使用底層數據庫的默認隔離級別。對大部分數據庫而言,通常這值就是READ_COMMITTED。以下是其他四種的具體情況:
①讀未提交:READ UNCOMMITTED
允許Transaction01讀取Transaction02未提交的修改。
②讀已提交:READ COMMITTED
要求Transaction01只能讀取Transaction02已提交的修改。
③可重復讀:REPEATABLE READ
確保Transaction01可以多次從一個字段中讀取到相同的值,即Transaction01執行期間禁止其它事務對這個字段進行更新。
④串行化:SERIALIZABLE
確保Transaction01可以多次從一個表中讀取到相同的行,在Transaction01執行期間,禁止其它事務對這個表進行添加、更新、刪除操作。可以避免任何並發問題,但性能十分低下
各個隔離級別解決並發問題的能力見下表
(四)事務超時
所謂事務超時,就是指一個事務所允許執行的最長時間,如果超過該時間限制但事務還沒有完成,則自動回滾事務。在 TransactionDefinition 中以 int 的值來表示超時時間,其單位是秒。
默認設置為底層事務系統的超時值,如果底層數據庫事務系統沒有設置超時值,那么就是none,沒有超時限制。
(五)事務只讀屬性
只讀事務屬性: 表示這個事務只讀取數據但不更新數據, 這樣可以幫助數據庫引擎優化事務。
七、聲明式事務:
聲明式事務是建立在AOP之上的。其本質是對方法前后進行攔截,然后在目標方法開始之前創建或者加入一個事務,在執行完目標方法之后根據執行情況提交或者回滾事務。聲明式事務最大的優點就是不需要通過編程的方式管理事務,這樣就不需要在業務邏輯代碼中摻雜事務管理的代碼,只需在配置文件中做相關的事務規則聲明(或通過基於@Transactional注解的方式),便可以將事務規則應用到業務邏輯中。
方法一:基於注解的聲明式事務(最簡單)
1)數據庫扔用上面的數據庫;
2)配置spring
<!-- 配置業務層類 --> <bean id="accountService" class="com.neuedu.spring.demo2.AccountService"> <property name="accountDao" ref="accountDao"></property> </bean> <!-- 配置DAO類 --> <bean id="accountDao" class="com.neuedu.spring.demo2.AccountDaoImpl"> <!-- 注入連接池 --> <property name="dataSource" ref="comboPooledDataSource"></property> </bean><!-- 配置事務管理器 --> <bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="comboPooledDataSource"></property> </bean> <!-- 開啟基於注解的事務管理 --> <tx:annotation-driven transaction-manager="dataSourceTransactionManager"/>
3)DAO層不變
package com.neuedu.spring.demo2; import org.springframework.jdbc.core.support.JdbcDaoSupport; /* * 項目名稱:spring-BankAccount * @author:wzc * @date 創建時間:2017年9月12日 下午5:48:06 * @Description:實現轉賬DAO * @parameter * */ public class AccountDaoImpl extends JdbcDaoSupport implements AccountDao { /*Method:outMoney *Description:實現從數據庫中將賬戶轉賬金額扣除 *param:out,轉賬賬戶 *param:money,轉賬金額 */ @Override public void outMoney(String out, Double money) { String sql="update t_account set salary=salary-? where name=?"; this.getJdbcTemplate().update(sql, money,out); } /*Method:inMoney *Description:實現從數據庫中向賬戶轉入轉賬金額 *param:in,收款賬戶 *param:money,轉賬金額 */ @Override public void inMoney(String in, Double money) { String sql="update t_account set salary=salary+? where name=?"; this.getJdbcTemplate().update(sql, money,in); } }
4)在業務層service類上加入注解
package com.neuedu.spring.demo2; import org.springframework.transaction.annotation.Transactional; /* * 項目名稱:spring-BankAccount * @author:wzc * @date 創建時間:2017年9月12日 下午5:39:04 * @Description:轉賬業務層實現 * @parameter * */ @Transactional() public class AccountService implements AccountServiceIn{ //注入accountDao private AccountDaoImpl accountDao; public void setAccountDao(AccountDaoImpl accountDao) { this.accountDao = accountDao; } /*Method:transfer *Description:實現轉賬操作 *param:out ,轉出賬戶 *param:in,轉入賬戶 *param:money 轉賬金額 */ @Override public void transfer(String out, String in, Double money) { //調用DAO層實現具體轉賬操作 accountDao.outMoney(out, 200d); //人為制造Debug //int i=10/0; accountDao.inMoney(in, 200d); } }
因為剛才正常執行后悟空八戒分別是800,1200,接着執行異常情況,我並沒有恢復數據庫數據,執行結果是800,1200,說明因為異常沒有再轉賬。
方法二:基於AspectJ的XML聲明式事務:
基於AspectJ的XML聲明式事務,實際就是AOP的XML實現方式。
spring配置:
<!-- 引入外部屬性文件 --> <context:property-placeholder location="classpath:jdbc.properties"/> <!-- 配置C3p0 --> <bean id="comboPooledDataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"> <property name="user" value="${jdbc.username}"></property> <property name="password" value="${jdbc.password}"></property> <property name="driverClass" value="${jdbc.driver}"></property> <property name="jdbcUrl" value="${jdbc.url}"></property> </bean> <!-- 配置業務層類 --> <bean id="accountService" class="com.neuedu.spring.demo3.AccountService"> <property name="accountDao" ref="accountDao"></property> </bean> <!-- 配置DAO類 --> <bean id="accountDao" class="com.neuedu.spring.demo3.AccountDaoImpl"> <!-- 注入連接池 --> <property name="dataSource" ref="comboPooledDataSource"></property> </bean><!-- 配置事務管理器 --> <bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="comboPooledDataSource"></property> </bean> <!-- 配置事務的通知,因為事務是建立在AOP上的 --> <tx:advice id="txAdivice" transaction-manager="dataSourceTransactionManager"> <tx:attributes> <!-- 配置事務管理的方法 --> <tx:method name="transfer" propagation="REQUIRED"/> </tx:attributes> </tx:advice> <!-- 配置切面 --> <aop:config> <!-- 配置切入點表達式 --> <aop:pointcut expression="execution(* com.neuedu.spring.demo3.AccountService+.*(..))" id="pointcut"/> <!-- 配置切面 --> <aop:advisor advice-ref="txAdivice" pointcut-ref="pointcut"/> </aop:config>
然后DAO,Service都是業務正常的代碼,里面沒有關於事務的任何代碼,顯然聲明式事務管理要優於編程式事務管理,這正是spring倡導的非侵入式的開發方式,使業務代碼不受污。
dao
package com.neuedu.spring.demo3; import org.springframework.jdbc.core.support.JdbcDaoSupport; /* * 項目名稱:spring-BankAccount * @author:wzc * @date 創建時間:2017年9月12日 下午5:48:06 * @Description:實現轉賬DAO * @parameter * */ public class AccountDaoImpl extends JdbcDaoSupport implements AccountDao { /*Method:outMoney *Description:實現從數據庫中將賬戶轉賬金額扣除 *param:out,轉賬賬戶 *param:money,轉賬金額 */ @Override public void outMoney(String out, Double money) { String sql="update t_account set salary=salary-? where name=?"; this.getJdbcTemplate().update(sql, money,out); } /*Method:inMoney *Description:實現從數據庫中向賬戶轉入轉賬金額 *param:in,收款賬戶 *param:money,轉賬金額 */ @Override public void inMoney(String in, Double money) { String sql="update t_account set salary=salary+? where name=?"; this.getJdbcTemplate().update(sql, money,in); } }
service
package com.neuedu.spring.demo3; import org.springframework.transaction.annotation.Transactional; /* * 項目名稱:spring-BankAccount * @author:wzc * @date 創建時間:2017年9月12日 下午5:39:04 * @Description:轉賬業務層實現 * @parameter * */ public class AccountService implements AccountServiceIn{ //注入accountDao private AccountDaoImpl accountDao; public void setAccountDao(AccountDaoImpl accountDao) { this.accountDao = accountDao; } /*Method:transfer *Description:實現轉賬操作 *param:out ,轉出賬戶 *param:in,轉入賬戶 *param:money 轉賬金額 */ @Override public void transfer(String out, String in, Double money) { //調用DAO層實現具體轉賬操作 accountDao.outMoney(out, 200d); //人為制造Debug //int i=10/0; accountDao.inMoney(in, 200d); } }
而此時如何配置事務屬性呢?在配置事務管理方法的時候可以添加事務的屬性。