摘要:點外賣時,你只需考慮如何拼單;選擇出行時,你只用想好目的地;手機支付時,你只需要保證餘額充足。但你不知道這些智慧的背後,是數以億計的強大資料的支援,這就是資料庫的力量。那麼龐大資料的背後一定會牽扯到資料安全的問題,那這些意外和衝突又是如何解決呢?
本文分享自華為雲社群《萬字詳解Spring如何用“宣告式事務”保護億萬資料安全?丨【綻放吧!資料庫】》,作者:灰小猿。
一、揭祕什麼是事務管理?
瞭解宣告式事務就要從它的基本概念開始。那麼什麼是事務呢?
在JavaEE的大型專案開發中,面對規模龐大的資料,需要保證資料的完整性和一致性,因此就有了資料庫事務的概念,因此它也是企業級專案應用開發必不可少的技術。
事務可以看做是一組由於邏輯上緊密相關而合併到一個整體(工作單元)的多個資料庫操作。這些操作要麼全執行,要麼全不執行。
同時事務有四個非常關鍵的屬性(ACID):
- 原子性(atomicity):“原子”的本意是“不可再分”,事務的原子性表現為一個事務中涉及到的多個操作在邏輯上缺一不可。事務的原子性要求事務中的所有操作要麼都執行,要麼都不執行。
- 一致性(consistency):“一致”指的是資料的一致,具體是指:所有資料都處於滿足業務規則的一致性狀態。一致性原則要求:一個事務中不管涉及到多少個操作,都必須保證事務執行之前資料是正確的,事務執行之後資料仍然是正確的。如果一個事務在執行的過程中,其中某一個或某幾個操作失敗了,則必須將其他所有操作撤銷,將資料恢復到事務執行之前的狀態,這就是回滾。
- 隔離性(isolation):在應用程式實際執行過程中,事務往往是併發執行的,所以很有可能有許多事務同時處理相同的資料,因此每個事務都應該與其他事務隔離開來,防止資料損壞。隔離性原則要求多個事務在併發執行過程中不會互相干擾。
- 永續性(durability):永續性原則要求事務執行完成後,對資料的修改永久的儲存下來,不會因各種系統錯誤或其他意外情況而受到影響。通常情況下,事務對資料的修改應該被寫入到持久化儲存器中。
所以進行事務控制就應該儘可能的滿足這四個屬性。既然進行事務控制的目的就是為了能夠在資料處理發生意外的時候進行事務回滾,那麼常見的錯誤型別有哪些、對於這種型別的錯誤又應該如何處理的呢?
二、宣告式事務使用詳解
相比於程式設計式事務,宣告式事務具有更大的優點,它能夠將事務管理程式碼從業務方法中分離出來,以宣告的方式來實現業務管理。
事務管理程式碼的固定模式作為一種橫切關注點,可以通過AOP方法模組化,進而藉助Spring AOP框架實現宣告式事務管理。
Spring在不同的事務管理API之上定義了一個抽象層,通過配置的方式使其生效,從而讓應用程式開發人員不必瞭解事務管理API的底層實現細節,就可以使用Spring的事務管理機制。
同時Spring既支援程式設計式事務管理,也支援宣告式的事務管理。
那麼在Spring中應該如何使用宣告式事務呢?
1、事務管理器的主要實現
Spring從不同的事務管理API中抽象出了一整套事務管理機制,讓事務管理程式碼從特定的事務技術中獨立出來。這樣我們只需通過配置的方式進行事務管理,而不必瞭解其底層是如何實現的。這也是使用宣告式事務的一大好處。
Spring的核心事務管理抽象是PlatformTransactionManager。它為事務管理封裝了一組獨立於技術的方法。無論使用Spring的哪種事務管理策略(程式設計式或宣告式),事務管理器都是必須的。
事務管理器可以以普通的bean的形式宣告在Spring IOC容器中。在Spring中我們常用的三種事務管理器是:
- DataSourceTransactionManager:在應用程式中只需要處理一個資料來源,而且通過JDBC存取。
- JtaTransactionManager:在JavaEE應用伺服器上用JTA(Java Transaction API)進行事務管理
- HibernateTransactionManager:用Hibernate框架存取資料庫
它們都是PlatformTransactionManager的子類,繼承關係圖如下:
現在我們已經基本瞭解了宣告式事務的實現原理和機制,百讀不如一練,接下來我們就實際講解一下如何配置使用Spring的宣告式事務。
2、基於註解的宣告式事務配置
我以DataSourceTransactionManager類為例來給大家講一下宣告式事務的實現過程,小夥伴們可以操作實現一下,有問題的話記得留言我一起交流。
(1)、配置資料來源
既然是對資料庫的操作,那麼首先第一步一定就是配置資料來源的,關於資料來源的配置相信小夥伴們應該都不陌生了,還不太瞭解的小夥伴們可以看我的上一篇關於Spring的文章。《Spring JDBC持久化層框架“全家桶”教程丨【綻放吧!資料庫】》
配置資料來源我以引入外部資料配置檔案為例,所以我這裡需要使用<context></context>標籤引入外部檔案,並使用“${}”的方式為屬性賦值:
程式碼如下:
<!-- 連線外部配置檔案 --> <context:property-placeholder location="classpath:jdbcconfig.properties"/> <!-- 連線資料庫 --> <bean id="pooldataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"> <property name="user" value="${jdbc.user}"></property> <property name="password" value="${jdbc.password}"></property> <property name="jdbcUrl" value="${jdbc.jdbcurl}"></property> <property name="driverClass" value="${jdbc.driverclass}"></property> </bean>
(2)、建立JdbcTemplate
既然是運算元據庫,而且是在spring框架中,那麼對於Spring中資料庫操作框架的使用也一定是必不可少的,關於jdbcTemplate這個框架技術點的詳細使用我也在上一篇文章中和大家講解了,小夥伴們可以學起來了!
在這裡我們直接在ioc的bean中宣告jdbcTemplate類,並設定資料來源為第一步的資料來源。
程式碼如下:
<!-- 建立jdbcTemplate --> <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> <property name="dataSource" ref="pooldataSource"></property> </bean>
(3)、進行事務控制
現在資料來源也配置好了,資料庫操作也整完了,那麼接下來就是今天的主題事務控制了。
我們知道事務控制本身就是基於面向切面程式設計來實現的,所以配置事務控制時就需要匯入相應的jar包:我把所需的jar包給大家羅列了出來:
- spring-aop-4.0.0.RELEASE.jar
- com.springsource.net.sf.cglib-2.2.0.jar
- com.springsource.org.aopalliance-1.0.0.jar
- com.springsource.org.aspectj.weaver-1.6.8.RELEASE.jar
在這裡插入一個補充,也可以說是一道面試題:說一說使用事務管理器的優點?
使用事務控制能夠節省平時進行事務控制是書寫的程式碼量,進行事務控制時,若一個事務的執行過程中發生差錯,則其他操作不會修改,保持事務的原子性。
我們在這裡使用DataSourceTransactionManager類來配置事務管理器。
具體方法是在ioc中的bean標籤中宣告該類的例項,設定好id,並給DataSource屬性賦上資料來源,
程式碼如下:
<bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <!-- 2、控制住資料來源 --> <property name="dataSource" ref="pooldataSource"></property> </bean>
這樣就已經配置好事務管理器了,是不是以為這樣就完了,並沒有噢!接下來也是最關鍵的一步!就是將事務管理器開啟,因為不開啟怎麼使用呢?
(4)、開啟基於註解的事務控制
開啟基於註解的事務控制的主要作用就是對方法和類增加相應的註解,從而實現自動的包掃描。開啟基於註解的事務控制需要引入tx表示式,使用其中的annotation-driven標籤,即可對執行的事務管理器開啟事務控制。
程式碼如下:
<!-- 3、開啟基於註解的事務控制 --> <tx:annotation-driven transaction-manager="dataSourceTransactionManager"/> <!-- 4、給方法增加事務控制,新增相應的註解-->
接下來的就是為方法新增相應的註解,增加事務控制了。
首先對資料庫操作的類一般都屬於業務邏輯層,所以我們要為該類新增@service註解,從而實現包掃描,之後為需要進行事務控制的方法新增事務控制專有的註解@Transactional來告訴Spring該方法是事務方法。當該方法中的操作發生錯誤的時候,該方法內其他對資料庫的操作也都會回滾。
程式碼如下:
@Service public class BookService { @Autowired BookDao bookDao; /** * 顧客買書 * */ // 開啟基於註解的事務控制 @Transactional public void buyBook(String username,String isbn) { bookDao.updateStockFromBookStock(isbn); int price = bookDao.getPriceFromBook(isbn); bookDao.updateBalanceFromAccount(username, price); System.out.println("【" +username + "】買書成功!"); } }
3、基於XML的宣告式事務配置
上面我們講解了使用註解如何配置宣告式事務,那麼配置宣告式事務還有另一種方法,就是在XML檔案中配置,而且他們在宣告資料來源的時候都是一樣的,在這裡我就不說了,我只說一下在配置完資料來源之後,如何通過XML宣告事務管理器和事務方法。
(1)、配置事務切面
Spring中有提供事務管理器(事務切面),所以首先我們需要配置這個事務切面。
<aop:config> <aop:pointcut expression="execution(* com.spring.service.*.*(..))" id="txpoint"/> <!-- 事務建議;advice-ref:指向事務管理器的配置 --> <aop:advisor advice-ref="myAdvice" pointcut-ref="txpoint"/> </aop:config>
(2)、配置事務管理器
配置事務管理器使用tx:advice標籤,其中的屬性transaction-manager="transactionManager" 指定是配置哪個事務管理器,指定好之後我們就需要在該標籤中配置出事務方法,
<!-- 配置事務管理器 transaction-manager="transactionManager" 指定是配置哪個事務管理器 --> <tx:advice id="myAdvice" transaction-manager="dataSourceTransactionManager"> </tx:advice>
(3)、指定事務方法
我們需要在tx:advice標籤中增加tx:method標籤告訴Spring哪些方法是事務方法(事務切面將按照我們的切入點表示式去切事務方法)。同時事務可以使用的各種引數可以在tx:method中宣告,
程式碼如下:
<!-- 配置事務管理器 transaction-manager="transactionManager" 指定是配置哪個事務管理器 --> <tx:advice id="myAdvice" transaction-manager="dataSourceTransactionManager"> <!-- 事務屬性 --> <tx:attributes> <!-- 指明哪些方法是事務方法, 切入點表示式只是說,事務管理器要切入這些方法, --> <!-- 指定所有的方法都是事務方法 --> <tx:method name="*"/> <!-- 僅僅指定一個方法是事務方法,並且指定事務的屬性 --> <tx:method name="buyBook" propagation="REQUIRED" timeout="-1"/> <!-- 表示所有以get開頭的方法 --> <tx:method name="get*" read-only="true"/> </tx:attributes> </tx:advice>
至此宣告式事務的初步使用才算完成,那麼到底什麼時候使用基於註解的事務管理器,什麼時候使用基於XML的呢,
注意:正確的應該是,基於註解的和基於註解的都用,重要的事務使用註解,不重要的事務使用配置。
你以為到這裡就結束了嘛?但是這僅僅只是一個開始,因為事務的控制一定是伴隨著多種情況一起執行的。
三、事務的傳播行為
當一個事務方法被另一個事務方法呼叫時,必須指定事務應該如何傳播。例如:方法可能繼續在現有事務中執行,也可能開啟一個新事務,並在自己的事務中執行。
事務的傳播行為可以在@Transactional註解的propagation屬性中指定。Spring定義了7種類傳播行為。
他們所對應的功能分別如下表所示:
這裡我再對最常使用的兩個傳播行為說一下:
1)REQUIRED:當前事務和之前的大事務公用一個事務
當事務使用REQUIRED的時候,事務的屬性都是整合於大事務的,所以對方法施加的屬性不會單獨生效如超時設定timeout。
當事務使用REQUIRES_NEW的時候,事務的屬性是可以調整的,
2)REQUIRES_NEW:當前事務總是使用一個新的事務,如果已經有事務,事務將會被掛起,當前事務提交執行完之後會繼續執行被掛起的事務
原理:REQUIRED,是將之前事務的connection傳遞給這個方法使用。
REQUIRES_NEW,是這個方法直接使用新的connection
四、事務的隔離級別
1、資料庫事務併發問題
我們在對資料庫中的資料進行操作的時候,往往不是隻有一個人在操作的,也就是說可能會有事務的併發執行,那麼既然存在併發執行,在這其中就一定會存在併發處理的問題。
那麼都會有哪些常見的事務併發問題呢?我們以兩個事務Transaction01和Transaction02併發執行為例來介紹一下:
(1)、髒讀
所謂髒讀就是讀取到了一個髒的資料,通俗一點理解為就是讀取到的資料無效。如下面的操作例項:
- Transaction01將某條記錄的AGE值從20修改為30。
- Transaction02讀取了Transaction01更新後的值:30。
- Transaction01回滾,AGE值恢復到了20。
- Transaction02讀取到的30就是一個無效的值。
這時Transaction02的事務就發生了髒讀,
(2)、不可重複讀
從裡面意思上我們應該也可以理解,就是同一個事務在對資料進行重複讀取的時候,兩次讀取到的資料不一致。
看下面的案例:
- Transaction01讀取了AGE值為20。
- Transaction02將AGE值修改為30。
- Transaction01再次讀取AGE值為30,和第一次讀取不一致。
這時Transaction01兩次讀取到的資料不一致,這就到之後Transaction01處理事務時會出現不知道使用哪個資料的情況,這就是不可重複讀。
(3)、幻讀
聽到這個名字是不是覺得很神奇,怎麼還會有幻讀呢?其實幻讀的意思還是兩次讀取到的資料不一致,
看下面的案例:
- Transaction01讀取了STUDENT表中的一部分資料。
- Transaction02向STUDENT表中插入了新的行。
- Transaction01讀取了STUDENT表時,多出了一些行。
在這裡Transaction01在第二次讀取資料表時,發現資料表中的資料和之前的相比多了,這就是發生了幻讀。
2、事務的隔離級別分析
那麼對於我們上面提到的那三種併發問題到底應該如何解決呢?這裡就用到了事務的隔離級別,因為這些問題都是由於併發執行而引起的,因此資料庫系統必須具備隔離併發執行各個事務的能力,使它們之間不會相互影響,避免各種併發問題。
一個事務與其他事務隔離的程度就稱為隔離級別。SQL標準中規定了多種事務隔離級別,不同隔離級別對應不同的干擾程度,隔離級別越高,資料一致性就越好,但併發性越弱。
常見的隔離級別有以下四種:
- ①讀未提交:READ UNCOMMITTED
允許Transaction01讀取Transaction02未提交的修改。 - ②讀已提交:READ COMMITTED
要求Transaction01只能讀取Transaction02已提交的修改。 - ③可重複讀:REPEATABLE READ
確保Transaction01可以多次從一個欄位中讀取到相同的值,即Transaction01執行期間禁止其它事務對這個欄位進行更新。 - ④序列化:SERIALIZABLE
確保Transaction01可以多次從一個表中讀取到相同的行,在Transaction01執行期間,禁止其它事務對這個表進行新增、更新、刪除操作。可以避免任何併發問題,但效能十分低下。
但是這些個隔離級別並不是都能解決上面所有的併發問題的,他們解決併發問題的能力如下:
同時不同的資料庫對不同隔離級別也是有不同的支援程度,就拿MySQL和Oracle為例:
3、為方法指定隔離級別
我們上面講了事務併發的問題,也提到了應該使用隔離級別來解決,那麼接下來就是如何在事務方法上增加隔離級別了。在這裡有兩種方法。
(1)、基於註解指定隔離級別
基於註解指定事務隔離級別可以在@Transactional註解宣告式地管理事務時,在@Transactional的isolation屬性中設定隔離級別。這樣該事務方法就有了該隔離級別。
@Transactional(readOnly=true,isolation=Isolation.READ_UNCOMMITTED) public int getPrice(String isbn) { return bookDao.getPriceFromBook(isbn); }
(2)。基於XML指定隔離級別
這種方法是在如果不使用註解的情況下,可以在XML配置檔案中為方法宣告隔離級別,可以在Spring 2.x事務通知中,在<tx:method>元素中的isolation屬性指定隔離級別。如下:
<tx:advice id="myAdvice" transaction-manager="dataSourceTransactionManager"> <!-- 事務屬性 --> <tx:attributes> <tx:method name="buyBook" propagation="REQUIRED" isolation="READ_COMMITTED"/> </tx:attributes> </tx:advice>
五、觸發事務回滾的異常
我們上面只是說在發生錯誤時進行回滾,那麼是否可以指定只有在發生特定錯誤的情況下才能發生回滾呢?當然是可以的。
1、預設回滾異常
在預設情況下:
系統捕獲到RuntimeException或Error時回滾,而捕獲到編譯時異常不回滾。
但是現在我們可以通過某一個屬性來指定只有在發生某一個或某多個錯誤時才回滾。
2、設定特定異常下回滾
設定特定異常下回滾同樣是可以在註解中或者在XML中宣告,
(1)、通過註解設定回滾
通過註解設定回滾的話,同樣是在@Transactional註解下,有兩個屬性:
- rollbackFor屬性:指定遇到時必須進行回滾的異常型別,可以為多個
- noRollbackFor屬性:指定遇到時不回滾的異常型別,可以為多個
當設定多個的時候使用大括號{}擴住,使用逗號隔開。
如下:
@Transactional(propagation=Propagation.REQUIRED,rollbackFor={IOException.class,SQLException.class}, noRollbackFor={ArithmeticException.class,NullPointerException.class}) public void updatePrice(String isbn,int price) { bookDao.updatePrice(isbn, price); }
(2)、通過XML設定回滾
在Spring 2.x事務通知中,可以在<tx:method>元素中指定回滾規則。如果有不止一種異常則用逗號分隔。
<!-- 配置事務管理器 transaction-manager="transactionManager" 指定是配置哪個事務管理器--> <tx:advice id="myAdvice" transaction-manager="dataSourceTransactionManager"> <!-- 事務屬性 --> <tx:attributes> <tx:method name="get*" read-only="true" rollback-for="java.io.IOException, java.sql.SQLException" no-rollback-for="java.langArithmeticException"/> </tx:attributes> </tx:advice>
六、事務的超時和只讀屬性
由於事務可以在行和表上獲得鎖,因此長事務會佔用資源,並對整體效能產生影響。
如果一個事物只讀取資料但不做修改,資料庫引擎可以對這個事務進行優化。使用readOnly=true即可(面試考點,如何在獲取資料上進行優化?)
所以這裡就引入了兩個屬性:
超時事務屬性:事務在強制回滾之前可以保持多久。這樣可以防止長期執行的事務佔用資源。使用屬性timeout
只讀事務屬性: 表示這個事務只讀取資料但不更新資料, 這樣可以幫助資料庫引擎優化事務。使用屬性readOnly
設定這兩個屬性同樣是可以通過註解或者XML方式。
1、註解設定超時和只讀
通過註解設定超時和回滾的話,是在@Transactional註解下使用timeout屬性和readOnly屬性,
readOnly:只讀的,引數是boolean;型別,設定事務為只讀事務(只可以進行查詢操作,對資料庫有修改的操作不會被執行) 對事務進行優化時可以使用readOnly=true,這樣可以增加查詢速度,忽略事務相關操作
Timeout:超時,引數是int(以秒為單位),事務超出指定執行時長後自動終止並回滾,引數是“-1”(或小於0)表示永不超時。
超時時會報錯:TransactionTimedOutException: Transaction timed out:
例項程式碼如下:
@Transactional(timeout=3,readOnly=false,propagation=Propagation.REQUIRES_NEW) public void buyBook(String username,String isbn){ bookDao.updateStockFromBookStock(isbn); int price = bookDao.getPriceFromBook(isbn); bookDao.updateBalanceFromAccount(username, price); System.out.println("【" +username + "】買書成功!"); }
2、XML設定超時和只讀
在Spring 2.x事務通知中,超時和只讀屬性可以在<tx:method>元素中進行指定,同樣也是使用timeout和readOnly兩個屬性。
程式碼如下:
<!-- 配置事務管理器 transaction-manager="transactionManager" 指定是配置哪個事務管理器 --> <tx:advice id="myAdvice" transaction-manager="dataSourceTransactionManager"> <!-- 事務屬性 --> <tx:attributes> <tx:method name="get*" read-only="true" timeout="3"/> </tx:attributes> </tx:advice>
七、寫在最後
直到這裡,Spring中宣告式事務管理器的使用教程才算完全結束了,但是其中還有很多細節需要我們在實際的開發中發現。