不知道MYSQL怎么控制并發(fā)數(shù)據(jù)的讀取,怎么辦?

點(diǎn)擊上方「藍(lán)字」關(guān)注我們
數(shù)據(jù)隔離是怎么實(shí)現(xiàn)的
?注意:本次數(shù)據(jù)隔離是建立在可重復(fù)讀的場(chǎng)景下
?
在可重復(fù)讀的場(chǎng)景下,我們了解每次啟動(dòng)事務(wù)的時(shí)候,會(huì)在當(dāng)前啟動(dòng)一個(gè)視圖,而這個(gè)視圖是整個(gè)數(shù)據(jù)庫(kù)的視圖快照。
嘿嘿,是不是想數(shù)據(jù)庫(kù)那么大,為啥我們沒(méi)有感覺(jué)到創(chuàng)建快照時(shí)間的消耗呢?
這是因?yàn)閿?shù)據(jù)庫(kù)創(chuàng)建的視圖快照利用了「所有數(shù)據(jù)都有多個(gè)版本的特性,來(lái)實(shí)現(xiàn)快速創(chuàng)建視圖快照的能力」。
那數(shù)據(jù)多個(gè)版本是怎么回事呢?
準(zhǔn)備下數(shù)據(jù)
先別急,我們準(zhǔn)備下數(shù)據(jù)。
現(xiàn)在創(chuàng)建一個(gè)表,并且插入三條數(shù)據(jù)。
create?table?scores
(
????id????int???not?null
????????primary?key,
????score?float?null
);
INSERT?INTO?scores?(id,?score)?VALUES?(1,?3.5);
INSERT?INTO?scores?(id,?score)?VALUES?(2,?3.65);
INSERT?INTO?scores?(id,?Score)?VALUES?(3,?4);
在開(kāi)始使用前我們要了解兩個(gè)小知識(shí)點(diǎn)。begin/start transaction 與 start transaction with consistent snapshot。
begin/start transaction 視圖的創(chuàng)建是建立在begin/ start transaction 之后SQL語(yǔ)句才會(huì)創(chuàng)建視圖, 比如 下面案例
begin
select?source?from?scores;??//視圖是在這里開(kāi)始創(chuàng)建?而不是在begin那里創(chuàng)建
commitstart transaction with consistent snapshot:則是該語(yǔ)句執(zhí)行后,就創(chuàng)建視圖。
了解上面兩個(gè)創(chuàng)建事務(wù)的區(qū)別后,我們來(lái)看下視圖是怎么創(chuàng)建出來(lái)多個(gè)數(shù)據(jù)版本的. 以下SQL在兩個(gè)窗口打開(kāi)。
| 事務(wù)A | 事務(wù)B | 結(jié)果 |
|---|---|---|
| start transaction with consistent snapshot | 開(kāi)啟事務(wù),并創(chuàng)建視圖 | |
| -- | start transaction with consistent snapshot | 開(kāi)啟事務(wù),并創(chuàng)建視圖 |
| select score from scors where id =2 | -- | 事務(wù)A中的值為3.65 |
| -- | update scores set scores = 10 where id =2 | 事務(wù)B修改為10 |
| -- | select score from scores where id =2 | 事務(wù)B顯示為10 |
| select score from scores where id =2 | -- | 事務(wù)A顯示為3.65 |
| select score from scores where id =2 for update | -- | 會(huì)被鎖住,等待事務(wù)B釋放鎖(間隙鎖) |
| -- | commit | 提交事務(wù)B |
| select score from scores where id =2 for update | -- | 這個(gè)語(yǔ)句可以看到變成了10(利用了當(dāng)前讀) |
| select score from scores where id =2 | -- | 不加 for update 那么結(jié)果還是3.65 |
| commit | --- | --- |
上述流程就是兩個(gè)不同的請(qǐng)求過(guò)來(lái),對(duì)數(shù)據(jù)庫(kù)同一個(gè)表的不同操作。
當(dāng)事務(wù)A執(zhí)行start transaction with consistent snapshot之后,A的視圖就開(kāi)始被創(chuàng)建了,這時(shí)候是看不到事務(wù)B對(duì)其中的修改,就算事務(wù)Bcommit之后,只要事務(wù)A不結(jié)束,它看到的結(jié)果就是它啟動(dòng)時(shí)刻的值。
「這就與不重復(fù)提交,執(zhí)行過(guò)程中看到的結(jié)果與啟動(dòng)的時(shí)候看到的結(jié)果是一致的這句話對(duì)應(yīng)上了」。
快照多版本
前面說(shuō)了,快照是事務(wù)的啟動(dòng)的時(shí)候是基于整個(gè)數(shù)據(jù)庫(kù)的,而整個(gè)數(shù)據(jù)庫(kù)是很大,那MYSQL是怎么讓我們無(wú)感并快速創(chuàng)建一個(gè)快照呢。
快照多版本你可以認(rèn)為是由以下兩部分構(gòu)成。
事務(wù)id(transaction id):這個(gè)是由事務(wù)啟動(dòng)的時(shí)候向InnoDB啟動(dòng)時(shí)申請(qǐng)的。并且一定注意哦它是遞增的。 row trx_id:這個(gè)id其實(shí)就是事務(wù)ID,每次事務(wù)更新數(shù)據(jù)的時(shí)候回將事務(wù)ID賦值給這個(gè)數(shù)據(jù)版本的事務(wù)ID上,將這個(gè)數(shù)據(jù)版本的事務(wù)ID稱為 row trx_id.
當(dāng)一行記錄存在多個(gè)數(shù)據(jù)版本的時(shí)候,那么就有多個(gè)row trx_id 。舉個(gè)例子
| 版本 | 值 | 事務(wù)ID | 對(duì)應(yīng)的語(yǔ)句操作 |
|---|---|---|---|
| v1 | score =3 | 89 | -- |
| v2 | score =5 | 90 | update scores set score = 5 where id =3; select score from scores where id =3; |
| v3 | score = 6 | 91 | update scores set score = 6 where id =3; |
v1->v2->v3 這里面涉及了三個(gè)版本的迭代。中間是通過(guò)undo log 日志來(lái)保存更新的記錄的。
注意啟動(dòng)快照之后,可重復(fù)讀隔離情況下,獲取到v1的值,不是說(shuō)MYSQL直接存儲(chǔ)的該值,而是利用現(xiàn)在這條記錄的最新版本與undo log日志計(jì)算出來(lái)的,比如通過(guò)v3 ->v2—>v1 計(jì)算出v1中score值。

版本計(jì)算
上面簡(jiǎn)單說(shuō)了下版本的計(jì)算規(guī)則,但是在MYSQL中,版本并不是那么簡(jiǎn)單的計(jì)算的,我們現(xiàn)在來(lái)看下到底怎么計(jì)算的,
這個(gè)兩點(diǎn)我們?cè)谧⒁庖幌拢?/p>
事務(wù)在啟動(dòng)的時(shí)候會(huì)向InnoDB的事務(wù)系統(tǒng)申請(qǐng)事務(wù)ID,這個(gè)事務(wù)ID是嚴(yán)格遞增的。 每行數(shù)據(jù)是多個(gè)版本,這個(gè)版本的id就是row trx_id,而事務(wù)「更新數(shù)據(jù)」(更新數(shù)據(jù)的時(shí)候才會(huì)生成一個(gè)新的版本)的時(shí)候會(huì)生成一個(gè)新的數(shù)據(jù)版本,并把事務(wù)ID賦值給這個(gè)數(shù)據(jù)的事務(wù)ID==row trx_id,
事務(wù)啟動(dòng)的時(shí)候,能看到所有已經(jīng)提交事務(wù)的結(jié)果,但是他啟動(dòng)之后,其他事務(wù)的變更是看不到的。
當(dāng)事務(wù)啟動(dòng)的瞬間,除了已經(jīng)提交的事務(wù),創(chuàng)建的瞬間還會(huì)存在正在運(yùn)行的事務(wù),MYSQL是把這些正在運(yùn)行的事務(wù)ID放入到一個(gè)數(shù)組中。「數(shù)組中最小的事務(wù)ID」記為低水位,當(dāng)前系統(tǒng)中「創(chuàng)建過(guò)的事務(wù)ID最大值+1」記為高水位。
?
舉個(gè)簡(jiǎn)單的例子。a. 注意一點(diǎn):獲取事務(wù)ID與創(chuàng)建數(shù)組不是一個(gè)原子操作,所以存在事務(wù)id為8,然后又存在當(dāng)前MYSQL中存在活躍事務(wù)ID為9 10的事務(wù)。
?b. 事務(wù)ID低于低水位那么對(duì)于當(dāng)前事務(wù)肯定是可見(jiàn)的,事務(wù)ID高于高水位的事務(wù)ID值,則對(duì)當(dāng)前事務(wù)不可見(jiàn). c. 事務(wù)ID 位于低水位與高水位之間分為兩種情況。
如果事務(wù)id是在活躍的數(shù)組中表示這個(gè)版本是正在執(zhí)行,但是結(jié)果還沒(méi)有提交,所以這些事務(wù)的變更是不會(huì)讓當(dāng)然事務(wù)看到的。 事務(wù)id如果沒(méi)有在活躍數(shù)組中,代表這個(gè)事務(wù)是已經(jīng)提交了,所以可見(jiàn)。比如現(xiàn)在創(chuàng)建了90,91,92三個(gè)事務(wù),91執(zhí)行的比較快,提交完畢,90和92還沒(méi)有提交.這時(shí)候創(chuàng)建了一個(gè)新的事務(wù)id為93,那么在活躍的數(shù)組中的事務(wù)就是90,92,93,你看91是已經(jīng)提交了,它的事務(wù)還在這個(gè)低水位與高水位之間,但結(jié)果對(duì)于93是可見(jiàn)。
總的上面來(lái)說(shuō)就是你在我創(chuàng)建的時(shí)候事務(wù)結(jié)果已經(jīng)提交,那么是可見(jiàn)的,之后提交那么就是不可見(jiàn)的。
讀取流程
上面簡(jiǎn)單說(shuō)了下老版本視圖中的數(shù)據(jù)是通過(guò)最新的版本與undo log 計(jì)算出來(lái)的,那到底怎么就算的呢?
| 事務(wù)A | 事務(wù)B | 結(jié)果 |
|---|---|---|
| start transaction with consistent snapshot ?事務(wù) id 89 | 開(kāi)啟事務(wù),并創(chuàng)建視圖 | |
| -- | start transaction with consistent snapshot ?事務(wù)id 92 | 開(kāi)啟事務(wù),并創(chuàng)建視圖 |
| select score from scors where id =2 | -- | 事務(wù)A中的值為3.65 |
| -- | update scores set scores = 10 where id =2 | 事務(wù)B修改為10 |
| -- | select score from scores where id =2 | 事務(wù)B顯示為10 |
| select score from scores where id =2 | -- | 事務(wù)A顯示為3.65 |
| commit | --- | --- |
還是看這個(gè)事務(wù)操作。下面是數(shù)據(jù)變動(dòng)的流程。
假設(shè)開(kāi)始之前有兩個(gè)活躍的事務(wù)ID為 78,88. 事務(wù)A啟動(dòng)的時(shí)候會(huì)將78 88,包含它自己放入到活躍數(shù)組中。 事務(wù)A 操作的語(yǔ)句 select score from scors where id =2將其看到的結(jié)果認(rèn)為是v1版本數(shù)據(jù)比如其現(xiàn)在row trx_id(**注意:**row trx_id是數(shù)據(jù)行被更新后事務(wù)id才會(huì)賦值給row trx id上)是86,并且保存好。事務(wù)B啟動(dòng)時(shí),會(huì)發(fā)現(xiàn)在活躍數(shù)組是78,88,89,自己的92. 事務(wù)B 執(zhí)行更新語(yǔ)句語(yǔ)句后,會(huì)生成一個(gè)新的版本V2,數(shù)據(jù)變換就是V1-->V2。記錄中間變化的是「undo log」日志。這樣ID 89存儲(chǔ)的數(shù)據(jù)就變成了歷史數(shù)據(jù)。數(shù)據(jù)版本row trx_id則是92 事務(wù)A 查詢score數(shù)據(jù),就會(huì)通過(guò)先查到現(xiàn)在的V2版本視圖,找到對(duì)應(yīng)的row trx_id = 92,發(fā)現(xiàn)row trx_id 位于高水位上,則拋棄這個(gè)值,通過(guò)V2找到V1,row trx_id為86,而86大于「低水位」,而低于「高水位」89+1.但是由于86沒(méi)有在活躍數(shù)組中,而且屬于已經(jīng)提交的事務(wù),則當(dāng)前事務(wù)是能看到該結(jié)果的,所以事務(wù)A能拿到讀取的值。
你看經(jīng)過(guò)簡(jiǎn)單的幾步,我們就拿到了想要讀取的事務(wù)數(shù)據(jù),所以不論事務(wù)A什么時(shí)候查詢,它拿到的結(jié)果都是跟它讀取的數(shù)據(jù)是一致的。
你看有了MVCC(多版本并發(fā)控制)計(jì)算別的事務(wù)更改了值也不會(huì)影響到當(dāng)前事務(wù)讀取結(jié)果的過(guò)程。
我們經(jīng)常說(shuō)不要寫一個(gè)長(zhǎng)事務(wù),通過(guò)上面的讀取流程可以看到,長(zhǎng)事務(wù)存在時(shí)間長(zhǎng)的話,數(shù)據(jù)版本就會(huì)有很多,那么undo log日志就需要保存好久,這些回滾日志會(huì)占用大量的「內(nèi)存」存儲(chǔ)空間。
當(dāng)沒(méi)有事務(wù)需要讀取該日志與版本數(shù)據(jù)的時(shí)候,這個(gè)日志才可以刪除,從而釋放內(nèi)存空間。
更新流程
| 事務(wù)A | 事務(wù)B | 結(jié)果 |
|---|---|---|
| start transaction with consistent snapshot ?事務(wù) id 89 | 開(kāi)啟事務(wù),并創(chuàng)建視圖 | |
| -- | start transaction with consistent snapshot ?事務(wù)id 92 | 開(kāi)啟事務(wù),并創(chuàng)建視圖 |
| select score from scors where id =2 | -- | 事務(wù)A中的值為3.65 |
| -- | update scores set scores = 10 where id =2 | 事務(wù)B修改為10 |
| -- | select score from scores where id =2 | 事務(wù)B顯示為10 |
| select score from scores where id =2 | -- | 事務(wù)A顯示為3.65 |
| select score from scores where id =2 for update | -- | 會(huì)被鎖住,等待事務(wù)B釋放鎖(間隙鎖) |
| -- | commit | 提交事務(wù)B |
| select score from scores where id =2 for update | -- | 這個(gè)語(yǔ)句可以看到變成了10(利用了當(dāng)前讀) |
| select score from scores where id =2 | -- | 不加 for update 那么結(jié)果還是3.65 |
| commit | --- | --- |
上面說(shuō)了讀取的過(guò)程,其實(shí)在事務(wù)中,我們還有更新流程,更新流程比較簡(jiǎn)單,更新過(guò)程我們需要保證數(shù)據(jù)的一致性,不能說(shuō)別人修改了,我們還看不到,那樣就會(huì)造成數(shù)據(jù)的不一致。
為了保證看到最新的數(shù)據(jù),會(huì)對(duì)更新行的操作加鎖(行鎖),加鎖之后,其他事務(wù)對(duì)行進(jìn)行更新操作,必須等待其他事務(wù)commit之后才能獲取到最新的值,這個(gè)過(guò)程被稱為「當(dāng)前讀」。
想要讀取過(guò)程中獲得最新的值可以使用 上面的語(yǔ)句select score from scores where id =2 for update ,就可以看到當(dāng)前最新值。
總結(jié)
本小節(jié)主要梳理了事務(wù)的隔離級(jí)別,事務(wù)的MVCC多版本并發(fā)控制實(shí)現(xiàn)原理。
事務(wù)在面試中是比較多的一個(gè)點(diǎn),這樣的題目可以多種變換,面試官:說(shuō)說(shuō)MySQL的事務(wù)隔離?提到的三個(gè)問(wèn)題已經(jīng)可以解答了。
你來(lái)嘗試回答下?
下期會(huì)說(shuō)下數(shù)據(jù)庫(kù)中的幻讀,幻讀也是面試中經(jīng)常遇到的問(wèn)題哦。
往期文章一覽

