1. 程式人生 > >Java中JDBC的事務問題

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  結束事務:commitrollback

在執行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();表示提交事務

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。

事務總結:

事務的特性:ACID

事務開始邊界與結束邊界:開始邊界(con.setAutoCommit(false)),結束邊界(con.commit()con.rollback());

事務的隔離級別: READ_UNCOMMITTEDREAD_COMMITTEDREPEATABLE_READSERIALIZABLE。多個事務併發執行時才需要考慮併發事務。