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