面試中的老大難-mysql事務和鎖,一次性講清楚!
本文來源:http://8rr.co/Q62L
眾所周知,事務和鎖是mysql中非常重要功能,同時也是面試的重點和難點。本文會詳細介紹事務和鎖的相關概念及其實現(xiàn)原理,相信大家看完之后,一定會對事務和鎖有更加深入的理解。
本文主要內(nèi)容是根據(jù)掘金小冊《從根兒上理解 MySQL》整理而來。如想詳細了解,建議購買掘金小冊閱讀。
什么是事務
在維基百科中,對事務的定義是:事務是數(shù)據(jù)庫管理系統(tǒng)(DBMS)執(zhí)行過程中的一個邏輯單位,由一個有限的數(shù)據(jù)庫操作序列構(gòu)成。
事務的四大特性
事務包含四大特性,即原子性(Atomicity)、一致性(Consistency)、隔離性(Isolation)和持久性(Durability)(ACID)。
原子性(Atomicity)?原子性是指對數(shù)據(jù)庫的一系列操作,要么全部成功,要么全部失敗,不可能出現(xiàn)部分成功的情況。以轉(zhuǎn)賬場景為例,一個賬戶的余額減少,另一個賬戶的余額增加,這兩個操作一定是同時成功或者同時失敗的。 一致性(Consistency)?一致性是指數(shù)據(jù)庫的完整性約束沒有被破壞,在事務執(zhí)行前后都是合法的數(shù)據(jù)狀態(tài)。這里的一致可以表示數(shù)據(jù)庫自身的約束沒有被破壞,比如某些字段的唯一性約束、字段長度約束等等;還可以表示各種實際場景下的業(yè)務約束,比如上面轉(zhuǎn)賬操作,一個賬戶減少的金額和另一個賬戶增加的金額一定是一樣的。 隔離性(Isolation)?隔離性指的是多個事務彼此之間是完全隔離、互不干擾的。隔離性的最終目的也是為了保證一致性。 持久性(Durability)?持久性是指只要事務提交成功,那么對數(shù)據(jù)庫做的修改就被永久保存下來了,不可能因為任何原因再回到原來的狀態(tài)。
事務的狀態(tài)
根據(jù)事務所處的不同階段,事務大致可以分為以下5個狀態(tài):
活動的(active) 當事務對應的數(shù)據(jù)庫操作正在執(zhí)行過程中,則該事務處于 活動狀態(tài)。部分提交的(partially committed) 當事務中的最后一個操作執(zhí)行完成,但還未將變更刷新到磁盤時,則該事務處于 部分提交狀態(tài)。失敗的(failed) 當事務處于 活動或者部分提交狀態(tài)時,由于某些錯誤導致事務無法繼續(xù)執(zhí)行,則事務處于失敗狀態(tài)。中止的(aborted) 當事務處于 失敗狀態(tài),且回滾操作執(zhí)行完畢,數(shù)據(jù)恢復到事務執(zhí)行之前的狀態(tài)時,則該事務處于中止狀態(tài)。提交的(committed) 當事務處于 部分提交狀態(tài),并且將修改過的數(shù)據(jù)都同步到磁盤之后,此時該事務處于提交狀態(tài)。

事務隔離級別
前面提到過,事務必須具有隔離性。實現(xiàn)隔離性最簡單的方式就是不允許事務并發(fā),每個事務都排隊執(zhí)行,但是這種方式性能實在太差了。為了兼顧事務的隔離性和性能,事務支持不同的隔離級別。
為了方便表述后續(xù)的內(nèi)容,我們先建一張示例表hero。
CREATE TABLE hero (number INT,name VARCHAR(100),country varchar(100),PRIMARY KEY (number)) Engine=InnoDB CHARSET=utf8;
事務并發(fā)執(zhí)行遇到的問題
在事務并發(fā)執(zhí)行時,如果不進行任何控制,可能會出現(xiàn)以下4類問題:
臟寫(Dirty Write)
臟寫是指一個事務修改了其它事務未提交的數(shù)據(jù)
。

如上圖,
Session A和
Session B各開啟了一個事務,
Session B中的事務先將
number列為1的記錄的
name列更新為'關羽',然后
Session A中的事務接著又把這條
number列為1的記錄的
name列更新為張飛。如果之后
Session B中的事務進行了回滾,那么
Session A中的更新也將不復存在,這種現(xiàn)象就稱之為臟寫。
臟讀(Dirty Read)
臟讀是指一個事務讀到了其它事務未提交的數(shù)據(jù)。

如上圖,
Session A和
Session B各開啟了一個事務,
Session B中的事務先將
number列為1的記錄的
name列更新為
'關羽',然后
Session A中的事務再去查詢這條
number為1的記錄,如果讀到列
name的值為
'關羽',而
Session B中的事務稍后進行了回滾,那么
Session A中的事務相當于讀到了一個不存在的數(shù)據(jù),這種現(xiàn)象就稱之為
臟讀。
不可重復讀(Non-Repeatable Read)
不可重復讀指的是在一個事務執(zhí)行過程中,讀取到其它事務已提交的數(shù)據(jù),導致兩次讀取的結(jié)果不一致。

如上圖,我們在
Session B中提交了幾個隱式事務(mysql會自動為增刪改語句加事務),這些事務都修改了
number列為1的記錄的列
name的值,每次事務提交之后,如果
Session A中的事務都可以查看到最新的值,這種現(xiàn)象也被稱之為
不可重復讀。
幻讀(Phantom)
幻讀是指的是在一個事務執(zhí)行過程中,讀取到了其他事務新插入數(shù)據(jù),導致兩次讀取的結(jié)果不一致。

如上圖,
Session A中的事務先根據(jù)條件
number > 0這個條件查詢表
hero,得到了
name列值為
'劉備'的記錄;之后
Session B中提交了一個隱式事務,該事務向表
hero中插入了一條新記錄;之后
Session A中的事務再根據(jù)相同的條件
number > 0查詢表
hero,得到的結(jié)果集中包含
Session B中的事務新插入的那條記錄,這種現(xiàn)象也被稱之為幻讀。
不可重復讀和幻讀的區(qū)別在于不可重復讀是讀到的是其他事務修改或者刪除的數(shù)據(jù),而幻讀讀到的是其它事務新插入的數(shù)據(jù)。
臟寫的問題太嚴重了,任何隔離級別都必須避免。其它無論是臟讀,不可重復讀,還是幻讀,它們都屬于數(shù)據(jù)庫的讀一致性的問題,都是在一個事務里面前后兩次讀取出現(xiàn)了不一致的情況。
四種隔離級別
在SQL標準中設立了4種隔離級別,用來解決上面的讀一致性問題。不同的隔離級別可以解決不同的讀一致性問題。
READ UNCOMMITTED:未提交讀。READ COMMITTED:已提交讀。REPEATABLE READ:可重復讀。SERIALIZABLE:串行化。
各個隔離級別下可能出現(xiàn)的讀一致性問題如下:
| 隔離級別 | 臟讀 | 不可重復讀 | 幻讀 |
|---|---|---|---|
| 未提交讀(READ UNCOMMITTED) | 可能 | 可能 | 可能 |
| 已提交讀(READ COMMITTED) | 不可能 | 可能 | 可能 |
| 可重復讀(REPEATABLE READ) | 不可能 | 不可能 | 可能(對InnoDB不可能) |
| 串行化(SERIALIZABLE) | 不可能 | 不可能 | 不可能 |
InnoDB支持四個隔離級別(和SQL標準定義的基本一致)。隔離級別越高,事務的并發(fā)度就越低。唯一的區(qū)別就在于,InnoDB?在可重復讀(REPEATABLE READ)的級別就解決了幻讀的問題。這也是InnoDB使用可重復讀?作為事務默認隔離級別的原因。
MVCC
MVCC(Multi Version Concurrency Control),中文名是多版本并發(fā)控制,簡單來說就是通過維護數(shù)據(jù)歷史版本,從而解決并發(fā)訪問情況下的讀一致性問題。
版本鏈
在InnoDB中,每行記錄實際上都包含了兩個隱藏字段:事務id(trx_id)和回滾指針(roll_pointer)。
trx_id:事務id。每次修改某行記錄時,都會把該事務的事務id賦值給trx_id隱藏列。roll_pointer:回滾指針。每次修改某行記錄時,都會把undo日志地址賦值給roll_pointer隱藏列。
假設hero表中只有一行記錄,當時插入的事務id為80。此時,該條記錄的示例圖如下:

假設之后兩個事務id分別為100、200的事務對這條記錄進行UPDATE操作,操作流程如下:

由于每次變動都會先把undo日志記錄下來,并用roll_pointer指向undo日志地址。因此可以認為,對該條記錄的修改日志串聯(lián)起來就形成了一個版本鏈,版本鏈的頭節(jié)點就是當前記錄最新的值。如下:

ReadView
如果數(shù)據(jù)庫隔離級別是未提交讀(READ UNCOMMITTED),那么讀取版本鏈中最新版本的記錄即可。如果是是串行化(SERIALIZABLE),事務之間是加鎖執(zhí)行的,不存在讀不一致的問題。但是如果是已提交讀(READ COMMITTED)或者可重復讀(REPEATABLE READ),就需要遍歷版本鏈中的每一條記錄,判斷該條記錄是否對當前事務可見,直到找到為止(遍歷完還沒找到就說明記錄不存在)。InnoDB通過ReadView實現(xiàn)了這個功能。ReadView中主要包含以下4個內(nèi)容:
m_ids:表示在生成ReadView時當前系統(tǒng)中活躍的讀寫事務的事務id列表。min_trx_id:表示在生成ReadView時當前系統(tǒng)中活躍的讀寫事務中最小的事務id,也就是m_ids中的最小值。max_trx_id:表示生成ReadView時系統(tǒng)中應該分配給下一個事務的id值。creator_trx_id:表示生成該ReadView事務的事務id。
有了ReadView之后,我們可以基于以下步驟判斷某個版本的記錄是否對當前事務可見。
如果被訪問版本的 trx_id屬性值與ReadView中的creator_trx_id值相同,意味著當前事務在訪問它自己修改過的記錄,所以該版本可以被當前事務訪問。如果被訪問版本的 trx_id屬性值小于ReadView中的min_trx_id值,表明生成該版本的事務在當前事務生成ReadView前已經(jīng)提交,所以該版本可以被當前事務訪問。如果被訪問版本的 trx_id屬性值大于或等于ReadView中的max_trx_id值,表明生成該版本的事務在當前事務生成ReadView后才開啟,所以該版本不可以被當前事務訪問。如果被訪問版本的 trx_id屬性值在ReadView的min_trx_id和max_trx_id之間,那就需要判斷一下trx_id屬性值是不是在m_ids列表中,如果在,說明創(chuàng)建ReadView時生成該版本的事務還是活躍的,該版本不可以被訪問;如果不在,說明創(chuàng)建ReadView時生成該版本的事務已經(jīng)被提交,該版本可以被訪問。
在MySQL中,READ COMMITTED和REPEATABLE READ隔離級別的的一個非常大的區(qū)別就是它們生成ReadView的時機不同。READ COMMITTED在每次讀取數(shù)據(jù)前都會生成一個ReadView,這樣就能保證每次都能讀到其它事務已提交的數(shù)據(jù)。REPEATABLE READ?只在第一次讀取數(shù)據(jù)時生成一個ReadView,這樣就能保證后續(xù)讀取的結(jié)果完全一致。
鎖
事務并發(fā)訪問同一數(shù)據(jù)資源的情況主要就分為讀-讀、寫-寫和讀-寫三種。
讀-讀?即并發(fā)事務同時訪問同一行數(shù)據(jù)記錄。由于兩個事務都進行只讀操作,不會對記錄造成任何影響,因此并發(fā)讀完全允許。寫-寫?即并發(fā)事務同時修改同一行數(shù)據(jù)記錄。這種情況下可能導致臟寫問題,這是任何情況下都不允許發(fā)生的,因此只能通過加鎖實現(xiàn),也就是當一個事務需要對某行記錄進行修改時,首先會先給這條記錄加鎖,如果加鎖成功則繼續(xù)執(zhí)行,否則就排隊等待,事務執(zhí)行完成或回滾會自動釋放鎖。讀-寫?即一個事務進行讀取操作,另一個進行寫入操作。這種情況下可能會產(chǎn)生臟讀、不可重復讀、幻讀。最好的方案是讀操作利用多版本并發(fā)控制(MVCC),寫操作進行加鎖。
鎖的粒度
按鎖作用的數(shù)據(jù)范圍進行分類的話,鎖可以分為行級鎖和表級鎖。
行級鎖:作用在數(shù)據(jù)行上,鎖的粒度比較小。表級鎖:作用在整張數(shù)據(jù)表上,鎖的粒度比較大。
鎖的分類
為了實現(xiàn)讀-讀之間不受影響,并且寫-寫、讀-寫之間能夠相互阻塞,Mysql使用了讀寫鎖的思路進行實現(xiàn),具體來說就是分為了共享鎖和排它鎖:
共享鎖(Shared Locks):簡稱S鎖,在事務要讀取一條記錄時,需要先獲取該記錄的S鎖。S鎖可以在同一時刻被多個事務同時持有。我們可以用select ...... lock in share mode;的方式手工加上一把S鎖。排他鎖(Exclusive Locks):簡稱X鎖,在事務要改動一條記錄時,需要先獲取該記錄的X鎖。X鎖在同一時刻最多只能被一個事務持有。X鎖的加鎖方式有兩種,第一種是自動加鎖,在對數(shù)據(jù)進行增刪改的時候,都會默認加上一個X鎖。還有一種是手工加鎖,我們用一個FOR UPDATE給一行數(shù)據(jù)加上一個X鎖。
還需要注意的一點是,如果一個事務已經(jīng)持有了某行記錄的S鎖,另一個事務是無法為這行記錄加上X鎖的,反之亦然。
除了共享鎖(Shared Locks)和排他鎖(Exclusive Locks),Mysql還有意向鎖(Intention Locks)。意向鎖是由數(shù)據(jù)庫自己維護的,一般來說,當我們給一行數(shù)據(jù)加上共享鎖之前,數(shù)據(jù)庫會自動在這張表上面加一個意向共享鎖(IS鎖);當我們給一行數(shù)據(jù)加上排他鎖之前,數(shù)據(jù)庫會自動在這張表上面加一個意向排他鎖(IX鎖)。意向鎖可以認為是S鎖和X鎖在數(shù)據(jù)表上的標識,通過意向鎖可以快速判斷表中是否有記錄被上鎖,從而避免通過遍歷的方式來查看表中有沒有記錄被上鎖,提升加鎖效率。例如,我們要加表級別的X鎖,這時候數(shù)據(jù)表里面如果存在行級別的X鎖或者S鎖的,加鎖就會失敗,此時直接根據(jù)意向鎖就能知道這張表是否有行級別的X鎖或者S鎖。
InnoDB中的表級鎖
InnoDB中的表級鎖主要包括表級別的意向共享鎖(IS鎖)和意向排他鎖(IX鎖)以及自增鎖(AUTO-INC鎖)。其中IS鎖和IX鎖在前面已經(jīng)介紹過了,這里不再贅述,我們接下來重點了解一下AUTO-INC鎖。
大家都知道,如果我們給某列字段加了AUTO_INCREMENT自增屬性,插入的時候不需要為該字段指定值,系統(tǒng)會自動保證遞增。系統(tǒng)實現(xiàn)這種自動給AUTO_INCREMENT修飾的列遞增賦值的原理主要是兩個:
AUTO-INC鎖:在執(zhí)行插入語句的時先加上表級別的AUTO-INC鎖,插入執(zhí)行完成后立即釋放鎖。如果我們的插入語句在執(zhí)行前無法確定具體要插入多少條記錄,比如INSERT ... SELECT這種插入語句,一般采用AUTO-INC鎖的方式。輕量級鎖:在插入語句生成AUTO_INCREMENT值時先才獲取這個輕量級鎖,然后在AUTO_INCREMENT值生成之后就釋放輕量級鎖。如果我們的插入語句在執(zhí)行前就可以確定具體要插入多少條記錄,那么一般采用輕量級鎖的方式對AUTO_INCREMENT修飾的列進行賦值。這種方式可以避免鎖定表,可以提升插入性能。
mysql默認根據(jù)實際場景自動選擇加鎖方式,當然也可以通過
innodb_autoinc_lock_mode強制指定只使用其中一種。
InnoDB中的行級鎖
前面說過,通過MVCC可以解決臟讀、不可重復讀、幻讀這些讀一致性問題,但實際上這只是解決了普通select語句的數(shù)據(jù)讀取問題。事務利用MVCC進行的讀取操作稱之為快照讀,所有普通的SELECT語句在READ COMMITTED、REPEATABLE READ隔離級別下都算是快照讀。除了快照讀之外,還有一種是鎖定讀,即在讀取的時候給記錄加鎖,在鎖定讀的情況下依然要解決臟讀、不可重復讀、幻讀的問題。由于都是在記錄上加鎖,這些鎖都屬于行級鎖。
InnoDB的行鎖,是通過鎖住索引來實現(xiàn)的,如果加鎖查詢的時候沒有使用過索引,會將整個聚簇索引都鎖住,相當于鎖表了。根據(jù)鎖定范圍的不同,行鎖可以使用記錄鎖(Record Locks)、間隙鎖(Gap Locks)和臨鍵鎖(Next-Key Locks)的方式實現(xiàn)。假設現(xiàn)在有一張表t,主鍵是id。我們插入了4行數(shù)據(jù),主鍵值分別是 1、4、7、10。接下來我們就以聚簇索引為例,具體介紹三種形式的行鎖。
記錄鎖(Record Locks) 所謂記錄,就是指聚簇索引中真實存放的數(shù)據(jù),比如上面的1、4、7、10都是記錄。

顯然,記錄鎖就是直接鎖定某行記錄。當我們使用唯一性的索引(包括唯一索引和聚簇索引)進行等值查詢且精準匹配到一條記錄時,此時就會直接將這條記錄鎖定。例如
select * from t where id =4 for update;就會將
id=4的記錄鎖定。
間隙鎖(Gap Locks) 間隙指的是兩個記錄之間邏輯上尚未填入數(shù)據(jù)的部分,比如上述的(1,4)、(4,7)等。

同理,間隙鎖就是鎖定某些間隙區(qū)間的。當我們使用用等值查詢或者范圍查詢,并且沒有命中任何一個
record,此時就會將對應的間隙區(qū)間鎖定。例如
select * from t where id =3 for update;或者
select * from t where id > 1 and id < 4 for update;就會將(1,4)區(qū)間鎖定。
臨鍵鎖(Next-Key Locks) 臨鍵指的是間隙加上它右邊的記錄組成的左開右閉區(qū)間。比如上述的(1,4]、(4,7]等。

臨鍵鎖就是記錄鎖(Record Locks)和間隙鎖(Gap Locks)的結(jié)合,即除了鎖住記錄本身,還要再鎖住索引之間的間隙。當我們使用范圍查詢,并且命中了部分
record記錄,此時鎖住的就是臨鍵區(qū)間。注意,臨鍵鎖鎖住的區(qū)間會包含最后一個record的右邊的臨鍵區(qū)間。例如
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)都會失效!
END
免費領取 1000+ 道面試資料?。?/span>小編這里有一份面試寶典《Java 核心知識點.pdf》,覆蓋了 JVM,鎖、高并發(fā)、Spring原理、微服務、數(shù)據(jù)庫、Zookeep人、數(shù)據(jù)結(jié)構(gòu)等等知識點,包含 Java 后端知識點 1000+ 個,部分如下:


