【JAVA】Spring事務管理詳解

事物的特性(ACID)

原子性: 事務是最小的執行單位,不允許分割。事務的原子性確保動作要么全部完成,要么完全不起作用;
一致性: 執行事務前后,數據保持一致;
隔離性: 并發訪問數據庫時,一個用戶的事物不被其他事物所干擾,各并發事務之間數據庫是獨立的;
持久性: 一個事務被提交之后。它對數據庫中數據的改變是持久的,即使數據庫發生故障也不應該對其有任何影響。

我們在使用JDBC或者Mybatis進行數據持久化操作時,我們的xml配置通常如下:

    <!-- 事務管理器 -->
    <bean id="transactionManager"
        class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <!-- 數據源 -->
        <property name="dataSource" ref="dataSource" />
    </bean>

    <!-- 開啟事務行為 -->
    <tx:annotation-driven transaction-manager="transactionManager" />

并發事務帶來的問題

在典型的應用程序中,多個事務并發運行,經常會操作相同的數據來完成各自的任務(多個用戶對統一數據進行操作)。并發雖然是必須的,但可能會導致一下的問題。

  • 臟讀(Dirty read): 當一個事務正在訪問數據并且對數據進行了修改,而這種修改還沒有提交到數據庫中,這時另外一個事務也訪問了這個數據,然后使用了這個數據。因為這個數據是還沒有提交的數據,那么另外一個事務讀到的這個數據是“臟數據”,依據“臟數據”所做的操作可能是不正確的。

  • 丟失修改(Lost to modify): 指在一個事務讀取一個數據時,另外一個事務也訪問了該數據,那么在第一個事務中修改了這個數據后,第二個事務也修改了這個數據。這樣第一個事務內的修改結果就被丟失,因此稱為丟失修改。
    例如:事務1讀取某表中的數據A=20,事務2也讀取A=20,事務1修改A=A-1,事務2也修改A=A-1,最終結果A=19,事務1的修改被丟失。

  • 不可重復讀(Unrepeatableread): 指在一個事務內多次讀同一數據。在這個事務還沒有結束時,另一個事務也訪問該數據。那么,在第一個事務中的兩次讀數據之間,由于第二個事務的修改導致第一個事務兩次讀取的數據可能不太一樣。這就發生了在一個事務內兩次讀到的數據是不一樣的情況,因此稱為不可重復讀。

  • 幻讀(Phantom read): 幻讀與不可重復讀類似。它發生在一個事務(T1)讀取了幾行數據,接著另一個并發事務(T2)插入了一些數據時。在隨后的查詢中,第一個事務(T1)就會發現多了一些原本不存在的記錄,就好像發生了幻覺一樣,所以稱為幻讀。

不可重復度和幻讀區別:
不可重復讀的重點是修改,幻讀的重點在于新增或者刪除。

spring事務的傳播性、隔離性

@Transactional
1.這里說明一下,有的把這個注解放在類名稱上面了,這樣你配置的這個@Transactional 對這個類中的所有public方法都起作用.
2.@Transactional 方法方法名上,只對這個方法有作用,同樣必須是public的方法

我們在使用Spring聲明式事務時,有一個非常重要的概念就是事務的屬性。事務屬性通常由事務的傳播行為、事務的隔離級別、事務的超時值和事務的只讀標識組成。我們在進行事務劃分時,需要進行事務定義,也就是配置事務的屬性。

屏幕快照 2018-08-30 下午2.15.21.png

Spring在TransactionDefinition接口中定義這些屬性,以供PlatfromTransactionManager使用, PlatfromTransactionManager是spring事務管理的核心接口。

public interface TransactionDefinition {
    int PROPAGATION_REQUIRED = 0;
    int PROPAGATION_SUPPORTS = 1;
    int PROPAGATION_MANDATORY = 2;
    int PROPAGATION_REQUIRES_NEW = 3;
    int PROPAGATION_NOT_SUPPORTED = 4;
    int PROPAGATION_NEVER = 5;
    int PROPAGATION_NESTED = 6;
    int ISOLATION_DEFAULT = -1; //默認的隔離級別
    int ISOLATION_READ_UNCOMMITTED = 1;
    int ISOLATION_READ_COMMITTED = 2;
    int ISOLATION_REPEATABLE_READ = 4;
    int ISOLATION_SERIALIZABLE = 8;
    int TIMEOUT_DEFAULT = -1;

    int getPropagationBehavior();

    int getIsolationLevel();

    int getTimeout();

    boolean isReadOnly();

    String getName();
}

getTimeout()方法,它返回事務必須在多少秒內完成。
isReadOnly(),事務是否只讀,事務管理器能夠根據這個返回值進行優化,確保事務是只讀的。
getIsolationLevel()方法返回事務的隔離級別,事務管理器根據它來控制另外一個事務可以看到本事務內的哪些數據。

隔離級別

在TransactionDefinition接口中定義了五個不同的事務隔離級別 :
a) ISOLATION_DEFAULT:(PlatfromTransactionManager的)默認的隔離級別。使用數據庫默認的事務隔離級別(MySql 默認為 REPEATABLE_READ;Oracle 默認為:READ_COMMITTED),另外四個與JDBC的隔離級別相對應 。
b)ISOLATION_READ_UNCOMMITTED:這是事務最低的隔離級別,它允許別外一個事務可以看到這個事務未提交的數據。這種隔離級別會產生臟讀,不可重復讀和幻像讀。 (未解決任何并發問題)

例如:
Mary的原工資為1000,財務人員將Mary的工資改為了8000,但未提交事務

Connection con1 = getConnection();  
con1.setAutoCommit(false);  
update employee set salary = 8000 where empId ="Mary"; 

與此同時,Mary正在讀取自己的工資 。

Connection con2 = getConnection();  
select salary from employee where empId ="Mary";  
con2.commit();

Mary發現自己的工資變為了8000,歡天喜地!
而財務發現操作有誤,而回滾了事務,Mary的工資又變為了1000 。

con1.rollback(); 

像這樣,Mary記取的工資數8000是一個臟數據。

c)ISOLATION_READ_COMMITTED: 保證一個事務修改的數據提交后才能被另外一個事務讀取。另外一個事務不能讀取該事務未提交的數據。這種事務隔離級別可以避免臟讀出現,但是可能會出現不可重復讀和幻像讀。
d)ISOLATION_REPEATABLE_READ : 這種事務隔離級別可以防止臟讀,不可重復讀。但是可能出現幻像讀。它除了保證一個事務不能讀取另一個事務未提交的數據外,還保證了避免下面的情況產生(不可重復讀)。

例如:
在事務1中,Mary 讀取了自己的工資為1000,操作并沒有完成 :

Connection con1 = getConnection();  
con1.setAutoCommit(false);  
select salary from employee empId ="Mary"; 

在事務2中,這時財務人員修改了Mary的工資為2000,并提交了事務.

Connection con2 = getConnection();  
update employee set salary = 2000;  
con2.commit();  

在事務1中,Mary 再次讀取自己的工資時,工資變為了2000

//con1 
select salary from employee empId ="Mary";  

在一個事務中前后兩次讀取的結果并不致,導致了不可重復讀。
使用ISOLATION_REPEATABLE_READ可以避免這種情況發生。

e)ISOLATION_SERIALIZABLE 這是花費最高代價但是最可靠的事務隔離級別。事務被處理為順序執行。除了防止臟讀,不可重復讀外,還避免了幻像讀。

例如:
目前工資為1000的員工有10人。
事務1,讀取所有工資為1000的員工。共讀取10條記錄

con1 = getConnection();  
Select * from employee where salary =1000; 

這時另一個事務向employee表插入了一條員工記錄,工資也為1000

con2 = getConnection();  
Insert into employee(empId,salary) values("Lili",1000);  
con2.commit();  

事務1再次讀取所有工資為1000的員工

//con1
select * from employee where salary =1000;  

共讀取到了11條記錄,這就產生了幻像讀。
ISOLATION_SERIALIZABLE能避免這樣的情況發生。但是這樣也耗費了最大的資源。

事務的傳播性

在TransactionDefinition接口中定義了七個事務傳播行為。
假如我寫了兩個service類。名字分別為ServiceA和ServiceB。如下:

@Service("ServiceA")
public class ServiceA {

    @Resource(name="ServiceB")
    private ServiceB serviceB;

    @Transactional(propagation=Propagation.REQUIRED)
    public methodA(){
       //doSomething
       serviceB.methodB(); 
       //doSomething
    }
}
@Service("ServiceB")
public class ServiceB {

    @Transactional(propagation=Propagation.REQUIRED)
    public methodB(){
       //doSomething
    }
}

a)PROPAGATION_REQUIRED:如果存在一個事務,則支持當前事務。如果沒有事務則開啟一個新的事務。
如果單獨調用serviceB.methodB方法:

public static void main(String[] args) {
   serviceB.methodB();
}

相當于:

public static void main(String[] args) {
  Connection con=null;  
  try{  
      con = getConnection();  
      con.setAutoCommit(false);  
      //方法調用  
      methodB();  
      //提交事務  
      con.commit();  
 }Catch(RuntimeException ex){  
    //回滾事務  
    con.rollback();    
 }finally{  
    //釋放資源  
    closeCon();  
 }  
}  

Spring保證在methodB方法中所有的調用都獲得到一個相同的連接。在調用methodB時,沒有一個存在的事務,所以獲得一個新的連接,開啟了一個新的事務。

如果單獨調用MethodA時,在MethodA內又會調用MethodB.
執行效果相當于:

public static void main(String[] args) {
   Connection con = null;  
   try{  
      con = getConnection();  
      con.setAutoCommit(false);  
      methodA();  
      con.commit();  
   }cathc(RuntimeException ex){  
      con.rollback();  
   }finally{  
      closeCon();  
   }   
}  

調用MethodA時,環境中沒有事務,所以開啟一個新的事務.
當在MethodA中調用MethodB時,環境中已經有了一個事務,所以methodB就加入當前事務。

b)PROPAGATION_SUPPORTS :如果存在一個事務,支持當前事務。如果沒有事務,則非事務的執行。但是對于事務同步的事務管理器,PROPAGATION_SUPPORTS與不使用事務有少許不同。

//事務屬性 PROPAGATION_REQUIRED   
methodA(){  
  serviceB.methodB();  
}  

//事務屬性 PROPAGATION_SUPPORTS   
methodB(){  
  ……  
} 

單純的調用methodB時,methodB方法是非事務的執行的。
當調用methdA時,methodB則加入了methodA的事務中,事務地執行。

c)PROPAGATION_MANDATORY :如果已經存在一個事務,支持當前事務。如果沒有一個活動的事務,則拋出異常。

//事務屬性 PROPAGATION_REQUIRED   
methodA(){  
  serviceB.methodB();  
}  

//事務屬性 PROPAGATION_MANDATORY   
methodB(){  
  ……  
}  

當單獨調用methodB時,因為當前沒有一個活動的事務,則會拋出異常
當調用methodA時,methodB則加入到methodA的事務中,事務地執行。

d)PROPAGATION_REQUIRES_NEW :總是開啟一個新的事務。如果一個事務已經存在,則將這個存在的事務掛起。

//事務屬性 PROPAGATION_REQUIRED   
methodA(){  
   doSomeThingA();  
   serviceB.methodB();  
   doSomeThingB();  
}  

//事務屬性 PROPAGATION_REQUIRES_NEW   
methodB(){  
  ……  
}  

當單獨調用methodB時,相當于把methodB聲明為REQUIRED。開啟一個新的事務,事務地執行。
當調用methodA時,情況就大不一樣了,相當于下面的效果。

public static void main(String[] args) {
 TransactionManager tm = null;  
 try{  
  //獲得一個JTA事務管理器  
   tm = getTransactionManager();  
   tm.begin();//開啟一個新的事務  
   Transaction ts1 = tm.getTransaction();  
   doSomeThing();  
   tm.suspend();//掛起當前事務  
   try{  
     tm.begin();//重新開啟第二個事務  
     Transaction ts2 = tm.getTransaction();  
     methodB();  
     ts2.commit();//提交第二個事務  
   }Catch(RunTimeException ex){  
     ts2.rollback();//回滾第二個事務  
   }finally{  
    //釋放資源  
   }  
   //methodB執行完后,復恢第一個事務  
   tm.resume(ts1);  
   doSomeThingB();  
   ts1.commit();//提交第一個事務  
 }catch(RunTimeException ex){  
  ts1.rollback();//回滾第一個事務  
 }finally{  
  //釋放資源  
 }  
}  

在這里,我把ts1稱為外層事務,ts2稱為內層事務。從上面的代碼可以看出,ts2與ts1是兩個獨立的事務,互不相干。Ts2是否成功并不依賴于ts1。如果methodA方法在調用methodB方法后的doSomeThingB方法失敗了,而methodB方法所做的結果依然被提交。而除了methodB之外的其它代碼導致的結果卻被回滾了。
使用PROPAGATION_REQUIRES_NEW,需要使用JtaTransactionManager作為事務管理器。

e)PROPAGATION_NOT_SUPPORTED: 總是非事務地執行,并掛起任何存在的事務。

事務屬性 PROPAGATION_REQUIRED   
methodA(){  
  doSomeThingA();  
  serviceB.methodB();  
  doSomeThingB();  
}  

事務屬性 PROPAGATION_NOT_SUPPORTED   
methodB(){  
  ……  
}  

當單獨調用methodB時,不啟用任何事務機制,非事務地執行。
當調用methodA時,相當于下面的效果

public static void main(String[] args) {
 TransactionManager tm = null;  
 try{  
  //獲得一個JTA事務管理器  
   tm = getTransactionManager();  
   tm.begin();//開啟一個新的事務  
   Transaction ts1 = tm.getTransaction();  
   doSomeThing();  
   tm.suspend();//掛起當前事務  
   methodB();  
   //methodB執行完后,復恢第一個事務  
   tm.resume(ts1);  
   doSomeThingB();  
   ts1.commit();//提交第一個事務  
 }catch(RunTimeException ex){  
   ts1.rollback();//回滾第一個事務  
 }finally{  
  //釋放資源  
 }  
}  

使用PROPAGATION_NOT_SUPPORTED,也需要使用JtaTransactionManager作為事務管理器。

f)PROPAGATION_NEVER :總是非事務地執行,如果存在一個活動事務,則拋出異常:

事務屬性 PROPAGATION_REQUIRED   
methodA(){  
  doSomeThingA();  
  seviceB.methodB();  
  doSomeThingB();  
}  

事務屬性 PROPAGATION_NEVER   
methodB(){  
  ……  
}

單獨調用methodB,則非事務的執行。 調用methodA則會拋出異常

g)PROPAGATION_NESTED:如果一個活動的事務存在,則運行在一個嵌套的事務中. 如果沒有活動事務, 則按TransactionDefinition.PROPAGATION_REQUIRED 屬性執行

這是一個嵌套事務,使用JDBC 3.0驅動時,僅僅支持DataSourceTransactionManager作為事務管理器。需要JDBC 驅動的java.sql.Savepoint類。有一些JTA的事務管理器實現可能也提供了同樣的功能。

使用PROPAGATION_NESTED,還需要把PlatformTransactionManager的nestedTransactionAllowed屬性設為true;
而nestedTransactionAllowed屬性值默認為false;

如下:

<bean id="system.platformTransactionManager" class="org.springframework.orm.hibernate3.HibernateTransactionManager">  
        <property name="sessionFactory" ref="system.sessionFactory"/>  
        <property name="nestedTransactionAllowed" value="true"/>  
 </bean>
事務屬性 PROPAGATION_REQUIRED   
methodA(){  
  doSomeThingA();  
  serviceB.methodB();  
  doSomeThingB();  
}  

事務屬性 PROPAGATION_NESTED  
methodB(){  
  ……  
} 

如果單獨調用methodB方法,則按REQUIRED屬性執行。
如果調用methodA方法,相當于下面的效果

main(){  
Connection con = null;  
Savepoint savepoint = null;  
try{  
  con = getConnection();  
  con.setAutoCommit(false);  
  doSomeThingA(); 
  //創造一個事務的保存點 
  savepoint = con2.setSavepoint();  
  try  
      methodB();  
  }catch(RuntimeException ex){  
     con.rollback(savepoint);  
  }finally{  
    //釋放資源  
  }  
  doSomeThingB();  
  con.commit();  
}catch(RuntimeException ex){  
  con.rollback();  
}finally{  
  //釋放資源  
}  
} 

當methodB方法調用之前,調用setSavepoint方法,保存當前的狀態到savepoint。如果methodB方法調用失敗,則恢復到之前保存的狀態。但是需要注意的是,這時的事務并沒有進行提交,如果后續的代碼doSomeThingB()方法調用失敗,則回滾包括methodB方法的所有操作。

嵌套事務一個非常重要的概念就是內層事務依賴于外層事務。外層事務失敗時,會回滾內層事務所做的動作。而內層事務操作失敗并不會引起外層事務的回滾。

PROPAGATION_NESTED 與PROPAGATION_REQUIRES_NEW的區別:它們非常類似,都像一個嵌套事務,如果不存在一個活動的事務,都會開啟一個新的事務。使用PROPAGATION_REQUIRES_NEW時,內層事務與外層事務就像兩個獨立的事務一樣,一旦內層事務進行了提交后,外層事務不能對其進行回滾。兩個事務互不影響。兩個事務不是一個真正的嵌套事務。同時它需要JTA事務管理器的支持。
使用PROPAGATION_NESTED時,外層事務的回滾可以引起內層事務的回滾。而內層事務的異常并不會導致外層事務的回滾,它是一個真正的嵌套事務。DataSourceTransactionManager使用savepoint支持PROPAGATION_NESTED時,需要JDBC 3.0以上驅動及1.4以上的JDK版本支持。其它的JTA TrasactionManager實現可能有不同的支持方式。

PROPAGATION_REQUIRED應該是我們首先的事務傳播行為。它能夠滿足我們大多數的事務需求。

事務的超時性、回滾和只讀

  • 超時:
    @Transactional(timeout=30) //默認是30秒
    異?;貪L:
    指定單一異常類:@Transactional(rollbackFor=RuntimeException.class)
    指定多個異常類:@Transactional(rollbackFor={RuntimeException.class, Exception.class})
    該屬性用于設置需要進行回滾的異常類數組,當方法中拋出指定異常數組中的異常時,則進行事務回滾。

正常的情況下也可以回滾:
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();

  • 只讀:
    @Transactional(readOnly=true)
    該屬性用于設置當前事務是否為只讀事務,設置為true表示只讀,false則表示可讀寫,默認值為false。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容