Spring-事務管理(Transaction)

1.事務介紹java

事務(Transaction):訪問並能更新數據庫中數據項的一個程序執行單元。mysql

事務是一系列的動做,它們綜合在一塊兒纔是一個完整的工做單元,這些動做必需要麼所有完成,要麼什麼都不作,若是有一個失敗了的話,那麼事務就會回滾(RollBack)到最開始的狀態,在企業級的應用程序中,事務管理是必不可少的,用來確保數據的完整性和一致性。spring

事務的特性(ACID)sql

  1. 原子性(Atomicity):事務是一個原子操做,事務的原子性保證這些動做要麼所有都作,要麼什麼都不作。
  2. 一致性(Consistency):一旦事務完成(不管是成功仍是失敗),事務必須始終保持系統處於一致的狀態,無論在任何給定的時間併發事務由多少。也便是說,若是事務是併發多個,系統也必須入串行事務同樣操做,其主要特徵是保護性和不可變性,以轉帳案例爲例子,假設有5個帳戶,每一個帳戶餘額是100,那麼五個帳戶的總額是500,無論併發是多少,五個帳戶之間怎麼頻繁進行轉帳,總額都會保證是500,這就是保護性和一致性。
  3. 隔離性(Isolation):可能有許多事務會同時處理相同的數據,所以每一個事務都應該與其餘事務隔離開來,防止數據損壞。
  4. 持久性(Durability):事務一旦完成,事務的結果必須被持久化到存儲器中。

舉個例子,也是後面實現過程當中會一直用的例子:
1.Tom和Marry在某銀行都有帳戶,且他們每一個人的帳戶中都有1000元數據庫

2.Tom要向Marry匯款100元apache

那麼這個匯款的執行能夠分紅下面幾步:編程

1.檢測Tom的帳戶裏面有沒有100塊錢,若是有則容許匯款,若是沒有則不容許匯款後端

2.Tom的銀行帳戶,帳戶餘額減去100元服務器

3.Marry的銀行帳戶,帳戶餘額加上100元併發

那麼問題來了,在2步和3步之間,可能會產生故障或者異常,最low的例子是2步完成以後,銀行的服務器斷電了。那麼這個時候會不會出現Tom的銀行帳戶少了100元,而Marry的銀行帳戶卻沒有增長100元呢?

這個問題能夠先說下:若是沒有使用事務管理,這種狀況確實是存在的。

那麼如何模擬服務器斷電這種異常呢:那就是在2步和3步之間加入一個異常(除零異常就能夠)

 

Spring的事務管理的核心接口:

接着打開spring-tx包,能夠繼續查看org-springframework.transaction包

TransactionDefinition    PlatformTransactionManager   TransactionStatus是Spring事務管理的三個頂級接口,下面咱們用圖來解釋下這三個接口之間的關係:

PlatformTransaction事務管理器

Spring事務管理器的接口是org.springframework.transaction.PlatformTransactionManager,如上圖所示Spring並非直接管理事務,經過PlatformTransaction這個接口,Spring爲各個平臺如JDBC,Hibernate等提供對應的事務管理。也就是將事務管理的職責委託給Hibernate或者JTA等持久化機制所提供的相關平臺框架的事務實現。

進入到PlatformTransactionManager接口,查看源碼:

  1. TransactionStatus getTransaction(TransactionDefinition definition) ,事務管理器經過TransactionDefinition,得到事務狀態,從而管理事務
  2. void commit( TransactionStatus status)根據狀態提交事務
  3. void rollback( TransactionStatus status)根據狀態回滾事務

也就是說Spring事務管理是爲不一樣的事務API提供統一的編程模型,具體的事務管理機制由對應的各個平臺去實現:

有jdbc,orm平臺:

 

TransactionDefinition基本事務屬性定義

事務屬性能夠理解成事務的一些基本配置,描述了事務策略如何應用到方法上

事務屬性:傳播行爲隔離規則,是否只讀,事務超時,回滾規則。

TransactionDefinition接口定義的方法以下:

事務的傳播行爲和隔離級別的具體定義:

// Compiled from TransactionDefinition.java (version 1.6 : 50.0, no super bit)
public abstract interface org.springframework.transaction.TransactionDefinition {
 
  // Field descriptor #4 I
  public static final int PROPAGATION_REQUIRED = 0;
 
  // Field descriptor #4 I
  public static final int PROPAGATION_SUPPORTS = 1;
 
  // Field descriptor #4 I
  public static final int PROPAGATION_MANDATORY = 2;
 
  // Field descriptor #4 I
  public static final int PROPAGATION_REQUIRES_NEW = 3;
 
  // Field descriptor #4 I
  public static final int PROPAGATION_NOT_SUPPORTED = 4;
 
  // Field descriptor #4 I
  public static final int PROPAGATION_NEVER = 5;
 
  // Field descriptor #4 I
  public static final int PROPAGATION_NESTED = 6;
 
  // Field descriptor #4 I
  public static final int ISOLATION_DEFAULT = -1;
 
  // Field descriptor #4 I
  public static final int ISOLATION_READ_UNCOMMITTED = 1;
 
  // Field descriptor #4 I
  public static final int ISOLATION_READ_COMMITTED = 2;
 
  // Field descriptor #4 I
  public static final int ISOLATION_REPEATABLE_READ = 4;
 
  // Field descriptor #4 I
  public static final int ISOLATION_SERIALIZABLE = 8;

具體解釋下傳播行爲:傳播行爲指的是當事務被另外一個事務調用時,必須指定事務如何傳播的。可能這麼說仍是有點不清晰,那麼能夠看具體定義的幾種傳播行爲的具體含義

  1. PROPAGATION_REQUIRED:required,必須,默認值,A若是有事務,B將使用該事務;若是A沒有事務,B將建立一個新事務。
  2. PROPAGATION_SUPPORTS:supports,支持,A若是有事務,B將使用該事務;若是A沒有事務,B將以非事務執行。
  3. PROPAGATION_MANDATORY:mandatory,強制,A若是有事務,B使用該事物;若是A沒有事務,B將拋出異常。
  4. PROPAGATION_REQUIRES_NEW:requires_new,必須新的,A若是有事務,將A的事務掛起,B建立一個新的事務;若是A沒有事務,B建立一個新的事務。
  5. PROPAGATION_NOT_SUPPORTED:not_supported,不支持,A若是有實物,將A的事務掛起,B以非事務執行;若是A沒有事務,B以非事務執行。
  6. PROPAGATION_NEVER:never,從不,若是A有事務,B將拋出異常,若是A沒有事務,B將以非事務執行。
  7. PROPAGATION_NESTED:nested,嵌套,A和B地產採用保存點機制,造成嵌套事務。

隔離級別:定義了一個事務可能受其餘併發事務影響的程度

併發事務引發的問題:

  1. 髒讀(dirty reads):髒讀數據發生在一個事務A讀取了另外一個事務B改寫可是還未提交的數據時,若是事務B稍後將事務回滾了,那麼A讀取到的數據就是無效的,也便是事務A讀取到了髒的數據。讀髒數據
  2. 不可重複讀(Nonrepeatable read):不可重複讀發生在一個事務A執行相同的查詢兩次或者兩次以上,可是會獲得不一樣的數據,這一般是由於在事務A讀取數據的同時,另外一個併發事務B在事務A的兩次查詢之間修改了數據。
  3. 幻讀(Phantom read):幻讀和不可重複讀相似,它發生在一個事務A,讀取了幾行數據,接着另外一個事務B插入了一些數據,在隨後的查詢中,第一個事務A就會看到一些本來不在的記錄。

不可重複的重點是「修改」,而幻讀的重點是「新增」

在Spring的事務管理中,定義了以下隔離級別:

  1. ISOLATION_DEFAULT:使用後端數據庫默認的隔離級別。
  2. ISOLATION_READ_UNCOMMITTED:最低隔離級別,容許讀取修改但還沒有提交的數據,髒讀,不可重複讀,幻讀都有可能發生。
  3. ISOLATION_REPEATABLE_READ:對同一個字段的屢次讀取結果都是一致的,除非是事務本身修改了數據,能夠阻止髒讀和不可重複讀,可是幻讀依然可能存在。
  4. ISOLATION_READ_COMMITTED:容許事務讀取修改且提交了的數據,能夠防止髒讀,但不可重複讀和幻讀依然可能存在。
  5. ISOLATION_SERIALIZABLE:最高隔離級別,髒讀,不可重複讀,幻讀均可以免,一般是徹底鎖定事務相關的數據表來實現,也是最慢的一種事務隔離級別。

只讀

這是事務的第三個特性,是否將事務設置爲只讀。數據庫能夠利用事務的只讀屬性來進行一些優化。經過將事務設置成只讀,你就能夠給數據庫一個機會,讓它應用它認爲合適的優化措施。

 

事務超時

爲了使應用更好的運行,事務不能運行太長時間,由於事務可能涉及對後端數據庫的鎖定,因此很長時間的事務會沒必要要的佔用數據庫資源,事務超時就是事務的一個定時器,在特定的時間內若是事務沒有執行完畢,那麼就會自動回滾,而不是一直等待事務結束。

回滾規則

回滾規則定義了哪些異常會致使事務回滾而哪些異常不會致使回滾。默認狀況下,事務遇到運行期異常時纔會回滾,在遇到檢查型異常時不會回滾。可是這些都是能夠設置的。好比你能夠設置檢查型異常也進行事務回滾。

 

Spring編程式事務和聲明式事務

 編程式事務:所謂編程式事務就是指經過編碼方式實現事務,容許用戶在代碼中精肯定義事務的邊界。即相似JDBC編程實現事務管理。管理使用TransactionTemplate或者直接使用底層的PlatformTransactionManager。對於編程式事務,推薦使用TransactionTemplate。

聲明式事務管理:管理創建在AOP上,其本質是對方法先後進行攔截,而後再目標方法開始以前建立或加入一個事務,在執行完目標方法以後根據執行狀況提交或回滾事務。聲明式事務最大的優勢就是不須要經過編程的凡是管理事務,這樣就不須要在業務邏輯代碼中摻瑣事務管理的代碼,只須要在配置文件中作相關的事務規則聲明(或者基於註解@Transaction的形式)。

 

實驗

不用事務實現轉帳

數據庫中有以下表:

有兩個帳戶Tom和Marry,他們的初始帳戶餘額都爲10000.這時候咱們進行以下業務:Tom向Marry轉帳100塊,那麼這在程序中能夠分解爲兩個步驟:
①、Tom的帳戶餘額1000減小100塊,剩餘900塊
②、Marry的帳戶餘額1000增長100塊,變爲1100塊
上面兩個步驟要麼都執行成功,要麼都不執行。咱們經過TransactionTemplate編程式事務來控制:
 
編程式實現事務:
第一步:編寫Dao層 AccountDao:
package com.fpc.Dao;
public interface AccountDao {
/*
* 匯款
* @param outer 匯款人
* @param money 匯款金額
* */
   public void out( String outer , int money );
  
  
   /*
    * 收款
    * @param inner 收款人
    * @param money 收款金額
    * */
   public void in( String inner , int money );
}

 

第二步:編寫Dao層接口的實現:AccountDaoImpl:

package com.fpc.DaoImpl;
import org.aspectj.weaver.patterns.ThisOrTargetAnnotationPointcut;
import org.springframework.jdbc.core.support.JdbcDaoSupport;
import com.fpc.Dao.AccountDao;
public class AccountDaoImpl extends JdbcDaoSupport implements AccountDao {
@Override
public void out(String outer, int money) {
// TODO 自動生成的方法存根
this.getJdbcTemplate().update("update account set money = money - ? where username = ?",money,outer);
}
@Override
public void in(String inner, int money) {
// TODO 自動生成的方法存根
this.getJdbcTemplate().update("update account set money = money + ? where username = ?",money,inner);
}
}

 

第三步:實現Service層 IAccountService

package com.fpc.Service;
public interface IAccountService {
/*
* 轉帳
* @param outer 匯款人
* @param inner 收款人
* @param money 交易金額
* */
public void transfer( String outer , String inner , int money );
}

 

第四步:Service的具體實現 AccountServiceImpl

package com.fpc.ServiceImpl;
import org.apache.shiro.authc.Account;
import com.fpc.Dao.AccountDao;
import com.fpc.Service.IAccountService;
public class AccountServiceImpl implements IAccountService{
private AccountDao accounDao;
public void setAccounDao(AccountDao accounDao) {
this.accounDao = accounDao;
}
@Override
public void transfer(String outer, String inner, int money) {
// TODO 自動生成的方法存根
accounDao.out(outer, money);
accounDao.in(inner, money);
}
}

 

第五步:配置applicationContext文件:

<bean id="accountDao" class="com.fpc.DaoImpl.AccountDaoImpl">
  <property name="dataSource" ref="dataSource"></property>
  </bean>
 
  <bean id="accountService" class="com.fpc.ServiceImpl.AccountServiceImpl">
  <property name="accounDao" ref="accountDao"></property>
  </bean>

 

第六步:編寫單元測試類

package com.fpc.UnitTest;
import org.apache.catalina.core.ApplicationContext;
import org.junit.Test;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.apple.eawt.Application;
import com.fpc.Service.IAccountService;
import com.fpc.ServiceImpl.AccountServiceImpl;
public class TransactionTest {
@Test
public void TestNoTransaction(){
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
IAccountService accountService = (IAccountService) context.getBean("accountService");
//Tom 向 Marry轉帳100元
accountService.transfer("Tom", "Marry", 100);
}
}

 

運行查看數據庫中的結果:

可見正常狀況下,轉帳是能夠成功的。

下面模擬異常狀況:

那麼這個時候咱們執行單元測試程序,很顯然會報「除零異常」

此時再去查看數據庫中該表:

咱們發現,Tom的帳戶中再次減小了100元,而Marray的帳戶中卻沒有增長100元。這在實際應用中確定是不被容許的。

那麼怎麼去解決這個問題呢?確定是引入事務管理

Dao層不變,咱們在Service層注入TransactionTemplate模板,由於是用模板來管理事務,因此模板須要注入事務管理器。DatasourceTransactionManager,而事務管理器說到底層是JDBC在管理,因此咱們須要在DatasourceTransactionManager中注入DataSource。

AccountServiceImpl:

package com.fpc.ServiceImpl;
import org.apache.shiro.authc.Account;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;
import com.fpc.Dao.AccountDao;
import com.fpc.Service.IAccountService;
public class AccountServiceImpl implements IAccountService{
private AccountDao accounDao;
private TransactionTemplate transactionTemplate;
public void setAccounDao(AccountDao accounDao) {
this.accounDao = accounDao;
}
public void setTransactionTemplate(TransactionTemplate transactionTemplate) {
this.transactionTemplate = transactionTemplate;
}
@Override
public void transfer(String outer, String inner, int money) {
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus arg0) {
// TODO 自動生成的方法存根
accounDao.out(outer, money);
int i = 1/0;
accounDao.in(inner, money);
}
});
}

更改配置文件:

<!-- 事務管理,transaction manager,user JtaTransactionManager for global tx -->
  <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
  <property name="dataSource" ref="dataSource"></property>
  </bean>
 
  <bean id="accountDao" class="com.fpc.DaoImpl.AccountDaoImpl">
  <property name="dataSource" ref="dataSource"></property>
  </bean>
 
  <bean id="accountService" class="com.fpc.ServiceImpl.AccountServiceImpl">
  <property name="accounDao" ref="accountDao"></property>
  <property name="transactionTemplate" ref="transactionTemplate"></property>
  </bean>
 
  <!-- 建立模板 -->
  <bean id="transactionTemplate" class="org.springframework.transaction.support.TransactionTemplate">
  <property name="transactionManager" ref="transactionManager"></property>
  </bean>
   
    <!-- 配置事物管理器,管理器須要事務,事務從Connection得到,鏈接從鏈接池得到 -->
    <!-- transactionManager配置在上面 -->
目前數據庫中的數據爲:
mysql> select * from account;
+----+----------+-------+
| id | username | money |
+----+----------+-------+
|  1 | Tom      |   800 |
|  2 | Marry    |  1100 |
+----+----------+-------+

單元測試文件保持不變,分爲兩次測試,第一次操做沒有「除零異常」,而後執行:數據能正常改變
mysql> select * from account;
+----+----------+-------+
| id | username | money |
+----+----------+-------+
|  1 | Tom      |   700 |
|  2 | Marry    |  1200 |
+----+----------+-------+
2 rows in set (0.00 sec)

 
第二次操做,加入「除零異常」:發現數據沒有被更新,也就是出現異常後數據庫進行了回滾。
mysql> select * from account;
+----+----------+-------+
| id | username | money |
+----+----------+-------+
|  1 | Tom      |   700 |
|  2 | Marry    |  1200 |
+----+----------+-------+
2 rows in set (0.00 sec)

 

聲明式事務處理實現(AOP)

DAO層和Service不須要改變,主要是applicationContext.xml文件發生了變化。
咱們在applicationContext.xml中配置AOP自動生成代理,進行事務管理。
  1. 配置管理器
  2. 配置事務詳情
  3. 配置AOP
  <!-- 事務管理,transaction manager,user JtaTransactionManager for global tx -->
  <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
  <property name="dataSource" ref="dataSource"></property>
  </bean>
 
  <!-- 事務詳情 :在AOP篩選基礎上,好比對ABC三個肯定使用什麼樣的事物,例如AC讀寫,B只讀等
  <tx:attributes>用於配置事物詳情(屬性)
  <tx:method name=""/>詳情具體配置
  propagation:傳播行爲,REQUIRED:必須,REQUIRED_NEW:必須是新的,isolation:隔離級別
  -->
  <tx:advice id="txAdvice" transaction-manager="transactionManager">
  <tx:attributes>
  <tx:method name="transfer" propagation="REQUIRED" isolation="DEFAULT"/>
  </tx:attributes>
  </tx:advice>
 
  <!-- AOP切面編程,利用切入點表達式從目標類方法中,肯定加強的鏈接器,從而得到切入點 -->
  <aop:config>
  <aop:advisor advice-ref="txAdvice" pointcut="execution(* com.fpc.ServiceImpl..*.*(..))"/>
  </aop:config>
 
  <bean id="accountDao" class="com.fpc.DaoImpl.AccountDaoImpl">
  <property name="dataSource" ref="dataSource"></property>
  </bean>
 
  <bean id="accountService" class="com.fpc.ServiceImpl.AccountServiceImpl">
  <property name="accounDao" ref="accountDao"></property>
  <!-- <property name="transactionTemplate" ref="transactionTemplate"></property> -->
  </bean>
 
  <!-- 建立模板 -->
  <bean id="transactionTemplate" class="org.springframework.transaction.support.TransactionTemplate">
  <property name="transactionManager" ref="transactionManager"></property>
  </bean>
   
    <!-- 配置事物管理器,管理器須要事務,事務從Connection得到,鏈接從鏈接池得到 -->
    <!-- transactionManager配置在上面 -->
 
</beans>
 
AccountServiceImpl修改:
package com.fpc.ServiceImpl;

import org.apache.shiro.authc.Account;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;

import com.fpc.Dao.AccountDao;
import com.fpc.Service.IAccountService;
@Transactional(propagation=Propagation.REQUIRED,isolation=Isolation.DEFAULT)
public class AccountServiceImpl implements IAccountService{
    private AccountDao accounDao;
    
    public void setAccounDao(AccountDao accounDao) {
        this.accounDao = accounDao;
    }
    
    
    @Override
    public void transfer(String outer, String inner, int money) {
                accounDao.out(outer, money);
                int i = 1/0;
                accounDao.in(inner, money);
            }
}

 

 
測試方法同上:兩次測試,一次加「除零異常」,一次不加「除零異常」
運行結果以下:
mysql> select * from account;
+----+----------+-------+
| id | username | money |
+----+----------+-------+
|  1 | Tom      |   700 |
|  2 | Marry    |  1200 |
+----+----------+-------+
2 rows in set (0.00 sec)
mysql> select * from account;
+----+----------+-------+
| id | username | money |
+----+----------+-------+
|  1 | Tom      |   600 |
|  2 | Marry    |  1300 |
+----+----------+-------+
2 rows in set (0.00 sec)
mysql> select * from account;
+----+----------+-------+
| id | username | money |
+----+----------+-------+
|  1 | Tom      |   600 |
|  2 | Marry    |  1300 |
+----+----------+-------+
2 rows in set (0.00 sec)
相關文章
相關標籤/搜索