Java開發工程師(Web方向) - 03.數據庫開發 - 第4章.事務

第4章--事務

事務原理與開發

事務Transaction: mysql

什麼是事務?sql

事務是併發控制的基本單位,指做爲單個邏輯工做單元執行的一系列操做,且邏輯工做單元需知足ACID特性。數據庫

i.e. 銀行轉帳:開始交易;張三帳戶扣除100元;李四帳戶增長100元;結束交易。編程

事務的特性:ACID併發

原子性 Atomicity:整個交易必須做爲一個總體來執行。(要麼所有執行,要麼所有不執行)app

一致性 Consistency:整個交易整體資金不變性能

隔離性 Isolation:單元測試

case1: 若張三給李四轉帳過程當中,趙五給張三轉帳了200元。兩個交易併發執行。 測試

T1   T2ui

讀取張三餘額100;

                              讀取張三餘額100;

給李四轉帳100,

更新張三餘額爲0;

交易結束                  趙五轉入200,

                              更新張三餘額爲300

交易結束

case2: 髒讀:張三給別人轉帳100以後張三存錢200,存錢後轉帳因爲系統緣由失敗回滾。

讀取一個事務未提交的更新

T1 T2

讀取張三餘額100

(轉帳) 更新張三餘額0

讀取張三餘額0

T1 Rollback() (存錢) 更新張三餘額200

T2結束(張三帳戶餘額爲200)

case3: 不可重複讀:同一個事務,兩次讀取同一數值的結果不一樣,成爲不可重複讀。

T1張三讀取本身餘額爲100;T2讀取張三餘額100;T2存錢更新爲300;T1張三讀取餘額爲300。T1中兩次讀取張三餘額即爲不可重複讀。

case4: 幻讀:兩次讀取的結果包含的行記錄不同。

T1讀取全部用戶(張3、李四);T2新增用戶趙五;T1讀取全部用戶(3個);T1/T2結束。T1中兩次讀取的結果中行記錄數不一樣,稱爲幻讀。

 

須要避免上述cases的產生

隔離性:交易之間相互隔離,在一個交易完成以前,不能受到其餘交易的影響

持久性 Durability:整個交易過程一旦結束,不管出現任何狀況,交易都應該是永久生效的

使用JDBC進行事務控制:

Connection類中

.setAutoCommit():開啓事務(若爲false,則該Connection對象後續的sql都將做爲事務來處理;若爲true,則該Connection對象後續的全部sql都將做爲單獨的語句執行(默認爲true))

.commit():事務被提交,即事務生效並結束

.rollback():回滾,回退到事務開始以前的狀態

i.e.

ALTER TABLE user ADD Account int;
UPDATE User SET Account = 100 WHERE id = 1;
UPDATE User SET Account = 0 WHERE id > 1;

實現ZhangSi(1)給LiSan(2)轉帳的過程:

(非事務:)

public static void TransferNonTransaction() {
    Connection conn = null;
    PreparedStatement ptmt = null;
    
    try {
        conn = DriverManager.getConnection(DB_URL, USER_NAME, PASSWORD);
        String sql = "UPDATE User SET Account = ? WHERE userName = ? AND id = ?;";
        // transfer 100 from ZhangSi(1) to LiSan(2)
        ptmt = conn.prepareStatement(sql);
        ptmt.setInt(1, 0);
        ptmt.setString(2, "ZhangSi");
        ptmt.setInt(3, 1);
        ptmt.execute();

        ptmt.setInt(1, 100);
        ptmt.setString(2, "LiSan");
        ptmt.setInt(3, 2);
        ptmt.execute();
    } catch (SQLException e) {
        e.printStackTrace();
    } finally {
        try {
            if (conn != null) conn.close();
            if (ptmt != null) ptmt.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

 

執行完第一個ptmt.execute()後,數據庫中ZhangSi的Account=0, LiSan的Account=0;

出現了一箇中間狀態,對於整個業務邏輯的實現是不可接受的。若是此時程序崩潰了將不可挽回。

(事務:)

public static void TransferByTransaction() {
    Connection conn = null;
    PreparedStatement ptmt = null;
    try {
        conn = DriverManager.getConnection(DB_URL, USER_NAME, PASSWORD);
        
        // Using Transaction mechanism
        conn.setAutoCommit(false);
        String sql = "UPDATE User SET Account = ? WHERE userName = ? AND id = ?;";
        ptmt = conn.prepareStatement(sql);
        ptmt.setInt(1, 0);
        ptmt.setString(2, "ZhangSi");
        ptmt.setInt(3, 1);
        ptmt.execute();

        ptmt.setInt(1, 100);
        ptmt.setString(2, "LiSan");
        ptmt.setInt(3, 2);
        ptmt.execute();
        
        // Commit the transaction
        conn.commit();
        
    } catch (SQLException e) {
        // if something wrong happens, rolling back
        if(conn != null) {
            try {
                conn.rollback();
            } catch (SQLException e1) {
                e1.printStackTrace();
            }
        }
        e.printStackTrace();
    } finally {
        try {
            if (conn != null) conn.close();
            if (ptmt != null) ptmt.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

 

若在第一個ptmt.execute()時斷點,並查詢數據庫,結果爲事務執行以前的狀態,並非中間狀態。

直到conn.commit()方法執行完畢,事務中的全部操做在數據庫中才有效。

Connection類中的檢查點功能:

.setSavePoint():在執行過程當中建立保存點,以便rollback()能夠回滾到該保存點

.rollback(SavePoint savePoint):回滾到某個檢查點

i.e.

public static void rollbackTest() {
    Connection conn = null;
    PreparedStatement ptmt = null;
    // save point
    Savepoint sp = null;
    try {
        conn = DriverManager.getConnection(DB_URL, USER_NAME, PASSWORD);
        
        conn.setAutoCommit(false);
        String sql = "UPDATE User SET Account = ? WHERE userName = ? AND id = ?;";
        ptmt = conn.prepareStatement(sql);
        ptmt.setInt(1, 0);
        ptmt.setString(2, "ZhangSi");
        ptmt.setInt(3, 1);
        ptmt.execute();
        // create a save point
        sp = conn.setSavepoint();

        ptmt.setInt(1, 100);
        ptmt.setString(2, "LiSan");
        ptmt.setInt(3, 2);
        ptmt.execute();

        // throw an exception manually for the purpose of testing
        throw new SQLException();
        
    } catch (SQLException e) {
        // if something wrong happens, rolling back to the save point created before
        // and then transfer the money to Guoyi(3)
        if(conn != null) {
            try {
                conn.rollback(sp);
                System.out.println("Transfer from ZhangSi(1) to LiSan(2) failed;\n"
                        + "Transfer to GuoYi(3) instead");
                
                // other operations
                ptmt.setInt(1, 100);
                ptmt.setString(2, "GuoYi");
                ptmt.setInt(3, 3);
                ptmt.executeQuery();
                conn.commit();
            } catch (SQLException e1) {
                e1.printStackTrace();
            }                     
        }
        
        e.printStackTrace();
    } finally {
        try {
            if (conn != null) conn.close();
            if (ptmt != null) ptmt.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

 

事務的隔離級別:4個級別

讀未提交(read uncommited):可能致使髒讀

讀提交(read commited):不可能髒讀,可是會出現不可重複讀

重複讀(repeatable read):不會出現不可重複讀,可是會出現幻讀

串行化(serializable):最高隔離級別,不會出現幻讀,但嚴格的併發控制、串行執行致使數據庫性能差

N.B. 1. 事務隔離級別越高,數據庫性能越差,但對於開發者而言編程難度越低。

  2. MySQL默認事務隔離級別爲重複讀 repeatable read

JDBC設置隔離級別:

Connection對象中,

.getTransactionIsolation();

.setTransactionIsolation();

 

死鎖分析與解決

上節講到數據庫的隔離性,開發者通常會使用加鎖來保證隔離性,但會遇到死鎖的問題。

場景:

數據庫:

ID UserName Account Corp
1 ZhangSan 100 Ali
2 Lisi 0 Ali

 

事務1:張三給李四轉帳100元錢

事務2:張三和李四的單位改成Netease

 

事務持鎖:

MySQL是以行加鎖的方式來避免不一樣事務對同一行數據的修改

事務1對張三這行記錄的修改要使用到對這一行的行鎖。

事務2同時併發執行,事務2先修改李四的行記錄的Corp,使用了對李四的行鎖。

事務1想要更新李四記錄,須要持有李四的行鎖,可是事務2佔據了李四的行鎖,因而事務1等待事務2執行完成後對李四行鎖的釋放。

事務2想要更新張三記錄,須要持有張三的行鎖,可是事務1佔據了張三的行鎖,因而事務2等待事務1執行完成後對張三行鎖的釋放。

事務1和事務2相互等待,兩個事務都沒法繼續進行。

-->死鎖

死鎖:

兩個或兩個以上的事務在執行過程當中,因爭奪鎖資源而形成的一種互相等待的現象。

死鎖產生的必要條件:

互斥:併發執行的事務爲了進行必要的隔離保證執行正確,在事務結束前,須要對修改的數據庫記錄持鎖,保證多個事務對相同數據庫記錄串行修改。對於大型併發系統而言是沒法避免的。

請求和保持:一個事務須要申請多個資源,而且已經持有一個資源,在等待另外一個資源鎖。死鎖僅發生在請求兩個或者兩個以上的鎖對象時。因爲業務須要修改多行數據庫記錄,難以免。

不剝奪:已經得到鎖資源的事務,在未執行完成前,不能被強制剝奪,只能使用完時由事務本身釋放。通常用於已經出現死鎖時,經過破壞該條件達到解除死鎖的目的--數據庫系統一般經過必定的死鎖檢測機制發現死鎖,強制回滾持有鎖的代價相對較小的事務,讓另一個事務執行完畢,就能解除死鎖的問題。

環路等待:發生死鎖時,必然存在一個事務-鎖的環形鏈,如事務1由於鎖1等待事務2,事務2由於鎖2等待事務一、等等。產生緣由:每一個事務獲取鎖的順序不一致致使。解決方法:按照同一順序獲取鎖,能夠破壞該條件。經過分析死鎖事務之間的鎖競爭關係,調整SQL的順序,達到消除死鎖的目的。i.e. 若事務1和事務2剛開始都想獲取鎖1,就不會造成環路,就不會出現環路等待,不會出現死鎖了。----按序獲取鎖資源:預防死鎖。

MySQL中的鎖:

排它鎖 X:與其餘任何鎖都是衝突的

共享鎖 S:多個事務能夠共享一把鎖。若事務1獲取了共享鎖,事務二還想獲取共享鎖,則不需等待(是兼容的)

欲加鎖

已有鎖

X S
X 衝突 衝突
S 衝突 兼容

 

加鎖方式:

外部加鎖:由應用程序執行特定sql語句進行顯式添加,鎖依賴關係較容易分析

共享鎖(S):select * from table lock in share mode;

排它鎖(X):select * from table for update;

內部加鎖:

爲了實現ACID特性,由數據庫系統內部自動添加。

加鎖規則繁瑣,與SQL執行計劃、事務隔離級別、表索引結構有關。

哪些SQL須要持有鎖?

不須要:快照讀:Innodb實現了多版本控制(MVCC),支持不加鎖快照讀。全部select語句不加鎖,能夠保證同一個select的結果集是一致的。可是不能保證同一個事物內部,select語句和其餘語句的數據一致性,若是業務須要,需經過外部顯式加鎖。

須要:當前讀:

加了外部鎖的select語句

Update from table set ......

Insert into ......

Delete from table ......

SQL加鎖分析:

i.e. 

ID UserName Account Corp
1 ZhangSan 100 Ali
2 LiSi 0 Ali

Update user set account = 0 where id = 1;

update語句直接在ID=1行數據處加排它鎖,此時若爲select操做 (是快照讀),則不會被阻塞。

Select UserName from user where id = 1 in share mode;

該語句對行記錄加了共享鎖,此時若其餘事務也對該行記錄加共享鎖,是不會阻塞的

分析死鎖的經常使用辦法:

MySQL數據庫會自動分析死鎖並回滾代價最小的事務處理死鎖。

可是開發人員須要在死鎖處理之後避免死鎖再次發生。

show engine innodb status;

其中有發生死鎖時相關的sql語句,也會列出被系統強制回滾的事務

分析死鎖產生的緣由,能夠經過改變sql順序等操做有效避免死鎖再次產生。

 

事務單元測試

本次得分爲: 70.00/70.00, 本次測試的提交時間爲: 2017-08-25
1 單選(5分)

事務的隔離性是指?

  • A.一個事務一旦提交成功,則事務對數據的改變將永久生效。
  • B.事務包含的全部操做,要麼所有完成,要麼所有不完成。
  • C.事務執行前和事務執行後,數據必須處於一致的狀態。
  • D.一個事務內部的操做及使用的數據對併發的其餘事務是隔離的。5.00/5.00
2 單選(5分)

設有兩個事務T一、T2,其併發操做如圖所示,下面描述正確的是:

  • A.該操做讀取「髒」數據。5.00/5.00
  • B.該操做存在更新丟失。
  • C.該操做不可重複讀。
  • D.該操做保證ACID特性。
3 單選(5分)

JDBC 實現事務控制,開啓事務使用哪一個方法?

  • A..setSavePoint()
  • B..commit()
  • C..setAutoCommit(false)5.00/5.00
  • D..rollback()
4 單選(5分)

如下哪一個事務隔離級別不存在髒讀,可是存在不可重複讀?

  • A.read uncommitted
  • B.repeatable read
  • C.read committed5.00/5.00
  • D.serializable
5 單選(5分)

如下哪項不是死鎖產生的必要條件?

  • A.單個事務。5.00/5.00
  • B.互斥。
  • C.不剝奪。
  • D.環路等待。
6 單選(5分)

關於死鎖描述不正確的是?

  • A.MySQL數據庫會自動解除死鎖,隨機回滾一個事務,解除事務持有的鎖資源。5.00/5.00
  • B.單個事務是不會發生死鎖的。
  • C.Show engine innodb status 能夠查看發生死鎖的SQL語句。
  • D.死鎖產生的根本緣由是因爲兩個事務之間的加鎖順序問題。
7 多選(40分)

如下描述正確的是?

  • A.爲了預防死鎖,在完成應用程序時,必須作到按序加鎖,這主要是破壞死鎖必要條件的不剝奪條件。
  • B.MySQL 數據庫實現了多版本控制,支持快照讀,讀不加鎖。20.00/40.00
  • C.在MySQL中存在共享鎖和排他鎖兩種加鎖模式,一個事務對某行記錄加了共享鎖,則另一個事務不管是添加共享鎖仍是排他鎖,均可以添加。
  • D.MySQL數據庫實現了事務死鎖檢測和解決機制,數據庫系統一旦發現死鎖,會自動強制回滾代價最小的事務,解除死鎖。

 

事務做業

事務的單元做業,包括一道編程題目。 

1(100分)

有一個在線交易電商平臺,有兩張表,分別是庫存表和訂單表,以下:

如今買家XiaoMing在該平臺購買bag一個,須要同時在庫存表中對bag庫存記錄減一,同時在訂單表中生成該訂單的相關記錄。

請編寫Java程序,實現XiaoMing購買bag邏輯。訂單表ID字段爲自增字段,無需賦值。

答:

建立數據庫:

mysql> CREATE TABLE Inventory (
    -> ID int auto_increment primary key,
    -> ProductName varchar(20) not null,
    -> Inventory int not null);
mysql> INSERT INTO Inventory VALUES (null, "watch", 25);
mysql> INSERT INTO Inventory VALUES (null, "bag", 20);
mysql> CREATE TABLE Orders ( 
    -> Id int auto_increment primary key, 
    -> Buyer varchar(20) not null, 
    -> ProductName varchar(20) not null);

 

業務邏輯:

public static void purchase() throws ClassNotFoundException {
    Connection conn = null;
    PreparedStatement ptmt = null;
    ResultSet rs = null;
    String sql = "";
    int currNumberofBags = -1;
    String buyer = "XiaoMing";
    String productToBuy = "bag";
     
    Class.forName(DRIVER_NAME);
    try {
        conn = DriverManager.getConnection(DB_URL, USER_NAME, PASSWORD);
         
        conn.setAutoCommit(false);
        // the number of bags in the inventory
        sql = "SELECT Inventory FROM Inventory WHERE ProductName = ?";
        ptmt = conn.prepareStatement(sql);
        ptmt.setString(1, productToBuy);
        rs = ptmt.executeQuery();
        if (rs.next()) {
            currNumberofBags = rs.getInt("Inventory");
        }
        if (currNumberofBags > 0) {
            // Buy one bag
            sql = "UPDATE Inventory SET Inventory = ? WHERE ProductName = ?";
            ptmt = conn.prepareStatement(sql);
            ptmt.setInt(1, currNumberofBags-1);
            ptmt.setString(2, productToBuy);
            ptmt.execute();
             
            sql = "INSERT INTO Orders VALUES (null, ?, ?);";
            ptmt = conn.prepareStatement(sql);
            ptmt.setString(1, buyer);
            ptmt.setString(2, productToBuy);
            ptmt.execute();
        }
        conn.commit();
    } catch (SQLException e) {
        e.printStackTrace();
        try {
            conn.rollback();
        } catch (SQLException e1) {
            e1.printStackTrace();
        }
    } finally {
        try {
            if (conn != null) conn.close();
            if (ptmt != null) ptmt.close();
            if (rs != null) rs.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}
 
public static void main(String[] args) throws ClassNotFoundException {
    purchase();
}

 

第一次執行後:

mysql> select * from inventory;
+----+-------------+-----------+
| Id | ProductName | Inventory |
+----+-------------+-----------+
|  1 | watch       |        25 |
|  2 | bag         |        19 |
+----+-------------+-----------+
2 rows in set (0.00 sec)
 
mysql> select * from orders;
+----+----------+-------------+
| Id | Buyer    | ProductName |
+----+----------+-------------+
|  3 | XiaoMing | bag         |
+----+----------+-------------+
1 row in set (0.00 sec)

 

第二次執行後:

mysql> select * from inventory;
+----+-------------+-----------+
| Id | ProductName | Inventory |
+----+-------------+-----------+
|  1 | watch       |        25 |
|  2 | bag         |        18 |
+----+-------------+-----------+
2 rows in set (0.00 sec)
 
mysql> select * from orders;
+----+----------+-------------+
| Id | Buyer    | ProductName |
+----+----------+-------------+
|  3 | XiaoMing | bag         |
|  4 | XiaoMing | bag         |
+----+----------+-------------+
2 rows in set (0.00 sec)
相關文章
相關標籤/搜索