Java中JDBC的事務問題
事務
l 事務的四大特性:ACID;
l mysql中操作事務
l jdbc中操作事務
事務概述
為了方便演示事務,我們需要建立一個account表:
CREATE TABLE account( id INT PRIMARY KEY AUTO_INCREMENT, NAME VARCHAR(30), balance NUMERIC(10.2) ); INSERT INTO account(NAME,balance) VALUES('zs', 100000); INSERT INTO account(NAME,balance) VALUES('ls', 100000); INSERT INTO account(NAME,balance) VALUES('ww', 100000); SELECT * FROM account; |
什麼是事務
銀行轉賬!張三轉帳10000塊到李四的賬戶,這其實需要兩條SQL語句:
1.給張三的賬戶減去10000元;
2.給李四的賬戶加上10000元。
如果在第一條SQL語句執行成功後,在執行第二條SQL語句之前,程式被中斷了(可能是丟擲了某個異常,也可能是其他什麼原因),那麼李四的賬戶沒有加上10000元,而張三卻減去了10000元。這肯定是不行的!
事務中的多個操作,要麼完全成功,要麼完全失敗!不可能存在成功一半的情況!也就是說給張三的賬戶減去10000元如果成功了,那麼給李四的賬戶加上10000元的操作也必須是成功的;否則給張三減去10000元,以及給李四加上10000元都是失敗的!
事務的四大特性(ACID)
常見關於事務面試題!
事務的四大特性是:
l 原子性(Atomicity):事務中所有操作是不可再分割的原子單位。事務中所有操作要麼全部執行成功,要麼全部執行失敗。
l 一致性(Consistency):事務執行後,資料庫狀態與其它業務規則保持一致。如轉賬業務,無論事務執行成功與否,參與轉賬的兩個賬號餘額之和應該是不變的。
l 隔離性(Isolation):隔離性是指在併發操作中,不同事務之間應該隔離開來,使每個併發中的事務不會相互干擾。
l 永續性(Durability):一旦事務提交成功,事務中所有的資料操作都必須被持久化到資料庫中,即使提交事務後,資料庫馬上崩潰,在資料庫重啟時,也必須能保證通過某種機制恢復資料。
MySQL中的事務
在預設情況下,MySQL每執行一條SQL語句,都是一個單獨的事務。如果需要在一個事務中包含多條SQL語句,那麼需要開啟事務和結束事務。
l 開啟事務:starttransaction;
l 結束事務:commit或rollback。
在執行SQL語句之前,先執行strat transaction,這就開啟了一個事務(事務的起點),然後可以去執行多條SQL語句,最後要結束事務,commit表示提交,即事務中的多條SQL語句所做出的影響會持久化到資料庫中。或者rollback,表示回滾,即回滾到事務的起點,之前做的所有操作都被撤消了!
下面演示張三給李四轉賬10000元的示例:
START TRANSACTION; UPDATE account SET balance=balance-10000 WHERE id=1; UPDATE account SET balance=balance+10000 WHERE id=2; ROLLBACK; |
START TRANSACTION; UPDATE account SET balance=balance-10000 WHERE id=1; UPDATE account SET balance=balance+10000 WHERE id=2; COMMIT; |
START TRANSACTION; UPDATE account SET balance=balance-10000 WHERE id=1; UPDATE account SET balance=balance+10000 WHERE id=2; quit; |
JDBC事務
在jdbc中處理事務,都是通過Connection完成的!
同一事務中所有的操作,都在使用同一個Connection物件!
JDBC中的事務
Connection的三個方法與事務相關:
l setAutoCommit(boolean):設定是否為自動提交事務,如果true(預設值就是true)表示自動提交,也就是每條執行的SQL語句都是一個單獨的事務,如果設定false,那麼就相當於開啟了事務了;con.setAutoCommit(false)表示開啟事務!
l commit():提交結束事務;con.commit();表示提交事務
l rollback():回滾結束事務。con.rollback();表示回滾事務
jdbc處理事務的程式碼格式:
try {
con.setAutoCommit(false);//開啟事務
con.commit();//try的最後提交事務
} catch() {
con.rollback();//回滾事務
}
public void transfer(boolean b) { Connection con = null; PreparedStatement pstmt = null; try { con = JdbcUtils.getConnection(); //手動提交 con.setAutoCommit(false); String sql = "update account set balance=balance+? where id=?"; pstmt = con.prepareStatement(sql); //操作 pstmt.setDouble(1, -10000); pstmt.setInt(2, 1); pstmt.executeUpdate(); // 在兩個操作中丟擲異常 if(b) { throw new Exception(); } pstmt.setDouble(1, 10000); pstmt.setInt(2, 2); pstmt.executeUpdate(); //提交事務 con.commit(); } catch(Exception e) { //回滾事務 if(con != null) { try { con.rollback(); } catch(SQLException ex) {} } throw new RuntimeException(e); } finally { //關閉 JdbcUtils.close(con, pstmt); } } |
儲存點
儲存點是JDBC3.0的東西!當要求資料庫伺服器支援儲存點方式的回滾。
校驗資料庫伺服器是否支援儲存點!
boolean b = con.getMetaData().supportsSavepoints(); |
儲存點的作用是允許事務回滾到指定的儲存點位置。在事務中設定好儲存點,然後回滾時可以選擇回滾到指定的儲存點,而不是回滾整個事務!注意,回滾到指定儲存點並沒有結束事務!!!只有回滾了整個事務才算是結束事務了!
Connection類的設定儲存點,以及回滾到指定儲存點方法:
l 設定儲存點:Savepoint setSavepoint();
l 回滾到指定儲存點:void rollback(Savepoint)。
/* * 李四對張三說,如果你給我轉1W,我就給你轉100W。 * ========================================== * * 張三給李四轉1W(張三減去1W,李四加上1W) * 設定儲存點! * 李四給張三轉100W(李四減去10W,張三加上10W) * 檢視李四餘額為負數,那麼回滾到儲存點。 * 提交事務 */ @Test public void fun() { Connection con = null; PreparedStatement pstmt = null; try { con = JdbcUtils.getConnection(); //手動提交 con.setAutoCommit(false); String sql = "update account set balance=balance+? where name=?"; pstmt = con.prepareStatement(sql); //操作1(張三減去1W) pstmt.setDouble(1, -10000); pstmt.setString(2, "zs"); pstmt.executeUpdate(); //操作2(李四加上1W) pstmt.setDouble(1, 10000); pstmt.setString(2, "ls"); pstmt.executeUpdate(); // 設定儲存點 Savepoint sp = con.setSavepoint(); //操作3(李四減去100W) pstmt.setDouble(1, -100000); pstmt.setString(2, "ls"); pstmt.executeUpdate(); //操作4(張三加上100W) pstmt.setDouble(1, 100000); pstmt.setString(2, "zs"); pstmt.executeUpdate(); //操作5(檢視李四餘額) sql = "select balance from account where name=?"; pstmt = con.prepareStatement(sql); pstmt.setString(1, "ls"); ResultSet rs = pstmt.executeQuery(); rs.next(); double balance = rs.getDouble(1); //如果李四餘額為負數,那麼回滾到指定儲存點 if(balance < 0) { con.rollback(sp); System.out.println("張三,你上當了!"); } //提交事務 con.commit(); } catch(Exception e) { //回滾事務 if(con != null) { try { con.rollback(); } catch(SQLException ex) {} } throw new RuntimeException(e); } finally { //關閉 JdbcUtils.close(con, pstmt); } } |
事務隔離級別
事務的併發讀問題
l 髒讀:讀取到另一個事務未提交資料;
l 不可重複讀:兩次讀取不一致;
l 幻讀:讀到另一事務已提交資料。
併發事務問題
因為併發事務導致的問題大致有5類,其中兩類是更新問題,三類是讀問題。
l 髒讀(dirty read):讀到另一個事務的未提交更新資料,即讀取到了髒資料;
l 不可重複讀(unrepeatable read):對同一記錄的兩次讀取不一致,因為另一事務對該記錄做了修改;
l 幻讀(虛讀)(phantom read):對同一張表的兩次查詢不一致,因為另一事務插入了一條記錄;
髒讀
事務1:張三給李四轉賬100元
事務2:李四檢視自己的賬戶
l t1:事務1:開始事務
l t2:事務1:張三給李四轉賬100元
l t3:事務2:開始事務
l t4:事務2:李四檢視自己的賬戶,看到賬戶多出100元(髒讀)
l t5:事務2:提交事務
l t6:事務1:回滾事務,回到轉賬之前的狀態
不可重複讀
事務1:酒店檢視兩次1048號房間狀態
事務2:預訂1048號房間
l t1:事務1:開始事務
l t2:事務1:檢視1048號房間狀態為空閒
l t3:事務2:開始事務
l t4:事務2:預定1048號房間
l t5:事務2:提交事務
l t6:事務1:再次檢視1048號房間狀態為使用
l t7:事務1:提交事務
對同一記錄的兩次查詢結果不一致!
幻讀
事務1:對酒店房間預訂記錄兩次統計
事務2:新增一條預訂房間記錄
l t1:事務1:開始事務
l t2:事務1:統計預訂記錄100條
l t3:事務2:開始事務
l t4:事務2:新增一條預訂房間記錄
l t5:事務2:提交事務
l t6:事務1:再次統計預訂記錄為101記錄
l t7:事務1:提交
對同一表的兩次查詢不一致!
不可重複讀和幻讀的區別:
l 不可重複讀是讀取到了另一事務的更新;
l 幻讀是讀取到了另一事務的插入(MySQL中無法測試到幻讀);
四大隔離級別
4個等級的事務隔離級別,在相同資料環境下,使用相同的輸入,執行相同的工作,根據不同的隔離級別,可以導致不同的結果。不同事務隔離級別能夠解決的資料併發問題的能力是不同的。
1 SERIALIZABLE(序列化)
l 不會出現任何併發問題,因為它是對同一資料的訪問是序列的,非併發訪問的;
l 效能最差;
2 REPEATABLE READ(可重複讀)(MySQL)
l 防止髒讀和不可重複讀,不能處理幻讀問題;
l 效能比SERIALIZABLE好
3 READ COMMITTED(讀已提交資料)(Oracle)
l 防止髒讀,沒有處理不可重複讀,也沒有處理幻讀;
l 效能比REPEATABLE READ好
4 READ UNCOMMITTED(讀未提交資料)
l 可能出現任何事務併發問題
l 效能最好
MySQL的預設隔離級別為REPEATABLE READ,這是一個很不錯的選擇吧!
MySQL隔離級別
MySQL的預設隔離級別為Repeatable read,可以通過下面語句檢視:
select @@tx_isolation |
也可以通過下面語句來設定當前連線的隔離級別:
set transaction isolationlevel [4先1] |
JDBC設定隔離級別
con.setTransactionIsolation(int level)
引數可選值如下:
l Connection.TRANSACTION_READ_UNCOMMITTED;
l Connection.TRANSACTION_READ_COMMITTED;
l Connection.TRANSACTION_REPEATABLE_READ;
l Connection.TRANSACTION_SERIALIZABLE。
事務總結:
l 事務的特性:ACID;
l 事務開始邊界與結束邊界:開始邊界(con.setAutoCommit(false)),結束邊界(con.commit()或con.rollback());
l 事務的隔離級別: READ_UNCOMMITTED、READ_COMMITTED、REPEATABLE_READ、SERIALIZABLE。多個事務併發執行時才需要考慮併發事務。