1. 程式人生 > >JDBC和資料庫事務詳解(一)

JDBC和資料庫事務詳解(一)

現在還在寫JDBC事務的文章,我覺得我一定是相當的Out了,現在主流的java應用,框架都是分散式的,各種分散式的事務,或者容器事務才是需要學習的重點,在這裡談JDBC確實有點不合時宜,但任何的java 開發人員,如果不能夠深入的理解資料庫的事務,那在做資料處理的方面就一定是有所欠缺的,另外確實很少有文章能夠談到JDBC和資料庫事務的精髓,希望這裡能夠讓你深度的瞭解到什麼是JDBC的事務以及它和資料庫的關係。

事務

事務應該說是資料庫最核心的能力之一,對於任何和資料打交道的開發人員而言,是非常重要的

事務的原子性

事務的最基本功能是原子性。比如張三給李四異地打錢5000元,假設同一銀行異地手續費是5‰,那麼資料庫要幹三件事情
張三的賬戶餘額扣除5025(含5‰手續費,中國特色)
李四的賬戶餘額增加5000
銀行自己的賬戶餘額增加25
這三件事情要麼全部成功,要麼全部失敗,絕對不能一些成功,一些失敗。
本地事務
對上面提出的問題,可以用一下程式碼簡單示範

    String sql = 
“update Account set Balance = Balance + ? where id=?”
    try (Connection con = dataSource.getConnection();
PreparedStatement pstmtForSource = con.preparedStatement(sql);
PreparedStatement pstmtForTarget = con.preparedStatement(sql);
PreparedStatement pstmtForBlank = con.preparedStatement(sql)) {
        con.setAutoCommit(false
); //關閉自動提交,手動事務開始 pstmtForSource.setInt(1, -5025); pstmtForSource.setLong(2, sourceAccountId); pstmtForSource.executeUpdate(); pstmtForTarget.setInt(1, +5000); pstmtForTarget.setLong(2, targetAccountId); pstmtForTarget.executeUpdate(); pstmtForBank.setInt(1
, +25); pstmtForBank.setLong(2, 1L);銀行自己卡號為1 pstmtForBank.executeUpdate(); con.commit(); //提交事務 } catch (SQLException | RuntimeException | Error ex) { con.rollback(); //回滾事務 throw ex; //不要忽略,繼續丟擲,讓ATM介面層報錯 }

資料庫連線使用setAutoCommit(false)來開始一個事務,此所做的所有事情都是原子性事務的一部分,最後一件事情做完後,呼叫con.commit來提交事務。如果整個過程有任何異常發生,可以呼叫con.rollback()來撤銷已經被執行的那部分修改。
資料庫連線的自動提交預設為true,自動提交為true的意思就是每句SQL執行完成後,資料庫都會自動根據成功與否來提交或回滾。這是毫無意義的,事務的原子性只有對多個操作而言才有意義,要麼全部成功要麼全部失敗這句話本身就隱含整個過程還有多個SQL操作的意思。所謂,預設的自動提交也可以理解成無事務的意思。
一旦setAutoCommit(false);就表示資料庫開啟一個需要手動提交或回滾的事務,從這句話開始,一直往後,到最接近的commit或rollback呼叫的程式碼之間,所執行的任何SQL修改都作為一個不可分割的一個整體,那理論性點的話說,就是一個原子。原子中所有語句要麼都成功,要麼都失敗。
特殊地,如果因為網路故障、客戶端崩潰或者資料庫本身崩潰而導致既沒有commit也沒有rollback。等資料庫察覺到這個異常情況後,都視為rollback。
一旦commit或rollback之後,下一個的事務又自動開始了。當前事務的最終結果已經成事實了,板上釘釘了。更後面的提交或回滾的呼叫只針對下一個事務。從這裡,你也可以往下延伸,即同一個connection 上可以執行多個事務,在connection close之前,你有多少個commit就代表你提交了多少個事務。

儲存點

資料庫事務回滾預設是整體回滾,即回滾到事務剛開始的地方,這樣做是為了保證原子性。但資料庫也提供一種故意破壞原子性的功能,叫做儲存點(Save Point),儲存點可以使用專用的SQL語句當前事務添加註冊。事務開始後,新增儲存點的SQL和操作資料的SQL可以任意混合地不斷執行,但在當前事務範圍內,各儲存點的名稱必須唯一,這樣,多個儲存點可以把很多個數據操作SQL的分成很多小段。最後可以使用指定一個儲存點名稱的rollback操作,這樣,就可以回滾到新增那個儲存點的SQL的位置,而不是預設的全部回滾。
資料庫支援此功能,JDBC也支援暴露資料庫的這個能力,所以大家還是有必要了解這個概念。但說實話,用得非常少,應用場景不多。

扁平事務和巢狀事務

對於所有資料庫而言,針對一個連線,事務的扁平結構是預設結構,結束上一個事務隱含了下一個事務的開始。事務總是被開始、結束、開始、結束,同一時刻,一個連線頂多能開啟一個事務。這種事務模型為扁平事務。
而對少數資料庫而言,針對一個連線,事務總是被開始、開始、結束、結束,但可能需要該資料產品特有的特殊的SQL命令。這是開啟了一個父事務和子事務,父事務和子事務各自遵循自己的原子性,雙方的提交回滾彼此不干擾。這就是巢狀事務。這個概念,有點類似spring裡面的Nested事務,但這裡是資料庫層面的,而且是針對同一個連線,對於絕大多數僅僅支援扁平事務的資料庫而言,可以讓當前執行緒建立兩個不同的資料庫連線,然後在兩個不同的連線上各開啟一個事務,屬於不同連線的不同事務各自遵循自己的原子性,各自的提交回滾彼此不干擾。這是扁平事務資料庫模擬巢狀事務的一個經典用法。也是事務傳播屬性裡,require new和nested的實現原理。

資料庫事務實現大致原理

以Oracle為例,Oracle資料都儲存在表空間上,表空間裡面有一個段,叫做Undo段,在一個事務中,所進行的所有增刪改操作被實施之前,都先要按照嚴格的順序在Undo段保持每條記錄的舊資料(對於INSERT操作而言,舊資料為空),這樣這對資料修改之前,Undo段就保證備份了所有被操作記錄的原資料。如果最終被提交,清空Undo段中的資料,如果最終rollback,則按照Undo中事先備份好的原資料進行逆向操作,每完成一項逆向操作,就清除一部分Undo資料,最後全部回滾後,Undo段的資料也被清空了。
如果網路掉線或客戶端崩潰,一定超時後,資料庫能發現超時的“死連結”,資料庫會清除死連結,並且解開死連線所持有的鎖,並且根據和死連線相關聯的Undo段資料開始逆向操作以撤銷修改。
如果資料庫本身崩潰、資料庫所在作業系統奔潰、伺服器硬體故障或者伺服器停電導致資料庫死掉。人工採取恢復措施(例如換主機板、或想辦法恢復電力供給)後重啟資料庫,剛重啟的資料庫會拒絕所有客戶的連線申請,專心看儲存介質上是否有Undo資料,如果有,開始撤銷,每撤銷一點就清除一點Undo資料。考慮更極端一點,如果在撤銷了一部分後,資料庫又出問題,那麼大不了再重啟一次再來,反正還沒有被用於逆操作的Undo資料還在,當所有的Undo資料被全部清空後,意味著所有的未提交操作全部非法資料都被逆操作了。這是標誌著資料庫得以全部恢復,自此,資料庫伺服器才開始接受外界申請連線,進入正常的服務狀態。
總之,只要儲存資料的儲存介質本身沒有損壞,無論多極端的軟體或硬體故障,資料庫一定能回滾。而事實上,儲存介質本身也很可能有硬體層面的有映象容錯能力,這就如虎添翼,更完美了。

Undo段故障

如果啟動一個過於龐大的事務,事務開始之後到提交之前的修改行為過於海量,當會導致Oracle表空間Undo段所允許儲存資源被耗盡,此時應用程式會得到異常。出現這個問題後,要仔細分析問題,辨別是應用程式寫得太二(比如可以用小一點的事務實現同樣的功能)還是資料庫配置太二。最終決定由開發人員改應用程式還是由DBA改資料庫軟硬體設定。

事務隔離級別

上面所講的事務的原子性,是對多條修改SQL具備意義。對於讀操作,事務同樣具備重大意義,這就是事務隔離級別
SQL標準定義了4類隔離級別,包括了一些具體規則,用來限定事務內外的哪些改變是可見的,哪些是不可見的。低級別的隔離級一般支援更高的併發處理,並擁有更低的系統開銷。

Read Uncommitted(讀取未提交內容)

特別提醒,Oracle不支援此級別!在該隔離級別,所有事務都可以看到其他未提交事務的執行結果。本隔離級別很少用於實際應用,因為它的效能也不比其他級別好多少,但讀取到的資料極其不靠譜。讀取未提交的資料,可能前腳剛讀到別人修改但未提交的資料,後腳資料就被別人回滾撤銷了,自己讀到了一份完全無效的資料還渾然不知,這種最無節操的問題稱之為髒讀(Dirty Read)。

Read Committed(讀取提交內容)

這是大多數資料庫系統的預設隔離級別(但不是MySQL預設的)。這個級別可以解決髒讀(Dirty Read)的問題,一個事務只能看見已經提交事務所做的改變,如果其它事務反覆修改資料,當前事務多次讀取同一條資料每次會讀到不同的資料,這種現象叫做不可重複讀(Nonrepeatable Read)。

Repeatable Read(可重讀)

特別提醒,Oracle不支援此級別!這是MySQL的預設事務隔離級別。這個級別可以解決不可重複讀的(Nonrepeatable Read)問題。它確保同一事務的多次同一條資料的時候,每次會看到同樣的資料行。 但是其它事務任然還是可以新增和刪除同一張表的其它資料,導致當前事務反覆看這張表的記錄總條數,有時變多有時變少,就如同看街上閃爍的霓虹燈一樣,這種問題叫做幻讀(Phantom Read)

Serializable(序列化讀)

這是最高的隔離級別,連幻讀(Phantom Read)問題也被解決了。所有企圖操作同一張表(無論讀寫)的事務必須割捨掉所有併發性,序列化地排隊。對一張表而言,此級別完全不具備任何併發性,讀取到的資料絕對可靠。

隔離級別表格總結

這裡寫圖片描述
越靠上,讀取到的資料越不嚴密,但併發度越高。
越靠下,讀取到的資料越嚴密,但併發度越低下。
典型的魚和熊掌難以兼得的問題,就連資料庫製造商自己都覺得難以取捨,就給了這個4檔變速箱,開發人員根據實際路況(專案具體情況)自己選。

隔離級別基本原理

由於部分資料庫對4種級別支援得未必全,比如Oracle就僅僅支援兩個級別,而且每種資料庫的實現細節會稍微有所差異,所以我們講解一種理論上最簡實現原理。實際資料庫實現完整隔離級別的原理只能比這個模型更復雜,不能更簡單。