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

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

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

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

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

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

假設(shè)之后兩個(gè)事務(wù)id分別為100、200的事務(wù)對(duì)這條記錄進(jìn)行UPDATE操作,操作流程如下:

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

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

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

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

臨鍵鎖就是記錄鎖(Record Locks)和間隙鎖(Gap Locks)的結(jié)合,即除了鎖住記錄本身,還要再鎖住索引之間的間隙。當(dāng)我們使用范圍查詢,并且命中了部分
record記錄,此時(shí)鎖住的就是臨鍵區(qū)間。注意,臨鍵鎖鎖住的區(qū)間會(huì)包含最后一個(gè)record的右邊的臨鍵區(qū)間。例如
select * from t where id > 5 and id <= 7 for update;會(huì)鎖住(4,7]、(7,+∞)。mysql默認(rèn)行鎖類型就是
臨鍵鎖(Next-Key Locks)。當(dāng)使用唯一性索引,等值查詢匹配到一條記錄的時(shí)候,臨鍵鎖(Next-Key Locks)會(huì)退化成記錄鎖;沒有匹配到任何記錄的時(shí)候,退化成間隙鎖。
間隙鎖(Gap Locks)和臨鍵鎖(Next-Key Locks)都是用來解決幻讀問題的,在已提交讀(READ COMMITTED)隔離級(jí)別下,間隙鎖(Gap Locks)和臨鍵鎖(Next-Key Locks)都會(huì)失效!
END
免費(fèi)領(lǐng)取 1000+ 道面試資料!!小編這里有一份面試寶典《Java 核心知識(shí)點(diǎn).pdf》,覆蓋了 JVM,鎖、高并發(fā)、Spring原理、微服務(wù)、數(shù)據(jù)庫、Zookeep人、數(shù)據(jù)結(jié)構(gòu)等等知識(shí)點(diǎn),包含 Java 后端知識(shí)點(diǎn) 1000+ 個(gè),部分如下:


