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