Spring事務管理詳解
事務的基本原理
Spring事務的本質其實就是資料庫對事務的支援,使用JDBC的事務管理機制,就是利用java.sql.Connection物件完成對事務的提交,那在沒有Spring幫我們管理事務之前,我們要怎麼做。
Connection conn = DriverManager.getConnection();
try {
conn.setAutoCommit(false); //將自動提交設定為false
執行CRUD操作
conn.commit(); //當兩個操作成功後手動提交
} catch (Exception e) {
conn.rollback(); //一旦其中一個操作出錯都將回滾,所有操作都不成功
e.printStackTrace();
} finally {
conn.colse();
}
事務是一系列的動作,一旦其中有一個動作出現錯誤,必須全部回滾,系統將事務中對資料庫的所有已完成的操作全部撤消,滾回到事務開始的狀態,避免出現由於資料不一致而導致的接下來一系列的錯誤。事務的出現是為了確保資料的完整性和一致性,在目前企業級應用開發中,事務管理是必不可少的。
與事務相關的理論知識
眾所周知,事務有四大特性(ACID)
1.原子性(Atomicity)事務是一個原子操作,由一系列動作組成。事務的原子性確保動作要麼全部完成,要麼完全不起作用。
2.一致性(Consistency)事務在完成時,必須是所有的資料都保持一致狀態。
3.隔離性(Isolation)併發事務執行之間無影響,在一個事務內部的操作對其他事務是不產生影響,這需要事務隔離級別來指定隔離性。
4.永續性(Durability)一旦事務完成,資料庫的改變必須是持久化的。
在企業級應用中,多使用者訪問資料庫是常見的場景,這就是所謂的事務的併發。事務併發所可能存在的問題:
1.髒讀:一個事務讀到另一個事務未提交的更新資料。
2.不可重複讀:一個事務兩次讀同一行資料,可是這兩次讀到的資料不一樣。
3.幻讀:一個事務執行兩次查詢,但第二次查詢比第一次查詢多出了一些資料行。
4.丟失更新:撤消一個事務時,把其它事務已提交的更新的資料覆蓋了。
我們可以在java.sql.Connection中看到JDBC定義了五種事務隔離級別來解決這些併發導致的問題:
/**
* A constant indicating that transactions are not supported.
*/
int TRANSACTION_NONE = 0;
/**
* A constant indicating that
* dirty reads, non-repeatable reads and phantom reads can occur.
* This level allows a row changed by one transaction to be read
* by another transaction before any changes in that row have been
* committed (a "dirty read"). If any of the changes are rolled back,
* the second transaction will have retrieved an invalid row.
*/
int TRANSACTION_READ_UNCOMMITTED = 1;
/**
* A constant indicating that
* dirty reads are prevented; non-repeatable reads and phantom
* reads can occur. This level only prohibits a transaction
* from reading a row with uncommitted changes in it.
*/
int TRANSACTION_READ_COMMITTED = 2;
/**
* A constant indicating that
* dirty reads and non-repeatable reads are prevented; phantom
* reads can occur. This level prohibits a transaction from
* reading a row with uncommitted changes in it, and it also
* prohibits the situation where one transaction reads a row,
* a second transaction alters the row, and the first transaction
* rereads the row, getting different values the second time
* (a "non-repeatable read").
*/
int TRANSACTION_REPEATABLE_READ = 4;
/**
* A constant indicating that
* dirty reads, non-repeatable reads and phantom reads are prevented.
* This level includes the prohibitions in
* <code>TRANSACTION_REPEATABLE_READ</code> and further prohibits the
* situation where one transaction reads all rows that satisfy
* a <code>WHERE</code> condition, a second transaction inserts a row that
* satisfies that <code>WHERE</code> condition, and the first transaction
* rereads for the same condition, retrieving the additional
* "phantom" row in the second read.
*/
int TRANSACTION_SERIALIZABLE = 8;
翻譯過來這幾個常量就是
TRANSACTION_NONE JDBC 驅動不支援事務
TRANSACTION_READ_UNCOMMITTED 允許髒讀、不可重複讀和幻讀。
TRANSACTION_READ_COMMITTED 禁止髒讀,但允許不可重複讀和幻讀。
TRANSACTION_REPEATABLE_READ 禁止髒讀和不可重複讀,單執行幻讀。
TRANSACTION_SERIALIZABLE 禁止髒讀、不可重複讀和幻讀。
隔離級別越高,意味著資料庫事務併發執行效能越差,能處理的操作就越少。你可以通過conn.setTransactionLevel去設定你需要的隔離級別。
JDBC規範雖然定義了事務的以上支援行為,但是各個JDBC驅動,資料庫廠商對事務的支援程度可能各不相同。
出於效能的考慮我們一般設定TRANSACTION_READ_COMMITTED就差不多了,剩下的通過使用資料庫的鎖來幫我們處理別的,關於資料庫的鎖這個之後再說。
瞭解了基本的JDBC事務,那有了Spring,在事務管理上會有什麼新的改變呢?
有了Spring,我們再也無需要去處理獲得連線、關閉連線、事務提交和回滾等這些操作,使得我們把更多的精力放在處理業務上。事實上Spring並不直接管理事務,而是提供了多種事務管理器。他們將事務管理的職責委託給Hibernate或者JTA等持久化機制所提供的相關平臺框架的事務來實現。
Spring事務管理
Spring事務管理的核心介面是PlatformTransactionManager
事務管理器介面通過getTransaction(TransactionDefinition definition)方法根據指定的傳播行為返回當前活動的事務或建立一個新的事務,這個方法裡面的引數是TransactionDefinition類,這個類就定義了一些基本的事務屬性。
在TransactionDefinition介面中定義了它自己的傳播行為和隔離級別
除去常量,主要的方法有:
int getIsolationLevel();// 返回事務的隔離級別
String getName();// 返回事務的名稱
int getPropagationBehavior();// 返回事務的傳播行為
int getTimeout(); // 返回事務必須在多少秒內完成
boolean isReadOnly(); // 事務是否只讀,事務管理器能夠根據這個返回值進行優化,確保事務是隻讀的
Spring事務的傳播屬性
由上圖可知,Spring定義了7個以PROPAGATION_開頭的常量表示它的傳播屬性。
名稱 | 值 | 解釋 |
---|---|---|
PROPAGATION_REQUIRED | 0 | 支援當前事務,如果當前沒有事務,就新建一個事務。這是最常見的選擇,也是Spring預設的事務的傳播。 |
PROPAGATION_SUPPORTS | 1 | 支援當前事務,如果當前沒有事務,就以非事務方式執行。 |
PROPAGATION_MANDATORY | 2 | 支援當前事務,如果當前沒有事務,就丟擲異常。 |
PROPAGATION_REQUIRES_NEW | 3 | 新建事務,如果當前存在事務,把當前事務掛起。 |
PROPAGATION_NOT_SUPPORTED | 4 | 以非事務方式執行操作,如果當前存在事務,就把當前事務掛起。 |
PROPAGATION_NEVER | 5 | 以非事務方式執行,如果當前存在事務,則丟擲異常。 |
PROPAGATION_NESTED | 6 | 如果當前存在事務,則在巢狀事務內執行。如果當前沒有事務,則進行與PROPAGATION_REQUIRED類似的操作。 |
Spring事務的隔離級別
名稱 | 值 | 解釋 |
---|---|---|
ISOLATION_DEFAULT | -1 | 這是一個PlatfromTransactionManager預設的隔離級別,使用資料庫預設的事務隔離級別。另外四個與JDBC的隔離級別相對應 |
ISOLATION_READ_UNCOMMITTED | 1 | 這是事務最低的隔離級別,它充許另外一個事務可以看到這個事務未提交的資料。這種隔離級別會產生髒讀,不可重複讀和幻讀。 |
ISOLATION_READ_COMMITTED | 2 | 保證一個事務修改的資料提交後才能被另外一個事務讀取。另外一個事務不能讀取該事務未提交的資料。 |
ISOLATION_REPEATABLE_READ | 4 | 這種事務隔離級別可以防止髒讀,不可重複讀。但是可能出現幻讀。 |
ISOLATION_SERIALIZABLE | 8 | 這是花費最高代價但是最可靠的事務隔離級別。事務被處理為順序執行。除了防止髒讀,不可重複讀外,還避免了幻讀。 |
呼叫PlatformTransactionManager介面的getTransaction()的方法得到的是TransactionStatus介面的一個實現
TransactionStatus介面
主要的方法有:
void flush();//如果適用的話,這個方法用於重新整理底層會話中的修改到資料庫,例如,所有受影響的Hibernate/JPA會話。
boolean hasSavepoint(); // 是否有恢復點
boolean isCompleted();// 是否已完成
boolean isNewTransaction(); // 是否是新的事務
boolean isRollbackOnly(); // 是否為只回滾
void setRollbackOnly(); // 設定為只回滾
可以看出返回的結果是一些事務的狀態,可用來檢索事務的狀態資訊。
配置事務管理器
介紹完Spring事務的管理的流程大概是怎麼走的。接下來可以動手試試Spring是如何配置事務管理器的
例如我在spring-mybatis中配置的:
<!-- 配置事務管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
這配置不是唯一的,可以根據自己專案選擇的資料訪問框架靈活配置事務管理器
配置了事務管理器後,事務當然還是得我們自己去操作,Spring提供了兩種事務管理的方式:程式設計式事務管理和宣告式事務管理,讓我們分別看看它們是怎麼做的吧。
程式設計式事務管理
程式設計式事務管理我們可以通過PlatformTransactionManager實現來進行事務管理,同樣的Spring也為我們提供了模板類TransactionTemplate進行事務管理,下面主要介紹模板類,我們需要在配置檔案中配置
<!--配置事務管理的模板-->
<bean id="transactionTemplate" class="org.springframework.transaction.support.TransactionTemplate">
<property name="transactionManager" ref="transactionManager"></property>
<!--定義事務隔離級別,-1表示使用資料庫預設級別-->
<property name="isolationLevelName" value="ISOLATION_DEFAULT"></property>
<property name="propagationBehaviorName" value="PROPAGATION_REQUIRED"></property>
</bean>
TransactionTemplate幫我們封裝了許多程式碼,節省了我們的工作。下面我們寫個單元測試來測測。
為了測試事務回滾,專門建了一張tbl_accont表,用於模擬存錢的一個場景。service層主要程式碼如下,後面會給出全部程式碼的github地址,有需要的朋友請移步檢視。
BaseSeviceImpl
//方便測試直接寫的sql
@Override
public void insert(String sql, boolean flag) throws Exception {
dao.insertSql(sql);
// 如果flag 為 true ,丟擲異常
if (flag){
throw new Exception("has exception!!!");
}
}
//獲取總金額
@Override
public Integer sum(){
return dao.sum();
}
dao對應的sum方法
<select id="sum" resultType="java.lang.Integer">
SELECT SUM(money) FROM tbl_account;
</select>
下面看看測試程式碼
package com.gray;
import com.gray.service.BaseSevice;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;
import javax.annotation.Resource;
/**
* Created by gray on 2017/4/8.
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:spring-test.xml"})
public class TransactionTest{
@Resource
private TransactionTemplate transactionTemplate;
@Autowired
private BaseSevice baseSevice;
@Test
public void transTest() {
System.out.println("before transaction");
Integer sum1 = baseSevice.sum();
System.out.println("before transaction sum: "+sum1);
System.out.println("transaction....");
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
try{
baseSevice.insert("INSERT INTO tbl_account VALUES (100);",false);
baseSevice.insert("INSERT INTO tbl_account VALUES (100);",false);
} catch (Exception e){
//對於丟擲Exception型別的異常且需要回滾時,需要捕獲異常並通過呼叫status物件的setRollbackOnly()方法告知事務管理器當前事務需要回滾
status.setRollbackOnly();
e.printStackTrace();
}
}
});
System.out.println("after transaction");
Integer sum2 = baseSevice.sum();
System.out.println("after transaction sum: "+sum2);
}
}
當baseSevice.insert的第二個引數為false時,我們假設插入資料沒有出現任何問題,測試結果如圖所示:
當第二個引數為true時,insert會丟擲一個異常,這是事務就應該回滾,資料前後不應該有變化,如圖所示:
宣告式事務管理
宣告式事務管理有兩種常用的方式,一種是基於tx和aop名稱空間的xml配置檔案,一種是基於@Transactional註解,隨著Spring和Java的版本越來越高,大家越趨向於使用註解的方式,下面我們兩個都說。
1.基於tx和aop名稱空間的xml配置檔案
配置檔案
<tx:advice id="advice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="insert" propagation="REQUIRED" read-only="false" rollback-for="Exception"/>
</tx:attributes>
</tx:advice>
<aop:config>
<aop:pointcut id="pointCut" expression="execution (* com.gray.service.*.*(..))"/>
<aop:advisor advice-ref="advice" pointcut-ref="pointCut"/>
</aop:config>
測試程式碼
@Test
public void transTest() {
System.out.println("before transaction");
Integer sum1 = baseSevice.sum();
System.out.println("before transaction sum: "+sum1);
System.out.println("transaction....");
try{
baseSevice.insert("INSERT INTO tbl_account VALUES (100);",true);
} catch (Exception e){
e.printStackTrace();
}
System.out.println("after transaction");
Integer sum2 = baseSevice.sum();
System.out.println("after transaction sum: "+sum2);
}
事務正常執行結果截圖
事務出現異常結果截圖
2.基於@Transactional註解
這種方式最簡單,也是最為常用的,只需要在配置檔案中開啟對註解事務管理的支援。
<!-- 宣告式事務管理 配置事物的註解方式注入-->
<tx:annotation-driven transaction-manager="transactionManager"/>
然後在需要事務管理的地方加上@Transactional註解,如:
@Transactional(rollbackFor=Exception.class)
public void insert(String sql, boolean flag) throws Exception {
dao.insertSql(sql);
// 如果flag 為 true ,丟擲異常
if (flag){
throw new Exception("has exception!!!");
}
}
rollbackFor屬性指定出現Exception異常的時候回滾,遇到檢查性的異常需要回滾,預設情況下非檢查性異常,包括error也會自動回滾。
測試程式碼和上面那個一樣
事務正常執行結果截圖
事務出現異常結果截圖
以上就是對Spring事務進行了詳細的分析和程式碼示例。
最後是測試程式碼Spring事務測試