概要
過度
我們前面介紹了Spring的基礎知識后,介紹了Spring對一些常用框架api的封裝:
- jdbc
- mybatis
這里主要介紹了對持久化層中的sql語句的封裝,使我們避免直接手寫sql,接下來介紹一下Spring對sql的事務的封裝,這能讓我們避免使用事務時的復雜設置操作。
內容簡介
本文分兩部分。
第一部分主要介紹事務的基礎知識,包括:
- 是什么(事務的定義、特點)
- 為什么(現(xiàn)實問題)
- 怎么做(api調用)
emmmmm,是不是有點粗糙,有點像是初中政治湊字數(shù)。但是我覺得把這三點大概熟悉之后,我們在工作中日常使用事務應該問題不大了。至于深入,等后面有時間再說吧。
第二部分主要介紹事務api直接使用時的一些不方便的地方,并引出Spring對事務api的封裝。
后面會在本文中提出的Spring對事務api封裝的基礎上進行spring-tx組件的介紹。
所屬環(huán)節(jié)
Spring-tx 組件引入
上下環(huán)節(jié)
上文: Spring 框架基本實現(xiàn)介紹、Spring基本的持久化框架介紹
下文: Spring-tx框架詳細介紹
事務基礎知識
事務介紹及基本特點
事務在計算機術語中是指訪問并可能更新數(shù)據(jù)庫中各種數(shù)據(jù)項的一個程序執(zhí)行單元(unit)
上面是拷貝自百度知道的一句話,我覺得其中 執(zhí)行單元 這個用詞比較準確。執(zhí)行單元有以下特點:要么成功要么失敗,不存在執(zhí)行到一半的情況。
事務有以下特點:
- 原子性(Atomic)
- 一致性(Consistency)
- 隔離性(Isolation)
- 持久性(Durability)
對各個性質簡要記錄如下:
原子性
和最上面拷貝的一句話差不多,事務是一個執(zhí)行單元,要么成功要么失敗,不存在成功一半的可能
一致性
事務中的修改狀態(tài)的操作對外部的可見是語義上有意義的狀態(tài),符合數(shù)據(jù)庫和業(yè)務的相關約束
一致性和原子性的區(qū)別在于:原子性強調的是事務執(zhí)行的不可分割性,要么成功要不失敗,不存在執(zhí)行一半的情況;一致性強調的是狀態(tài)的對外暴露,及數(shù)據(jù)的可見性,只有最初和最終的狀態(tài)是可見的
隔離性
并發(fā)執(zhí)行的事務會排除彼此的影響。
對于隔離性,數(shù)據(jù)庫提供了多種隔離級別:
Read Uncommitted (讀取未提交內容)
在該隔離級別,所有事務都可以看到其他未提交事務的執(zhí)行結果。本隔離級別很少用于實際應用,因為它的性能也不比其他級別好多少。讀取未提交的數(shù)據(jù),也被稱之為臟讀(Dirty Read)。
Read Committed (讀取提交內容)
這是大多數(shù)數(shù)據(jù)庫系統(tǒng)的默認隔離級別(但不是MySQL默認的)。它滿足了隔離的簡單定義:一個事務只能看見已經(jīng)提交事務所做的改變。這種隔離級別 也支持所謂的不可重復讀(Nonrepeatable Read),因為同一事務的其他實例在該實例處理其間可能會有新的commit,所以同一select可能返回不同結果。
Repeatable Read (可重讀)
這是MySQL的默認事務隔離級別,它確保同一事務的多個實例在并發(fā)讀取數(shù)據(jù)時,會看到同樣的數(shù)據(jù)行。不過理論上,這會導致另一個棘手的問題:幻讀 (Phantom Read)。簡單的說,幻讀指當用戶讀取某一范圍的數(shù)據(jù)行時,另一個事務又在該范圍內插入了新行,當用戶再讀取該范圍的數(shù)據(jù)行時,會發(fā)現(xiàn)有新的“幻影” 行。InnoDB和Falcon存儲引擎通過多版本并發(fā)控制(MVCC,Multiversion Concurrency Control)機制解決了該問題。
Serializable (可串行化)
這是最高的隔離級別,它通過強制事務排序,使之不可能相互沖突,從而解決幻讀問題。簡言之,它是在每個讀的數(shù)據(jù)行上加上共享鎖。在這個級別,可能導致大量的超時現(xiàn)象和鎖競爭。這四種隔離級別采取不同的鎖類型來實現(xiàn),若讀取的是同一個數(shù)據(jù)的話,就容易發(fā)生問題。
例如:
臟讀(Drity Read):某個事務已更新一份數(shù)據(jù),另一個事務在此時讀取了同一份數(shù)據(jù),由于某些原因,前一個RollBack了操作,則后一個事務所讀取的數(shù)據(jù)就會是不正確的。
不可重復讀(Non-repeatable read):在一個事務的兩次查詢之中數(shù)據(jù)不一致,這可能是兩次查詢過程中間插入了一個事務更新的原有的數(shù)據(jù)【一般是修改】。
幻讀(Phantom Read):在一個事務的兩次查詢中數(shù)據(jù)不一致,幻讀是重點在插入和刪除,不可重復讀重點在修改
隔離級別 臟讀 不可重復讀 幻讀 讀未提交 是 是 是 讀提交 否 是 是 可重復讀 否 否 是 可串行化 否 否 否
一般情況下,MySQL的事務隔離級別是可重復讀,也就是說會鎖行,一個事務中兩次讀到的同一行數(shù)據(jù)是不會變化【被修改的】,但是兩次讀取可能會多出來或者少新的數(shù)據(jù)行。
持久性
事務在執(zhí)行完成提交后,造成的改動會被持久化保存。
使用事務的原因
默認情況下,數(shù)據(jù)庫執(zhí)行一條SQL語句就是一個原子的操作,但是很多時候業(yè)務邏輯要求我們將幾個操作綁定在一起,比如最經(jīng)典的“轉賬案例”:A賬戶扣減100元和B賬戶打入100元的操作是不可分割的。對于這種單個流程操作的整合,我們需要使用事務。
當然,數(shù)據(jù)庫事務和分布式事務的使用場景還是不太一樣的:
數(shù)據(jù)庫事務是用來鎖一臺機器的,也就是說事務是保證這一臺機器的操作具有不可分割、讀取的每一個狀態(tài)都是一致的這種效果?!臼褂媚J狀態(tài)】【具體是鎖的機器還是和線程對應的
connector,這個沒弄特別透徹】在多個機器并發(fā)的情況下很容易出問題,所以我們在工作中一般使用的是分布式鎖,保證關鍵事務能鎖住整個應用,同時其他的事務能正常執(zhí)行。
事務API
我們先看一個普通的jdbc調用:
public static void main(String args[]) throws ClassNotFoundException, SQLException {
// 在 com.mysql.cj.jdbc.Driver 類的靜態(tài)代碼塊中進行了DriverManager的注冊。
Class.forName("com.mysql.cj.jdbc.Driver");
Connection connection = DriverManager.getConnection("jdbc:mysql:", "", "");
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery("select * from form");
while (resultSet.next()) {
String x = resultSet.getString(1);
System.out.println("x=" + x);
}
}
基本就是獲得connection,得到statement,執(zhí)行,關閉幾個步驟。
MySQL默認是將執(zhí)行的sql丟進去,他會自行執(zhí)行并提交,一個SQL就是一個事務,所以如果我們想自己控制事務提交就要做如下改動:
- 禁止
Connection自動提交事務 - 如果有可回滾的小步驟,自行設置保存點,并在執(zhí)行后根據(jù)情況決定是否回滾至保存點
- 在執(zhí)行完整體的事務之后,看情況回滾還是提交
- 執(zhí)行結束后關閉或者釋放
Connection
所以使用事務API的demo如下:
public static void main(String args[]) throws ClassNotFoundException, SQLException {
// 在 com.mysql.cj.jdbc.Driver 類的靜態(tài)代碼塊中進行了DriverManager的注冊。
Class.forName("com.mysql.cj.jdbc.Driver");
Connection connection = DriverManager.getConnection("jdbc:mysql://", "", "");
// 設置事務
connection.setAutoCommit(false);
PreparedStatement statement = connection.prepareStatement(sql_a);
statement.setString(1,"lpc create 2");
statement.setInt(2,1);
statement.setString(3,"lpc modify2");
statement.setLong(4,123L);
statement.setString(5,"form name2");
statement.setLong(6,123L);
statement.setString(7,"aaaaa2");
statement.execute();
Savepoint savepoint = connection.setSavepoint();
PreparedStatement statement1 = connection.prepareStatement(sql_a);
statement1.setString(1,"lpc create 1");
statement1.setInt(2,1);
statement1.setString(3,"lpc modify 1");
statement1.setLong(4,123L);
statement1.setString(5,"form name 1");
statement1.setLong(6,123L);
statement1.setString(7,"aaaaa 1");
statement1.execute();
// 回滾至保存點【保存點之后的修改作廢】
connection.rollback(savepoint);
// 整體事務執(zhí)行完成,提交【當然這里也可以回滾】
connection.commit();
// 關閉
statement.close();
connection.close();
}
事務API的使用規(guī)律及不足
不足
這個和前面的問題相似,就是存在大量的廢代碼,比如:
- 加載jdbc著一系列模版代碼,還有
Connection和Statement的創(chuàng)建關閉啥的一大堆東西 - 事務的管理和
Connection相關,如果有多層子函數(shù)調用,可能要到處傳Connection - 安全點的相關創(chuàng)建管理也特變容易亂
規(guī)律
首先:
- jdbc的相關東西我們在前面都實現(xiàn)了相關的操作
所以我們專門關注事務的就行了,我們很容易發(fā)現(xiàn)
- 安全點的創(chuàng)建和回滾是對應的,從回滾往上找到最近的一個安全點即可
- 事務的提交/回滾和事務創(chuàng)建也是對應的,從提交/回滾往上找最近的也就可以了
這個操作過程是不是特別像棧。我們后面是否可以考慮用AOP對原有業(yè)務做一個較小侵入式的事務功能支持!
Spring 對事務的封裝
Spring 實現(xiàn)的demo
Spring 配置的xml:
<!--增加對事務的聲明式支持-->
<tx:annotation-driven/>
<!--注冊事務-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
代碼:
在需要使用事務的地方【接口、方法、具體類】打上注解@Transactional(propagation = Propagation.XXX)
其中:
Propagation.REQUIRED:如果有事務就復用,沒有就新建
Propagation.SUPPORTS:如果有事務就復用,沒有就不在事務中跑
Propagation.MANDATORY:必須在事務中跑,沒有就拋出異常
Propagation.REQUIRES_NEW:必須在新事務中跑,如果當前有事務就先阻塞掉當前事務
Propagation.NOT_SUPPORTED:必須不在事務中跑,如果當前有事務就先阻塞掉當前事務
Propagation.NEVER:必須不在事務中跑,如果當前有事務就拋出異常
Propagation.NESTED:如果當前有事務,就復用,如果沒有就拋出異常
這些感覺不用記,有需要看api的注釋就行,主要是后面過代碼時用。
猜測實現(xiàn)思路
我們前面已經(jīng)了解了Spring框架的基本工作原理,大概熟悉了他的主要功能,根據(jù)使用方法我們可以大概反推一下Spring-tx的實現(xiàn)過程:
- 使用了
<tx:annotation-driven/>標簽,和前面的MyBatis思路相似,應該是注冊一個BeanFactory的后處理器,它在完成注冊會被ApplicationContext調用,新創(chuàng)建一個實現(xiàn)了Advisor接口的含有事務邏輯的增強器。 - 在新創(chuàng)建的含有事務邏輯的增強器中會依賴我們注冊的
transactionManager,并在執(zhí)行到增強器時進行注解內容的讀取,并根據(jù)注解配置執(zhí)行指定的事務操作。
因為我們的AOP切面恰好和方法調用堆棧一樣,正好契合了事務的安全點、阻塞+恢復、回滾、提交,所以上面這個思路是可行的。
擴展
問題遺留
參考文獻
事務特性:https://www.cnblogs.com/dooor/p/5303904.html
幻想讀和不可重讀讀讀區(qū)別:https://www.cnblogs.com/xiaohanlin/p/8644749.html