事務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順序等操做有效避免死鎖再次產生。
事務的隔離性是指?
設有兩個事務T一、T2,其併發操做如圖所示,下面描述正確的是:
JDBC 實現事務控制,開啓事務使用哪一個方法?
如下哪一個事務隔離級別不存在髒讀,可是存在不可重複讀?
如下哪項不是死鎖產生的必要條件?
關於死鎖描述不正確的是?
如下描述正確的是?
事務的單元做業,包括一道編程題目。
有一個在線交易電商平臺,有兩張表,分別是庫存表和訂單表,以下:
如今買家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)