MySQL數據庫事務詳解


 

微信公眾號【黃小斜】大廠程序員,互聯網行業新知,終身學習踐行者。關注后回復「Java」、「Python」、「C++」、「大數據」、「機器學習」、「算法」、「AI」、「Android」、「前端」、「iOS」、「考研」、「BAT」、「校招」、「筆試」、「面試」、「面經」、「計算機基礎」、「LeetCode」 等關鍵字可以獲取對應的免費學習資料。 

 

 

                     

 

 

事務的概念

事務指邏輯上的一組操作,組成這組操作的各個單元,要不全部成功,要不全部不成功。 
例如:A向B轉賬100元,對應於如下兩條sql語句:

update from account set money=money+100 where name='b'; update from account set money=money-100 where name='a';
  • 1
  • 2
  • 1
  • 2

數據庫默認事務是自動提交的,也就是發一條sql它就執行一條,如果想多條sql放在一個事務中執行,則需要使用如下語句:

start transaction … … commit
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

數據庫開啟事務命令:

  • start transaction :開啟事務
  • rollback:回滾事務
  • commit:提交事務

MySQL數據庫中操作事務命令

編寫測試SQL腳本,如下:

/* 創建數據庫 */ create database day16; use day16; /* 創建賬戶表 */ create table account ( id int primary key auto_increment, name varchar(40), money float ) character set utf8 collate utf8_general_ci; /* 插入測試數據 */ insert into account(name,money) values('aaa',1000); insert into account(name,money) values('bbb',1000); insert into account(name,money) values('ccc',1000); 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

下面我們在MySQL數據庫中模擬aaa向bbb轉帳這個業務場景。

  • 開啟事務(start transaction) 
    使用”start transaction”開啟MySQL數據庫的事務,如下所示: 
    這里寫圖片描述 
    我們首先在數據庫中模擬轉賬失敗的場景,首先執行update語句讓aaa用戶的money減少100塊錢,如下圖所示: 
    這里寫圖片描述 
    現在假設程序拋出異常,也即該鏈接斷了,代碼塊沒有完成,此時數據庫會自動回滾掉此sql語句造成的影響,也就是說這條sql語句沒有執行。我們現在就來模擬這種情況,我們關閉當前操作的dos命令行窗口,這樣就導致了剛才執行的update語句的數據庫的事務沒有被提交,那么我們對aaa用戶的修改就不算是真正的修改了,下次在查詢aaa用戶的money時,依然還是之前的1000,如下圖所示: 
    這里寫圖片描述

  • 提交事務(commit) 
    下面我們在數據庫模擬aaa向bbb轉賬成功的場景。 
    這里寫圖片描述 
    我們手動提交(commit)數據庫事務之后,aaa向bbb轉賬100塊錢的這個業務操作算是真正成功了,aaa賬戶中少了100,bbb賬戶中多了100。

  • 回滾事務(rollback) 
    這里寫圖片描述 
    通過手動回滾事務,讓所有的操作都失效,這樣數據就會回到最初的初始狀態!

JDBC中使用事務

當Jdbc程序向數據庫獲得一個Connection對象時,默認情況下這個Connection對象會自動向數據庫提交在它上面發送的SQL語句。若想關閉這種默認提交方式,讓多條SQL在一個事務中執行,可使用下列的JDBC控制事務語句:

  • Connection.setAutoCommit(false); //開啟事務(start transaction)
  • Connection.rollback(); //回滾事務(rollback)
  • Connection.commit(); //提交事務(commit)

JDBC使用事務范例

在JDBC代碼中演示銀行轉帳案例,使如下轉帳操作在同一事務中執行:

update from account set money=money-100 where name=‘aaa’; update from account set money=money+100 where name=‘bbb’;
  • 1
  • 2
  • 1
  • 2
  • 模擬aaa向bbb轉賬成功時的業務場景

    public class Demo1 { /* * a--->b轉100元 */ public static void main(String[] args) throws SQLException { Connection conn = null; PreparedStatement st = null; ResultSet rs = null; try { conn = JdbcUtils.getConnection(); conn.setAutoCommit(false); // 相當於start transaction,開啟事務 String sql1 = "update account set money=money-100 where name='aaa'"; String sql2 = "update account set money=money+100 where name='bbb'"; st = conn.prepareStatement(sql1); st.executeUpdate(); st = conn.prepareStatement(sql2); st.executeUpdate(); conn.commit(); } finally { JdbcUtils.release(conn, st, rs); } } }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
  • 模擬aaa向bbb轉賬過程中出現異常導致有一部分SQL執行失敗后讓數據庫自動回滾事務

    public class Demo1 { /* * a--->b轉100元 */ public static void main(String[] args) throws SQLException { Connection conn = null; PreparedStatement st = null; ResultSet rs = null; try { conn = JdbcUtils.getConnection(); conn.setAutoCommit(false); // 相當於start transaction,開啟事務 String sql1 = "update account set money=money-100 where name='aaa'"; String sql2 = "update account set money=money+100 where name='bbb'"; st = conn.prepareStatement(sql1); st.executeUpdate(); int x = 1/0; // 程序運行到這個地方拋異常,后面的代碼就不執行,數據庫沒有收到commit命令 st = conn.prepareStatement(sql2); st.executeUpdate(); conn.commit(); } finally { JdbcUtils.release(conn, st, rs); } } }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
  • 模擬aaa向bbb轉賬過程中出現異常導致有一部分SQL執行失敗時手動通知數據庫回滾事務

    public class Demo1 { /* * a--->b轉100元 */ public static void main(String[] args) throws SQLException { Connection conn = null; PreparedStatement st = null; ResultSet rs = null; try { conn = JdbcUtils.getConnection(); conn.setAutoCommit(false); // 相當於start transaction,開啟事務 String sql1 = "update account set money=money-100 where name='aaa'"; String sql2 = "update account set money=money+100 where name='bbb'"; st = conn.prepareStatement(sql1); st.executeUpdate(); int x = 1/0; // 程序運行到這個地方拋異常,后面的代碼就不執行,數據庫沒有收到commit命令 st = conn.prepareStatement(sql2); st.executeUpdate(); conn.commit(); } catch (Exception e) { e.printStackTrace(); conn.rollback(); // 捕獲到異常之后手動通知數據庫執行回滾事務的操作 } finally { JdbcUtils.release(conn, st, rs); } } }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

設置事務回滾點

在開發中,有時候可能需要手動設置事務的回滾點,在JDBC中使用如下的語句設置事務回滾點:

Savepoint sp = conn.setSavepoint(); Conn.rollback(sp); Conn.commit(); // 回滾后必須通知數據庫提交事務
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

設置事務回滾點范例:

public class Demo2 { // 事務回滾點概念 public static void main(String[] args) throws SQLException { Connection conn = null; PreparedStatement st = null; ResultSet rs = null; Savepoint sp = null; try { conn = JdbcUtils.getConnection(); // MySQL默認的隔離級別——REPEATABLE-READ,並且是嚴格遵循數據庫規范設計的,即支持4種隔離級別 // Oracle默認的隔離級別——Read committed,並且不支持這4種隔離級別,只支持這4種隔離級別中的2種,Read committed和Serializable // conn.setTransactionIsolation(); // 相當於設置CMD窗口的隔離級別 conn.setAutoCommit(false); // 相當於start transaction,開啟事務 // 不符合實際需求 String sql1 = "update account set money=money-100 where name='aaa'"; String sql2 = "update account set money=money+100 where name='bbb'"; String sql3 = "update account set money=money+100 where name='ccc'"; st = conn.prepareStatement(sql1); st.executeUpdate(); /* * 只希望回滾掉這一條sql語句,上面那條sql語句讓其執行成功 * 這時可設置事務回滾點 */ sp = conn.setSavepoint(); st = conn.prepareStatement(sql2); st.executeUpdate(); int x = 1/0; // 程序運行到這個地方拋異常,后面的代碼就不執行,數據庫沒有收到commit命令 st = conn.prepareStatement(sql3); st.executeUpdate(); conn.commit(); } catch (Exception e) { e.printStackTrace(); conn.rollback(sp); // 回滾到sp點,sp點上面的sql語句發給數據庫執行,由於數據庫沒收到commit命令,數據庫又會自動將這條sql語句的影響回滾掉,所以回滾完,一定要記得commit命令。 conn.commit(); // 手動回滾后,一定要記得提交事務 } finally { JdbcUtils.release(conn, st, rs); } } }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47

事務的四大特性(ACID)

  • 原子性(Atomicity) 
    原子性是指事務是一個不可分割的工作單位,事務中的操作要么全部成功,要么全部失敗。比如在同一個事務中的SQL語句,要么全部執行成功,要么全部執行失敗。
  • 一致性(Consistency) 
    官網上事務一致性的概念是:事務必須使數據庫從一個一致性狀態變換到另外一個一致性狀態。還要一種說法是事務前后數據的完整性必須保持一致。以轉賬為例子,A向B轉賬,假設轉賬之前這兩個用戶的錢加起來總共是2000,那么A向B轉賬之后,不管這兩個賬戶怎么轉,A用戶的錢和B用戶的錢加起來的總額還是2000,這個就是事務的一致性。
  • 隔離性(Isolation) 
    事務的隔離性是多個用戶並發訪問數據庫時,數據庫為每一個用戶開啟的事務,不能被其他事務的操作數據所干擾,多個並發事務之間要相互隔離。
  • 持久性(Durability) 
    持久性是指一個事務一旦被提交,它對數據庫中數據的改變就是永久性的,接下來即使數據庫發生故障也不應該對其有任何影響。

事務的隔離級別

事務的四大特性中最麻煩的是隔離性,下面重點介紹一下事務的隔離級別。 
多個線程開啟各自事務操作數據庫中數據時,數據庫系統要負責隔離操作,以保證各個線程在獲取數據時的准確性。

事務不考慮隔離性可能會引發的問題

如果事務不考慮隔離性,可能會引發如下問題:

  • 臟讀 
    指一個事務讀取了另外一個事務未提交的數據。 
    這是非常危險的,假設a向b轉帳100元,對應sql語句如下所示:

    1.update account set money=money+100 while name=‘b’; 2.update account set money=money-100 while name=‘a’; 
    • 1
    • 2
    • 3
    • 1
    • 2
    • 3

    當第1條sql執行完,第2條還沒執行(a未提交時),如果此時b查詢自己的帳戶,就會發現自己多了100元錢。如果a等b走后再回滾,b就會損失100元。

  • 不可重復讀 
    在一個事務內讀取表中的某一行數據,多次讀取結果不同。(一個事務讀取到了另外一個事務提交的數據) 
    例如銀行想查詢a帳戶余額,第一次查詢a帳戶為200元,此時a向帳戶內存了100元並提交了,銀行接着又進行了一次查詢,此時a帳戶為300元了。銀行兩次查詢不一致,可能就會很困惑,不知道哪次查詢是准的。可將例子簡化為:讀表中某一行數據,例如a賬戶第一次讀為1000,第二次讀為1100。 
    不可重復讀臟讀的區別是,臟讀是讀取前一事務未提交的臟數據,不可重復讀是重新讀取了前一事務已提交的數據。 
    很多人認為這種情況就對了,無須困惑,當然是以后面的結果為准了。我們可以考慮這樣一種情況,比如銀行程序需要將查詢結果分別輸出到電腦屏幕和寫到文件中,結果在一個事務中針對輸出的目的地,進行的兩次查詢不一致,導致文件和屏幕中的結果不一致,銀行工作人員就不知道以哪個為准了。
  • 虛讀(幻讀) 
    虛讀(幻讀)是指在一個事務內讀取到了別的事務插入的數據,導致前后讀取不一致。 
    如丙存款100元未提交,這時銀行做報表統計account表中所有用戶的總額為500元,然后丙提交了,這時銀行再統計發現帳戶為600元了,造成虛讀同樣會使銀行不知所措,到底以哪個為准。可將例子簡化為:讀整個表,即表的行數,例如第一次讀某個表有3條記錄,第二次讀該表又有4條記錄

數據庫共定義了四種隔離級別,應用《高性能mysql》一書中有說明: 
這里寫圖片描述
這里寫圖片描述
這里寫圖片描述

  • Serializable(串行化):可避免臟讀、不可重復讀、虛讀情況的發生。
  • Repeatable read(可重復讀):可避免臟讀、不可重復讀情況的發生。
  • Read committed(讀已提交):可避免臟讀情況發生。
  • Read uncommitted(讀未提交):最低級別,以上情況均無法保證。

總結:在MySQL中,實現了這四種隔離級別,分別有可能產生問題如下所示: 
這里寫圖片描述

下面說說修改事務隔離級別的方法:

  1. 全局修改,修改my.ini(或mysql.ini)配置文件,在最后加上

    
    #可選參數有:READ-UNCOMMITTED, READ-COMMITTED, REPEATABLE-READ, SERIALIZABLE. [mysqld] transaction-isolation = REPEATABLE-READ
    • 1
    • 2
    • 3
    • 4
    • 5
    • 1
    • 2
    • 3
    • 4
    • 5

    注意:MySQL默認的隔離級別為REPEATABLE-READ,並且是嚴格遵循數據庫規范設計的,即支持4種隔離級別;Oracle默認的隔離級別為Read committed,並且不支持這4種隔離級別,只支持這4種隔離級別中的2種,Read committed和Serializable

  2. 對當前session修改,在登錄mysql客戶端后,執行命令:

    set session transaction isolation level read uncommitted; // 設置當前事務隔離級別
    • 1
    • 1

    注意:session是不能掉的,不然你設置不會成功,MySQL的隔離級別還是默認的隔離級別——REPEATABLE-READ,如下所示: 
    這里寫圖片描述

查詢當前事務隔離級:

select @@tx_isolation; // 查詢當前事務隔離級別
  • 1
  • 1

下面,將利用MySQL的客戶端程序,分別測試幾種隔離級別。測試數據庫為day16,表為account;表如下: 
這里寫圖片描述 
兩個命令行客戶端分別為a(黑色背景窗口),b(藍色背景窗口);不斷改變b的隔離級別,在a端修改數據。

  • 將b的隔離級別設置為read uncommitted(未提交讀) 
    在a未更新數據之前,b客戶端 
    這里寫圖片描述 
    a更新數據,a向b轉帳100元 
    這里寫圖片描述 
    此時b查詢自己的帳戶,就會發現自己多了100元錢,出現了臟讀(這個事務讀取到了別的事務未提交的數據) 
    這里寫圖片描述 
    如果a等b走后再回滾  
    這里寫圖片描述 
    此時b查詢自己的帳戶,發現又少掉了100元錢,兩次讀取的數據不一樣,出現不可重復讀現象 
    這里寫圖片描述 
    a提交完事務,再開啟一個事務,向表account中新增一條記錄 
    這里寫圖片描述 
    此時b再次查詢account表,發現表account中多了一條記錄,出現幻讀現象 
    這里寫圖片描述
  • 將客戶端b的事務隔離級別設置為read committed(已提交讀) 
    在a未更新數據之前,b客戶端 
    這里寫圖片描述 
    a更新數據,a向b轉帳100元 
    這里寫圖片描述 
    b查詢自己的帳戶,金額沒有發生任何變化,說明已提交讀隔離級別解決了臟讀的問題 
    這里寫圖片描述 
    a此刻提交事務 
    這里寫圖片描述 
    b再次查詢自己的帳戶,發現自己又多了100元錢,這時就發生不可重復讀(指這個事務讀取到了別的事務提交的數據) 
    這里寫圖片描述 
    a再開啟一個事務,向表account中新增一條記錄 
    這里寫圖片描述 
    然后b再次查詢account表,發現表account中多了一條記錄,出現幻讀現象 
    這里寫圖片描述
  • 將b的隔離級別設置為repeatable read(可重復讀) 
    在a未更新數據之前,b客戶端 
    這里寫圖片描述 
    a更新數據,a向b轉帳100元 
    這里寫圖片描述 
    b查詢自己的帳戶,金額沒有發生任何變化,這說明repeatable read這種級別可避免臟讀 
    這里寫圖片描述 
    a此刻提交事務 
    這里寫圖片描述 
    b再次查詢自己的帳戶,金額沒有發生任何變化,這說明repeatable read這種級別還可以避免不可重復讀 
    這里寫圖片描述 
    a再開啟一個事務,向表account中新增一條記錄 
    這里寫圖片描述 
    然后b再次查詢account表,發現表中可能會多出一條ddd的記錄(也有可能不會多出一條ddd的記錄,我測試時就是這種情況),這就發生了虛讀,也就是在這個事務內讀取了別的事務插入的數據(幻影數據) 
    這里寫圖片描述
  • 將b的隔離級別設置為可串行化 (Serializable) 
    為可串行化 (Serializable)均可避免臟讀、不可重復讀、幻讀。避免臟讀和不可重復讀的情況我就不測試了,測試步驟同上,下面我重點講解可串行化 (Serializable)避免幻讀的情況。 
    事務b端 
    這里寫圖片描述 
    事務a端 
    這里寫圖片描述 
    因為此時事務b的隔離級別設置為serializable,開始事務后,並沒有提交,所以事務a只能等待。 
    事務b提交事務,事務b端 
    這里寫圖片描述 
    事務a端 
    這里寫圖片描述  
    serializable完全鎖定字段,若一個事務來查詢同一份數據就必須等待,直到前一個事務完成並解除鎖定為止,是完整的隔離級別,會鎖定對應的數據表格,因而會有效率的問題。 
    結論:Serializable隔離級別,雖然可避免所有問題,但性能、效率是最低的,原因是它采取的是鎖表的方式,即單線程的方式,即有一個事務來操作這個表了,另外一個事務只能等在外面進不來

下面,將利用Java程序來測試Serializable隔離級別。

public class Demo3 { public static void main(String[] args) throws SQLException, InterruptedException { Connection conn = null; PreparedStatement st = null; ResultSet rs = null; Savepoint sp = null; try { conn = JdbcUtils.getConnection(); conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE); // 相當於設置CMD窗口的隔離級別 conn.setAutoCommit(false); String sql = "select * from account"; conn.prepareStatement(sql).executeQuery(); // 故意讓程序睡眠20秒,睡眠20秒之后事務才結束,程序運行完 Thread.sleep(1000*20); conn.commit(); } finally { JdbcUtils.release(conn, st, rs); } } }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

程序運行,同時在客戶端開啟一個事務,插入一條記錄,需要等待一段時間才能插入進去。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM