MySQL數據庫事務詳解

 

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

 

 

                     

 

 

事務的概念

事務指邏輯上的一組操做,組成這組操做的各個單元,要不所有成功,要不所有不成功。 
例如:A向B轉帳100元,對應於以下兩條sql語句:java

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放在一個事務中執行,則須要使用以下語句:mysql

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,以下圖所示: 
    這裏寫圖片描述sql

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

  • 回滾事務(rollback) 
    這裏寫圖片描述 
    經過手動回滾事務,讓全部的操做都失效,這樣數據就會回到最初的初始狀態!微信

JDBC中使用事務

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

  • 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

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

相關文章
相關標籤/搜索