JDBC和数据库事务详解
現(xiàn)在還在寫 JDBC 事務(wù)的文章,我覺得我一定是相當(dāng)?shù)?Out 了,現(xiàn)在主流的 java 應(yīng)用,框架都是分布式的,各種分布式的事務(wù),或者容器事務(wù)才是需要學(xué)習(xí)的重點,在這里談 JDBC 確實有點不合時宜,但任何的 java 開發(fā)人員,如果不能夠深入的理解數(shù)據(jù)庫的事務(wù),那在做數(shù)據(jù)處理的方面就一定是有所欠缺的,另外確實很少有文章能夠談到 JDBC 和數(shù)據(jù)庫事務(wù)的精髓,希望這里能夠讓你深度的了解到什么是 JDBC 的事務(wù)以及它和數(shù)據(jù)庫的關(guān)系。
事務(wù)
事務(wù)應(yīng)該說是數(shù)據(jù)庫最核心的能力之一,對于任何和數(shù)據(jù)打交道的開發(fā)人員而言,是非常重要的
事務(wù)的原子性
事務(wù)的最基本功能是原子性。比如張三給李四異地打錢5000元,假設(shè)同一銀行異地手續(xù)費是5‰,那么數(shù)據(jù)庫要干三件事情
- 張三的賬戶余額扣除5025(含5‰手續(xù)費,中國特色)
- 李四的賬戶余額增加5000
- 銀行自己的賬戶余額增加25
這三件事情要么全部成功,要么全部失敗,絕對不能一些成功,一些失敗。
本地事務(wù)
對上面提出的問題,可以用一下代碼簡單示范
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); //關(guān)閉自動提交,手動事務(wù)開始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(); //提交事務(wù) } catch (SQLException | RuntimeException | Error ex) {con.rollback(); //回滾事務(wù)throw ex; //不要忽略,繼續(xù)拋出,讓ATM界面層報錯 }數(shù)據(jù)庫連接使用 setAutoCommit(false) 來開始一個事務(wù),此所做的所有事情都是原子性事務(wù)的一部分,最后一件事情做完后,調(diào)用 con.commit 來提交事務(wù)。如果整個過程有任何異常發(fā)生,可以調(diào)用 con.rollback() 來撤銷已經(jīng)被執(zhí)行的那部分修改。
數(shù)據(jù)庫連接的自動提交默認(rèn)為 true,自動提交為 true 的意思就是每句 SQL 執(zhí)行完成后,數(shù)據(jù)庫都會自動根據(jù)成功與否來提交或回滾。這是毫無意義的,事務(wù)的原子性只有對多個操作而言才有意義,要么全部成功要么全部失敗這句話本身就隱含整個過程還有多個 SQL 操作的意思。所謂,默認(rèn)的自動提交也可以理解成無事務(wù)的意思。
一旦 setAutoCommit(false);就表示數(shù)據(jù)庫開啟一個需要手動提交或回滾的事務(wù),從這句話開始,一直往后,到最接近的 commit 或 rollback 調(diào)用的代碼之間,所執(zhí)行的任何 SQL 修改都作為一個不可分割的一個整體,那理論性點的話說,就是一個原子。原子中所有語句要么都成功,要么都失敗。
特殊地,如果因為網(wǎng)絡(luò)故障、客戶端崩潰或者數(shù)據(jù)庫本身崩潰而導(dǎo)致既沒有commit也沒有rollback。等數(shù)據(jù)庫察覺到這個異常情況后,都視為 rollback。
一旦 commit 或 rollback 之后,下一個的事務(wù)又自動開始了。當(dāng)前事務(wù)的最終結(jié)果已經(jīng)成事實了,板上釘釘了。更后面的提交或回滾的調(diào)用只針對下一個事務(wù)。從這里,你也可以往下延伸,即同一個 connection 上可以執(zhí)行多個事務(wù),在 connection close 之前,你有多少個 commit 就代表你提交了多少個事務(wù)。
保存點
數(shù)據(jù)庫事務(wù)回滾默認(rèn)是整體回滾,即回滾到事務(wù)剛開始的地方,這樣做是為了保證原子性。但數(shù)據(jù)庫也提供一種故意破壞原子性的功能,叫做保存點(Save Point),保存點可以使用專用的 SQL 語句當(dāng)前事務(wù)添加注冊。事務(wù)開始后,添加保存點的 SQL 和操作數(shù)據(jù)的 SQL 可以任意混合地不斷執(zhí)行,但在當(dāng)前事務(wù)范圍內(nèi),各保存點的名稱必須唯一,這樣,多個保存點可以把很多個數(shù)據(jù)操作 SQL 的分成很多小段。最后可以使用指定一個保存點名稱的 rollback 操作,這樣,就可以回滾到添加那個保存點的 SQL 的位置,而不是默認(rèn)的全部回滾。
數(shù)據(jù)庫支持此功能,JDBC 也支持暴露數(shù)據(jù)庫的這個能力,所以大家還是有必要了解這個概念。但說實話,用得非常少,應(yīng)用場景不多。
扁平事務(wù)和嵌套事務(wù)
對于所有數(shù)據(jù)庫而言,針對一個連接,事務(wù)的扁平結(jié)構(gòu)是默認(rèn)結(jié)構(gòu),結(jié)束上一個事務(wù)隱含了下一個事務(wù)的開始。事務(wù)總是被開始、結(jié)束、開始、結(jié)束,同一時刻,一個連接頂多能開啟一個事務(wù)。這種事務(wù)模型為扁平事務(wù)。
而對少數(shù)數(shù)據(jù)庫而言,針對一個連接,事務(wù)總是被開始、開始、結(jié)束、結(jié)束,但可能需要該數(shù)據(jù)產(chǎn)品特有的特殊的 SQL 命令。這是開啟了一個父事務(wù)和子事務(wù),父事務(wù)和子事務(wù)各自遵循自己的原子性,雙方的提交回滾彼此不干擾。這就是嵌套事務(wù)。這個概念,有點類似 spring 里面的 Nested 事務(wù),但這里是數(shù)據(jù)庫層面的,而且是針對同一個連接,對于絕大多數(shù)僅僅支持扁平事務(wù)的數(shù)據(jù)庫而言,可以讓當(dāng)前線程創(chuàng)建兩個不同的數(shù)據(jù)庫連接,然后在兩個不同的連接上各開啟一個事務(wù),屬于不同連接的不同事務(wù)各自遵循自己的原子性,各自的提交回滾彼此不干擾。這是扁平事務(wù)數(shù)據(jù)庫模擬嵌套事務(wù)的一個經(jīng)典用法。也是事務(wù)傳播屬性里,require new 和 nested 的實現(xiàn)原理。
數(shù)據(jù)庫事務(wù)實現(xiàn)大致原理
以 Oracle 為例,Oracle 數(shù)據(jù)都存儲在表空間上,表空間里面有一個段,叫做 Undo 段,在一個事務(wù)中,所進(jìn)行的所有增刪改操作被實施之前,都先要按照嚴(yán)格的順序在 Undo 段保持每條記錄的舊數(shù)據(jù)(對于 INSERT 操作而言,舊數(shù)據(jù)為空),這樣這對數(shù)據(jù)修改之前,Undo 段就保證備份了所有被操作記錄的原數(shù)據(jù)。如果最終被提交,清空 Undo 段中的數(shù)據(jù),如果最 終rollback,則按照 Undo 中事先備份好的原數(shù)據(jù)進(jìn)行逆向操作,每完成一項逆向操作,就清除一部分 Undo 數(shù)據(jù),最后全部回滾后,Undo 段的數(shù)據(jù)也被清空了。
如果網(wǎng)絡(luò)掉線或客戶端崩潰,一定超時后,數(shù)據(jù)庫能發(fā)現(xiàn)超時的“死鏈接”,數(shù)據(jù)庫會清除死鏈接,并且解開死連接所持有的鎖,并且根據(jù)和死連接相關(guān)聯(lián)的 Undo 段數(shù)據(jù)開始逆向操作以撤銷修改。
如果數(shù)據(jù)庫本身崩潰、數(shù)據(jù)庫所在操作系統(tǒng)奔潰、服務(wù)器硬件故障或者服務(wù)器停電導(dǎo)致數(shù)據(jù)庫死掉。人工采取恢復(fù)措施(例如換主板、或想辦法恢復(fù)電力供給)后重啟數(shù)據(jù)庫,剛重啟的數(shù)據(jù)庫會拒絕所有客戶的連接申請,專心看儲存介質(zhì)上是否有 Undo 數(shù)據(jù),如果有,開始撤銷,每撤銷一點就清除一點 Undo 數(shù)據(jù)。考慮更極端一點,如果在撤銷了一部分后,數(shù)據(jù)庫又出問題,那么大不了再重啟一次再來,反正還沒有被用于逆操作的 Undo 數(shù)據(jù)還在,當(dāng)所有的 Undo 數(shù)據(jù)被全部清空后,意味著所有的未提交操作全部非法數(shù)據(jù)都被逆操作了。這是標(biāo)志著數(shù)據(jù)庫得以全部恢復(fù),自此,數(shù)據(jù)庫服務(wù)器才開始接受外界申請連接,進(jìn)入正常的服務(wù)狀態(tài)。
總之,只要存儲數(shù)據(jù)的存儲介質(zhì)本身沒有損壞,無論多極端的軟件或硬件故障,數(shù)據(jù)庫一定能回滾。而事實上,存儲介質(zhì)本身也很可能有硬件層面的有鏡像容錯能力,這就如虎添翼,更完美了。
Undo段故障
如果啟動一個過于龐大的事務(wù),事務(wù)開始之后到提交之前的修改行為過于海量,當(dāng)會導(dǎo)致 Oracle 表空間 Undo 段所允許儲存資源被耗盡,此時應(yīng)用程序會得到異常。出現(xiàn)這個問題后,要仔細(xì)分析問題,辨別是應(yīng)用程序?qū)懙锰?#xff08;比如可以用小一點的事務(wù)實現(xiàn)同樣的功能)還是數(shù)據(jù)庫配置太二。最終決定由開發(fā)人員改應(yīng)用程序還是由 DBA 改數(shù)據(jù)庫軟硬件設(shè)置。
事務(wù)隔離級別
上面所講的事務(wù)的原子性,是對多條修改 SQL 具備意義。對于讀操作,事務(wù)同樣具備重大意義,這就是事務(wù)隔離級別 SQL 標(biāo)準(zhǔn)定義了4類隔離級別,包括了一些具體規(guī)則,用來限定事務(wù)內(nèi)外的哪些改變是可見的,哪些是不可見的。低級別的隔離級一般支持更高的并發(fā)處理,并擁有更低的系統(tǒng)開銷。
Read Uncommitted(讀取未提交內(nèi)容)
特別提醒,Oracle 不支持此級別!在該隔離級別,所有事務(wù)都可以看到其他未提交事務(wù)的執(zhí)行結(jié)果。本隔離級別很少用于實際應(yīng)用,因為它的性能也不比其他級別好多少,但讀取到的數(shù)據(jù)極其不靠譜。讀取未提交的數(shù)據(jù),可能前腳剛讀到別人修改但未提交的數(shù)據(jù),后腳數(shù)據(jù)就被別人回滾撤銷了,自己讀到了一份完全無效的數(shù)據(jù)還渾然不知,這種最無節(jié)操的問題稱之為臟讀(Dirty Read)。
Read Committed(讀取提交內(nèi)容)
這是大多數(shù)數(shù)據(jù)庫系統(tǒng)的默認(rèn)隔離級別(但不是 MySQL 默認(rèn)的)。這個級別可以解決臟讀(Dirty Read)的問題,一個事務(wù)只能看見已經(jīng)提交事務(wù)所做的改變,如果其它事務(wù)反復(fù)修改數(shù)據(jù),當(dāng)前事務(wù)多次讀取同一條數(shù)據(jù)每次會讀到不同的數(shù)據(jù),這種現(xiàn)象叫做不可重復(fù)讀(Nonrepeatable Read)。
Repeatable Read(可重讀)
特別提醒,Oracle 不支持此級別!這是 MySQL 的默認(rèn)事務(wù)隔離級別。這個級別可以解決不可重復(fù)讀的(Nonrepeatable Read)問題。它確保同一事務(wù)的多次同一條數(shù)據(jù)的時候,每次會看到同樣的數(shù)據(jù)行。 但是其它事務(wù)任然還是可以添加和刪除同一張表的其它數(shù)據(jù),導(dǎo)致當(dāng)前事務(wù)反復(fù)看這張表的記錄總條數(shù),有時變多有時變少,就如同看街上閃爍的霓虹燈一樣,這種問題叫做幻讀(Phantom Read)
Serializable(串行化讀)
這是最高的隔離級別,連幻讀(Phantom Read)問題也被解決了。所有企圖操作同一張表(無論讀寫)的事務(wù)必須割舍掉所有并發(fā)性,串行化地排隊。對一張表而言,此級別完全不具備任何并發(fā)性,讀取到的數(shù)據(jù)絕對可靠。
隔離級別表格總結(jié)
越靠上,讀取到的數(shù)據(jù)越不嚴(yán)密,但并發(fā)度越高。越靠下,讀取到的數(shù)據(jù)越嚴(yán)密,但并發(fā)度越低下。典型的魚和熊掌難以兼得的問題,就連數(shù)據(jù)庫制造商自己都覺得難以取舍,就給了這個4檔變速箱,開發(fā)人員根據(jù)實際路況(項目具體情況)自己選。
隔離級別基本原理
由于部分?jǐn)?shù)據(jù)庫對4種級別支持得未必全,比如 Oracle 就僅僅支持兩個級別,而且每種數(shù)據(jù)庫的實現(xiàn)細(xì)節(jié)會稍微有所差異,所以我們講解一種理論上最簡實現(xiàn)原理。實際數(shù)據(jù)庫實現(xiàn)完整隔離級別的原理只能比這個模型更復(fù)雜,不能更簡單。
行鎖
假設(shè)每個數(shù)據(jù)行支持兩種鎖 RS 和 RX;RS 表示 Row Share,行共享鎖,不同的連接可以對同一行記錄同時上 RS 鎖,即行共享鎖,多個連接被允許同時對一條記錄上共享鎖;RX 表示 Row Exclusive,即行排它鎖,只能有一個連接可以對一行記錄上 RX 鎖。另外,鎖可以升級,如果期望給一行數(shù)據(jù)上 RX 鎖而當(dāng)前行已經(jīng)存在一個 RS 鎖,那么RS所會升級成RX鎖。但是反過來,鎖不能降低級,如果已經(jīng)存在 RX鎖,希望上一個 RS 鎖,那么必須等待解鎖。
已存在行鎖 期望新加行鎖 執(zhí)行方式
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;他們所對應(yīng)的鎖行為都是:
任何隔離級別下的悲觀鎖查詢
SELECT … FOR UPDATE均如此工作:
可以發(fā)現(xiàn),悲觀鎖查詢和類似修改語句
普通查詢的行鎖和表鎖
普通查詢語句在不同的隔離級別下工作機(jī)制不一樣
在一般的數(shù)據(jù)庫事務(wù)中,一個事務(wù)就代表著一個鏈接,事務(wù)的隔離級別既是鏈接的隔離級別,不同的鎖行為即代表了不同的執(zhí)行效率,這點是需要大家透徹理解的。
總結(jié)
以上是生活随笔為你收集整理的JDBC和数据库事务详解的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 数智化巩固智慧变电站“防汛墙”
- 下一篇: 计算机毕业设计 SSM书画拍卖平台系统