MySQL數據庫事務,鎖和MVCC

事務可以用來維護數據庫的完整性,保證成批的 SQL 語句要么全部執行,要么全部不執行。MySQL 中只有使用了 Innodb 數據庫引擎的數據庫或表才支持事務。

一般來說,數據庫事務必須支持4個特性(ACID):原子性,一致性,隔離性和持久性

在 MySQL 命令行的默認設置下,事務都是自動提交的。在自動提交模式下,如果沒有start transaction顯式地開始一個事務,那么每個sql語句都會被當做一個事務來執行并自動進行提交操作。如果要顯式地開啟一個事務務須使用命令 BEGIN 或 START TRANSACTION,或者執行命令 SET AUTOCOMMIT=0,用來禁止使用當前會話的自動提交。

1. 數據庫事務特性

原子性

一個事務(transaction)中的所有操作,要么全部完成,要么全部不完成,不會結束在中間某個環節。事務在執行過程中發生錯誤,會被回滾(Rollback)到事務開始前的狀態,就像這個事務從來沒有執行過一樣。

想要保證事務的原子性,就需要在異常發生時,對已經執行的操作進行回滾。而在 MySQL 中,恢復機制是通過回滾日志(undo log)實現的,所有事務進行的修改都會先記錄到這個回滾日志中,然后才對數據庫中的數據執行修改操作。

一致性

一個事務執行結束后,數據庫的完整性約束沒有被破壞。數據庫的完整性約束包括但不限于:實體完整性(如行的主鍵存在且唯一)、列完整性(如字段的類型、大小、長度要符合要求)、外鍵約束、用戶自定義完整性(如轉賬前后,兩個賬戶余額的和應該不變)。

可以說,一致性是事務追求的最終目標。前面提到的原子性、持久性和隔離性,都是為了保證數據庫狀態的一致性。此外,除了數據庫層面的保障,一致性的實現也需要應用層面進行保障。

隔離性

數據庫允許多個并發事務同時對其數據進行讀寫和修改的能力,隔離性可以防止多個事務并發執行時由于交叉執行而導致數據的不一致。事務隔離分為不同級別,包括讀未提交(Read uncommitted)、讀提交(Read committed)、可重復讀(Repeatable read)和串行化(Serializable)。

  • Read uncommitted:所有事務都可以看到其他未提交事務的更新。此種隔離級別會出現臟讀的問題,即其他并發事務能夠讀到還未提交的數據。
  • Read committed:這是大多數數據庫系統的默認隔離級別(但不是MySQL默認的)。它滿足了隔離的簡單定義:一個事務只能看見已經提交事務所做的改變。這種隔離級別存在不可重復讀(Nonrepeatable Read)的問題。不可重復讀是指在一個事務中的兩次查詢結果集不一致,這可能是兩次查詢過程中間,有另外一個事務更新的原有的數據。
  • Repeatable read:這是MySQL的默認事務隔離級別,它確保同一事務的多個實例在并發讀取數據時,會看到同樣的數據行。不過理論上,這會導致另一個棘手的問題:幻讀 (Phantom Read)。簡單的說,幻讀指當用戶讀取某一范圍的數據行時,另一個事務又在該范圍內插入了新行,當用戶再讀取該范圍的數據行時,會發現有新的“幻影” 行。InnoDB和Falcon存儲引擎通過多版本并發控制(MVCC,Multiversion Concurrency Control)機制解決了該問題。
  • Serializable:這是最高的隔離級別,它通過強制事務排序,使之不可能相互沖突,從而解決幻讀問題。

MySQL通過MVCC機制避免了不可重復讀和幻讀的問題。MVCC的原理在后文中我們會介紹。

持久性

一個事務處理結束后,對數據的修改就是永久的,即便系統故障也不會丟失。

InnoDB作為MySQL的存儲引擎,數據是存放在磁盤中的,但如果每次讀寫數據都需要磁盤IO,效率會很低。為此,InnoDB提供了緩存(Buffer Pool),Buffer Pool中包含了磁盤中部分數據頁的映射,作為訪問數據庫的緩沖:當從數據庫讀取數據時,會首先從Buffer Pool中讀取,如果Buffer Pool中沒有,則從磁盤讀取后放入Buffer Pool;當向數據庫寫入數據時,會首先寫入Buffer Pool,Buffer Pool中修改的數據會定期刷新到磁盤中(這一過程稱為刷臟)。

Buffer Pool的使用大大提高了讀寫數據的效率,但是也帶了新的問題:如果MySQL宕機,而此時Buffer Pool中修改的數據還沒有刷新到磁盤,就會導致數據的丟失,事務的持久性無法保證。

于是,redo log被引入來解決這個問題:當數據修改時,除了修改Buffer Pool中的數據,還會在redo log記錄這次操作;當事務提交時,會調用fsync接口對redo log進行刷盤。如果MySQL宕機,重啟時可以讀取redo log中的數據,對數據庫進行恢復。redo log采用的是WAL(Write-ahead logging,預寫式日志),所有修改先寫入日志,再更新到Buffer Pool,保證了數據不會因MySQL宕機而丟失,從而滿足了持久性要求。

既然redo log也需要在事務提交時將日志寫入磁盤,為什么它比直接將Buffer Pool中修改的數據寫入磁盤(即刷臟)要快呢?主要有以下兩方面的原因:

  1. 刷臟是隨機IO,因為每次修改的數據位置隨機,但寫redo log是追加操作,屬于順序IO。
  2. 刷臟是以數據頁(Page)為單位的,MySQL默認頁大小是16KB,一個Page上一個小修改都要整頁寫入;而redo log中只包含真正需要寫入的部分,無效IO大大減少。

redo log與binlog

我們知道,在MySQL中還存在binlog(二進制日志)也可以記錄寫操作并用于數據的恢復,但二者是有著根本的不同的:

(1)作用不同:redo log是用于crash recovery的,保證MySQL宕機也不會影響持久性;binlog是用于point-in-time recovery的,保證服務器可以基于時間點恢復數據,此外binlog還用于主從復制。

(2)層次不同:redo log是InnoDB存儲引擎實現的,而binlog是MySQL的服務器層(可以參考文章前面對MySQL邏輯架構的介紹)實現的,同時支持InnoDB和其他存儲引擎。

(3)內容不同:redo log是物理日志,內容基于磁盤的Page;binlog的內容是二進制的,根據binlog_format參數的不同,可能基于sql語句、基于數據本身或者二者的混合。

2. 數據庫鎖

數據庫鎖從邏輯角度可以分為兩類:樂觀鎖和悲觀鎖。

樂觀鎖:一般是用戶業務中實現的鎖機制。樂觀鎖假設數據一般情況下不會造成沖突,因此在提交更新的時候才對數據是否存在沖突進行檢驗。如果存在數據沖突則更新失敗。具體的實現是位數據庫表添加一個version字段,每次更新操作都會修改version。提交時使用CAS操作修改version,如果修改成功,則更新數據成功,否則操作失敗。

悲觀鎖:悲觀鎖一般就是我們通常說的數據庫層面的鎖機制。一般分為表鎖和行鎖。MyISAM中只用到表鎖,不會有死鎖的問題,鎖的開銷也很小,但是相應的并發能力很差。innodb實現了行級鎖和表鎖,鎖的粒度變小了,并發能力變強,但是相應的鎖的開銷變大,很有可能出現死鎖。同時inodb需要協調這兩種鎖,算法也變得復雜。InnoDB行鎖是通過給索引上的索引項加鎖來實現的,只有通過索引條件檢索數據,InnoDB才使用行級鎖,否則,InnoDB將使用表鎖。另外,表鎖和行鎖都分為共享鎖和排他鎖,共享鎖負責只讀,而排他鎖負責寫。

2.1 共享鎖和排他鎖

共享鎖用于讀取數據。當事務A對某資源加了共享鎖后,其它事務也只能對該資源加共享鎖。若想加排他鎖,需等待所有事務釋放共享鎖。

而排他鎖用于修改數據。當事務A對某資源加了排他鎖后,事務A可以讀取和修改該資源。其它事務不能對該資源加任何鎖,直到事務A釋放排他鎖。

下面是添加共享鎖和排他鎖的例子:

SELECT * FROM table_name WHERE id = 1 LOCK IN SHARE MODE; -- 顯式加共享鎖
SELECT * FROM table_name WHERE id = 1 FOR UPDATE; -- 顯式加排他鎖

另外,在InnoDB引擎下,所有的insertupdatedelete語句都會給相關記錄添加排他鎖。

select語句由于MVCC機制不用添加任何鎖,因此通過MVCC能夠大幅提高并發事務間的讀效率。

當一個事務結束時,在事務內的添加的鎖就會隱式的釋放。不需要客戶端顯示的執行鎖的釋放。

3. MVCC機制

MySQL MVCC機制實現了Read committed和Repeatable Read隔離級別。如果要實現Read committed隔離級別,最簡單的方案就是給事務中所有的讀操作添加共享鎖,所有的寫操作添加排他鎖。這樣就能夠確保所有事務內的讀操作都能夠讀到已經提交了的更新(未提交的更新加了排他鎖,因此無法讀到)。

然而直接通過讀鎖和排他鎖實現的Read committed隔離級別,并發事務讀吞吐量很低。因此,MySQL通過MVCC機制,避免了對于讀操作的加鎖,并發競爭的概率大大降低,從而提升了并發事務間的讀效率。

3.1 MVCC原理

首先每一個事務在開始時都會獲得一個id(Mysql維護的遞增id),每張表都擁有兩個隱藏列:

  • DB_TRX_ID: 記錄操作該行數據的最新事務的事務ID
  • DB_ROLL_PTR:指向上一個版本數據在undo log里的位置指針(事務開始時都會寫一份undo log)

每一次事務內的update操作,都會修改該行數據的DB_TRX_ID和DB_ROLL_PTR。因此,通過每行數據的事務id和指向歷史數據的指針,我們就可以根據當前事務id獲得其對應版本的數據了。

Mysql MVCC具體的實現機制如下:

一個事務在第一次執行到select語句時,Mysql會為其創建一個ReadView。ReadView中包含4個重要屬性:

  • trx_ids: 當前系統活躍(未提交)事務版本號集合
  • low_limit_id: 創建當前read view 時當前系統活躍的事務最大ID號
  • up_limit_id: 創建當前read view 時系統活躍的事務最小ID號
  • creator_trx_id: 創建當前read view的事務ID號

執行Select語句時,MVCC機制會根據每一條數據的DB_TRX_ID和當前事務的ReadView,來決定是否展示改行數據,具體流程如下:

  1. 如果該行數據的DB_TRX_ID小于up_limit_id(活躍事務的最小ID號),則說明該數據是在所有活躍事務開啟之前就已經存在的,因此可以顯示。
  2. 如果該行數據的DB_TRX_ID大于low_limit_id(活躍事務的最大ID號),則說明該數據是在所有活躍事務開始之后才創建的,所以數據不予顯示。
  3. 如果該行數據的DB_TRX_ID大于up_limit_id (活躍事務的最小ID號)并且小于low_limit_id(活躍事務的最大ID號),則用該行數據事務ID與trx_ids活躍事務集合中id進行匹配,如果沒有查找到,則表明該事務已經提交了,因此該行數據可以顯示。另外,如果該行數據事務ID等于creator_trx_id,這說明該數據就是當前事務修改的,因此也可以直接展示。
  4. 上述規則都不滿足時,通過改行數據的歷史數據指針DB_ROLL_PTR,從undo log里進行查找歷史數據,然后用歷史數據的事務id回頭再來和read view 條件匹配 ,直到找到一條滿足條件的歷史數據,或者找不到則返回空結果。

更多關于MVCC的實現,請參考:https://zhuanlan.zhihu.com/p/52977862?utm_source=wechat_session&utm_medium=social&utm_oi=61516120326144

注意,Read Commit隔離級別和Repeatable Read隔離級別都是通過MVCC實現。對于Read Commit隔離級別,一個事務內的每一條select語句都會創建一個最新的read view,因此會產生不可重復讀的問題。而對于Repeatable Read隔離級別,只有事務內的第一條select語句開始時,才會創建一個read view,之后同一個事務內所有的select語句都會使用這個read view。

另外,MVCC在更新每行數據時(更新DB_TRX_ID和DB_ROLL_PTR屬性),都會使用排他鎖來進行更新。

4. 再談數據庫鎖

在Repeatable read級別中,通過MVCC機制,雖然讓數據變得可重復讀,但我們讀到的數據可能是歷史數據,不是數據庫最新的數據。這種讀取歷史數據的方式,我們叫它快照讀 (snapshot read),而讀取數據庫最新版本數據的方式,叫當前讀 (current read)。

不顯式加『lock in share mode』與『for update』的『select』操作都屬于快照讀,使用MVCC,保證事務執行過程中只有第一次讀之前提交的修改和自己的修改可見,其他的均不可見。下面是快照讀的例子:

SELECT * FROM table WHERE ?;

而當前讀的方式則是在讀時顯示的加上共享鎖或排他鎖:

select * from table where ? lock in share mode; 
select * from table where ? for update; 

因此,如果想要在一個數據庫事務中讀取到最新的數據,要么將隔離級別設置為Read committed,要么使用加鎖讀(即當前讀)的方式。

4.1 行鎖算法

InnoDB的行鎖算法主要分為三種:Record lock,Gap lock和Next-Key lock。

  • Record lock:單個行記錄上的鎖。
  • Gap lock:間隙鎖,鎖定一個范圍,但不包括記錄本身。注意,Gap lock并不是排他的。這意味一條記錄被添加了Gap lock后仍然能添加Gap lock。但是添加了Gap lock的間隙不能再獲得插入鎖。從而阻止了幻讀。
  • Next-Key lock:即Record lock和Gap lock的結合。即鎖定一個范圍,并且鎖定記錄本身。

注意:Gap lock和Next-Key lock的目的是為了解決幻讀,僅在Repeatable read的隔離模式下有效。

對于根據主鍵的查詢,MySQL會通過Record lock對對應的主鍵加鎖。如果對應主鍵記錄不存在,那么MySQL會找到該主鍵所在區間,然后對該區間添加Gap lock。

主鍵查詢.png

對于唯一索引的等值查詢,MySQL會通過Record lock對定位的索引和主鍵進行加鎖。

唯一索引.png

而對于非唯一索引或者范圍查詢,MySQL會通過Next-Key lock對于整個范圍區間加鎖。

普通索引.png

注:Gap鎖,鎖定的是索引記錄之間的間隙,是防止幻讀的關鍵。如果沒有上圖中綠色標識的Gap Lock,其他并發事務在間隙中插入了一條記錄如:『insert into stock (id,sku_id) values(2,103);』并提交,那么在此事務中重復執行上圖中SQL,就會查詢出并發事務新插入的記錄,即出現幻讀(select for update方式的當前讀不會觸發MVCC機制,因此需要靠Gap lock來保證幻讀不出現)。Gap鎖詳解:https://blog.csdn.net/u022812849/article/details/122528923

注意:僅僅當MySQL隔離級別為Repeatable read時,InnoDB才會啟用Gap lock和Next-Key lock。如果隔離級別為Read committed,那么行鎖只有Record lock這一種類別。

另外,如果查詢語句中不包含任何索引信息,即根據普通字段來查詢,那么InndoDB就會對整顆索引樹上的每一個索引節點加鎖。

針對插入操作,為了增加插入操作的吞吐量,InnoDB提出了插入意向鎖(Insert Intention)的概念。插入意向鎖本質上是一種Gap鎖。例如當前數據庫存在的索引為(4,7),此時事務A插入一條主鍵為5的記錄,事務B插入一條主鍵為6的記錄,事務A和事務B首先獲得區間(4,7)上的插入意向鎖,然后再獲得對應插入位置的排他鎖,最后執行插入動作。因此,當多個事務在同一區間(gap)插入位置不同的多條數據時,事務之間不需要互相等待。

而對于update或delete而言,如果兩個事務再同一個區間內工作,那么一定會進行互相等待,因為update或delete會對行添加排他鎖。

鎖排他性.png

上圖展示了當一個區間獲得了Gap鎖后,針對該區間的Insert Intention請求就會被拒絕,從而達到了禁止幻讀的目的。并且Record鎖是一個排他鎖。

另外,對于Repeatable read隔離級別,是不支持更新的可重復讀的。下圖是一個例子:

修改的不可重復讀.PNG

此時如果必須保證可重復讀,那么應該在事務中使用當前讀來代替快照讀,從而形成加鎖阻塞的目的。

4.2 意向鎖

意向鎖是針對表級別而言的,分為兩種:意向共享鎖和意向排他鎖。一個事務在申請表鎖/行鎖時,必須先申請該表的意向鎖。

意向鎖的目的是為了加速表鎖的申請。如果不存在意向鎖,那么通過LOCK TABLE … WRITE申請表鎖的流程如下:

  1. 判斷表是否存在表鎖
  2. 判斷每一行是否存在行鎖,如果有數據行存在行鎖,那么申請表排他鎖就阻塞。

因此,如果申請一個表鎖要遍歷所有的表記錄查看是否存在行鎖,那就太費時了。因此提出了表的意向鎖。

一個事務在申請為一個數據行或一整張表添加共享鎖時,必須先為這張表添加一個意向共享鎖。同理,一個事務在申請為一個數據行或一整張表添加排他鎖時,必須先為這張表添加一個意向排他鎖。

此時一個事務通過LOCK TABLE … WRITE申請表鎖的流程如下:

  1. 判斷表是否存在表鎖
  2. 判斷表是否存在意向鎖,如果存在,說明有事務正在對表執行讀寫操作,那么申請排他鎖就阻塞。

5. 死鎖場景

下面是一個常見死鎖場景,先讀取數據庫判斷記錄是否存在,不存在則插入:

以id為主鍵為例,目前還沒有id=22的行,數據庫最大id為21

Session1:

select * from t3 where id=22 for update;

Empty set (0.00 sec)


session2:

select * from t3 where id=23  for update;

Empty set (0.00 sec)

 
Session1:

insert into t3 values(22, "Ivan2");
 

Session2:

insert into t3 values(23, "Ivan3");

ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction

上面的代碼中session1和session2都將獲得(21, +∞)的Gap lock。

因此session1在執行插入語句時,首先嘗試獲取insert intention lock,此時由于session2持有了Gap lock,因此等待session2釋放這個Gap lock。

而session2在執行插入語句時,也同樣嘗試獲取insert intention lock,此時由于session1持有了Gap lock,因此等待session1釋放這個Gap lock。從而形成了死鎖。

解決方案:使用insert into t3(xx,xx) on duplicate key update `xx`='XX';語法。或者使用select語句替換select for update語句,再最終插入時捕捉key重復的錯誤再返回失敗或者重試。

6. 為什么大廠將默認隔離級別設置成RC

7. 解決幻讀的兩種方式

解決幻讀有兩種方式:間隙鎖和MVCC。https://blog.csdn.net/m0_48847163/article/details/124082312

參考文章

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,501評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,673評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,610評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,939評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,668評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,004評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,001評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,173評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,705評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,426評論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,656評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,139評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,833評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,247評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,580評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,371評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,621評論 2 380

推薦閱讀更多精彩內容