【MySQL系列4】深入分析MySQL中事務以及MVCC的實現原理

前言

前面幾篇,我們分析了MySQL中索引的相關知識以及explain執行計劃分析,想必大家對索引已經有了基本的認識,那麼這一篇,我將爲大家介紹一下MySQL中事務以及MVCC相關知識

什麼是事務

事務(Transaction)是由一系列對數據庫中的數據進行訪問與更新的操作所組成的一個程序執行單元。

在同一個事務中所進行的操作,要麼都成功,要麼就什麼都不做。理想中的事務必須滿足四大特性,這就是大名鼎鼎的ACID。

事務的ACID特性

並不是所有的事務都滿足ACID特性,比如:對於Oracle和SQL Server數據庫,其默認隔離級別是Read COMMITTED,就不滿足I(隔離性)的要求;對於MySQL的NDB Cluster引擎來說,不滿足D(持久性)的要求。

A(Atomicity)-原子性

原子性指的是數據庫事務是不可分割的一部分,只有一個事務中的所有操作都成功,這個事務纔算執行成功,一旦有一個操作失敗,那麼其他成功的操作也必須回滾。
以轉賬1000元場景爲例,一個轉賬過程就是一個事務,這個事務主要包括以下兩步:
1、從A賬戶扣除1000元
2、將B賬戶中增加1000元
試想,如果第一步成功了,那麼第二步失敗了,那就等於A的1000元錢直接消失了,相信這是任何人都不能接受的事項,所以數據庫事務才需要保證原子性。

C(Consistent)-一致性

指的是在事務開始之前和事務結束之後,數據庫的完整性約束都沒有被破壞,事務執行的前後都是合法的數據狀態。

比如我們有一張表中有一個字段name建立了一個唯一約束,那麼當我們進行事務提交或者事務回滾之後,這個name必須依然保證唯一。

I(Isolation)-隔離性

隔離性就是說每個事務之間的操作應該相互隔離,互不干擾。比如說一個事務提交之前對另一個事務不可見。

隔離是一個相對抽象而複雜的概念,比如說事務之間的隔離性我們到底要隔離到哪種程度呢?所以,針對隔離,SQL92標準定義了4種隔離級別,這個放在後面事務的隔離級別中介紹。

D(Durable)-持久性

持久性這個概念就比較容易理解了,就是說事務一旦提交成功了,那麼就應該是持久的,即使是數據庫重啓,服務器宕機等情況發生,數據都不會丟失(當然這個不能包括因爲地震等自然災害導致的存儲數據的硬盤損發生不可逆的損壞)。

事務的管理

可能很多人會說自己都感知不到MySQL的事務,其實這是因爲MySQL事務是默認開啓了自動提交的,因此,如果要感知到事務,我們需要關閉自動提交或者顯示開啓事務。

事務的自動提交

查看自動提交語句:

SHOW VARIABLES LIKE 'autocommit';-- ON表示開啓了自動提交
SELECT @@autocommit;-- 1表示開啓了自動提交

執行如下語句關閉自動提交:

SET autocommit='OFF';
SET @@autocommit = 0;

不過需要注意的是,這種修改方式只是在當前會話窗口生效,對其他會話窗口是不生效的,MySQL幾乎所有變量設置都會分成兩個級別,session(會話)和global(全局)級別,默認就是session級別。

常用的事務控制語句

  • START TRANSACTION或者BEGIN:顯示的開啓事務。需要注意的是在存儲過程中只能用START TRANSACTION開啓事務,因爲存儲過程本來有BEGIN…END語法,兩者會衝突。
  • COMMIT:提交事務。也可以寫成COMMIT WORK。
  • ROLLBACK:回滾事務。也可以寫成ROLLBACK WORK。
  • SAVEPOINT identifier:自定義保存點,適用於長事務,可以回滾到我們自定義的位置。
  • RELEASE SAVEPOINT identifier:刪除一定保存點,如果沒有保存點的時候,會報錯
  • ROLLBACK TO[SAVEPOINT] identifier:回滾到指定保存點。

COMMIT和COMMIT WORK的區別

這兩個都能提交一個事務,區別就在於提交事務之後的操作,同樣的還有ROLLBACK和ROLLBACK WORK,主要是通過一個變量來控制:completion_type,可以執行下面的sql來查看結果:

SHOW VARIABLES LIKE '%completion_type%';

completion_type有如下三種結果:

描述
NO_CHAIN或者0 默認值,此時commit和rollback等價於commit work和rollback work
CHAIN或者1 此時commit work和rollback work等價於commit and chain和rollback and chain,在提交或者回滾事務之後會立刻啓動一個相同隔離級別的新事務
RELEASE或者2 此時commit work和rollback work等價於commit release和rollback release,在提交或者回滾事務之後會斷開當前數據庫連接連接

舉個栗子1:

SET completion_type=1; --1
begin;--2
INSERT test2 VALUES(1,'張1');--3
commit work;--4
INSERT test2 VALUES(2,'張1');--5
select * from test2;--6
rollback;--7
select * from test2;--8

第4條語句中,我們提交了一個事務,第5條語句中我們又插入了一條數據,此時第六條語句可以查詢出2條數據,接下來我們回滾,語句8再去查詢就會發現只剩一條數據了,因爲語句6倍回滾了,我們在語句4之後並沒有顯示的開啓一個事務,這就說明語句4自動開啓了一個新的事務。

舉個栗子2:

SET completion_type=2;
begin;
INSERT test2 VALUES(3,'張1');
commit work;
select * from test2;

最後一條語句返回如下結果:
在這裏插入圖片描述
先提示的斷開連接,然後自動重連。測試這個例子的時候用工具比如sqlyog可能會不是很明顯,因爲工具會自動幫忙重連,看起來就好像沒斷開一樣,建議用命令窗口的形式測試

事務的分類

從事務的理論角度來說,我們可以把事務分爲以下五大類:

扁平事務

這種是最簡單也是最常用的一種事務,這種事務中的所有操作都是原子的,要麼全部成功,要麼什麼都不做。

帶有保存點的扁平事務

這種一般比較適合於長事務,事務處理到後面報錯的時候,我們可以選擇不全部回滾事務,而是回滾到我們自定義好的某一個保存點。如下例子:

BEGIN;
INSERT test VALUES(1,'張1');
SAVEPOINT A
INSERT test VALUES(2,'張2');
ROLLBACK TO A
COMMIT;

上面示例語句中,我麼你定義了一個保存點A,然後在後面又回滾到A,這時候提交事務,那麼第二條插入語句是失敗的,而第一條語句是成功的。

注意:回滾到指定保存點之後,事務仍然還在活動狀態,我們依然需要執行COMMIT或者ROLLBACK語句纔算結束了事務

鏈事務

在提交一個事務之後,釋放掉我們不需要的數據,將必要的數據隱式的傳給下一個事務。(注意:提交事務操作和開始下一個事務操作是一個原子操作)這就意味着下一個事務能看到上一個事務的結果。

鏈事務可以看成帶有保存點的特殊事務,他們的區別就是帶有保存點的事務可以回滾到任意保存點,但是回滾之後事務仍然活躍,需要執行COMMIT或者ROLLBACK之後才結束事務,而鏈事務中只能回滾到最近的一個保存點(即開始事務的點)。

鏈事務可以通過上面的completion_type參數來實現。上文中有舉例使用方法,這裏就不重複舉例了。

嵌套事務

嵌套事務就是說一個事務之中嵌套另一個事務,事務之間存在父子關係,子事務的提交之後並不生效,需要等到父事務提交之後纔會生效。

需要注意的是MySQL原生並不支持嵌套事務,但是可以通過保存點模擬嵌套事務,只是說這麼模擬的話就沒有真正的嵌套事務這麼靈活。

分佈式事務

分佈式事務通常就是在分佈式環境下,多個數據庫下運行不同的扁平事務。多個數據庫環境下運行的扁平事務就合成了一個分佈式事務。

事務的隔離級別

Read Uncommitted(未提交讀)

簡稱RU。這種是最低的隔離級別,等於沒有隔離,基本上沒有數據庫會使用這個級別。一個事務可以讀取到其他事務未提交的數據,這種也叫做髒讀。

什麼是髒讀?請看下面這個例子:
在這裏插入圖片描述
左邊是事務1,先查一次,查到id爲1的數據name爲張三,這時候事務2又來了,把張三改成了李四,然後事務1又進行了一次查詢,查出來了name爲李四,那麼假如這時候事務2發生了回滾,也就是name還是張三,但是事務1卻讀到了李四,這就是髒讀。

Read Committed(已提交讀)

簡稱RC。一個事務只能讀取到其他事務已提交的數據,就是說在一個事務裏面,執行同樣的查詢,會出現兩次不一樣的結果。Oracle和SQL Server數據庫默認的數據庫隔離級別。這種隔離級別解決了髒讀問題,但是會出現不可重複讀的問題。

什麼是不可重複讀?還是看上面那個例子,假設事務2更新之後馬上就提交,然後事務1第二次查詢查出來的結果還是李四,只是這次就不算是髒讀了,因爲事務2提交了,這種就叫不可重複讀,因爲事務1中兩次查詢同一條數據結果不一樣。

Repeatable Read(可重複讀)

簡稱RR。這種隔離級別解決了不可重複讀問題,就是說在同一個事務中,執行相同的查詢,結果都是一樣的,但是這種級別會出現幻讀問題(InnoDB引擎例外,InnoDB引擎通過間隙鎖解決了幻讀問題)。

什麼是幻讀?請看下面這個例子:
在這裏插入圖片描述
上面圖形中,事務1進行了一個範圍查詢,第一次只能查出一條記錄,這時候事務2來插入了一條數據,然後事務1再次執行同一個查詢,這時候就能查出來兩條記錄,也就是多了一條,給人一種幻覺,所以稱之爲幻讀。(InnoDB通過臨鍵鎖解決了幻讀問題,想詳細瞭解的請點擊這裏)

說到這裏,可能有人就有疑問了,因爲感覺不可重複讀和幻讀都是讀取到已提交事務的結果,好像沒什麼區別?確實如此,不可重複讀和幻讀本質上是一樣的,但是不可重複讀針對的是更新和刪除操作,而幻讀僅針對插入操作。

Serializable(串行化)

這種是隔離的最高級別,也就是說所有的事務都是串行執行的,也就不存在併發事務,髒讀,可重複讀和幻讀問題自然也就沒有了。

不同隔離級別對比

不同的隔離級別可以解決不同的問題,大致如下圖:
在這裏插入圖片描述
對於未提交讀和已提交讀大家可能都很好理解,只要控制一個事務提交之後才能對另一個事務可見,但是對於可重複讀,MySQL到底是如何實現即使一個事務已經提交了,還能對另一個事務不可見呢?這就是接下來我們要講解的MVCC了。

事務隔離的實現方案

事務隔離的實現方案有兩種,LBCC和MVCC

LBCC

本章節開始會涉及到一些鎖的概念,這個我會在下一篇文章專門講解鎖,本文不會講解過多鎖的概念。想了解的請關注我,和孤狼一起學習進步

LBCC,基於鎖的併發控制,英文全稱Based Concurrency Control。這種方案比較簡單粗暴,就是一個事務去讀取一條數據的時候,就上鎖,不允許其他事務來操作(當然這個鎖的實現也比較重要,如果我們只鎖定當前一條數據依然無法解決幻讀問題)。

當前讀

這個概念其實很好理解,MySQL加鎖之後就是當前讀。假如當前事務只是加共享鎖,那麼其他事務就不能有排他鎖,也就是不能修改數據;而假如當前事務需要加排他鎖,那麼其他事務就不能持有任何鎖。總而言之,能加鎖成功,就確保了除了當前事務之外,其他事務不會對當前數據產生影響,所以自然而然的,當前事務讀取到的數據就只能是最新的,而不會是快照數據(後文MVCC會解釋快照讀概念)。

LBCC方案中,如果我們的業務系統是讀多寫少的話,這種方案就會極大影響了效率,所以我們就有了另一種解決方案:MVCC。

MVCC

MVCC,多版本的併發控制,英文全稱:Multi Version Concurrency Control。就是當我們在修改數據的時候,可以爲這條數據創建一個快照,後面就可以直接讀取這個快照。

那麼MVCC具體到底是如何實現的呢?

爲了實現MVCC機制,InnoDB內部爲每一行添加了兩個隱藏列:DB_TRX_ID和DB_ROLL_PTR(MySQL另外還有一個隱藏列DB_ROW_ID,這是在InnoDB表沒有主鍵的時候會用來作爲主鍵,想詳細瞭解可以點擊這裏)。

DB_TRX_ID

長度爲6字節,存儲了插入或更新語句的最後一個事務的事務ID。

DB_ROLL_PTR

長度爲7字節,稱之爲:回滾指針。回滾指針指向寫入回滾段的undo log記錄,讀取記錄的時候會根據指針去讀取undo log中的記錄。

正因爲MySQL中undo log中會維護一個歷史數據記錄,所以我們應該養成定期提交事務的習慣,否則回滾段會越來越大,甚至佔滿了表空間。

快照讀

快照讀是針對上文的當前讀而言,指的是在RR隔離級別下,在不加鎖的情況下MySQL會根據回滾指針選擇從undo log記錄中獲取快照數據,而不總是獲取最新的數據,這也就是爲什麼另一個事務提交了數據,在當前事務中看到的依然是另一個事務提交之前的數據。

MySQL什麼時候開始讀取快照

我們先看看MySQL默認隔離級別RR下的一個例子(注意,test和test2兩張表一開始都是空表,均只有id和name兩個字段)。

  • 場景1(事務1操作數據之後再進行第一次查詢):
事務1 事務2
BEGIN; BEGIN;
INSERT INTO test VALUE(1,‘張三’);
COMMIT;
SELECT * FROM test WHERE id=1;
(查出id=1,name=張三)
UPDATE test SET NAME=‘李四’ WHERE id=1;
SELECT * FROM test WHERE id=1;
(查出id=1,name=張三)
COMMIT; COMMIT;
  • 場景2(事務1不進行任何操作,事務2先開始第一次查詢)
事務1 事務2
BEGIN; BEGIN;
SELECT * FROM test WHERE id=1;
(查出id=1,name=李四)
UPDATE test SET NAME=‘王五’ WHERE id=1;
COMMIT;
SELECT * FROM test WHERE id=1;
(查出id=1,name=李四)
COMMIT;

通過上面兩個場景中我們可以得出結論:
RR隔離級別快照並不是在BEGIN就開始產生了,而是要等到事務當中的第一次查詢之後纔會產生快照,之後的查詢就只讀取這個快照數據

  • 場景3(事務2先進行一次t1表查詢之後,事務1再去操作其他表t2)
事務1 事務2
BEGIN; BEGIN;
SELECT * FROM test WHERE id=1;
(查出id=1,name=王五)
INSERT INTO test2 VALUE(1,‘楊過’);
COMMIT;
SELECT * FROM test2 WHERE id=1;
(空)
COMMIT;

從場景3我們可以得出結論:RR隔離級別快照並不只是針對當前所查詢的數據,而是針對當前MySQL中的所有數據(跨庫也一樣,只要在同一個MySQL)

MVCC查詢機制

MVCC機制到底如何查詢的呢?假設由很多個事務同時進行,那麼就會產生很多快照,查詢的時候又到底是怎麼做的呢?

接下來我們把抽象的概念具體化,假定DB_TRX_ID和DB_ROLL_PTR均爲整型,接下來我們進行查詢演示:

1、清空原先的test表,事務A插入兩條數據,此時DB_TRX_ID(事務id)爲1,DB_ROLL_PTR(回滾指針爲null)

id name 事務id 回滾指針
1 張三 1 null
2 李四 1 null

2、這時候事務B進行了一次查詢,會得到上面的結果,事務2還沒提交的時候又來了事務C,事務C插入了id=3的數據,此時表中的數據如下:

id name 事務id 回滾指針
1 張三 1 null
2 李四 1 null
3 王五 3 null

注意,這時候第3條數據的事務id爲3,因爲事務2也會產生一個事務id
3、這時候事務B再次進行查詢,根據上面瞭解的,我們知道,這時候應該是查詢不出王五的,所以實際上二次查詢可能是這麼查的:

select * from test where 事務id<=2-- 因爲當前的事務id爲2

4、假如這時候事務D又來了,把id=1的數據給刪除了,這時候會把原數據的回滾指針記錄爲當前的事務id:4,所以此時數據如下:

id name 事務id 回滾指針
1 張三 1 4
2 李四 1 null
3 王五 3 null

5、回到事務B,繼續查詢,應該還是隻有1和2兩條數據,那麼他可能是這麼查詢的:

select * from test where 事務id<=2 and (回滾指針 is null or 回滾指針 >2)

6、假如這時候又來了事務E,對第2條數據進行了更新,這時候會生產一條事務id爲5的數據,並把原數據的回滾指針也同時標記爲當前的事務id:5,那麼會得到如下數據:

id name 事務id 回滾指針
1 張三 1 null
2 李四 1 5
3 王五 3 4
2 王五 5 null

根據上面猜測,執行下面的查詢:

select * from test where 事務id<=2 and (回滾指針 is null or 回滾指針 >2)

這時候發現,查出來的數據還是隻有1和2兩條。

MVCC查詢兩大規則

綜上,MVCC大致查詢規則如下:
1、只查詢事務id小於等於當前事務id的數據。(這裏要等於是因爲假如自己的事務插入了一條數據,會生成一條當前事務id的數據,所以必須包含本事務自己插入的數據)
2、只查詢未刪除(回滾指針爲空)或者回滾指針大於當前事務id的數據。(這裏不能等於是因爲假如自己的事務刪除了一條數據,會生成數據的回滾指針爲當前事務id,所以必須排除掉自己刪除的數據)

當然,上面規則只是簡化了,實際查詢遠比這裏複雜,只是希望藉助這種簡單化的概念可以幫助大家更好的理解MVCC工作機制。

下一篇,介紹了鎖相關知識,以及InnoDB是如何通過鎖來解決幻讀問題,想詳細瞭解的,請點擊這裏

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章