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);銀行自己卡號為1pstmtForBank.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 就僅僅支持兩個級別,而且每種數據庫的實現細節會稍微有所差異,所以我們講解一種理論上最簡實現原理。實際數據庫實現完整隔離級別的原理只能比這個模型更復雜,不能更簡單。
行鎖
假設每個數據行支持兩種鎖 RS 和 RX;RS 表示 Row Share,行共享鎖,不同的連接可以對同一行記錄同時上 RS 鎖,即行共享鎖,多個連接被允許同時對一條記錄上共享鎖;RX 表示 Row Exclusive,即行排它鎖,只能有一個連接可以對一行記錄上 RX 鎖。另外,鎖可以升級,如果期望給一行數據上 RX 鎖而當前行已經存在一個 RS 鎖,那么RS所會升級成RX鎖。但是反過來,鎖不能降低級,如果已經存在 RX鎖,希望上一個 RS 鎖,那么必須等待解鎖。
已存在行鎖 期望新加行鎖 執行方式
null RS 成功,加上RS鎖
null RX 成功,加上RX鎖
RS RS 成功,因為RS是共享的,多個連接可同時鎖
RS RX 成功,因為RS鎖支持升級為RX鎖
RX(其它連接加的) RS 等待解鎖再上鎖,因為RX是排它的,可能超時
RX(同一連接加的) RS 忽略操作直接完成,鎖保持RX不變
RX(其它連接加的) RX 等待解鎖再上鎖,因為RX是排它的,可能超時
RX(同一連接加的) RX 忽略操作直接完成,鎖保持RX不變
表鎖
類似地,對一張表級別的鎖而言, 也有兩種鎖 TS 和 TX,工作原理 RS, RX 非常類似,不再描述
修改語句和悲觀鎖查詢語句的行鎖和表鎖
對于修改語句,典型如下:
INSERT INTO MY_TABLE(C1, C2, …, CN) VALUES(V1, V2, …, VN); UPDATE MY_TABLE SET C1 = V1, C2 = V2, … CN = VN WHERE C1 = OV1; DELETE FROM MY_TABLE WHERE C1 = V1;他們所對應的鎖行為都是:
任何隔離級別下的悲觀鎖查詢
SELECT … FOR UPDATE均如此工作:
可以發現,悲觀鎖查詢和類似修改語句
普通查詢的行鎖和表鎖
普通查詢語句在不同的隔離級別下工作機制不一樣
在一般的數據庫事務中,一個事務就代表著一個鏈接,事務的隔離級別既是鏈接的隔離級別,不同的鎖行為即代表了不同的執行效率,這點是需要大家透徹理解的。
總結
以上是生活随笔為你收集整理的JDBC和数据库事务详解的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 数智化巩固智慧变电站“防汛墙”
- 下一篇: Android Studio中进行单元测