數據並發的問題
一個數據庫可能擁有多個訪問客戶端,這些客戶端都可以並發方式訪問數據庫。數據庫中的相同數據可能同時被多個事務訪問,如果沒有采取必要的隔離措施,就會導致各種並發問題,破壞數據的完整性。這些問題可以歸結為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{
- …
- }
在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();//③提交事務
並非所有數據庫都支持保存點功能,用戶可以通過DatabaseMetaData#supportsSavepoints()方法查看是否支持。