陌陌面試官:談談你對MySQL中事務和鎖的理解?

正文以下:mysql

衆所周知,事務和鎖是mysql中很是重要功能,同時也是面試的重點和難點。本文會詳細介紹事務和鎖的相關概念及其實現原理,相信你們看完以後,必定會對事務和鎖有更加深刻的理解。整理了一份328頁MySQL,PDF文檔面試

什麼是事務

在維基百科中,對事務的定義是:事務是數據庫管理系統(DBMS)執行過程當中的一個邏輯單位,由一個有限的數據庫操做序列構成。sql

事務的四大特性

事務包含四大特性,即原子性(Atomicity)、一致性(Consistency)、隔離性(Isolation)和持久性(Durability)(ACID)。數據庫

  • 原子性(Atomicity) 原子性是指對數據庫的一系列操做,要麼所有成功,要麼所有失敗,不可能出現部分紅功的狀況。以轉帳場景爲例,一個帳戶的餘額減小,另外一個帳戶的餘額增長,這兩個操做必定是同時成功或者同時失敗的。併發

  • 一致性(Consistency) 一致性是指數據庫的完整性約束沒有被破壞,在事務執行先後都是合法的數據狀態。這裏的一致能夠表示數據庫自身的約束沒有被破壞,好比某些字段的惟一性約束、字段長度約束等等;還能夠表示各類實際場景下的業務約束,好比上面轉帳操做,一個帳戶減小的金額和另外一個帳戶增長的金額必定是同樣的。ide

  • 隔離性(Isolation) 隔離性指的是多個事務彼此之間是徹底隔離、互不干擾的。隔離性的最終目的也是爲了保證一致性。性能

  • 持久性(Durability) 持久性是指只要事務提交成功,那麼對數據庫作的修改就被永久保存下來了,不可能由於任何緣由再回到原來的狀態。

事務的狀態

根據事務所處的不一樣階段,事務大體能夠分爲如下5個狀態:3d

  • 活動的(active) 當事務對應的數據庫操做正在執行過程當中,則該事務處於活動狀態。指針

  • 部分提交的(partially committed) 當事務中的最後一個操做執行完成,但還未將變動刷新到磁盤時,則該事務處於部分提交狀態。日誌

  • 失敗的(failed) 當事務處於活動或者部分提交狀態時,因爲某些錯誤致使事務沒法繼續執行,則事務處於失敗狀態。

  • 停止的(aborted) 當事務處於失敗狀態,且回滾操做執行完畢,數據恢復到事務執行以前的狀態時,則該事務處於停止狀態。

  • 提交的(committed) 當事務處於部分提交狀態,而且將修改過的數據都同步到磁盤以後,此時該事務處於提交狀態。

陌陌面試官:談談你對MySQL中事務和鎖的理解?

事務隔離級別

前面提到過,事務必須具備隔離性。實現隔離性最簡單的方式就是不容許事務併發,每一個事務都排隊執行,可是這種方式性能實在太差了。爲了兼顧事務的隔離性和性能,事務支持不一樣的隔離級別。

爲了方便表述後續的內容,咱們先建一張示例表hero。

CREATE TABLE hero (  
    number INT,  
    name VARCHAR(100),  
    country varchar(100),  
    PRIMARY KEY (number)  
) Engine=InnoDB CHARSET=utf8;

事務併發執行遇到的問題

在事務併發執行時,若是不進行任何控制,可能會出現如下4類問題:

  • 髒寫(Dirty Write) 髒寫是指一個事務修改了其它事務未提交的數據。

陌陌面試官:談談你對MySQL中事務和鎖的理解?

如上圖,Session A和Session B各開啓了一個事務,Session B中的事務先將number列爲1的記錄的name列更新爲'關羽',而後Session A中的事務接着又把這條number列爲1的記錄的name列更新爲張飛。若是以後Session B中的事務進行了回滾,那麼Session A中的更新也將不復存在,這種現象就稱之爲髒寫。

  • 髒讀(Dirty Read) 髒讀是指一個事務讀到了其它事務未提交的數據。

陌陌面試官:談談你對MySQL中事務和鎖的理解?
如上圖,Session A和Session B各開啓了一個事務,Session B中的事務先將number列爲1的記錄的name列更新爲'關羽',而後Session A中的事務再去查詢這條number爲1的記錄,若是讀到列name的值爲'關羽',而Session B中的事務稍後進行了回滾,那麼Session A中的事務至關於讀到了一個不存在的數據,這種現象就稱之爲髒讀。

  • 不可重複讀(Non-Repeatable Read) 不可重複讀指的是在一個事務執行過程當中,讀取到其它事務已提交的數據,致使兩次讀取的結果不一致。

陌陌面試官:談談你對MySQL中事務和鎖的理解?

如上圖,咱們在Session B中提交了幾個隱式事務(mysql會自動爲增刪改語句加事務),這些事務都修改了number列爲1的記錄的列name的值,每次事務提交以後,若是Session A中的事務均可以查看到最新的值,這種現象也被稱之爲不可重複讀。

  • 幻讀(Phantom) 幻讀是指的是在一個事務執行過程當中,讀取到了其餘事務新插入數據,致使兩次讀取的結果不一致。

陌陌面試官:談談你對MySQL中事務和鎖的理解?

如上圖,Session A中的事務先根據條件number > 0這個條件查詢表hero,獲得了name列值爲'劉備'的記錄;以後Session B中提交了一個隱式事務,該事務向表hero中插入了一條新記錄;以後Session A中的事務再根據相同的條件number > 0查詢表hero,獲得的結果集中包含Session B中的事務新插入的那條記錄,這種現象也被稱之爲幻讀。

不可重複讀和幻讀的區別在於不可重複讀是讀到的是其餘事務修改或者刪除的數據,而幻讀讀到的是其它事務新插入的數據。

髒寫的問題太嚴重了,任何隔離級別都必須避免。其它不管是髒讀,不可重複讀,仍是幻讀,它們都屬於數據庫的讀一致性的問題,都是在一個事務裏面先後兩次讀取出現了不一致的狀況。

四種隔離級別

在SQL標準中設立了4種隔離級別,用來解決上面的讀一致性問題。不一樣的隔離級別能夠解決不一樣的讀一致性問題。

  • READ UNCOMMITTED:未提交讀。

  • READ COMMITTED:已提交讀。

  • REPEATABLE READ:可重複讀。

  • SERIALIZABLE:串行化。

各個隔離級別下可能出現的讀一致性問題以下:

陌陌面試官:談談你對MySQL中事務和鎖的理解?

InnoDB支持四個隔離級別(和SQL標準定義的基本一致)。隔離級別越高,事務的併發度就越低。惟一的區別就在於,InnoDB 在可重複讀(REPEATABLE READ)的級別就解決了幻讀的問題。這也是InnoDB使用可重複讀 做爲事務默認隔離級別的緣由。

MVCC

MVCC(Multi Version Concurrency Control),中文名是多版本併發控制,簡單來講就是經過維護數據歷史版本,從而解決併發訪問狀況下的讀一致性問題。

版本鏈

在InnoDB中,每行記錄實際上都包含了兩個隱藏字段:事務id(trx_id)和回滾指針(roll_pointer)。

  1. trx_id:事務id。每次修改某行記錄時,都會把該事務的事務id賦值給trx_id隱藏列。

  2. roll_pointer:回滾指針。每次修改某行記錄時,都會把undo日誌地址賦值給roll_pointer隱藏列。

假設hero表中只有一行記錄,當時插入的事務id爲80。此時,該條記錄的示例圖以下:

陌陌面試官:談談你對MySQL中事務和鎖的理解?

假設以後兩個事務id分別爲100、200的事務對這條記錄進行UPDATE操做,操做流程以下:

陌陌面試官:談談你對MySQL中事務和鎖的理解?

因爲每次變更都會先把undo日誌記錄下來,並用roll_pointer指向undo日誌地址。所以能夠認爲,對該條記錄的修改日誌串聯起來就造成了一個版本鏈,版本鏈的頭節點就是當前記錄最新的值。以下:
陌陌面試官:談談你對MySQL中事務和鎖的理解?

ReadView

若是數據庫隔離級別是未提交讀(READ UNCOMMITTED),那麼讀取版本鏈中最新版本的記錄便可。若是是是串行化(SERIALIZABLE),事務之間是加鎖執行的,不存在讀不一致的問題。可是若是是已提交讀(READ COMMITTED)或者可重複讀(REPEATABLE READ),就須要遍歷版本鏈中的每一條記錄,判斷該條記錄是否對當前事務可見,直到找到爲止(遍歷完還沒找到就說明記錄不存在)。InnoDB經過ReadView實現了這個功能。ReadView中主要包含如下4個內容:

  • m_ids:表示在生成ReadView時當前系統中活躍的讀寫事務的事務id列表。

  • min_trx_id:表示在生成ReadView時當前系統中活躍的讀寫事務中最小的事務id,也就是m_ids中的最小值。

  • max_trx_id:表示生成ReadView時系統中應該分配給下一個事務的id值。

  • creator_trx_id:表示生成該ReadView事務的事務id。

有了ReadView以後,咱們能夠基於如下步驟判斷某個版本的記錄是否對當前事務可見。

  • 若是被訪問版本的trx_id屬性值與ReadView中的creator_trx_id值相同,意味着當前事務在訪問它本身修改過的記錄,因此該版本能夠被當前事務訪問。

  • 若是被訪問版本的trx_id屬性值小於ReadView中的min_trx_id值,代表生成該版本的事務在當前事務生成ReadView前已經提交,因此該版本能夠被當前事務訪問。

  • 若是被訪問版本的trx_id屬性值大於或等於ReadView中的max_trx_id值,代表生成該版本的事務在當前事務生成ReadView後纔開啓,因此該版本不能夠被當前事務訪問。

  • 若是被訪問版本的trx_id屬性值在ReadView的min_trx_id和max_trx_id之間,那就須要判斷一下trx_id屬性值是否是在m_ids列表中,若是在,說明建立ReadView時生成該版本的事務仍是活躍的,該版本不能夠被訪問;若是不在,說明建立ReadView時生成該版本的事務已經被提交,該版本能夠被訪問。

在MySQL中,READ COMMITTED和REPEATABLE READ隔離級別的的一個很是大的區別就是它們生成ReadView的時機不一樣。READ COMMITTED在每次讀取數據前都會生成一個ReadView,這樣就能保證每次都能讀到其它事務已提交的數據。REPEATABLE READ 只在第一次讀取數據時生成一個ReadView,這樣就能保證後續讀取的結果徹底一致。

事務併發訪問同一數據資源的狀況主要就分爲讀-讀、寫-寫和讀-寫三種。

  • 讀-讀 即併發事務同時訪問同一行數據記錄。因爲兩個事務都進行只讀操做,不會對記錄形成任何影響,所以併發讀徹底容許。

  • 寫-寫 即併發事務同時修改同一行數據記錄。這種狀況下可能致使髒寫問題,這是任何狀況下都不容許發生的,所以只能經過加鎖實現,也就是當一個事務須要對某行記錄進行修改時,首先會先給這條記錄加鎖,若是加鎖成功則繼續執行,不然就排隊等待,事務執行完成或回滾會自動釋放鎖。

  • 讀-寫 即一個事務進行讀取操做,另外一個進行寫入操做。這種狀況下可能會產生髒讀、不可重複讀、幻讀。最好的方案是讀操做利用多版本併發控制(MVCC),寫操做進行加鎖。

鎖的粒度

按鎖做用的數據範圍進行分類的話,鎖能夠分爲行級鎖和表級鎖。

  1. 行級鎖:做用在數據行上,鎖的粒度比較小。

  2. 表級鎖:做用在整張數據表上,鎖的粒度比較大。

鎖的分類

爲了實現讀-讀之間不受影響,而且寫-寫、讀-寫之間可以相互阻塞,Mysql使用了讀寫鎖的思路進行實現,具體來講就是分爲了共享鎖和排它鎖:

1.共享鎖(Shared Locks):簡稱S鎖,在事務要讀取一條記錄時,須要先獲取該記錄的S鎖。S鎖能夠在同一時刻被多個事務同時持有。咱們能夠用select ...... lock in share mode;的方式手工加上一把S鎖。

2.排他鎖(Exclusive Locks):簡稱X鎖,在事務要改動一條記錄時,須要先獲取該記錄的X鎖。X鎖在同一時刻最多隻能被一個事務持有。X鎖的加鎖方式有兩種,第一種是自動加鎖,在對數據進行增刪改的時候,都會默認加上一個X鎖。還有一種是手工加鎖,咱們用一個FOR UPDATE給一行數據加上一個X鎖。

還須要注意的一點是,若是一個事務已經持有了某行記錄的S鎖,另外一個事務是沒法爲這行記錄加上X鎖的,反之亦然。

除了共享鎖(Shared Locks)和排他鎖(Exclusive Locks),Mysql還有意向鎖(Intention Locks)。意向鎖是由數據庫本身維護的,通常來講,當咱們給一行數據加上共享鎖以前,數據庫會自動在這張表上面加一個意向共享鎖(IS鎖);當咱們給一行數據加上排他鎖以前,數據庫會自動在這張表上面加一個意向排他鎖(IX鎖)。意向鎖能夠認爲是S鎖和X鎖在數據表上的標識,經過意向鎖能夠快速判斷表中是否有記錄被上鎖,從而避免經過遍歷的方式來查看錶中有沒有記錄被上鎖,提高加鎖效率。例如,咱們要加表級別的X鎖,這時候數據表裏面若是存在行級別的X鎖或者S鎖的,加鎖就會失敗,此時直接根據意向鎖就能知道這張表是否有行級別的X鎖或者S鎖。

InnoDB中的表級鎖

InnoDB中的表級鎖主要包括表級別的意向共享鎖(IS鎖)和意向排他鎖(IX鎖)以及自增鎖(AUTO-INC鎖)。其中IS鎖和IX鎖在前面已經介紹過了,這裏再也不贅述,咱們接下來重點了解一下AUTO-INC鎖。

你們都知道,若是咱們給某列字段加了AUTO_INCREMENT自增屬性,插入的時候不須要爲該字段指定值,系統會自動保證遞增。系統實現這種自動給AUTO_INCREMENT修飾的列遞增賦值的原理主要是兩個:

1.AUTO-INC鎖:在執行插入語句的時先加上表級別的AUTO-INC鎖,插入執行完成後當即釋放鎖。若是咱們的插入語句在執行前沒法肯定具體要插入多少條記錄,好比INSERT ... SELECT這種插入語句,通常採用AUTO-INC鎖的方式。

2.輕量級鎖:在插入語句生成AUTO_INCREMENT值時先才獲取這個輕量級鎖,而後在AUTO_INCREMENT值生成以後就釋放輕量級鎖。若是咱們的插入語句在執行前就能夠肯定具體要插入多少條記錄,那麼通常採用輕量級鎖的方式對AUTO_INCREMENT修飾的列進行賦值。這種方式能夠避免鎖定表,能夠提高插入性能。

「mysql默認根據實際場景自動選擇加鎖方式,固然也能夠經過innodb_autoinc_lock_mode強制指定只使用其中一種。」

InnoDB中的行級鎖

前面說過,經過MVCC能夠解決髒讀、不可重複讀、幻讀這些讀一致性問題,但實際上這只是解決了普通select語句的數據讀取問題。事務利用MVCC進行的讀取操做稱之爲快照讀,全部普通的SELECT語句在READ COMMITTED、REPEATABLE READ隔離級別下都算是快照讀。除了快照讀以外,還有一種是鎖定讀,即在讀取的時候給記錄加鎖,在鎖定讀的狀況下依然要解決髒讀、不可重複讀、幻讀的問題。因爲都是在記錄上加鎖,這些鎖都屬於行級鎖。

InnoDB的行鎖,是經過鎖住索引來實現的,若是加鎖查詢的時候沒有使用過索引,會將整個聚簇索引都鎖住,至關於鎖表了。根據鎖定範圍的不一樣,行鎖可使用記錄鎖(Record Locks)、間隙鎖(Gap Locks)和臨鍵鎖(Next-Key Locks)的方式實現。假設如今有一張表t,主鍵是id。咱們插入了4行數據,主鍵值分別是 一、四、七、10。接下來咱們就以聚簇索引爲例,具體介紹三種形式的行鎖。

  • 記錄鎖(Record Locks) 所謂記錄,就是指聚簇索引中真實存放的數據,好比上面的一、四、七、10都是記錄。

陌陌面試官:談談你對MySQL中事務和鎖的理解?

顯然,記錄鎖就是直接鎖定某行記錄。當咱們使用惟一性的索引(包括惟一索引和聚簇索引)進行等值查詢且精準匹配到一條記錄時,此時就會直接將這條記錄鎖定。例如select * from t where id =4 for update;就會將id=4的記錄鎖定。

  • 間隙鎖(Gap Locks) 間隙指的是兩個記錄之間邏輯上還沒有填入數據的部分,好比上述的(1,4)、(4,7)等。

陌陌面試官:談談你對MySQL中事務和鎖的理解?

同理,間隙鎖就是鎖定某些間隙區間的。當咱們使用用等值查詢或者範圍查詢,而且沒有命中任何一個record,此時就會將對應的間隙區間鎖定。例如select from t where id =3 for update;或者select from t where id > 1 and id < 4 for update;就會將(1,4)區間鎖定。

  • 臨鍵鎖(Next-Key Locks) 臨鍵指的是間隙加上它右邊的記錄組成的左開右閉區間。好比上述的(1,4]、(4,7]等。

陌陌面試官:談談你對MySQL中事務和鎖的理解?

臨鍵鎖就是記錄鎖(Record Locks)和間隙鎖(Gap Locks)的結合,即除了鎖住記錄自己,還要再鎖住索引之間的間隙。當咱們使用範圍查詢,而且命中了部分record記錄,此時鎖住的就是臨鍵區間。注意,臨鍵鎖鎖住的區間會包含最後一個record的右邊的臨鍵區間。例如select * from t where id > 5 and id <= 7 for update;會鎖住(4,7]、(7,+∞)。mysql默認行鎖類型就是臨鍵鎖(Next-Key Locks)。當使用惟一性索引,等值查詢匹配到一條記錄的時候,臨鍵鎖(Next-Key Locks)會退化成記錄鎖;沒有匹配到任何記錄的時候,退化成間隙鎖。

間隙鎖(Gap Locks)和臨鍵鎖(Next-Key Locks)都是用來解決幻讀問題的,在已提交讀(READ COMMITTED)隔離級別下,間隙鎖(Gap Locks)和臨鍵鎖(Next-Key Locks)都會失效!整理了一份328頁MySQL,PDF文檔

相關文章
相關標籤/搜索