[1] Java反射知識-->Spring IoC :http://www.iteye.com/topic/1123081
[2] Java動態代理-->Spring AOP :http://www.iteye.com/topic/1123293
[3] 屬性編輯器,即PropertyEditor-->Spring IoC:http://www.iteye.com/topic/1123628
[4] XML基礎知識-->Spring配置:http://www.iteye.com/topic/1123630
[5] 註解-->Spring配置:http://www.iteye.com/topic/1123823
[6] 線程本地變更,即ThreadLocal-->Spring事務管理:http://www.iteye.com/topic/1123824
[7] 事務基礎知識-->Spring事務管理:http://www.iteye.com/topic/1124043
[8] 國際化信息-->MVC:http://www.iteye.com/topic/1124044
[9] HTTP報文-->MVC:http://www.iteye.com/topic/1124408
Spring雖然提供了靈活方便的事務管理功能,但這些功能都是基於底層數據庫本身的事務處理機制工作的。要深入瞭解Spring的事務管理和配置,有必要先對數據庫事務的基礎知識進行學習。
何爲數據庫事務
“一榮俱榮,一損俱損”這句話很能體現事務的思想,很多複雜的事物要分步進行,但它們組成一個整體,要麼整體生效,要麼整體失效。這種思想反映到數據庫上,就是多個SQL語句,要麼所有執行成功,要麼所有執行失敗。
數據庫事務有嚴格的定義,它必須同時滿足 4 個特性:原子性(Atomic)、一致性(Consistency)、隔離性(Isolation)和持久性(Durabiliy),簡稱爲ACID。下面是對每個特性的說明。
- 原子性:表示組成一個事務的多個數據庫操作是一個不可分割的原子單元,只有所有的操作執行成功,整個事務才提交,事務中任何一個數據庫操作失敗,已經執行的任何操作都必須撤銷,讓數據庫返回到初始狀態。
- 一致性:事務操作成功後,數據庫所處的狀態和它的業務規則是一致的,即數據不會被破壞。如從A賬戶轉賬100元到B賬戶,不管操作成功與否,A和B的存款總額是不變的。
- 隔離性:在併發數據操作時,不同的事務擁有各自的數據空間,它們的操作不會對對方產生干擾。準確地說,並非要求做到完全無干擾,數據庫規定了多種事務隔離級別,不同隔離級別對應不同的干擾程度,隔離級別越高,數據一致性越好,但併發性越弱。
- 持久性:一旦事務提交成功後,事務中所有的數據操作都必須被持久化到數據庫中,即使提交事務後,數據庫馬上崩潰,在數據庫重啓時,也必須能保證能夠通過某種機制恢復數據。
在這些事務特性中,數據“一致性”是最終目標,其他的特性都是爲達到這個目標的措施、要求或手段。
數據庫管理系統一般採用重執行日誌保證原子性、一致性和持久性,重執行日誌記錄了數據庫變化的每一個動作,數據庫在一個事務中執行一部分操作後發生錯誤退出,數據庫即可以根據重執行日誌撤銷已經執行的操作。此外,對於已經提交的事務,即使數據庫崩潰,在重啓數據庫時也能夠根據日誌對尚未持久化的數據進行相應的重執行操作。
和Java程序採用對象鎖機制進行線程同步類似,數據庫管理系統採用數據庫鎖機制保證事務的隔離性。當多個事務試圖對相同的數據進行操作時,只有持有鎖的事務才能操作數據,直到前一個事務完成後,後面的事務纔有機會對數據進行操作。Oracle數據庫還使用了數據版本的機制,在回滾段爲數據的每個變化都保存一個版本,使數據的更改不影響數據的讀取。
數據併發的問題
一個數據庫可能擁有多個訪問客戶端,這些客戶端都可以併發方式訪問數據庫。數據庫中的相同數據可能同時被多個事務訪問,如果沒有采取必要的隔離措施,就會導致各種併發問題,破壞數據的完整性。這些問題可以歸結爲5類,包括3類數據讀問題(髒讀、不可重複讀和幻象讀)以及2類數據更新問題(第一類丟失更新和第二類丟失更新)。下面,我們分別通過實例講解引發問題的場景。
髒讀(dirty read)
A事務讀取B事務尚未提交的更改數據,並在這個數據的基礎上操作。如果恰巧B事務回滾,那麼A事務讀到的數據根本是不被承認的。來看取款事務和轉賬事務併發時引發的髒讀場景:
在這個場景中,B希望取款500元而後又撤銷了動作,而A往相同的賬戶中轉賬100元,就因爲A事務讀取了B事務尚未提交的數據,因而造成賬戶白白丟失了500元。在Oracle數據庫中,不會發生髒讀的情況。
不可重複讀(unrepeatable read)
不可重複讀是指A事務讀取了B事務已經提交的更改數據。假設A在取款事務的過程中,B往該賬戶轉賬100元,A兩次讀取賬戶的餘額發生不一致:
在同一事務中,T4時間點和T7時間點讀取賬戶存款餘額不一樣。
幻象讀(phantom read)
A事務讀取B事務提交的新增數據,這時A事務將出現幻象讀的問題。幻象讀一般發生在計算統計數據的事務中,舉一個例子,假設銀行系統在同一個事務中,兩次統計存款賬戶的總金額,在兩次統計過程中,剛好新增了一個存款賬戶,並存入100元,這時,兩次統計的總金額將不一致:
如果新增數據剛好滿足事務的查詢條件,這個新數據就進入了事務的視野,因而產生了兩個統計不一致的情況。
幻象讀和不可重複讀是兩個容易混淆的概念,前者是指讀到了其他已經提交事務的新增數據,而後者是指讀到了已經提交事務的更改數據(更改或刪除),爲了避免這兩種情況,採取的對策是不同的,防止讀取到更改數據,只需要對操作的數據添加行級鎖,阻止操作中的數據發生變化,而防止讀取到新增數據,則往往需要添加表級鎖——將整個表鎖定,防止新增數據(Oracle使用多版本數據的方式實現)。
第一類丟失更新
A事務撤銷時,把已經提交的B事務的更新數據覆蓋了。這種錯誤可能造成很嚴重的問題,通過下面的賬戶取款轉賬就可以看出來:
A事務在撤銷時,“不小心”將B事務已經轉入賬戶的金額給抹去了。
第二類丟失更新
A事務覆蓋B事務已經提交的數據,造成B事務所做操作丟失:
上面的例子裏由於支票轉賬事務覆蓋了取款事務對存款餘額所做的更新,導致銀行最後損失了100元,相反如果轉賬事務先提交,那麼用戶賬戶將損失100元。
數據庫鎖機制
數據併發會引發很多問題,在一些場合下有些問題是允許的,但在另外一些場合下可能卻是致命的。數據庫通過鎖的機制解決併發訪問的問題,雖然不同的數據庫在實現細節上存在差別,但原理基本上是一樣的。
按鎖定的對象的不同,一般可以分爲表鎖定和行鎖定,前者對整個表進行鎖定,而後者對錶中特定行進行鎖定。從併發事務鎖定的關係上看,可以分爲共享鎖定和獨佔鎖定。共享鎖定會防止獨佔鎖定,但允許其他的共享鎖定。而獨佔鎖定既防止其他的獨佔鎖定,也防止其他的共享鎖定。爲了更改數據,數據庫必須在進行更改的行上施加行獨佔鎖定,INSERT、UPDATE、DELETE和SELECT FOR UPDATE語句都會隱式採用必要的行鎖定。下面我們介紹一下Oracle數據庫常用的5種鎖定。
- 行共享鎖定:一般通過SELECT FOR UPDATE語句隱式獲得行共享鎖定,在Oracle中用戶也可以通過LOCK TABLE IN ROW SHARE MODE語句顯式獲得行共享鎖定。行共享鎖定並不防止對數據行進行更改的操作,但是可以防止其他會話獲取獨佔性數據表鎖定。允許進行多個併發的行共享和行獨佔性鎖定,還允許進行數據表的共享或者採用共享行獨佔鎖定。
- 行獨佔鎖定:通過一條INSERT、UPDATE或DELETE語句隱式獲取,或者通過一條LOCK TABLE IN ROW EXCLUSIVE MODE語句顯式獲取。這個鎖定可以防止其他會話獲取一個共享鎖定、共享行獨佔鎖定或獨佔鎖定。
- 表共享鎖定:通過LOCK TABLE IN SHARE MODE語句顯式獲得。這種鎖定可以防止其他會話獲取行獨佔鎖定(INSERT、UPDATE或DELETE),或者防止其他表共享行獨佔鎖定或表獨佔鎖定,它允許在表中擁有多個行共享和表共享鎖定。該鎖定可以讓會話具有對錶事務級一致性訪問,因爲其他會話在用戶提交或者回溯該事務並釋放對該表的鎖定之前不能更改這個被鎖定的表。
- 表共享行獨佔:通過LOCK TABLE IN SHARE ROW EXCLUSIVE MODE語句顯式獲得。這種鎖定可以防止其他會話獲取一個表共享、行獨佔或者表獨佔鎖定,它允許其他行共享鎖定。這種鎖定類似於表共享鎖定,只是一次只能對一個表放置一個表共享行獨佔鎖定。如果A會話擁有該鎖定,則B會話可以執行SELECT FOR UPDATE操作,但如果B會話試圖更新選擇的行,則需要等待。
- 表獨佔:通過LOCK TABLE IN EXCLUSIVE MODE顯式獲得。這個鎖定防止其他會話對該表的任何其他鎖定。
事務隔離級別
儘管數據庫爲用戶提供了鎖的DML操作方式,但直接使用鎖管理是非常麻煩的,因此數據庫爲用戶提供了自動鎖機制。只要用戶指定會話的事務隔離級別,數據庫就會分析事務中的SQL語句,然後自動爲事務操作的數據資源添加上適合的鎖。此外數據庫還會維護這些鎖,當一個資源上的鎖數目太多時,自動進行鎖升級以提高系統的運行性能,而這一過程對用戶來說完全是透明的。
ANSI/ISO SQL 92標準定義了4個等級的事務隔離級別,在相同數據環境下,使用相同的輸入,執行相同的工作,根據不同的隔離級別,可以導致不同的結果。不同事務隔離級別能夠解決的數據併發問題的能力是不同的,如表9-1所示。
事務的隔離級別和數據庫併發性是對立的,兩者此增彼長。一般來說,使用READ UNCOMMITED隔離級別的數據庫擁有最高的併發性和吞吐量,而使用SERIALIZABLE隔離級別的數據庫併發性最低。
SQL 92定義READ UNCOMMITED主要是爲了提供非阻塞讀的能力,Oracle雖然也支持READ UNCOMMITED,但它不支持髒讀,因爲Oracle使用多版本機制徹底解決了在非阻塞讀時讀到髒數據的問題並保證讀的一致性,所以,Oracle的READ COMMITTED隔離級別就已經滿足了SQL 92標準的REPEATABLE READ隔離級別。
SQL 92推薦使用REPEATABLE READ以保證數據的讀一致性,不過用戶可以根據應用的需要選擇適合的隔離等級。
JDBC對事務支持
並不是所有的數據庫都支持事務,即使支持事務的數據庫也並非支持所有的事務隔離級別,用戶可以通過Connection#getMetaData()方法獲取DatabaseMetaData對象,並通過該對象的supportsTransactions()、supportsTransactionIsolationLevel(int level)方法查看底層數據庫的事務支持情況。
Connection默認情況下是自動提交的,也即每條執行的SQL都對應一個事務,爲了能夠將多條SQL當成一個事務執行,必須先通過Connection#setAutoCommit(false)阻止Connection自動提交,並可通過Connection#setTransactionIsolation()設置事務的隔離級別,Connection中定義了對應SQL 92標準4個事務隔離級別的常量。通過Connection#commit()提交事務,通過Connection#rollback()回滾事務。下面是典型的JDBC事務數據操作的代碼:
代碼清單9-1 JDBC事務代碼
- Connection conn ;
- try{
- conn = DriverManager.getConnection();//①獲取數據連接
- conn.setAutoCommit(false); //②關閉自動提交的機制
- conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE); //③設置事務隔離級別
- Statement stmt = conn.createStatement();
- int rows = stmt.executeUpdate( "INSERT INTO t_topic VALUES(1,’tom’) " );
- rows = stmt.executeUpdate( "UPDATE t_user set topic_nums = topic_nums +1 "+
- "WHERE user_id = 1");
- conn.commit();//④提交事務
- }catch(Exception e){
- …
- conn.rollback();//⑤回滾事務
- }finally{
- …
- }
Connection conn ;
try{
conn = DriverManager.getConnection();//①獲取數據連接
conn.setAutoCommit(false); //②關閉自動提交的機制
conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE); //③設置事務隔離級別
Statement stmt = conn.createStatement();
int rows = stmt.executeUpdate( "INSERT INTO t_topic VALUES(1,’tom’) " );
rows = stmt.executeUpdate( "UPDATE t_user set topic_nums = topic_nums +1 "+
"WHERE user_id = 1");
conn.commit();//④提交事務
}catch(Exception e){
…
conn.rollback();//⑤回滾事務
}finally{
…
}
在JDBC 2.0中,事務最終只能有兩個操作:提交和回滾。但是,有些應用可能需要對事務進行更多的控制,而不是簡單地提交或回滾。JDBC 3.0(JDK 1.4及以後的版本)引入了一個全新的保存點特性,Savepoint 接口允許用戶將事務分割爲多個階段,用戶可以指定回滾到事務的特定保存點,而並非像JDBC 2.0一樣只回滾到開始事務的點,如圖9-1所示。
下面的代碼使用了保存點的功能,在發生特定問題時,回滾到指定的保存點,而非回滾整個事務,如代碼清單9-2所示:
代碼清單9-2 使用保存點的事務代碼
- …
- Statement stmt = conn.createStatement();
- int rows = stmt.executeUpdate( "INSERT INTO t_topic VALUES(1,’tom’)");
- Savepoint svpt = conn.setSavepoint("savePoint1");//①設置一個保存點
- rows = stmt.executeUpdate( "UPDATE t_user set topic_nums = topic_nums +1 "+
- "WHERE user_id = 1");
- …
- //②回滾到①處的savePoint1,①之前的SQL操作,在整個事務提交後依然提交,
- //但①到②之間的SQL操作被撤銷了
- conn.rollback(svpt);
- …
- conn.commit();//③提交事務
…
Statement stmt = conn.createStatement();
int rows = stmt.executeUpdate( "INSERT INTO t_topic VALUES(1,’tom’)");
Savepoint svpt = conn.setSavepoint("savePoint1");//①設置一個保存點
rows = stmt.executeUpdate( "UPDATE t_user set topic_nums = topic_nums +1 "+
"WHERE user_id = 1");
…
//②回滾到①處的savePoint1,①之前的SQL操作,在整個事務提交後依然提交,
//但①到②之間的SQL操作被撤銷了
conn.rollback(svpt);
…
conn.commit();//③提交事務
並非所有數據庫都支持保存點功能,用戶可以通過DatabaseMetaData#supportsSavepoints()方法查看是否支持。