MySQL不可重復(fù)讀及事務(wù)的隔離級(jí)別和MVCC、LBCC實(shí)現(xiàn)
上一篇文章講解了MySQL的事務(wù)的相關(guān)概念MySQL的事務(wù)特性概念梳理總結(jié)
文章末尾提出了事務(wù)因并發(fā)出現(xiàn)的問(wèn)題有哪些?
本篇將著重講述這個(gè)問(wèn)題的前因后果及解決方式。
事務(wù)因并發(fā)出現(xiàn)的問(wèn)題有哪些 臟讀
概念:一個(gè)事務(wù)讀取到其他事務(wù)未提交的數(shù)據(jù)。
用一個(gè)圖來(lái)講解,在并發(fā)環(huán)境下,多個(gè)事務(wù)操作同一對(duì)象帶來(lái)的問(wèn)題:
不可重復(fù)讀
概念:一個(gè)事務(wù)在一個(gè)時(shí)間段內(nèi) 前后讀取的數(shù)據(jù)不一致,或者出現(xiàn)了修改/刪除。
幻讀
概念:事務(wù)A 按照查詢(xún)條件讀取某個(gè)范圍的記錄,其他事務(wù)又在該范圍內(nèi)出入了滿(mǎn)足條件的新記錄,當(dāng)事務(wù)A再次讀取數(shù)據(jù)到時(shí)候我們發(fā)現(xiàn)多了滿(mǎn)足記錄的條數(shù)(幻行)
建議大家把幻讀記作幻行,以免和不可重復(fù)讀記混淆
不可重復(fù)讀與幻讀的區(qū)別
前提:兩者都是讀取到已經(jīng)提交的數(shù)據(jù)
不可重復(fù)讀:重點(diǎn)是在于修改,在一個(gè)事務(wù)中,同樣的條件,第一次讀取的數(shù)據(jù)與第二次【數(shù)據(jù)不一樣】(因?yàn)橹虚g有其他事務(wù)對(duì)這個(gè)數(shù)據(jù)進(jìn)行了修改)
幻讀:重點(diǎn)在于新增或者刪除,在一個(gè)事務(wù)中,同樣的條件(范圍),第一次讀取和第二讀取【記錄條數(shù)不一樣】(因?yàn)橹虚g有其他事務(wù)在這個(gè)范圍里插入、刪除了的數(shù)據(jù))
我們現(xiàn)在已經(jīng)知道,原來(lái)事務(wù)并發(fā)會(huì)出現(xiàn),臟讀,不可重復(fù)讀,幻讀的問(wèn)題。
那這些問(wèn)題我們都是需要去解決的,怎么解決呢?
有興趣可以看看官網(wǎng)是怎么解釋的
鏈接: 官網(wǎng)地址
事務(wù)并發(fā)的三大問(wèn)題其實(shí)都是數(shù)據(jù)庫(kù)讀一致性問(wèn)題,必須由數(shù)據(jù)庫(kù)提供一定的事務(wù)隔離機(jī)制來(lái)解決。
事務(wù)的四個(gè)隔離級(jí)別
我們通過(guò)事務(wù)的隔離級(jí)別來(lái)解決不同的問(wèn)題,那么,不同的隔離級(jí)別解決了什么問(wèn)題呢?
其實(shí)sql標(biāo)準(zhǔn)92版 官方都有定義出來(lái)
另外,sql標(biāo)準(zhǔn)不是數(shù)據(jù)庫(kù)廠(chǎng)商定義出來(lái)的,大家不要以為sql語(yǔ)言是什么mysql,sqlserver搞出來(lái)的,我們會(huì)發(fā)現(xiàn)每個(gè)數(shù)據(jù)庫(kù)語(yǔ)句的sql語(yǔ)句都是差不多的。sql是獨(dú)立于廠(chǎng)商的??!SQL是Structured Query Language的縮寫(xiě),本來(lái)就屬于一種查詢(xún)語(yǔ)言??!
官網(wǎng)支持四種隔離級(jí)別:
# 修改當(dāng)前會(huì)話(huà)的隔離級(jí)別 # 讀未提交 SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; # 讀已提交 SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; # 可重復(fù)讀 SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ; # 串行化 SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
我們也可以通過(guò)SQL去查詢(xún)當(dāng)前的隔離級(jí)別
SHOW GLOBAL VARIABLES LIKE '%isolation%'; //全局隔離級(jí)別 SHOW SESSION VARIABLES LIKE '%isolation%'; set SESSION autocommit=0; //關(guān)閉自動(dòng)提交
InnoDB默認(rèn)的隔離級(jí)別是RR
事務(wù)隔離級(jí)別越高,多個(gè)事務(wù)在并發(fā)訪(fǎng)問(wèn)數(shù)據(jù)庫(kù)時(shí)互相產(chǎn)生數(shù)據(jù)干擾的可能性越低,但是并發(fā)訪(fǎng)問(wèn)的性能就越差。(相當(dāng)于犧牲了一定的性能去保證數(shù)據(jù)的安全性)
Read UnCommited 讀未提交 RU
多個(gè)事務(wù)同時(shí)修改一條記錄,A事務(wù)對(duì)其的改動(dòng)在A事務(wù)還沒(méi)提交時(shí),在B事務(wù)中就可以看到A事務(wù)對(duì)其的改動(dòng)。
結(jié)論:沒(méi)有解決任何問(wèn)題,存在臟讀,因?yàn)樗褪亲x取最新的數(shù)據(jù)。
Read Commited 讀已提交 RC
多個(gè)事務(wù)同時(shí)修改一條記錄,A事務(wù)對(duì)其的改動(dòng)在A事務(wù)提交之后,在B事務(wù)中可以看到A事務(wù)對(duì)其的改動(dòng)。
結(jié)論:我就讀取你已經(jīng)提交的事務(wù)就完事,解決臟讀。
Repeatable Read 可重復(fù)讀 RR
多個(gè)事務(wù)同時(shí)修改一條記錄,這條記錄在A事務(wù)執(zhí)行期間是不變的(別的事務(wù)對(duì)這條記錄的修改不被A事務(wù)感知)。
結(jié)論:RR級(jí)別解決了臟讀、不可重復(fù)讀、幻讀的問(wèn)題。
Serializable 串行化
多個(gè)事務(wù)同時(shí)訪(fǎng)問(wèn)一條記錄(CRUD),讀加讀鎖,寫(xiě)加寫(xiě)鎖,完全退化成了串行的訪(fǎng)問(wèn),自然不會(huì)收到任何其他事務(wù)的干擾,性能最低。
結(jié)論:加鎖排隊(duì)讀取,性能最低。
可以看出,RU與串行化都沒(méi)啥實(shí)用意義,主要還是看RC和RR,那么Mysql是怎么實(shí)現(xiàn)這兩種隔離級(jí)別的呢?
我們要先學(xué)習(xí)Mysql的兩種機(jī)制,undo
版本鏈機(jī)制以及read view
快照讀機(jī)制,讀已提交和可重復(fù)讀隔離級(jí)別的實(shí)現(xiàn)都是建立在這兩個(gè)核心機(jī)制之上。
undo 版本鏈
undo 版本鏈就是指undo log的存儲(chǔ)在邏輯上的表現(xiàn)形式,它被用于事務(wù)當(dāng)中的回滾操作以及實(shí)現(xiàn)MVCC
,這里介紹一下undo log之所以能實(shí)現(xiàn)回滾記錄的原理。
對(duì)于每一行記錄,會(huì)有兩個(gè)隱藏字段:row_trx_id
和roll_pointer
row_trx_id
表示更新(改動(dòng))本條記錄的全局事務(wù)id (每個(gè)事務(wù)創(chuàng)建都會(huì)分配id,全局遞增,因此事務(wù)id區(qū)別對(duì)某條記錄的修改是由哪個(gè)事務(wù)作出的)roll_pointer
是回滾指針,指向當(dāng)前記錄的前一個(gè)undo log版本,如果是第一個(gè)版本則roll_pointer
指向null,這樣如果有多個(gè)事務(wù)對(duì)同一條記錄進(jìn)行了多次改動(dòng),則會(huì)在undo log中以鏈的形式存儲(chǔ)改動(dòng)過(guò)程。
在上圖中,最下方的undo log中記錄了當(dāng)前行的最新版本,而該條記錄之前的版本則以版本鏈的形式可追溯,這也是事務(wù)回滾所做的事。那undo log版本鏈和事務(wù)的隔離性有什么關(guān)系呢?那就要引入另一個(gè)核心機(jī)制:read view。
read view
read view表示讀視圖,這個(gè)快照讀會(huì)記錄四個(gè)關(guān)鍵的屬性:
- create_trx_id: 當(dāng)前事務(wù)的
- idm_idx: 當(dāng)前正在活躍的所有事務(wù)id(id數(shù)組),沒(méi)有提交的事務(wù)的
- idmin_trx_id: 當(dāng)前系統(tǒng)中活躍的事務(wù)的id最小值
- max_trx_id: 當(dāng)前系統(tǒng)中已經(jīng)創(chuàng)建過(guò)的最新事務(wù)(id最大)的id+1的值
當(dāng)一個(gè)事務(wù)讀取某條記錄時(shí)會(huì)追溯undo log版本鏈,找到第一個(gè)可以訪(fǎng)問(wèn)的版本,而該記錄的某一個(gè)版本是否能被這個(gè)事務(wù)讀取到遵循如下規(guī)則:
(這個(gè)規(guī)則永遠(yuǎn)成立,這個(gè)需要好好理解,對(duì)后面講解可重復(fù)讀和讀已提交兩個(gè)級(jí)別的實(shí)現(xiàn)密切相關(guān))
- 如果當(dāng)前記錄行的row_trx_id小于min_trx_id,表示該版本的記錄在當(dāng)前事務(wù)開(kāi)啟之前創(chuàng)建,因此可以訪(fǎng)問(wèn)到
- 如果當(dāng)前記錄行的row_trx_id大于等于max_trx_id,表示該版本的記錄創(chuàng)建晚于當(dāng)前活躍的事務(wù),因此不能訪(fǎng)問(wèn)到
- 如果當(dāng)前記錄行的row_trx_id大于等于min_trx_id且小于max_trx_id,則要分兩種情況:
- 當(dāng)前記錄行的row_trx_id在m_idx數(shù)組中,則當(dāng)前事務(wù)無(wú)法訪(fǎng)問(wèn)到這個(gè)版本的記錄 (除非這個(gè)版本的row_trx_id等于當(dāng)前事務(wù)本身的trx_id,本事務(wù)當(dāng)然能訪(fǎng)問(wèn)自己修改的記錄) ,在m_idx數(shù)組中又不是當(dāng)前事務(wù)自己創(chuàng)建的undo版本,表示是并發(fā)訪(fǎng)問(wèn)的其他事務(wù)對(duì)這條記錄的修改的結(jié)果,則不能訪(fǎng)問(wèn)到。
- 當(dāng)前記錄行的row_trx_id不在m_idx數(shù)組中,則表示這個(gè)版本是當(dāng)前事務(wù)開(kāi)啟之前,其他事務(wù)已經(jīng)提交了的undo版本,當(dāng)前事務(wù)可訪(fǎng)問(wèn)到。
RR中 Read View是事務(wù)第一次查詢(xún)的時(shí)候建立的。RC的Read View是事務(wù)每次查詢(xún)的時(shí)候建立的。
Oracle、Postgres等等其他數(shù)據(jù)庫(kù)都有MVCC的實(shí)現(xiàn)。
需要注意,在InnoDB中,MVCC和鎖是協(xié)同使用的,這兩種方案并不是互斥的。
配合使用read view和undo log版本鏈就能實(shí)現(xiàn)事務(wù)之間并發(fā)訪(fǎng)問(wèn)相同記錄時(shí),可以根據(jù)事務(wù)id不同,獲取同一行的不同undo log版本(多版本并發(fā)控制)。
MVCC(Multi-Version Concurrent Control )多版本并發(fā)控制
多版本并發(fā)控制,是什么意思呢?版本控制,我們?cè)谶M(jìn)行查詢(xún)的時(shí)候是有版本的,后續(xù)在同一個(gè)事務(wù)里查詢(xún)的時(shí)候,我們都是使用我們當(dāng)初創(chuàng)建的快照版本。
比如說(shuō)嘛,快照,你10歲20歲30歲40歲去照相,你只能看到你之前照相的模樣,但是不能看到你未來(lái)的模樣。
MVCC怎么去實(shí)現(xiàn)?
每個(gè)事務(wù)都有一個(gè)事務(wù)ID,并且是遞增,我們后續(xù)MVCC的原理都是基于它去完成。
效果:建立一個(gè)快照,同一個(gè)事務(wù)無(wú)論查詢(xún)多少次都是相同的數(shù)據(jù)。
一個(gè)事務(wù)能看見(jiàn)的版本:
- 第一次查詢(xún)之前已經(jīng)提交的版本
- 本事務(wù)的修改
一個(gè)事務(wù)不能看見(jiàn)的版本:
- 在本事務(wù)第一次查詢(xún)之后創(chuàng)建的事務(wù)(事務(wù)ID比我大)
- 活躍中的(未提交)的時(shí)候的修改。
下面通過(guò)模擬并發(fā)訪(fǎng)問(wèn)的兩個(gè)事務(wù)操作,介紹MVCC的實(shí)現(xiàn)(具體來(lái)說(shuō)就是可重復(fù)讀和讀已提交兩個(gè)隔離級(jí)別的實(shí)現(xiàn))
可重復(fù)讀實(shí)現(xiàn)
下面模擬兩個(gè)并發(fā)訪(fǎng)問(wèn)同一條記錄的事務(wù)AB的行為,假設(shè)這條記錄初始時(shí)id=1,a=0,該記錄兩個(gè)隱藏字段row_trx_id = 100,roll_pointer = null
注意:在可重復(fù)讀隔離級(jí)別下,當(dāng)事務(wù)sql執(zhí)行的時(shí)候,會(huì)生成一個(gè)read view快照,且在本事務(wù)周期內(nèi)一直使用這個(gè)read view,下面給出了并發(fā)訪(fǎng)問(wèn)同一條記錄的兩個(gè)事務(wù)AB的具體執(zhí)行過(guò)程,并解釋可重復(fù)讀是如何實(shí)現(xiàn)的(解決了臟讀和不可重復(fù)讀)。
事務(wù)A的read view:
create_trx_id = 101| m_idx = [101, 102]|min_trx_id = 101|max_trx_id = 103
事務(wù)B的read view:
create_trx_id = 102| m_idx = [101, 102]|min_trx_id = 101|max_trx_id = 103
(ps. 這里因?yàn)锳B事務(wù)是并發(fā)執(zhí)行,因此兩個(gè)事務(wù)創(chuàng)建的read view的max_trx_id = 103)
這里要注意的是,每次對(duì)一條記錄發(fā)生修改,就會(huì)記錄一個(gè)undo log的版本,則在A事務(wù)中第二次查詢(xún)id=1的記錄的a的值的時(shí)候,B事務(wù)對(duì)該記錄的修改已經(jīng)添加到版本鏈上了,此時(shí)這個(gè)undo log的trx_id = 102,在A事務(wù)的read view的m_idx數(shù)組中且不等于A事務(wù)的trx_id = 101,因此無(wú)法訪(fǎng)問(wèn)到,需要在向前回溯,這里找到trx_id = 100的記錄版本(小于A事務(wù)read view的min_trx_id屬性,因此可以訪(fǎng)問(wèn)到),故A事務(wù)第二次查詢(xún)依舊得到a = 0,而不是B事務(wù)修改的a = 1。
你可能有疑問(wèn),在A事務(wù)第二次查詢(xún)的時(shí)候,B事務(wù)已經(jīng)完成提交了,那么A事務(wù)的read view的m_idx數(shù)組應(yīng)該移除102才對(duì)啊,它存的不是當(dāng)前活躍的事務(wù)的id嗎?·
注意:在可重復(fù)讀隔離級(jí)別下,當(dāng)事務(wù)sql執(zhí)行的時(shí)候,會(huì)生成一個(gè)read view快照,且在本事務(wù)周期內(nèi)一直使用這個(gè)read view,雖然102確實(shí)應(yīng)該從A事務(wù)的read view中移除,但是因?yàn)閞ead view在可重復(fù)讀隔離級(jí)別下只會(huì)在第一條SQL執(zhí)行時(shí)創(chuàng)建一次,并始終保持不變直到事務(wù)結(jié)束。
那么也就明白了,在可重復(fù)讀隔離級(jí)別下,因?yàn)閞ead view只在第一條SQL執(zhí)行時(shí)創(chuàng)建,因此并發(fā)訪(fǎng)問(wèn)的其他事務(wù)提交前改動(dòng)的臟數(shù)據(jù)、以及并發(fā)訪(fǎng)問(wèn)的其他事務(wù)提交的改動(dòng)數(shù)據(jù)都對(duì)當(dāng)前事務(wù)是透明的(盡管確實(shí)是記錄在了undo log版本鏈中) ,這就解決了臟讀和不可重復(fù)讀(即使其他事務(wù)提交的修改,對(duì)A事務(wù)來(lái)說(shuō)前后查詢(xún)結(jié)果相同)的問(wèn)題!
讀已提交實(shí)現(xiàn)
還是借助上面事務(wù)處理的例子,所有的事務(wù)處理流程不變,只是將隔離級(jí)別調(diào)整為讀已提交,讀已提交依舊遵守read view和undo log版本鏈機(jī)制,它和可重復(fù)讀級(jí)別的區(qū)別在于,每次執(zhí)行sql,都會(huì)創(chuàng)建一個(gè)read view,獲取最新的事務(wù)快照。 而因?yàn)檫@個(gè)區(qū)別,讀已提交產(chǎn)生了不可重復(fù)讀的問(wèn)題,下面來(lái)分析一下原因:
事務(wù)A第一次查詢(xún)創(chuàng)建的read view:
create_trx_id = 101| m_idx = [101, 102]|min_trx_id = 101|max_trx_id = 103
事務(wù)B的read view:
create_trx_id = 102| m_idx = [101, 102]|min_trx_id = 101|max_trx_id = 103
事務(wù)A第二次查詢(xún)創(chuàng)建的read view:
create_trx_id = 101| m_idx = [101]|min_trx_id = 101|max_trx_id = 103
(ps. 這里因?yàn)锳B事務(wù)是并發(fā)執(zhí)行,因此兩個(gè)事務(wù)創(chuàng)建的read view的max_trx_id = 103)
這里重點(diǎn)觀(guān)察A事務(wù)的第二次查詢(xún),之前你可能就意識(shí)到了,在事務(wù)B完成提交后,當(dāng)前系統(tǒng)中活躍的事務(wù)id應(yīng)該移除102,但是因?yàn)?strong>在可重復(fù)讀隔離級(jí)別下,A事務(wù)的read view只會(huì)在第一個(gè)SQL執(zhí)行時(shí)創(chuàng)建,而在讀已提交隔離級(jí)別下,每次執(zhí)行SQL都會(huì)創(chuàng)建最新的read view,且此時(shí) m_idx數(shù)組中移除了102,那么事務(wù)A在追溯undo log版本鏈的時(shí)候,最新版本記錄的trx_id = 102,102不在A事務(wù)的m_idx數(shù)組中,且101 = min_trx_id <= 102 < max_trx_id = 103,因此可以訪(fǎng)問(wèn)到B事務(wù)的提交結(jié)果。
那么對(duì)A事務(wù)來(lái)說(shuō),在事務(wù)過(guò)程中讀取同一條記錄第一次得到a=0,第二次得到a=1,所以出現(xiàn)了不可重復(fù)讀的問(wèn)題(這里B不提交的話(huà)A如果就進(jìn)行了第二次查詢(xún),則102不會(huì)從A事務(wù)的read view移除,則A事務(wù)依舊訪(fǎng)問(wèn)不到B事務(wù)未提交的修改,因此臟讀還是可以避免的!)
MVCC多版本并發(fā)控制的實(shí)現(xiàn)可以理解成讀已提交、可重復(fù)讀兩種隔離級(jí)別的實(shí)現(xiàn),通過(guò)控制read view的創(chuàng)建時(shí)機(jī)(其訪(fǎng)問(wèn)機(jī)制是不變的),配合undo log版本鏈可以實(shí)現(xiàn)事務(wù)之間對(duì)同一條記錄的并發(fā)訪(fǎng)問(wèn),并獲得不同的結(jié)果。
但是,大家有沒(méi)有想過(guò),剛才的一切都是對(duì)A提供便利,對(duì)B呢?
而且,MVCC 是適合用于處查詢(xún)的時(shí)候使用,能提供很高的性能,我們的事務(wù)不僅僅
是只有讀,我們還有寫(xiě)情況,剛才介紹的情況,B的事務(wù)是不是會(huì)被直接覆蓋掉?這不就造成了事務(wù)丟失了嘛
針對(duì)寫(xiě)的情況,Mysql還有另一種基于鎖的機(jī)制
LBCC
鎖的作用是什么?它跟Java里面的鎖是一樣的,是為了解決資源競(jìng)爭(zhēng)的問(wèn)題,Java里面的資源是對(duì)象,數(shù)據(jù)庫(kù)的資源就是數(shù)據(jù)表或者數(shù)據(jù)行。
基于鎖的方式起始比較簡(jiǎn)單,就是一個(gè)事務(wù)在進(jìn)行數(shù)據(jù)查詢(xún)時(shí),不允許其他事務(wù)修改。也就是說(shuō),基于鎖的機(jī)制就使得數(shù)據(jù)庫(kù)無(wú)法支持并發(fā)事務(wù)的讀寫(xiě)操作,這種方案在一定程度上影響了操作數(shù)據(jù)的效率。
本文著重講InnoDB引擎
- 基于鎖的屬性:共享鎖和排它鎖
- 基于鎖的狀態(tài):意向共享鎖和意向排它
- 基于鎖的粒度:表鎖、頁(yè)鎖、行鎖 鎖的粒度
在之前講MySQL存儲(chǔ)引擎的時(shí)候,我們知道了 InnoDB和MylSAM支持的鎖 的類(lèi)型是不同的。InnoDB同時(shí)支持表鎖和行鎖,而MylSAM只支持表鎖,用lock table的語(yǔ)法加鎖。
lock tables xxx read; lock tables xxx write; unlock tables ;
為什么支持行鎖會(huì)成為InnoDB的優(yōu)勢(shì)?表鎖和行鎖的區(qū)別到底在哪?
- 鎖定粒度:表鎖 > 行鎖
- 加鎖效率:表鎖 > 行鎖
- 沖突概率:表鎖 > 行鎖
- 并發(fā)性能:表鎖 < 行鎖
鎖的類(lèi)型
我們可以看到,官網(wǎng)把鎖分成了8類(lèi)。我們把前面的兩個(gè)行級(jí)別的鎖(Shared andExclusive Locks),和兩個(gè)表級(jí)別的鎖(Intention Locks)稱(chēng)為鎖的基本模式。
- 鎖的基本模式: (Shared And Exclusive Locks)行級(jí)別鎖 和 (Intention Locks)表級(jí)別鎖
- 后面三個(gè):Record Locks、Gap Locks、Next-Key Locs ,我們稱(chēng)為鎖的算法,也就是說(shuō)在什么情況下鎖定什么范圍。
- 插入意向鎖(Insert Intention Locks):是一個(gè)特殊的間隙鎖。間隙鎖不允許插入數(shù)據(jù),但是插入意向鎖允許 多個(gè)事務(wù)同時(shí)插入數(shù)據(jù)到同一個(gè)范圍。比如(4,7), —個(gè)事務(wù)插入5, —個(gè)事務(wù)插入6,不 會(huì)發(fā)生鎖等待。
- 自增鎖(AUTO-INC Locks):是一種特殊的表鎖,用來(lái)防止自增字段重復(fù),數(shù)據(jù)插入以后就會(huì)釋放,不需要等到事務(wù)提交才釋放。如果需要選擇更快的自增值生成速度或者更加連續(xù)的自增值,就要通過(guò)修改自增鎖的模式改變。
show variables like 'innodb_autoinc_lock_mode'; --0: traditonal(每次都會(huì)產(chǎn)生表鎖) --1: consecutive(會(huì)產(chǎn)生一個(gè)輕量鎖,simple insert 會(huì)獲得批量的鎖,保證連續(xù)插入,默認(rèn)值) --2: interleaved(不會(huì)鎖表,來(lái)一個(gè)處理一個(gè),并發(fā)最高)
空間索引的謂詞鎖:Predicate Locks for Spatial Indexes是5.7版本里面新增的空間索引的謂詞鎖。
共享鎖
第一個(gè)行級(jí)別的鎖就是我們?cè)诠倬W(wǎng)看到的Shared Locks(共享鎖),我們獲取了一行數(shù)據(jù)的讀鎖以后,可以用來(lái)讀取數(shù)據(jù),所以它也叫做讀鎖,注意不要在加上了讀鎖以后去寫(xiě)數(shù)據(jù),不然的話(huà)可能會(huì)出現(xiàn)死鎖的情況。而且多個(gè)事務(wù)可以共享一把讀鎖。
共享鎖的作用:因?yàn)楣蚕礞i會(huì)阻塞其他事務(wù)的修改,所以可以用在不允許其他事務(wù)修改數(shù)據(jù)的情況。
那怎么給一行數(shù)據(jù)加上讀鎖呢?
我們可以用select… lock in share mode;的方式手工加上一把讀鎖。
釋放鎖有兩種方式,只要事務(wù)結(jié)束,鎖就會(huì)自動(dòng)事務(wù),包括提交事務(wù)和結(jié)束事務(wù)。
排它鎖
第二個(gè)行級(jí)別的鎖叫做Exclusive Locks(排它鎖),它是用來(lái)操作數(shù)據(jù)的,所以又叫做寫(xiě)鎖。只要一個(gè)事務(wù)獲取了一行數(shù)據(jù)的排它鎖,其他的事務(wù)就不能再獲取這一行數(shù)據(jù)的共享鎖和排它鎖。
排它鎖的加鎖方式有兩種
第一種是自動(dòng)加排他鎖,可能是同學(xué)們沒(méi)有注意到的:我們?cè)诓僮鲾?shù)據(jù)的時(shí)候,包括增刪改,都會(huì)默認(rèn)加上一個(gè)排它鎖。
第二種是手工加鎖,我們用一個(gè)FOR UPDATE給一行數(shù)據(jù)加上一個(gè)排它鎖,這個(gè)無(wú)論是在我們的代碼里面還是操作數(shù)據(jù)的工具里面,都比較常用。
釋放鎖的方式跟前面是一樣的。
這個(gè)是兩個(gè)行鎖,接下來(lái)就是兩個(gè)表鎖。
意向鎖
意向鎖是什么呢?我們好像從來(lái)沒(méi)有聽(tīng)過(guò),也從來(lái)沒(méi)有使用過(guò),其實(shí)他們是由數(shù)據(jù)庫(kù)自己維護(hù)的。
也就是說(shuō):
- 當(dāng)我們給一行數(shù)據(jù)加上共享鎖之前,數(shù)據(jù)庫(kù)會(huì)自動(dòng)在這張表上面加一個(gè)意向共享鎖。
- 當(dāng)我們給一行數(shù)據(jù)加上排他鎖之前,數(shù)據(jù)庫(kù)會(huì)自動(dòng)在這張表上面加一個(gè)意向排他鎖。
反過(guò)來(lái):
- 如果一張表上面至少有一個(gè)意向共享鎖,說(shuō)明有其他的事務(wù)給其中的某些數(shù)據(jù)行加上了共享鎖。
意向鎖跟意向鎖是不沖突的,意向鎖跟行鎖也不沖突。
那么這兩個(gè)表級(jí)別的鎖存在的意義是什么呢?
如果說(shuō)沒(méi)有意向鎖的話(huà),當(dāng)我們準(zhǔn)備給一張表加上表鎖的時(shí)候,我們首先要做什么?是不是必須先要去判斷有沒(méi)其他的事務(wù)鎖定了其中了某些行?如果有的話(huà),肯定不能加上表鎖。那么這個(gè)時(shí)候我們就要去掃描整張表才能確定能不能成功加上一個(gè)表鎖,如果數(shù)據(jù)量特別大,比如有上千萬(wàn)的數(shù)據(jù)的時(shí)候,加表鎖的效率是不是很低?
但是我們引入了意向鎖之后就不一樣了。我只要判斷這張表上面有沒(méi)有意向鎖,如果有,就直接返回失敗。如果沒(méi)有,就可以加鎖成功。所以InnoDB里面的表鎖,我們可以把它理解成一個(gè)標(biāo)志。就像火車(chē)上衛(wèi)生間有沒(méi)有人使用的燈,讓你不用去推門(mén),是用來(lái)提高加鎖的效率的。
所以鎖是用來(lái)解決事務(wù)對(duì)數(shù)據(jù)的并發(fā)訪(fǎng)問(wèn)的問(wèn)題的。那么,鎖到底鎖住了什么呢?
當(dāng)一個(gè)事務(wù)鎖住了一行數(shù)據(jù)的時(shí)候,其他的事務(wù)不能操作這一行數(shù)據(jù),那它到底是鎖住了這一行數(shù)據(jù),還是鎖住了這一個(gè)字段,還是鎖住了別的什么東西呢?
行鎖的原理
沒(méi)有索引的表
首先我們有三張表,一張沒(méi)有索引的t1,一張有主鍵索引的t2,一張有唯一索引的t3。
我們先假設(shè) InnoDB的行鎖 鎖住的是一行數(shù)據(jù)或者一條記錄。
我們假設(shè)t1的表結(jié)構(gòu),它有兩個(gè)字段, int類(lèi)型的id和varchar類(lèi)型的name。里面有4條數(shù)據(jù),1、2、3、4。
我們?cè)趦蓚€(gè)會(huì)話(huà)里面手工開(kāi)啟兩個(gè)事務(wù)。
在第一個(gè)事務(wù)里面,我們通過(guò) where id =1鎖住第一行數(shù)據(jù)。
在第二個(gè)事務(wù)里面,我們嘗試給id=3的這一行數(shù)據(jù)加鎖,能成功嗎?
很遺憾,我們看到紅燈亮起,這個(gè)加鎖的操作被阻塞了。這就有點(diǎn)奇怪了,第一個(gè)事務(wù)鎖住了id=1的這行數(shù)據(jù),為什么我不能操作id=3的數(shù)據(jù)呢?
我們?cè)賮?lái)操作一條不存在的數(shù)據(jù),插入 id=5。它也被阻塞了。實(shí)際上這里整張表都被鎖住了。所以,我們的第一個(gè)猜想被推翻了,InnoDB的行鎖鎖住的應(yīng)該不是Record。
那為什么在沒(méi)有索引或者沒(méi)有用到索引的情況下,會(huì)鎖住整張表?這個(gè)問(wèn)題我們先留在這里。
有主鍵索引的表
我們假設(shè)t2的表結(jié)構(gòu)。字段和t1是一樣的,不同的地方是id上創(chuàng)建了一個(gè)主鍵索引。里面的數(shù)據(jù)是1、4、7、10。
第一種情況,使用相同的id值去加鎖,沖突;使用不同的id 加鎖,可以加鎖成功。那么,既然不是鎖定一行數(shù)據(jù),有沒(méi)有可能是鎖住了id 的這個(gè)字段
呢?
有唯一索引的表(上面假設(shè)鎖住了字段)
我們假設(shè)t3的表結(jié)構(gòu)字段還是一樣的, id上創(chuàng)建了一個(gè)主鍵索引,name 上創(chuàng)建了一個(gè)唯一索引。里面的數(shù)據(jù)是1、4、7、10。
在第一個(gè)事務(wù)里面,我們通過(guò)name字段去鎖定值是4的這行數(shù)據(jù)。
在第二個(gè)事務(wù)里面,嘗試獲取一樣的排它鎖,肯定是失敗的,這個(gè)不用懷疑。
在這里我們懷疑InnoDB的行鎖鎖住的是字段,所以這次我換一個(gè)字段,用id=4去給這行數(shù)據(jù)加鎖,能成功嗎?
很遺憾,又被阻塞了,說(shuō)明行鎖鎖住的是字段的這個(gè)推測(cè)也是錯(cuò)的,否則就不會(huì)出現(xiàn)第一個(gè)事務(wù)鎖住了name,第二個(gè)字段鎖住id失敗的情況。
既然鎖住的不是record,也不是column,,行列都沒(méi)鎖,那InnoDB的行鎖鎖住的到底是什么呢?在這三個(gè)案例里面,我們要去分析一下他們的差異在哪里,也就是這三張表的結(jié)構(gòu),是什么區(qū)別導(dǎo)致了加鎖的行為的差異?其實(shí)答案就是索引。InnoDB的行鎖,就是通過(guò)鎖住索引來(lái)實(shí)現(xiàn)的。
那么我們還有兩個(gè)問(wèn)題沒(méi)有解決:
1、為什么表里面沒(méi)有索引的時(shí)候,鎖住一行數(shù)據(jù)會(huì)導(dǎo)致鎖表?或者說(shuō),如果鎖住的是索引,一張表沒(méi)有索引怎么辦?
所以,一張表有沒(méi)有可能沒(méi)有索引?
- 1)如果我們定義了主鍵(PRIMARY KEY),那么InnoDB會(huì)選擇主鍵作為聚集索引。
- 2)如果沒(méi)有顯式定義主鍵,則InnoDB會(huì)選擇第一個(gè)不包含有NULL值的唯一索引作為主鍵索引。
- 3)如果也沒(méi)有這樣的唯一索引,則InnoDB會(huì)選擇內(nèi)置6字節(jié)長(zhǎng)的
ROWID
(每一行都有的內(nèi)置,或者說(shuō)隱藏的列
)作 為隱藏的聚集索引,它會(huì)隨著行記錄的寫(xiě)入而主鍵遞增。
所以,為什么鎖表,是因?yàn)椴樵?xún)沒(méi)有使用索引,會(huì)進(jìn)行全表掃描,然后把每一個(gè)隱藏的聚集索引都鎖住了。
2、為什么通過(guò)唯一索引給數(shù)據(jù)行加鎖,主鍵索引也會(huì)被鎖住?
大家還記得在InnoDB里面,當(dāng)我們使用輔助索引(二級(jí)索引)的時(shí)候,它是怎么檢索數(shù)據(jù)的嗎?輔助索引的葉子節(jié)點(diǎn)存儲(chǔ)的是什么內(nèi)容?
在輔助索引里面,索引存儲(chǔ)的是二級(jí)索引和主鍵的值。比如name=4,存儲(chǔ)的是name的索引和主鍵id 的值4。
而主鍵索引里面除了索引之外,還存儲(chǔ)了完整的數(shù)據(jù)。所以我們通過(guò)輔助索引鎖定一行數(shù)據(jù)的時(shí)候,它跟我們檢索數(shù)據(jù)的步驟是一樣的,會(huì)通過(guò)主鍵值找到主鍵索引,然后也鎖定。
本質(zhì)上是因?yàn)殒i定的是同一行數(shù)據(jù),是相互沖突的。
InnoDB中LBCC要解決的問(wèn)題
問(wèn)題1-幻讀問(wèn)題(InnoDB)
范圍查詢(xún)的時(shí)候,多次查詢(xún)結(jié)果的數(shù)據(jù)行數(shù)一致
select * from table where id >=1 and id<=4 //鎖定2,3 [解決幻讀問(wèn)題]
問(wèn)題二, for update 實(shí)現(xiàn)了排他鎖(行鎖)
--transaction1 select * from table where id=1 for update; //查詢(xún)主鍵id=1 (行 鎖,只鎖定行) --transaction2 update table set name='111' where id=1; //阻塞 update table set name='222' where name =''; //阻塞
基于索引來(lái)決定的,如果where是索引,那么這個(gè)時(shí)候,直接加行鎖.
問(wèn)題三, 鎖定整個(gè)表
select * from table for update; //表鎖 update table set name='111' where id=1; //阻塞
鎖的算法
我們先來(lái)看一下我們測(cè)試用的表,t2,這張表有一個(gè)主鍵索引,前面我們已經(jīng)見(jiàn)過(guò)了。我們插入了4行數(shù)據(jù),主鍵id分別是1、4、7、10。
為了讓大家真正理解這三種行鎖算法的區(qū)別,我也來(lái)花一點(diǎn)時(shí)間給大家普及一下這三種范圍的概念。
因?yàn)槲覀冇弥麈I索引加鎖,我們這里的劃分標(biāo)準(zhǔn)就是主鍵索引的值。
這些數(shù)據(jù)庫(kù)里面存在的主鍵值,我們把它叫做Record(記錄),那么這里我們就有4個(gè)Record。
根據(jù)主鍵,這些存在的Record隔開(kāi)的數(shù)據(jù)不存在的區(qū)間,我們把它叫做Gap(間隙),它是一個(gè)左開(kāi)右開(kāi)
的區(qū)間。
假設(shè)我們有N個(gè)Record,那么所有的數(shù)據(jù)會(huì)被劃分成多少個(gè)Gap 區(qū)間?答案是N+1,就像我們把一條繩子砍N刀,它最后肯定是變成N+1段。
最后一個(gè),間隙(Gap)連同它左邊的記錄(Record),我們把它叫做臨鍵的區(qū)間
,它是一個(gè)左開(kāi)右閉的區(qū)間。再重復(fù)一次,是左開(kāi)右閉。
整型的主鍵索引,它是可以排序,所以才有這種區(qū)間。如果我的主鍵索引不是整形,是字符怎么辦呢?
任何一個(gè)字符集,都有相應(yīng)的排序規(guī)則:
Record Lock (記錄鎖) [鎖定的是索引]
第一種情況,當(dāng)我們對(duì)于唯一性的索引(包括唯一索引和主鍵索引)使用等值查詢(xún),精準(zhǔn)匹配到一條記錄的時(shí)候,這個(gè)時(shí)候使用的就是記錄鎖。
顧名思義,記錄鎖就是為某行記錄加鎖,它封鎖該行的索引記錄,并不是真正的數(shù)據(jù)記錄,鎖的是索引的鍵值對(duì)。
-- 記錄鎖:id 列為主鍵列或唯一索引列 SELECT * FROM user WHERE id = 1 FOR UPDATE; --意味著id=1的這條記錄會(huì)被鎖住
Gap Lock(間隙鎖 鎖定索引區(qū)間,不包括record lock)
第二種情況,當(dāng)我們查詢(xún)的記錄不存在,沒(méi)有命中任何一個(gè)record,無(wú)論是用等值查詢(xún)還是范圍查詢(xún)的時(shí)候,它使用的都是間隙鎖。
還有個(gè)情況,假如我們只命中間隙的一邊,另一邊無(wú)法命中怎么辦?
這種情況下,會(huì)鎖住另一邊的無(wú)限空間
顧名思義 鎖間隙,不鎖記錄。
重復(fù)一遍,當(dāng)查詢(xún)的記錄不存在的時(shí)候,使用間隙鎖。
注意,間隙鎖主要是阻塞插入insert。相同的間隙鎖之間不沖突。
間隙鎖是基于非唯一索引,它鎖定一段范圍內(nèi)的索引記錄,比如下面這個(gè)查詢(xún)
SELECT * FROM user WHERE id BETWEN 1 AND 4 FOR UPDATE;
那么意味著所有在(1,4)區(qū)間內(nèi)的記錄行都會(huì)被鎖住,它是一個(gè)左右開(kāi)區(qū)間的范圍,意味著在這種情況下, 會(huì)鎖住id為2,3的索引,但是1、4不會(huì)被鎖定
next Key Lock(臨鍵鎖 鎖定索引區(qū)間,包括record lock)
第三種情況,當(dāng)我們使用了范圍查詢(xún),不僅僅命中了Record記錄,還包含了Gap間隙,在這種情況下我們使用的就是臨鍵鎖,它是MySQL里面默認(rèn)的行鎖算法,相當(dāng)于記錄鎖加上間隙鎖。
唯一性索引,等值查詢(xún)匹配到一條記錄的時(shí)候,退化成記錄鎖。
沒(méi)有匹配到任何記錄的時(shí)候,退化成間隙鎖。
next Key Lock 可以理解為一種特殊的間隙鎖,也可以理解為一種特殊的算法,每個(gè)數(shù)據(jù)行上的非唯一索引列上都會(huì)存在一把臨鍵鎖,當(dāng)某個(gè)事務(wù)持有該數(shù)據(jù)行的臨鍵鎖時(shí),會(huì)鎖住一段左開(kāi)右閉區(qū)間的數(shù)據(jù)。
為什么要鎖住下一個(gè)左開(kāi)右閉的區(qū)間?——就是為了解決幻讀的問(wèn)題。
小結(jié)
所以,我們?cè)倩剡^(guò)頭來(lái)看下這張圖片,為什么InnoDB的RR級(jí)別能夠解決幻讀的問(wèn)題,就是用臨鍵鎖實(shí)現(xiàn)的。
我們?cè)倩剡^(guò)頭來(lái)看下這張圖片,這個(gè)就是MySQL InnoDB里面事務(wù)隔離級(jí)別的實(shí)現(xiàn)。
最后我們來(lái)總結(jié)一下四個(gè)事務(wù)隔離級(jí)別:
Read Uncommited
RU隔離級(jí)別:不加鎖。Serializable
Serializable 所有的select語(yǔ)句都會(huì)被隱式的轉(zhuǎn)化為select … in share mode,會(huì)和update、delete互斥。
這兩個(gè)很好理解,一般也不用,主要是RR和RC的區(qū)別?
Repeatable Read:RR隔離級(jí)別下,普通的select使用快照讀(snapshot read),底層使用MVCC來(lái)實(shí)
現(xiàn)。
加鎖的select(select … in share mode / select … for update)以及更新操作update, delete等語(yǔ)句使用當(dāng)前讀(current read),底層使用記錄鎖、或者間隙鎖、臨鍵鎖
。
Read Commited:RC隔離級(jí)別下,普通的select 都是快照讀,使用MVCC 實(shí)現(xiàn)。加鎖的select都使用記錄鎖,因?yàn)闆](méi)有Gap Lock。
除了兩種特殊情況——外鍵約束檢查(foreign-key constraint checking)以及重復(fù)鍵檢查(duplicate-key checking)時(shí)會(huì)使用間隙鎖封鎖區(qū)間。
所以RC會(huì)出現(xiàn)幻讀的問(wèn)題。
事務(wù)隔離級(jí)別怎么選?
RU和Serializable肯定不能用
RC和RR主要有幾個(gè)區(qū)別:
- 1、 RR的間隙鎖會(huì)導(dǎo)致鎖定范圍的擴(kuò)大。
- 2、 條件列未使用到索引, RR鎖表,RC鎖行。
- 3、 RC的"半一致性”(semi-consistent)讀可以增加update操作的并發(fā)性。
在RC中,一個(gè)update語(yǔ)句,如果讀到一行已經(jīng)加鎖的記錄,此時(shí) InnoDB返回記錄最近提交的版本,由MySQL上層判斷此版本是否滿(mǎn)足update的where 條件。若滿(mǎn)足(需要更新),則MySQL會(huì)重新發(fā)起一次讀操作,此時(shí)會(huì)讀取行的最新版本(并加鎖)。
實(shí)際上,如果能夠正確地使用鎖(避免不使用索引去枷鎖),只鎖定需要的數(shù)據(jù),用默認(rèn)的RR級(jí)別就可以了
在我們使用鎖的時(shí)候,有一個(gè)問(wèn)題是需要注意和避免的,我們知道,排它鎖有互斥的特性。一個(gè)事務(wù)或者說(shuō)一個(gè)線(xiàn)程持有鎖的時(shí)候,會(huì)阻止其他的線(xiàn)程獲取鎖,這個(gè)時(shí)候會(huì)造成阻塞等待,如果循環(huán)等待,會(huì)有可能造成死鎖。
死鎖的相關(guān)信息,可以看我的下一篇博客,MySQL死鎖的解析
鏈接:MySQL死鎖使用詳解及檢測(cè)和避免方法
到此這篇關(guān)于MySQL臟讀幻讀不可重復(fù)讀及事務(wù)的隔離級(jí)別和MVCC、LBCC實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)MySQL臟讀幻讀 內(nèi)容請(qǐng)搜索本站以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持本站!
版權(quán)聲明:本站文章來(lái)源標(biāo)注為YINGSOO的內(nèi)容版權(quán)均為本站所有,歡迎引用、轉(zhuǎn)載,請(qǐng)保持原文完整并注明來(lái)源及原文鏈接。禁止復(fù)制或仿造本網(wǎng)站,禁止在非www.sddonglingsh.com所屬的服務(wù)器上建立鏡像,否則將依法追究法律責(zé)任。本站部分內(nèi)容來(lái)源于網(wǎng)友推薦、互聯(lián)網(wǎng)收集整理而來(lái),僅供學(xué)習(xí)參考,不代表本站立場(chǎng),如有內(nèi)容涉嫌侵權(quán),請(qǐng)聯(lián)系alex-e#qq.com處理。