面試官問我MVCC,我笑了
面試官:平時用的數(shù)據(jù)庫有哪些呢
表妹:親愛
mysql的默認(rèn)存儲引擎是innodb,該引擎是默認(rèn)支持事務(wù)以及事務(wù)的回滾
事務(wù)就是通過各種讀寫鎖來實現(xiàn)的,那么讀寫鎖就涉及到讀鎖和寫鎖之間的沖突
而innodb為了提高讀取的效率,增加了MVCC多版本并發(fā)控制來更高效率的支持mysql中的讀取
事務(wù)
SQL語言共分為四大類
數(shù)據(jù)查詢語言DQL,數(shù)據(jù)操縱語言DML,數(shù)據(jù)定義語言DDL,數(shù)據(jù)控制語言DCL。
1. 數(shù)據(jù)查詢語言DQL:數(shù)據(jù)查詢語言DQL基本結(jié)構(gòu)是由SELECT子句,F(xiàn)ROM子句,WHERE
2 .數(shù)據(jù)操縱語言DML:數(shù)據(jù)操縱語言DML主要有三種形式,插入,更新,刪除。
3. 數(shù)據(jù)定義語言DDL:數(shù)據(jù)定義語言DDL用來創(chuàng)建數(shù)據(jù)庫中的各種對象如:表 視圖 索引 同義詞 簇。DDL操作是隱性提交的,不能rollback
4. 數(shù)據(jù)控制語言DCL:數(shù)據(jù)控制語言DCL用來授予或回收訪問數(shù)據(jù)庫的某種特權(quán),并控制數(shù)據(jù)庫操縱事務(wù)發(fā)生的時間及效果,對數(shù)據(jù)庫實行監(jiān)視等。
事務(wù)
事務(wù)指的是一組SQL語句,要么全部執(zhí)行成功,要么全部執(zhí)行失敗,要么提交,要么回滾,這句話大家聽得耳朵都長繭子了吧
事務(wù)特性ACID
原子性:事務(wù)是最小單元,不可再分,要么全部執(zhí)行成功,要么全部失敗回滾。
一致性:一致性是指事務(wù)必須使數(shù)據(jù)庫從一個一致的狀態(tài)變到另外一個一致的狀態(tài),也就是執(zhí)行事務(wù)之前和之后的狀態(tài)都必須處于一致的狀態(tài)。不一致性包含三點:臟讀,不可重復(fù)讀,幻讀
隔離性:隔離性是指當(dāng)多個用戶并發(fā)訪問數(shù)據(jù)庫時,比如操作同一張表時,數(shù)據(jù)庫為每一個用戶開啟的事務(wù),不能被其他事務(wù)的操作所干擾,多個并發(fā)事務(wù)之間要相互隔離
持久性:一旦事務(wù)提交,則其所做的修改將會永遠(yuǎn)保存到數(shù)據(jù)庫中。即使系統(tǒng)發(fā)生崩潰,事務(wù)執(zhí)行的結(jié)果也不能丟。
事務(wù)隔離級別
未提交讀:即能夠讀取到?jīng)]有被提交的數(shù)據(jù),所以很明顯這個級別的隔離機(jī)制無法解決臟讀、不可重復(fù)讀、幻讀中的任何一種。
已提交讀:即能夠讀到那些已經(jīng)提交的數(shù)據(jù),自然能夠防止臟讀,但是無法限制不可重復(fù)讀和幻讀
可重復(fù)讀:讀取了一條數(shù)據(jù),這個事務(wù)不結(jié)束,別的事務(wù)就不可以改這條記錄,這樣就解決了臟讀、不可重復(fù)讀的問題,
串行化:多個事務(wù)時,只有運行完一個事務(wù)之后,才能運行其他事務(wù)。
隔離級別問題詳解
臟讀:一個事務(wù)處理過程里讀取了另一個未提交的事務(wù)中的數(shù)據(jù)
不可重復(fù)讀:一個事務(wù)在它運行期間,兩次查找相同的表,出現(xiàn)了不同的數(shù)據(jù)
幻讀:在一個事務(wù)中讀取到了別的事務(wù)插入的數(shù)據(jù),導(dǎo)致前后不一致
和不可重復(fù)讀的區(qū)別,這里是新增,不可重復(fù)讀是更改(或刪除)。
這兩種情況對策是不一樣的,對于不可重復(fù)讀,只需要采取行級鎖防止該記錄數(shù)據(jù)被更改或刪除,然而對于幻讀必須加表級鎖,防止在這個表中新增一條數(shù)據(jù)。
再議鎖和事務(wù)問題
相信大家讀到這里,應(yīng)該也大致對鎖和事務(wù)的關(guān)系有了更進(jìn)一步的理解了吧,不清楚鎖的同學(xué)趕緊去mysql鎖的那一篇看看
來,給大家捋一捋
共享鎖,也就是讀鎖,對一行數(shù)據(jù)加上共享鎖之后,別的事務(wù)就無法獲得該行數(shù)據(jù)的排他鎖了,別的事務(wù)也就暫時無法對這個數(shù)據(jù)進(jìn)行修改操作了,也就避免了不可重復(fù)讀這個問題
排他鎖,也就是寫鎖,一個事務(wù)對數(shù)據(jù)進(jìn)行修改的時候,就獲得相應(yīng)數(shù)據(jù)的寫鎖,這時候別的事務(wù)也就無法獲得該數(shù)據(jù)的讀鎖和寫鎖了,也就避免了臟讀問題
臨鍵鎖的主要目的,也是為了避免幻讀(Phantom Read)。如果把事務(wù)的隔離級別降級為RC,臨鍵鎖則也會失效。
MVCC多版本并發(fā)控制
什么是MVCC
全稱Multi-Version Concurrency Control,多版本并發(fā)控制,屬于一種并發(fā)控制的手段,一般在數(shù)據(jù)庫管理系統(tǒng)中,實現(xiàn)對數(shù)據(jù)庫的并發(fā)訪問
數(shù)據(jù)庫就必然涉及到讀和寫的存在,讀寫就必然涉及到讀寫沖突,MVCC在mysql中的innodb引擎實現(xiàn)就是為了更好的解決讀寫沖突,提高數(shù)據(jù)庫的性能,做到即使有讀寫沖突的時候,也可以不用加鎖的方式,非阻塞方式來實現(xiàn)并發(fā)讀
最早的數(shù)據(jù)庫系統(tǒng),只有讀讀之間可以并發(fā),讀寫,寫讀,寫寫都要阻塞。引入多版本之后,只有寫寫之間相互阻塞,其他三種操作都可以并行,這樣大幅度提高了InnoDB的并發(fā)度
MVCC只在 READ COMMITTED 和 REPEATABLE READ 兩個隔離級別下工作。其他兩個隔離級別夠和MVCC不兼容, 因為READ UNCOMMITTED 總是讀取最新的數(shù)據(jù)行, 而不是符合當(dāng)前事務(wù)版本的數(shù)據(jù)行。而SERIALIZABLE 則會對所有讀取的行都加鎖
MVCC屬于一種悲觀鎖的實現(xiàn)
當(dāng)前讀和快照讀
當(dāng)前讀:像select lock in share mode這是共享鎖,select for update , update , insert , delete都是屬于排他鎖,上面說的采用共享鎖和排他鎖的這種方式,都是屬于當(dāng)前讀,當(dāng)前讀就是讀取的記錄的最新版本,讀取的時候還會保證其他并發(fā)事務(wù)不會修改當(dāng)前的記錄,會對當(dāng)前的記錄進(jìn)行加鎖,防止修改
快照讀:不加鎖的正常的select查詢都是屬于快照讀,也就是不加鎖的非阻塞讀。
當(dāng)然,快照讀的前提是隔離級別不是串行級別,此時便會退化成當(dāng)前讀,之所以出現(xiàn)快照讀的情況,是mysql中的innodb引擎基于提高并發(fā)性能的考慮,快照讀也就是基本多版本的并發(fā)控制,來更高效的解決讀和寫之間的沖突問題
根據(jù)業(yè)務(wù)場景來考慮可以接受的問題,避免了加鎖的操作,降低了開銷,既然是多版本并發(fā)控制,那么就要接受讀取到的并不一定是最新版本的歷史數(shù)據(jù)這一場景
實現(xiàn)
MVCC只是一個抽象概念,innodb實現(xiàn)這個靠的是三個隱式字段、undo log日志、Read View來實現(xiàn)的
三個隱式字段
數(shù)據(jù)庫在每行記錄中除了記錄我們自定義的那些字段之外,還有數(shù)據(jù)庫的隱藏的定義字段,DB_TRX_ID、DB_ROLL_PTR、DB_ROW_ID
DB_TRX_ID:最近修改事務(wù)ID,也會記錄創(chuàng)建這條記錄和最后一次修改這個記錄的事務(wù)ID
DB_ROLL_PTR:回滾指針,指向這條記錄的上一個版本,存儲在undo log日志中的Rollback segment回滾段中
DB_ROW_ID:這個不是一定有,如果表沒有創(chuàng)建主鍵,innodb會自動以這列為主鍵,以這一列來創(chuàng)建B+樹,產(chǎn)生一個聚簇索引,也就是創(chuàng)建的其余索引的B+樹的葉子節(jié)點存儲的是這個主鍵
實際還有一個刪除 flag 隱藏字段, 既記錄被更新或刪除并不代表真的刪除,而是刪除flag 變了
再說undo log日志
Undo log日志分為兩種insert undo log和update undo log
Insert undo log:這種是事務(wù)在insert新數(shù)據(jù)的時候產(chǎn)生的日志,只有在事務(wù)回滾的時候需要,所以在事務(wù)commit之后可以立即丟棄該日志
Update undo log:這個是在進(jìn)行update或者delete而產(chǎn)生的日志,這個不僅是事務(wù)回滾的時候需要,在快照讀的時候也是需要的,也就是innodb的MVCC機(jī)制會用到歷史的數(shù)據(jù),所以不能隨便刪除,需要等快照讀和事務(wù)回滾都不涉及到該日志的時候,這個日志才會被相應(yīng)的線程統(tǒng)一清楚
Read View
這哥們的作用可以理解為生成的一個鏡像數(shù)據(jù),記錄當(dāng)時的情況
事務(wù)快照是用來存儲數(shù)據(jù)庫的事務(wù)運行情況。一個事務(wù)快照ReadView的創(chuàng)建過程可以概括為:
m_ids:一個數(shù)值列表,用于維護(hù) Read View 生成時刻系統(tǒng)正活躍的事務(wù)ID列表
up_limit_id:是m_ids活躍事務(wù)ID中的最小的事務(wù)ID
low_limit_id:ReadView 生成時刻系統(tǒng)尚未分配的下一個事務(wù)ID ,也就是目前已出現(xiàn)過的事務(wù)ID 的最大值 + 1
可見性比較算法
當(dāng)事務(wù)執(zhí)行快照讀的時候,對該記錄創(chuàng)建一個Read View讀視圖,用于記錄此時的情景,把它比做條件用來判斷當(dāng)前事務(wù)可以看到哪個版本的數(shù)據(jù),到底是看到最新版本,還是看到指向undo log日志中的歷史版本呢
我們來一起看可見性算法,來決定該版本是否可見
此圖來源于知乎,侵刪
https://www.zhihu.com/question/66320138/answer/241418502
算法的流程
1. 當(dāng)行記錄的事務(wù)ID小于當(dāng)前系統(tǒng)的最小活動id,就是可見的。
- if (trx_id < view->up_limit_id) {
- return(TRUE);
- }
2. 當(dāng)行記錄的事務(wù)ID大于當(dāng)前系統(tǒng)的最大活動id,就是不可見的。
- if (trx_id >= view->low_limit_id) {
- return(FALSE);
- }
3. 當(dāng)行記錄的事務(wù)ID在活動范圍之中時,判斷是否在活動鏈表中,如果在就不可見,如果不在就是可見的。
這里我也別用那些官方語言給大家解釋了,我就舉個簡單的例子給大家解釋
滴滴滴,跟上思路,加油,就快結(jié)束了
M_ids:一個數(shù)值列表,用于維護(hù) Read View 生成時刻系統(tǒng)正活躍的事務(wù)ID列表
up_limit_id:是m_ids活躍事務(wù)ID中的最小的事務(wù)ID
low_limit_id:ReadView 生成時刻系統(tǒng)尚未分配的下一個事務(wù)ID ,也就是目前已出現(xiàn)過的事務(wù)ID 的最大值 + 1
插入一個記錄,事務(wù)ID是10,此時版本鏈?zhǔn)?0
執(zhí)行一個update操作,事務(wù)ID是20,此時版本鏈?zhǔn)?0-10,commit
執(zhí)行一個update操作,事務(wù)ID是30,此時版本連是30-20-10,未Commit
執(zhí)行select,事務(wù)ID是40,生成一個ReadView,這是一個鏡像,此時可能已經(jīng)有更多事務(wù)操作這條數(shù)據(jù)了,活躍列表是m_ids是[30],最小事務(wù)up_limit_id也是30,最大事務(wù)low_limit_id是41
比較過程
按照這個ReadView的事務(wù)鏈30-20-10進(jìn)行上述算法的比較,30不合適,因為在活躍事務(wù)中,20滿足條件,所以此時事務(wù)ID為40的讀取的就是ID為20更新的數(shù)據(jù)
事務(wù)ID30Commit,事務(wù)ID50執(zhí)行update,鏈變成了50-30-20-10,未提交
關(guān)鍵
此時事務(wù)ID為40的再次執(zhí)行了select操作,查詢了該記錄
如果事務(wù)隔離級別是已提交讀隔離級別,這時候會重新生成一個新的ReadView,那此時ReadView已經(jīng)變了,活躍列表m_ids是[50],最小事務(wù)up_limit_id也是50,最大事務(wù)low_limit_id是51
于是按照上述比較,30便符合條件了,所以此時讀出來的版本就是事務(wù)ID30的update數(shù)據(jù)了
如果事務(wù)隔離級別是可重復(fù)讀,此時不會生成新的ReadView,用的還是開始時候生成的,所以還是20符合條件
兩種隔離級別
我們上面說了MVCC只在READ COMMITTED 和REPEATABLE READ 兩個隔離級別下工作,已提交讀和可重復(fù)讀的區(qū)別在于他們生成ReadView的策略不同
也就是說已提交讀隔離級別下的事務(wù)在每次查詢的開始都會生成一個獨立的ReadView,而可重復(fù)讀隔離級別則在第一次讀的時候生成一個ReadView,之后的讀都復(fù)用之前的ReadView
我們根據(jù)名字也可以推斷,可重復(fù)讀,如果每次讀取的時候生成新的ReadView了,那符合條件的版本很可能就不一樣了,所以查出來的也就不一樣了,就不符合條件了,于是用的就是同一個ReadView

































