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

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

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

