[轉(zhuǎn)載]數(shù)據(jù)庫(kù)MVCC 隔離級(jí)別

1. 什么是MVCC?

多版本并發(fā)控制(Multi-Version Concurrency Control, MVCC),顧名思義,在并發(fā)訪問(wèn)的時(shí)候,數(shù)據(jù)存在版本的概念,可以有效地提升數(shù)據(jù)庫(kù)并發(fā)能力,常見的數(shù)據(jù)庫(kù)如MySQL、MS SQL Server、IBM DB2、Hbase、MongoDB等等都在使用。
簡(jiǎn)單講,如果沒有MVCC,當(dāng)想要讀取的數(shù)據(jù)被其他事務(wù)用排它鎖鎖住時(shí),只能互斥等待;而這時(shí)MVCC可以通過(guò)提供歷史版本從而實(shí)現(xiàn)讀取被鎖的數(shù)據(jù)(的歷史版本),避免了互斥等待。

在 MySQL中,多版本并發(fā)控制是 的 InnoDB 存儲(chǔ)引擎實(shí)現(xiàn)隔離級(jí)別的一種具體方式,用于實(shí)現(xiàn)提交讀和可重復(fù)讀這兩種隔離級(jí)別。而未提交讀隔離級(jí)別總是讀取最新的數(shù)據(jù)行,無(wú)需使用 MVCC;可串行化隔離級(jí)別需要對(duì)所有讀取的行都加鎖,單純使用 MVCC 無(wú)法實(shí)現(xiàn)。

MVCC一般有兩種實(shí)現(xiàn)方式,本文所講的InnoDB采用的是后者:

  • 實(shí)時(shí)保留數(shù)據(jù)的一個(gè)或多個(gè)歷史版本
  • 在需要時(shí)通過(guò)undo日志構(gòu)造出歷史版本
2. 數(shù)據(jù)庫(kù)的鎖
2.1 數(shù)據(jù)庫(kù)的鎖加在哪里?

在學(xué)習(xí)之前,我一直想當(dāng)然地認(rèn)為鎖是加在數(shù)據(jù)行上的,然而,數(shù)據(jù)庫(kù)的鎖是加在數(shù)據(jù)行對(duì)應(yīng)的索引上的,這個(gè)概念在后續(xù)理解鎖的范圍時(shí)至關(guān)重要。機(jī)智如你可能會(huì)問(wèn),沒有索引怎么辦?答案是,不管你建或不建,總有索引在那里!下面簡(jiǎn)單過(guò)一下
InnoDB有兩類索引:

    1. Clustered Index: 聚集索引(聚簇索引),通過(guò)聚集索引可以直接定位到數(shù)據(jù)的物理存儲(chǔ)位置,從而進(jìn)行IO讀寫,它是接觸到數(shù)據(jù)的必經(jīng)之路。每張表都會(huì)默默地建立聚集索引,具體的建立規(guī)則戳鏈接
    1. Secondary Index: 輔助索引(非聚集索引),除了Clustered Index,其他都是Secondary Index(所以我們自己建的都叫輔助索引)。通過(guò)輔助索引,可以查到數(shù)據(jù)的主鍵或者數(shù)據(jù)行id,然后再通過(guò)主鍵或者數(shù)據(jù)行id查聚集索引獲取數(shù)據(jù)的物理存儲(chǔ)位置,才能進(jìn)行IO讀寫。

講了這么多想說(shuō)什么呢?綜上所述!在真正接觸到數(shù)據(jù)之前,任何數(shù)據(jù)庫(kù)操作都會(huì)先走索引,這也就不難理解為什么鎖是加在索引上的了

2.2 什么時(shí)候會(huì)加鎖?

在數(shù)據(jù)庫(kù)增刪改查四種操作中,insert、delete和update都是會(huì)加排它鎖(即下文中的Exclusive Lock)的,而select只有顯式聲明才會(huì)加鎖:

  1. select: 即最常用的查詢,是不加任何鎖的
  2. select ... lock in share mode: 會(huì)加共享鎖(即下文中的Shared Lock)
  3. select ... for update: 會(huì)加排它鎖

至于后兩種的使用場(chǎng)景,感興趣的戳鏈接

2.2 鎖的分類

InnoDB有很多鎖,單是在官網(wǎng)上列出來(lái)的就有8種,如果組合使用就更多了。這里只簡(jiǎn)單講一下后續(xù)會(huì)用到的鎖。其中鎖的范圍需要對(duì)索引有較深的了解,可以戳鏈接自行學(xué)習(xí)

Shared and Exclusive Locks

這兩個(gè)鎖類似于Java中的讀寫鎖,其中Shared Lock相當(dāng)于Java中的讀鎖,讀寫、寫寫是互斥的,讀讀是可以并發(fā)的;Exclusive Lock相當(dāng)于Java中的寫鎖,讀寫、寫寫、讀讀都是互斥的

Record Locks

行鎖,顧名思義,是加在索引行(對(duì)!是索引行!不是數(shù)據(jù)行!)上的鎖。比如select * from user where id=1 and id=10 for update,就會(huì)在id=1id=10的索引行上加Record Lock

Gap Locks

間隙鎖,它會(huì)鎖住兩個(gè)索引之間的區(qū)域。比如select * from user where id>1 and id<10 for update,就會(huì)在id為(1,10)的索引區(qū)間上加Gap Lock

Next-Key Locks

也叫行間鎖,它是Record Lock + Gap Lock形成的一個(gè)閉區(qū)間鎖。比如select * from user where id>=1 and id<=10 for update,就會(huì)在id為[1,10]的索引閉區(qū)間上加Next-Key Lock

至此,所有預(yù)備知識(shí)都已經(jīng)講完了(對(duì)!預(yù)備知識(shí)就是這么多!怕不怕?!),開始本文的主題

3.四種隔離級(jí)別

這四種隔離級(jí)別,是在SQL:1992標(biāo)準(zhǔn)中定義的

接下來(lái),我們以文章開頭提到的兩個(gè)"不同"為主線,依次解開四種隔離級(jí)別的實(shí)現(xiàn)原理,同時(shí)可以慢慢品味這句話: 不同的隔離級(jí)別是在數(shù)據(jù)可靠性和并發(fā)性之間的均衡取舍,隔離級(jí)別越高,對(duì)應(yīng)的并發(fā)性能越差,數(shù)據(jù)越安全可靠

3.1READ UNCOMMITTED

顧名思義,事務(wù)之間可以讀取彼此未提交的數(shù)據(jù)。機(jī)智如你會(huì)記得,在前文有說(shuō)到所有寫操作都會(huì)加排它鎖,那還怎么讀未提交呢?該級(jí)別主要的特點(diǎn)是釋放鎖的時(shí)機(jī)與眾不同:在執(zhí)行完寫操作后立即釋放,而不像其他隔離級(jí)別在事務(wù)提交以后釋放。因此極易出現(xiàn)臟讀(不可重復(fù)讀和幻讀就更不用說(shuō)了)
但該級(jí)別的并發(fā)性能也正因?yàn)殒i釋放得很早而變得很高,就連寫寫操作都很難產(chǎn)生鎖競(jìng)爭(zhēng),并發(fā)性能可見一斑

3.2READ COMMITTED

既然讀未提交有那么大的數(shù)據(jù)可靠性問(wèn)題,那就往前邁一小步,讀已提交。該級(jí)別下將鎖的釋放時(shí)機(jī)延遲到事務(wù)提交之后,從而實(shí)現(xiàn)了讀已提交,解決了臟讀
但是!好像哪里不太對(duì)?!鎖的釋放時(shí)機(jī)延遲了,寫與寫操作之間產(chǎn)生鎖競(jìng)爭(zhēng)就算了,那在鎖釋放之前,讀也不能讀了嗎?這并發(fā)性能不能忍!這時(shí)就該MVCC出馬了,既然不想阻塞等待最新的數(shù)據(jù),那就無(wú)視當(dāng)前持有鎖的操作,讀取最新的歷史版本數(shù)據(jù)先用著
因此,在讀已提交的級(jí)別下,每次select時(shí)都會(huì)通過(guò)MVCC獲取當(dāng)前數(shù)據(jù)的最新快照,不加任何鎖,也無(wú)視任何鎖(因?yàn)闅v史數(shù)據(jù)是構(gòu)造出來(lái)的,身上不可能有鎖),完美解決讀寫之間的并發(fā)問(wèn)題,和READ UNCOMMITTED的并發(fā)性能只差在寫寫操作上
而為了進(jìn)一步提升寫寫操作上的并發(fā)性能,該級(jí)別下不會(huì)使用前文提到的間隙鎖,無(wú)論什么查詢都只會(huì)加行鎖,而且在執(zhí)行完WHERE條件篩選之后,會(huì)立即釋放掉不符合條件的行鎖,對(duì)于并發(fā)性能的追求可謂仁至義盡了
但是,正因?yàn)閷?duì)并發(fā)性能的極致追求或者說(shuō)貪婪,該級(jí)別下還是遺留了不可重復(fù)讀和幻讀問(wèn)題:

  1. MVCC版本的生成時(shí)機(jī): 是每次select時(shí),這就意味著,如果我們?cè)谑聞?wù)A中執(zhí)行多次的select,在每次select之間有其他事務(wù)更新了我們讀取的數(shù)據(jù)并提交了,那就出現(xiàn)了不可重復(fù)讀
  2. 鎖的范圍: 因?yàn)闆]有間隙鎖,這就意味著,如果我們?cè)谑聞?wù)A中多次執(zhí)行select * from user where age>18 and age<30 for update時(shí),其他事務(wù)是可以往age為(18,30)這個(gè)區(qū)間插入/刪除數(shù)據(jù)的,那就出現(xiàn)了幻讀
RC如何解決臟讀問(wèn)題,同樣的,丟失修改問(wèn)題也解決了

3.3 REPEATABLE READ

既然讀已提交依然有較大的數(shù)據(jù)可靠性能問(wèn)題,那就再往前邁一小步,可重復(fù)讀,該級(jí)別在讀已提交的基礎(chǔ)上做了兩點(diǎn)修改,從而避免了不可重復(fù)讀和幻讀:

  1. MVCC版本的生成時(shí)間: 一次事務(wù)中只在第一次select時(shí)生成版本,后續(xù)的查詢都是在這個(gè)版本上進(jìn)行,從而實(shí)現(xiàn)了可重復(fù)讀
可重復(fù)讀的實(shí)現(xiàn)
  1. 鎖的范圍: 在行鎖的基礎(chǔ)上,加上Gap Lock,從而形成Next-Key Lock,在所有遍歷過(guò)的(不管是否匹配條件)索引行上以及之間的區(qū)域上,都加上鎖,阻塞其他事務(wù)在遍歷范圍內(nèi)進(jìn)行寫操作,從而避免了幻讀
    看似很完美了對(duì)吧,并發(fā)性能上、讀讀、讀寫操作依舊兩不誤,寫寫操作為了數(shù)據(jù)可靠性做了妥協(xié)也是能接受的,皆大歡喜?
    圖樣圖森破!這個(gè)世界怕什么?豬隊(duì)友??!InnoDB在可重復(fù)讀級(jí)別下已經(jīng)將數(shù)據(jù)可靠性和并發(fā)性能兩方面做得盡善盡美了,但前提是用戶查詢時(shí)能夠主動(dòng)善用Locking Reads,即前文提到的select ... lock in share modeselect ... for update。如果只是使用普通的select,依然防不住幻讀
    這是因?yàn)镸VCC的快照只對(duì)讀操作有效,對(duì)寫操作無(wú)效,舉例說(shuō)明會(huì)更清晰一點(diǎn): 事務(wù)A依次執(zhí)行如下3條sql,事務(wù)B在語(yǔ)句1和2之間,插入10條age=20的記錄,事務(wù)A就幻讀了
1\. select count(1) from user where age=20;
-- return 0: 當(dāng)前沒有age=20的
2\. update user set name=test where age=20;
-- Affects 10 rows: 因?yàn)槭聞?wù)B剛寫入10條age=20的記錄,而寫操作是不受MVCC影響,能看到最新數(shù)據(jù)的,所以更新成功,而一旦操作成功,這些被操作的數(shù)據(jù)就會(huì)對(duì)當(dāng)前事務(wù)可見
3\. select count(1) from user where age=20;
-- return 10: 出現(xiàn)幻讀  

這種場(chǎng)景,需要用戶主動(dòng)使用Locking Read來(lái)防止其他事務(wù)在查詢范圍內(nèi)進(jìn)行寫操作,因此,為了防患于未然,隔離級(jí)別又往前邁了一步

3.4 SERIALISABLE

大殺器,該級(jí)別下,會(huì)自動(dòng)將所有普通select轉(zhuǎn)化為select ... lock in share mode執(zhí)行,即針對(duì)同一數(shù)據(jù)的所有讀寫都變成互斥的了,可靠性大大提高,并發(fā)性大大降低
機(jī)智如你可能會(huì)問(wèn),那可重復(fù)讀級(jí)別下使用Locking Read不也變成讀寫互斥了嘛,那這兩個(gè)有什么區(qū)別呢?可重復(fù)讀你可以自己選擇是否使用Locking Read呀,藝高人膽大可以使用普通的select讀寫并發(fā)的嘛

4.總結(jié)

一篇文章下來(lái)有太多的概念,但正是這么多的概念相輔相成打造了隔離級(jí)別,真是剪不斷理還亂,最后用一張表做個(gè)小結(jié)

隔離級(jí)別 MVCC版本生成時(shí)機(jī) 寫操作釋放鎖的時(shí)機(jī) 鎖的范圍 丟失修改 臟讀 不可重復(fù)度 幻讀
READ UNCOMMITTED / SQL執(zhí)行完立即釋放 行鎖
READ COMMITTED 每次select時(shí) 事務(wù)結(jié)束后 行鎖
REPEATABLE READ 事務(wù)第一次select時(shí) 事務(wù)結(jié)束后 行鎖或間隙鎖 特定情況下
SERIALIZABLE 事務(wù)第一次select時(shí) 事務(wù)結(jié)束后 行鎖或間隙鎖

該表有兩點(diǎn)需要說(shuō)明:

  • 不同級(jí)別下,只有寫操作釋放鎖的時(shí)機(jī)不同,而Locking Read的鎖,不論什么級(jí)別,都是在事務(wù)結(jié)束后釋放
  • REPEATABLE READ級(jí)別,可以防止大部分的幻讀,但像前邊舉例讀-寫-讀的情況,使用不加鎖的select依然會(huì)幻讀

所用到的命令

Prior to MySQL 5.7.20, use tx_isolation rather than transaction_isolation.

  • 設(shè)置事務(wù)隔離級(jí)別: set (session/global) transaction isolation level [read uncommitted/read committed/repeatable read/serilisable]

  • 查看事務(wù)隔離級(jí)別: select @@(session./global.)tx_isolation;
    start transaction / rollback / commit
    select ... lock in share mode / select ... for update
    select * from information_schema.innodb_locks;

  • 查看索引使用情況: explain [sql語(yǔ)句]

  • 查看鎖等待情況

    • select * from information_schema.innodb_locks;
    • select * from information_schema.innodb_lock_waits;
    • select * from information_schema.innodb_trx;
  • 查看InnoDB狀態(tài)(包括鎖): show engine innodb status;

參考:
http://hulichao.top/posts/1324.html
MySQL innodb 存儲(chǔ)引擎每一行后有

  • 隱藏的ID
  • 6字節(jié)的事務(wù)ID(DB_TRX_ID )
  • 7字節(jié)的回滾指針(DB_ROLL_PTR)

https://blog.csdn.net/chen77716/article/details/6742128
事務(wù)隔離級(jí)別與Spring傳播事務(wù)
https://mp.weixin.qq.com/s/xdQKOYW0HOC4mpYinXDS8A

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容