網(wǎng)站安全如何做百度 營銷推廣多少錢
MVCC是什么?
MVCC,是MultiVersion Concurrency Control的縮寫,翻譯成中文就是多版本并發(fā)控制,多個事務(wù)同時訪問同一數(shù)據(jù)時,調(diào)控每一個事務(wù)獲取到數(shù)據(jù)的具體版本。和數(shù)據(jù)庫鎖一樣,它也是一種并發(fā)控制的解決方案。主要用于解決臟讀、可重復(fù)讀和部分幻讀問題,所以MVCC主要被運(yùn)用于讀提交、可重復(fù)讀這兩種事務(wù)隔離級別下。
MVCC解決并發(fā)讀寫問題主要依賴于UndoLog、隱藏字段和ReadView讀視圖這三個部分。其中最重要的就是ReadView和隱藏字段。
MVCC解決了哪些問題
在讀未提交事務(wù)隔離級別下,直接讀取UndoLog版本鏈中的最新記錄即可。
在讀提交事務(wù)隔離級別下,通過MVCC中每次讀取數(shù)據(jù)的時候生成一次ReadView解決臟讀問題
在可重復(fù)讀事務(wù)隔離級別下,通過MVCC中每次開啟一次事務(wù)的時候生成一次ReadView解決不可重復(fù)讀問題
在可重復(fù)讀事務(wù)隔離級別下,通過MVCC中每次開啟一次事務(wù)的時候生成一次ReadView讀視圖,后續(xù)的查詢都是根據(jù)這個ReadView讀視圖來判斷哪些數(shù)據(jù)是可讀的,即使中途有其他事務(wù)插入了新紀(jì)錄,也是查詢不出來這條數(shù)據(jù)的,所以就很好了避免幻讀問題。
快照讀與當(dāng)前讀
MVCC在MySQL InnoDB中的實現(xiàn)主要是為了提高數(shù)據(jù)庫并發(fā)性能,用更好的方式去處理 讀-寫沖突 ,做到即使有讀寫沖突時,也能做到 不加鎖 ,即非阻塞并發(fā)讀 ,而這個讀指的就是 快照讀 , 而非 當(dāng)前讀 。當(dāng)前讀實際上是一種加鎖的操作,是悲觀鎖的實現(xiàn)。而MVCC本質(zhì)是采用樂觀鎖思想的一種方式。
快照讀
快照讀又叫一致性讀,讀取的是快照數(shù)據(jù)。不加鎖的簡單的 SELECT 都屬于快照讀,即不加鎖的非阻塞 讀;比如這樣:
SELECT * FROM player WHERE ...
之所以出現(xiàn)快照讀的情況,是基于提高并發(fā)性能的考慮,快照讀的實現(xiàn)是基于MVCC,它在很多情況下, 避免了加鎖操作,降低了開銷。
既然是基于多版本,那么快照讀可能讀到的并不一定是數(shù)據(jù)的最新版本,而有可能是之前的歷史版本。
快照讀的前提是隔離級別不是串行級別,串行級別下的快照讀會退化成當(dāng)前讀。
當(dāng)前讀
當(dāng)前讀讀取的是記錄的最新版本(最新數(shù)據(jù),而不是歷史版本的數(shù)據(jù)),讀取時還要保證其他并發(fā)事務(wù) 不能修改當(dāng)前記錄,會對讀取的記錄進(jìn)行加鎖。加鎖的 SELECT,或者對數(shù)據(jù)進(jìn)行增刪改都會進(jìn)行當(dāng)前 讀。比如:
SELECT * FROM student LOCK IN SHARE MODE; # 共享鎖
SELECT * FROM student FOR UPDATE; # 排他鎖
INSERT INTO student values ... # 排他鎖
DELETE FROM student WHERE ... # 排他鎖
UPDATE student SET ... # 排他鎖
Read View 在 MVCC 里如何工作的?
我們需要了解兩個知識:
- Read View 中四個字段作用;
- 聚簇索引記錄中兩個跟事務(wù)有關(guān)的隱藏列;
什么是ReadView?
那 Read View 到底是個什么東西?
Read View 是事務(wù)開始或讀取數(shù)據(jù)時創(chuàng)建的一致性視圖,記錄了當(dāng)前系統(tǒng)中活躍事務(wù)的集合。這個視圖幫助事務(wù)確定哪些版本的數(shù)據(jù)是可見的。通過 Read View,事務(wù)能夠看到在其開始時已經(jīng)提交的事務(wù)產(chǎn)生的數(shù)據(jù)版本,而忽略未提交的事務(wù)的數(shù)據(jù)版本。
Read View 有四個重要的字段:
- m_ids :當(dāng)前活躍的事務(wù)編號列表。指的是在創(chuàng)建 Read View 時,當(dāng)前數(shù)據(jù)庫中「活躍事務(wù)」的事務(wù)id編號列表,注意是一個列表,“活躍事務(wù)”指的就是,啟動了但還沒提交的事務(wù)。
- min_trx_id :最小活躍事務(wù)編號。指的是在創(chuàng)建 Read View 時,當(dāng)前數(shù)據(jù)庫中「活躍事務(wù)」中事務(wù) id編號最小的事務(wù),也就是 m_ids 的最小值。
- max_trx_id :預(yù)分配事務(wù)編號。這個并不是 m_ids 的最大值,而是創(chuàng)建 Read View 時當(dāng)前數(shù)據(jù)庫中應(yīng)該給下一個事務(wù)的 id 值,也就是全局事務(wù)中最大的事務(wù) id 值加1;
- creator_trx_id :ReadView 創(chuàng)建者的事務(wù)編號。指的是創(chuàng)建該 Read View 的事務(wù)的事務(wù) id。
ReadView結(jié)構(gòu)如下圖所示:
知道了 Read View 的字段,我們還需要了解聚簇索引記錄中的兩個隱藏列。
隱藏列和Undo Log版本鏈
假設(shè)在賬戶余額表插入一條小林余額為 100 萬的記錄,然后我把這兩個隱藏列也畫出來,該記錄的整個示意圖如下:
對于使用 InnoDB 存儲引擎的數(shù)據(jù)庫表,它的聚簇索引記錄中都包含下面兩個隱藏列:
- trx_id,當(dāng)一個事務(wù)對某條聚簇索引記錄進(jìn)行改動時,就會把該事務(wù)的事務(wù) id賦值給trx_id 隱藏列;
- roll_pointer,每次對某條聚簇索引記錄進(jìn)行改動時,都會把舊版本的記錄寫入到 undo 日志中,然后這個隱藏列是個指針,指向每一個舊版本記錄,于是就可以通過它找到修改前的記錄。
創(chuàng)建student表,表結(jié)構(gòu)如下:
假設(shè)此時插入一條新記錄,該記錄的事務(wù)id為8,那么此刻該條記錄的示意圖如下所示:
注意:insert undo只在事務(wù)回滾時起作用,當(dāng)事務(wù)提交后,該類型的undo日志就沒用了,它占用的Undo Log Segment也會被系統(tǒng)回收(也就是該undo日志占用的Undo頁面鏈表要么被重用,要么被釋放)。
假設(shè)之后兩個事務(wù)id分別為 10 、 20 的事務(wù)對這條記錄進(jìn)行UPDATE 更新操作,操作流程如下圖:
每次對記錄進(jìn)行改動,都會記錄一條undo日志,每條undo日志也都有一個 roll_pointer 屬性 ( INSERT 操作對應(yīng)的undo日志沒有該屬性,因為該記錄并沒有更早的版本),可以將這些 undo日志 都連起來,串成一個鏈表:
對該記錄每次更新后,都會將舊值放到一條 undo日志 中,就算是該記錄的一個舊版本,隨著更新次數(shù) 的增多,所有的版本都會被 roll_pointer 屬性連接成一個鏈表,我們把這個鏈表稱之為 版本鏈 ,版本鏈的頭節(jié)點(diǎn)就是當(dāng)前記錄最新的值。
每個版本中還包含生成該版本時對應(yīng)的事務(wù)id。
判斷規(guī)則
在創(chuàng)建 Read View 后,我們可以將記錄中的 trx_id 劃分這三種情況:
一個事務(wù)去訪問記錄的時候,除了自己的更新記錄總是可見之外,還有這幾種判斷規(guī)則:
- 如果被訪問表行記錄的 trx_id 值與ReadView中的 creator_trx_id 值相同,意味著當(dāng)前事務(wù)在訪問它自己修改過的記錄,所以該版本可以被當(dāng)前事務(wù)訪問,該版本的記錄對當(dāng)前事務(wù)可見。
- 如果被訪問表行記錄的 trx_id 值小于 Read View 中的 min_trx_id 值,表示這個版本的記錄是在創(chuàng)建 Read View 前已經(jīng)提交的事務(wù)生成的,所以該版本的記錄對當(dāng)前事務(wù)可見。
- 如果被訪問表行記錄的 trx_id 值大于等于 Read View 中的 max_trx_id 值,表示這個版本的記錄是在創(chuàng)建 Read View 后才啟動的事務(wù)生成的,所以該版本的記錄對當(dāng)前事務(wù)不可見。
- 如果被訪問表行記錄的 trx_id 值在 Read View 的 min_trx_id 和 max_trx_id 之間,需要判斷 trx_id 是否在 m_ids 列表中:
-
- 如果表行記錄的 trx_id 在 m_ids 列表中,表示生成該版本記錄的活躍事務(wù)依然活躍著(還沒提交事務(wù)),所以該版本的記錄對當(dāng)前事務(wù)不可見。
- 如果表行記錄的 trx_id 不在 m_ids列表中,表示生成該版本記錄的活躍事務(wù)已經(jīng)被提交,所以該版本的記錄對當(dāng)前事務(wù)可見。
判斷方法
判斷方法是先根據(jù) Read View 中的 4 個重要字段,先去 Undo Log 中最新的數(shù)據(jù)行進(jìn)行比對,如果滿足下面 Read View 的判斷規(guī)則中的可見規(guī)則,則返回當(dāng)前行的數(shù)據(jù),如果不滿足則繼續(xù)從Undo Log 鏈中查找當(dāng)前行的數(shù)據(jù)的下一個歷史版本數(shù)據(jù),直到找到滿足的條件的數(shù)據(jù)為止,如果查詢完Undolog版本鏈,發(fā)現(xiàn)沒有滿足條件的數(shù)據(jù),則返回 NULL。
可重復(fù)讀是如何工作的?
可重復(fù)讀隔離級別是啟動事務(wù)時生成一個 Read View,然后整個事務(wù)期間都在用這個 Read View。
假設(shè)事務(wù) A (事務(wù) id 為51)啟動后,緊接著事務(wù) B (事務(wù) id 為52)也啟動了,那這兩個事務(wù)創(chuàng)建的 Read View 如下:
事務(wù) A 和 事務(wù) B 的 Read View 具體內(nèi)容如下:
- 在事務(wù) A 的 Read View 中,它的事務(wù) id 是 51,由于它是第一個啟動的事務(wù),所以此時活躍事務(wù)的事務(wù) id 列表就只有 51,活躍事務(wù)的事務(wù) id 列表中最小的事務(wù) id 是事務(wù) A 本身,下一個事務(wù) id 則是 52。
- 在事務(wù) B 的 Read View 中,它的事務(wù) id 是 52,由于事務(wù) A 是活躍的,所以此時活躍事務(wù)的事務(wù) id 列表是 51 和 52,活躍的事務(wù) id 中最小的事務(wù) id 是事務(wù) A,下一個事務(wù) id 應(yīng)該是 53。
接著,在可重復(fù)讀隔離級別下,事務(wù) A 和事務(wù) B 按順序執(zhí)行了以下操作:
- 事務(wù) B 讀取小林的賬戶余額記錄,讀到余額是 100 萬;
- 事務(wù) A 將小林的賬戶余額記錄修改成 200 萬,并沒有提交事務(wù);
- 事務(wù) B 讀取小林的賬戶余額記錄,讀到余額還是 100 萬;
- 事務(wù) A 提交事務(wù);
- 事務(wù) B 讀取小林的賬戶余額記錄,讀到余額依然還是 100 萬;
接下來,跟大家具體分析下。
事務(wù) B 第一次讀小林的賬戶余額記錄,在找到記錄后,它會先看這條記錄的 trx_id,此時發(fā)現(xiàn) trx_id 為 50,比事務(wù) B 的 Read View 中的 min_trx_id 值(51)還小,這意味著修改這條記錄的事務(wù)早就在事務(wù) B 啟動前提交過了,所以該版本的記錄對事務(wù) B 可見的,也就是事務(wù) B 可以獲取到這條記錄。
接著,事務(wù) A 通過 update 語句將這條記錄修改了(還未提交事務(wù)),將小林的余額改成 200 萬,這時 MySQL 會記錄相應(yīng)的 undo log,并以鏈表的方式串聯(lián)起來,形成版本鏈,如下圖:
你可以在上圖的「記錄的字段」看到,由于事務(wù) A 修改了該記錄,以前的記錄就變成舊版本記錄了,于是最新記錄和舊版本記錄通過鏈表的方式串起來,而且最新記錄的 trx_id 是事務(wù) A 的事務(wù) id(trx_id = 51)。
然后事務(wù) B 第二次去讀取該記錄,發(fā)現(xiàn)這條記錄的 trx_id 值為 51,在事務(wù) B 的 Read View 的 min_trx_id 和 max_trx_id 之間,則需要判斷 trx_id 值是否在 m_ids 范圍內(nèi),判斷的結(jié)果是在的,那么說明這條記錄是被還未提交的事務(wù)修改的,這時事務(wù) B 并不會讀取這個版本的記錄。而是沿著 undo log 鏈條往下找舊版本的記錄,直到找到 trx_id 「小于」事務(wù) B 的 Read View 中的 min_trx_id 值的第一條記錄,所以事務(wù) B 能讀取到的是 trx_id 為 50 的記錄,也就是小林余額是 100 萬的這條記錄。
最后,當(dāng)事物 A 提交事務(wù)后,由于隔離級別時「可重復(fù)讀」,所以事務(wù) B 再次讀取記錄時,還是基于啟動事務(wù)時創(chuàng)建的 Read View 來判斷當(dāng)前版本的記錄是否可見。所以,即使事物 A 將小林余額修改為 200 萬并提交了事務(wù), 事務(wù) B 第三次讀取記錄時,讀到的記錄都是小林余額是 100 萬的這條記錄。
就是通過這樣的方式實現(xiàn)了,「可重復(fù)讀」隔離級別下在事務(wù)期間讀到的記錄都是事務(wù)啟動前的記錄。
讀提交是如何工作的?
讀提交隔離級別是在每次讀取數(shù)據(jù)時,都會生成一個新的 Read View。
也意味著,事務(wù)期間的多次讀取同一條數(shù)據(jù),前后兩次讀的數(shù)據(jù)可能會出現(xiàn)不一致,因為可能這期間另外一個事務(wù)修改了該記錄,并提交了事務(wù)。
那讀提交隔離級別是怎么工作呢?我們還是以前面的例子來聊聊。
假設(shè)事務(wù) A (事務(wù) id 為51)啟動后,緊接著事務(wù) B (事務(wù) id 為52)也啟動了,接著按順序執(zhí)行了以下操作:
- 事務(wù) B 讀取數(shù)據(jù)(創(chuàng)建 Read View),小林的賬戶余額為 100 萬;
- 事務(wù) A 修改數(shù)據(jù)(還沒提交事務(wù)),將小林的賬戶余額從 100 萬修改成了 200 萬;
- 事務(wù) B 讀取數(shù)據(jù)(創(chuàng)建 Read View),小林的賬戶余額為 100 萬;
- 事務(wù) A 提交事務(wù);
- 事務(wù) B 讀取數(shù)據(jù)(創(chuàng)建 Read View),小林的賬戶余額為 200 萬;
那具體怎么做到的呢?我們重點(diǎn)看事務(wù) B 每次讀取數(shù)據(jù)時創(chuàng)建的 Read View。前兩次 事務(wù) B 讀取數(shù)據(jù)時創(chuàng)建的 Read View 如下圖:
我們來分析下為什么事務(wù) B 第二次讀數(shù)據(jù)時,讀不到事務(wù) A (還未提交事務(wù))修改的數(shù)據(jù)?
事務(wù) B 在找到小林這條記錄時,會看這條記錄的 trx_id 是 51,在事務(wù) B 的 Read View 的 min_trx_id 和 max_trx_id 之間,接下來需要判斷 trx_id 值是否在 m_ids 范圍內(nèi),判斷的結(jié)果是在的,那么說明這條記錄是被還未提交的事務(wù)修改的,這時事務(wù) B 并不會讀取這個版本的記錄。而是,沿著 undo log 鏈條往下找舊版本的記錄,直到找到 trx_id 「小于」事務(wù) B 的 Read View 中的 min_trx_id 值的第一條記錄,所以事務(wù) B 能讀取到的是 trx_id 為 50 的記錄,也就是小林余額是 100 萬的這條記錄。
我們來分析下為什么事務(wù) A 提交后,事務(wù) B 就可以讀到事務(wù) A 修改的數(shù)據(jù)?
在事務(wù) A 提交后,由于隔離級別是「讀提交」,所以事務(wù) B 在每次讀數(shù)據(jù)的時候,會重新創(chuàng)建 Read View,此時事務(wù) B 第三次讀取數(shù)據(jù)時創(chuàng)建的 Read View 如下:
事務(wù) B 在找到小林這條記錄時,會發(fā)現(xiàn)這條記錄的 trx_id 是 51,比事務(wù) B 的 Read View 中的 min_trx_id 值(52)還小,這意味著修改這條記錄的事務(wù)早就在創(chuàng)建 Read View 前提交過了,所以該版本的記錄對事務(wù) B 是可見的。
正是因為在讀提交隔離級別下,事務(wù)每次讀數(shù)據(jù)時都重新創(chuàng)建 Read View,那么在事務(wù)期間的多次讀取同一條數(shù)據(jù),前后兩次讀的數(shù)據(jù)可能會出現(xiàn)不一致,因為可能這期間另外一個事務(wù)修改了該記錄,并提交了事務(wù)。
可重復(fù)讀事務(wù)隔離級別下使用MVCC可能發(fā)生幻讀現(xiàn)象的場景有哪些?
1. 快照讀和當(dāng)前讀穿插的場景下使用MVCC依舊會發(fā)生幻讀
當(dāng)?shù)谝粋€事務(wù)如果先使用快照讀來讀取范圍數(shù)據(jù),讀取期間第二個事務(wù)對這個范圍進(jìn)行插入數(shù)據(jù)的操作,然后第一個事務(wù)又使用當(dāng)前讀來讀取這個范圍的數(shù)據(jù),此時就會出現(xiàn)幻讀問題。
例如如下場景:
- T1 時刻:事務(wù) A 先執(zhí)行「快照讀語句」:select * from t_test where id > 100 得到了 3 條記錄。
- T2 時刻:事務(wù) B 往插入一個 id= 200 的記錄并提交;
- T3 時刻:事務(wù) A 再執(zhí)行「當(dāng)前讀語句」 select * from t_test where id > 100 for update 就會得到 4 條記錄,此時也發(fā)生了幻讀現(xiàn)象。
要避免這類特殊場景下發(fā)生幻讀的現(xiàn)象的話,就是盡量在開啟事務(wù)之后,馬上執(zhí)行 select ... for update 這類當(dāng)前讀的語句,因為它會對記錄加 next-key lock,從而避免其他事務(wù)插入一條新記錄。