面試官靈魂的一擊:MySQL事務你懂嗎?

目錄

  • 概念
  • 隔離性與隔離級別
  • 事務隔離的實現
  • 事務啓動方式
  • MVCC工做原理
  • 總結

1、概念

事務究竟是什麼東西呢?想必你們學習的時候也是對事務的概念很模糊的。接下來經過一個經典例子講解事務。mysql

銀行在兩個帳戶之間轉帳,從A帳戶轉入B帳戶1000元,系統先減小A帳戶的1000元,而後再爲B帳號增長1000元。若是所有執行成功,數據庫處於一致性;sql

若是僅執行完A帳戶金額的修改,而沒有增長B帳戶的金額,則數據庫就處於不一致狀態,這時就須要取消前面的操做。數據庫

這過程當中會有一系列的操做,好比餘額查詢餘額作加減法更新餘額等,這些操做必須保證是一個總體執行,要麼所有成功,要麼所有失敗,不能讓A帳戶錢扣了,可是中途某些操做失敗了,致使B帳戶更新餘額失敗。這樣用戶就不樂意了,銀行這不是坑我嗎?數組

事務就是要保證一組數據庫操做,要麼所有成功,要麼所有失敗。markdown

在MySQL中,事務支持是在引擎層實現的。你如今知道,MySQL是一個支持多引擎的系統,但並非全部的引擎都支持事務。併發

好比MySQL原生的MyISAM引擎就不支持事務,這也是MyISAM被InnoDB取代的重要緣由之一。框架

接下來會以InnoDB爲例,抽絲剝繭MySQL在事務支持方面的特定實現。性能

2、隔離性與隔離級別

提到事務,你確定會想到ACIDAtomicityConsistencyIsolationDurability,即原子性一致性隔離性持久性),接下來咱們就要講解其中的I,也就是隔離性學習

當數據庫上存在多個事務同時執行的時候,就可能出現髒讀(dirty read)、不可重複讀(non-repeatable read)、幻讀(phantom read)的問題,爲了解決這些問題,就有了隔離級別的概念。spa

咱們知道,隔離級別越高,效率就越低,所以咱們不少狀況下須要在兩者之間找到一個平衡點。

SQL標準的事務隔離級別包括:

  1. 讀未提交(read uncommitted)
  2. 讀提交(read committed)
  3. 可重複讀(repeatable read)
  4. 串行化(serializable )

下面我逐一爲你解釋:

  1. 讀未提交:事務中的修改,即便沒有提交,對其餘事務也都是可見的,事務能夠讀取未提交的數據,也被稱爲髒讀。這個級別會致使不少問題,從性能上來講也不會比其餘隔離級別好不少,但卻缺少其餘級別的不少好處,通常實際應用中不多用,甚至有些數據庫內部根本就沒有實現。

  2. 讀已提交:事務從開始直到提交以前,所作的任何修改對其餘事務都是不可見的,這個級別有時候也叫作不可重複讀(Nonrepeatable Read),由於同一事務中兩次執行一樣的查詢,可能會獲得不同的結果

  3. 可重複度:同個事務中屢次查詢結果是一致的,解決了不可重複讀的問題。此隔離級別下仍是沒法解決另一個幻讀(Phantom Read)的問題,幻讀是指當某個事務在讀取某個範圍內的記錄時,另一個事務又在該範圍內插入了新的記錄,以前的事務再次讀取該範圍的記錄時,會產生幻行

  4. 串行化:顧名思義是對於同一行記錄,會加寫鎖會加讀鎖。當出現讀寫鎖衝突的時候,後訪問的事務必須等前一個事務執行完成,才能繼續執行。

對於上面的概念中,可能 讀已提交可重複讀比較難理解,下面會用一個例子說明這種集中隔離級別。假設數據表T中只有一列,其中一行的值爲1,下面是按照時間順序執行兩個事務的行爲。

mysql> create table T(c int) engine=InnoDB;
insert into T(c) values(1);
複製代碼

接下來說解不一樣的隔離級別下,事務A會有哪些不一樣的返回結果,也就是圖裏面V1V2V3的返回值分別是什麼。

  1. 若隔離級別是讀未提交, 則V1的值就是2。這時候事務B雖然尚未提交,可是結果已經被A看到了。所以,V2V3也都是2

  2. 若隔離級別是讀提交,則V1是1,V2的值是2。事務B的更新在提交後才能被A看到。因此, V3的值也是2。

  3. 若隔離級別是可重複讀,則V1V2是1,V3是2。之因此V2仍是1,遵循的就是這個要求:事務在執行期間看到的數據先後必須是一致的

  4. 若隔離級別是串行化,則在事務B執行「將1改爲2」的時候,會被鎖住。直到事務A提交後,事務B才能夠繼續執行。因此從A的角度看, V1V2值是1,V3的值是2。

在實現上,數據庫裏面會建立一個視圖,訪問的時候以視圖的邏輯結果爲準。在可重複讀隔離級別下,這個視圖是在事務啓動時建立的,整個事務存在期間都用這個視圖。

讀提交隔離級別下,這個視圖是在每一個SQL語句開始執行的時候建立的。這裏須要注意的是,讀未提交隔離級別下直接返回記錄上的最新值,沒有視圖概念;而串行化隔離級別下直接用加鎖的方式來避免並行訪問。

注意一下,每種數據庫的行爲會有所不同,Oracle數據庫的默認隔離界別是讀提交,所以,當咱們須要進行不一樣數據庫種類之間遷移的時候,爲了保證數據庫隔離級別的一致,切記將MYSQL的隔離級別設置爲讀提交

配置的方式是,將啓動參數transaction-isolation的值設置成READ-COMMITTED。你能夠用show variables來查看當前的值。

每種隔離級別都有它本身的使用場景,你要根據本身的業務狀況來定。我想你可能會問那何時須要「可重複讀」的場景呢?咱們來看一個數據校對邏輯的案例。

假設你在管理一個我的銀行帳戶表。一個表存了每月月底的餘額,一個表存了帳單明細。這時候你要作數據校對,也就是判斷上個月的餘額和當前餘額的差額,是否與本月的帳單明細一致。你必定但願在校對過程當中,即便有用戶發生了一筆新的交易,也不影響你的校對結果。

這時候使用可重複讀隔離級別就很方便。事務啓動時的視圖能夠認爲是靜態的,不受其餘事務更新的影響。

3、事務隔離的實現

接下來以可重複度來展開事務隔離具體是怎麼實現的。

在MySQL中,實際上每條記錄在更新的時候都會同時記錄一條回滾操做。記錄上的最新值,經過回滾操做,均可以獲得前一個狀態的值。

假設一個值從1被按順序改爲了二、三、4,在回滾日誌裏面就會有相似下面的記錄。

能夠看到當前值是4,從圖中能夠看到在查詢的時候,不一樣時刻啓動的事務會有不一樣的read-view。如圖中看到的,在視圖ABC裏面,這一個記錄的值分別是一、二、4,同一條記錄在系統中能夠存在多個版本,就是數據庫的多版本併發控制(MVCC)。

對於read-view A,要獲得1,就必須將當前值依次執行圖中全部的回滾操做獲得。同時你會發現,即便如今有另一個事務正在將4改爲5,這個事務跟read-view A、B、C對應的事務是不會衝突的。

你必定會問,回滾日誌總不能一直保留吧,何時刪除呢?

這是確定不能一直保留的,在不須要的時候才刪除。系統會判斷,當沒有事務再須要用到這些回滾日誌時,回滾日誌會被刪除。

那麼何時纔不須要了呢?就是當系統裏沒有比這個回滾日誌更早的read-view的時候。

基於上面的說明,咱們來討論一下爲何建議你儘可能不要使用長事務。

長事務意味着系統裏面會存在很老的事務視圖。因爲這些事務隨時可能訪問數據庫裏面的任何數據,因此這個事務提交以前,數據庫裏面它可能用到的回滾記錄都必須保留,這就會致使大量佔用存儲空間。

MySQL 5.5及之前的版本,回滾日誌是跟數據字典一塊兒放在ibdata文件裏的,即便長事務最終提交,回滾段被清理,文件也不會變小。我見過數據只有20GB,而回滾段有200GB的庫。最終只好爲了清理回滾段,重建整個庫。

除了對回滾段的影響,長事務還佔用鎖資源,也可能拖垮整個庫,這個咱們會在後面講鎖的時候展開。

4、事務啓動方式

MySQL的事務啓動方式有如下幾種:

  1. 顯式啓動事務語句, beginstart transaction。配套的提交語句是commit,回滾語句是rollback
  2. set autocommit=0,這個命令會將這個線程的自動提交關掉。意味着若是你只執行一個select語句,這個事務就啓動了,並且並不會自動提交。這個事務持續存在直到你主動執行commitrollback 語句,或者斷開鏈接。

有些客戶端鏈接框架會默認鏈接成功後先執行一個set autocommit=0的命令。這就致使接下來的查詢都在事務中,若是是長鏈接,就致使了意外的長事務。

所以,我會建議你老是使用set autocommit=1, 經過顯式語句的方式來啓動事務。

可是有的開發同窗會糾結多一次交互的問題。對於一個須要頻繁使用事務的業務,第二種方式每一個事務在開始時都不須要主動執行一次 begin,減小了語句的交互次數。若是你也有這個顧慮,我建議你使用commit work and chain語法。

autocommit爲1的狀況下,用begin顯式啓動的事務,若是執行commit則提交事務。若是執行 commit work and chain,則是提交事務並自動啓動下一個事務,這樣也省去了再次執行begin語句的開銷。同時帶來的好處是從程序開發的角度明確地知道每一個語句是否處於事務中。

你能夠在information_schema庫的innodb_trx這個表中查詢長事務,好比下面這個語句,用於查找持續時間超過60s的事務。

select * from information_schema.innodb_trx where TIME_TO_SEC(timediff(now(),trx_started))>60
複製代碼

5、MVCC工做原理

可重複讀隔離級別下,事務在啓動的時候就「拍了個快照」。請注意,這個快照是基於整個庫的,這時候你確定以爲難以想象,若是一個庫上百G的數據,那麼我啓動一個事務,那MYSQL豈不是要將上百G的數據拷貝出來,這個過程不是很是慢嗎?可是爲何咱們平時並無感受到它🈵️呢?

事實上,咱們並不須要拷貝出這100G的數據。

咱們先來看看這個快照是怎麼實現的。InnoDB裏面每一個事務有一個惟一的事務ID,叫做transaction id。它是在事務開始的時候向InnoDB的事務系統申請的,是按申請順序嚴格遞增的。

每次事務更新數據的時候,都會生成一個新的數據版本,而且把transaction id賦值給這個數據版本的事務ID,記爲row trx_id。同時,舊的數據版本要保留,而且在新的數據版本中,可以有信息能夠直接拿到它。這也說明了,數據表中的一行記錄,可能存在多個版本(row),每一個版本有本身的row_trx_id.

下面用一張圖說明一個記錄被多個事務連續更新後的狀態,以下圖所示:

圖中用打括號表示一行數據的4個版本,當前最新版本是V4,k的值是12,它是被transaction id 爲25的事務更新的,所以它的row trx_id也是25。

你可能會問,前面的文章不是說,語句更新會生成undo log(回滾日誌)嗎?那麼,undo log在哪呢?

實際上,圖2中的三個虛線箭頭,就是undo log;而V一、V二、V3並非物理上真實存在的,而是每次須要的時候根據當前版本和undo log計算出來的。好比,須要V2的時候,就是經過V4依次執行U三、U2算出來。

明白了多版本和row trx_id的概念後,咱們再來想一下,InnoDB是怎麼定義那個「100G」的快照的。

按照可重複讀的定義,一個事務啓動的時候,可以看到全部已經提交的事務結果。可是以後,這個事務執行期間,其餘事務的更新對它不可見。

所以,一個事務只須要在啓動的時候聲明說,以我啓動的時刻爲準,若是一個數據版本是在我啓動以前生成的,就認;若是是我啓動之後才生成的,我就不認,我必需要找到它的上一個版本

固然,若是「上一個版本」也不可見,那就得繼續往前找。還有,若是是這個事務本身更新的數據,它本身仍是要認的。在實現上, InnoDB爲每一個事務構造了一個數組,用來保存這個事務啓動瞬間,當前正在「活躍」的全部事務ID。「活躍」指的就是,啓動了但還沒提交。

數組裏面事務ID的最小值記爲低水位,當前系統裏面已經建立過的事務ID的最大值加1記爲高水位。

這個視圖數組和高水位,就組成了當前事務的一致性視圖(read-view)。而數據版本的可見性規則,就是基於數據的row trx_id和這個一致性視圖的對比結果獲得的。

這個視圖數組把全部的row trx_id 分紅了幾種不一樣的狀況。以下圖所示:

上圖是數據庫版本可見性規則,對於當前事務的啓動瞬間來講,一個數據版本的row trx_id,有如下幾種可能:

  1. 若是落在綠色部分,表示這個版本是已提交的事務或者是當前事務本身生成的,這個數據是可見的;

  2. 若是落在灰色部分,表示這個版本是由未來啓動的事務生成的,是確定不可見的;

  3. 若是落在粉色部分,那就包括兩種狀況

    • (a) 若 row trx_id在數組中,表示這個版本是由還沒提交的事務生成的,不可見;

    • (b) 若 row trx_id不在數組中,表示這個版本是已經提交了的事務生成的,可見。

好比,對於圖2中的數據來講,若是有一個事務,它的低水位是21,那麼當它訪問這一行數據時,就會從V4經過U3計算出V3,因此在它看來,這一行的值是11。

你看,有了這個聲明後,系統裏面隨後發生的更新,是否是就跟這個事務看到的內容無關了呢?由於以後的更新,生成的版本必定屬於上面的2或者3(a)的狀況,而對它來講,這些新的數據版本是不存在的,因此這個事務的快照,就是「靜態」的了。

因此你如今知道了,InnoDB利用了全部數據都有多個版本的這個特性,實現了「秒級建立快照」的能力。

接下來咱們用一個例子來鞏固一下MVCC的知識,例子以下:

下面是一個只有兩行的表的初始化語句。

mysql> CREATE TABLE `t` (
  `id` int(11) NOT NULL,
  `k` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB;
insert into t(id, k) values(1,1),(2,2);
複製代碼

begin/start transaction 命令並非一個事務的起點,在執行到它們以後的第一個操做InnoDB表的語句,事務才真正啓動。若是你想要立刻啓動一個事務,可使用start transaction with consistent snapshot 這個命令。

還須要注意的是,咱們的例子中若是沒有特別說明,都是默認autocommit=1

在這個例子中,事務C沒有顯式地使用begin/commit,表示這個update語句自己就是一個事務,語句完成的時候會自動提交。事務B在更新了行以後查詢; 事務A在一個只讀事務中查詢,而且時間順序上是在事務B的查詢以後。

讓咱們想一下圖中的三個事務,分析一下事務A的語句返回的結果是什麼?

答案:事務B查到的k的值是3,而事務A查到的k的值是1,是否是感到有點奇怪?

接下來咱們用假設分析法,進行以下的假設:

  1. 事務A開始前,系統裏面只有一個活躍事務ID是99;

  2. 事務A、B、C的版本號分別是100、10一、102,且當前系統裏只有這四個事務;

  3. 三個事務開始前,(1,1)這一行數據的row trx_id是90。

這樣,事務A的視圖數組就是[99,100], 事務B的視圖數組是[99,100,101], 事務C的視圖數組是[99,100,101,102]

爲了便於咱們分析,接下來咱們經過一個圖去分析,以下圖所示:

這裏須要說明一下,start transaction with consistent snapshot;的意思是從這個語句開始,建立一個持續整個事務的一致性快照。因此,在讀提交隔離級別下,這個用法就沒意義了,等效於普通的start transaction

從圖中能夠看到,第一個有效更新是事務C,把數據從(1,1)改爲了(1,2)。這時候,這個數據的最新版本的row trx_id是102,而90這個版本已經成爲了歷史版本。

第二個有效更新是事務B,把數據從(1,2)改爲了(1,3)。這時候,這個數據的最新版本(即row trx_id)是101,而102又成爲了歷史版本。

你可能注意到了,在事務A查詢的時候,其實事務B尚未提交,可是它生成的(1,3)這個版本已經變成當前版本了。但這個版本對事務A必須是不可見的,不然就變成髒讀了。

好,如今事務A要來讀數據了,它的視圖數組是[99,100]。固然了,讀數據都是從當前版本讀起的。因此,事務A查詢語句的讀數據流程是這樣的:

  1. 找到(1,3)的時候,判斷出row trx_id=101,比高水位大,處於紅色區域,不可見;

  2. 接着,找到上一個歷史版本,一看row trx_id=102,比高水位大,處於紅色區域,不可見;

  3. 再往前找,終於找到了(1,1),它的row trx_id=90,比低水位小,處於綠色區域,可見。

這樣執行下來,雖然期間這一行數據被修改過,可是事務A不論在何時查詢,看到這行數據的結果都是一致的,因此咱們稱之爲一致性讀。

這個判斷規則是我經過一些資料和高性能MYSQL中從代碼邏輯直接轉譯過來的,可是正如你所見,用於人肉分析可見性很麻煩。

一個數據版本,對於一個事務視圖來講,除了本身的更新老是可見之外,有三種狀況:

  1. 版本未提交,不可見;

  2. 版本已提交,可是是在視圖建立後提交的,不可見;

  3. 版本已提交,並且是在視圖建立前提交的,可見。

如今,咱們用這個規則來判斷圖4中的查詢結果,事務A的查詢語句的視圖數組是在事務A啓動的時候生成的,這時候:

  • (1,3)還沒提交,屬於狀況1,不可見;

  • (1,2)雖然提交了,可是是在視圖數組建立以後提交的,屬於狀況2,不可見;

  • (1,1)是在視圖數組建立以前提交的,可見。

你看,去掉數字對比後,只用時間前後順序來判斷,分析起來是否是輕鬆多了。因此,後面咱們就都用這個規則來分析。

這時候你是否是有一個這樣的疑問:事務B的update語句,若是按照一致性讀,好像結果不對哦?

事務B的視圖數組是先建立的,以後事務C才提交,不是應該看不見(1,2)嗎,怎麼能算出(1,3)來?

確實如此,若是事務B在更新以前查詢一次數據,這個查詢返回的k的值確實是1。

可是,當它要去更新數據的時候,就不能再在歷史版本上更新了,不然事務C的更新就丟失了。

所以,事務B此時的set k=k+1是在(1,2)的基礎上進行的操做,這裏就用到了這樣一條規則:更新數據都是先讀後寫的,而這個讀,只能讀當前的值,稱爲**當前讀。

所以,在更新的時候,當前讀拿到的數據是(1,2),更新後生成了新版本的數據(1,3),這個新版本的row trx_id是101。因此,在執行事務B查詢語句的時候,一看本身的版本號是101,最新數據的版本號也是101,是本身的更新,能夠直接使用,因此查詢獲得的k的值是3。

這裏咱們提到了一個概念,叫做當前讀。其實,除了update語句外,select語句若是加鎖,也是當前讀。

所以,若是把事務A的查詢語句select * from t where id=1修改一下,加上lock in share modefor update,也均可以讀到版本號是101的數據,返回的k的值是3。下面這兩個select語句,就是分別加了讀鎖(S鎖,共享鎖)和寫鎖(X鎖,排他鎖)。

mysql> select k from t where id=1 lock in share mode;
mysql> select k from t where id=1 for update;
複製代碼

假設事務C不是立刻提交的,而是變成了下面的事務C’,會怎麼樣呢?以下圖所示:

事務C’的不一樣是,更新後並無立刻提交,在它提交前,事務B的更新語句先發起了。前面說過了,雖然事務C’還沒提交,可是(1,2)這個版本也已經生成了,而且是當前的最新版本。那麼,事務B的更新語句會怎麼處理呢?

這時候,咱們的兩階段鎖協議就要上場了。事務C’沒提交,也就是說(1,2)這個版本上的寫鎖還沒釋放。而事務B是當前讀,必需要讀最新版本,並且必須加鎖,所以就被鎖住了,必須等到事務C’釋放這個鎖,才能繼續它的當前讀。

那麼回到以前的隔離界別中的事務的可重複讀的能力是怎麼實現的?

可重複讀的核心就是一致性讀(consistent read);而事務更新數據的時候,只能用當前讀。若是當前的記錄的行鎖被其餘事務佔用的話,就須要進入鎖等待。

而讀提交的邏輯和可重複讀的邏輯相似,它們最主要的區別是:

  1. 在可重複讀隔離級別下,只須要在事務開始的時候建立一致性視圖,以後事務裏的其餘查詢都共用這個一致性視圖;

  2. 在讀提交隔離級別下,每個語句執行前都會從新算出一個新的視圖。

接下來再看一下,在讀提交隔離級別下,事務A和事務B的查詢語句查到的k,分別應該是多少呢?以下圖所示:

能夠看到此時事務A的查詢語句的視圖數組是在執行這個語句的時候建立的,時間線上(1,2)(1,3)的生成時間都在建立這個視圖數組的時刻以前。

可是,在這個時刻:(1,3)還沒提交,屬於狀況1,不可見;(1,2)提交了,屬於狀況3,可見。因此,這時候事務A查詢語句返回的是k=2。顯然地,事務B查詢結果k=3。

6、總結

本文從底層分析了MySQL的事務原理,但願對大家有所幫助,最後別忘了點贊喲!!!

相關文章
相關標籤/搜索