前言
最近在學(xué)MySQL,決定記錄一下,能寫多少寫多少,不定時更新,加油。
正文
分幾個部分來吧,大致如下:
字符集與比較規(guī)則
行格式與數(shù)據(jù)頁
InnoDB索引訪問方法與連接
explain 與 子查詢優(yōu)化
redo與undo日志MVCC與 鎖
本文為第四部分
MVCC原理解析
主要就是想先寫這個, 其他的后面有空再補~
由于我們跳過一些東西, 下面說MVCC的原理可能會各種懵

所以, 我們先簡單過幾個點吧.
一、溫故知新 -- 兩個隱藏列的含義
行格式的時候說過,InnoDB會為每行加上兩個隱藏列(row_id并不是必加的)trx_id 和 roll_pointer 這倆哥們簡直生猛的一塌糊涂.
小聲明:
事務(wù)T1的編號是100
事務(wù)T2的編號是200
-
trx_id這個明顯是事務(wù)ID的意思, 就是記錄一下最近操作此條記錄的事務(wù)ID. 比如事務(wù)T1中插入了一條記錄X,那X的trx_id就是100,如果在T1中繼續(xù)操作這條記錄是不會修改X的trx_id的.如果事務(wù)T2修改了這X記錄,那么其trx_id就變成200. -
roll_pointer字面意思就是回滾指針, 指針存放的是一個地址, 指向了某個值. 它指向的是一條undo日志記錄.
這里簡單提一下undo日志,后面會專門寫一篇詳細講的[有生之年系列]。
一條undo日志就是一條記錄, 存放在頁中,叫undo日志頁.大致分為兩種,插入類型(insert)和修改類型(update、delete),這里只需要知道他們有一個很大區(qū)別:插入類型的undo日志是沒有指向下一條undo日志的屬性的,也就是說他們組成了一個undo日志鏈表,又稱版本鏈。

- 第一條是真實記錄,0是記錄類型,H是記錄頭,300是
trx_idR是roll_pointer, 1、3.. 是記錄的真實數(shù)據(jù) - 可以看到每條undo日志都有一個事務(wù)ID(圖中我畫在最后面,實際并不是存在最后),這個屬性其實是叫
old trx_id, 表示當(dāng)前undo日志對應(yīng)的事務(wù)ID,也稱此版本的創(chuàng)建事務(wù)ID - 插入類型undo日志沒有
old roll_pointer指向上一條roll_pointer,因為它本來就是版本鏈的第一條 - 這條undo鏈表可以看出,此記錄由事務(wù)ID為100的事務(wù)插入,在事務(wù)ID為200的事務(wù)中修改了兩次,而事務(wù)ID為300的事務(wù)正在修改此記錄。
- 記住這個順序,后面有用。
二、老生常談 -- 隔離級別
說這個隔離級別之前,我們先想想為什么要有這個東東?
其實每個新技術(shù)或新名詞的出現(xiàn), 都可以問這幾個問題
1.這個東東解決了什么問題嗎?
2.現(xiàn)有的技術(shù)解決不了嗎?
3.如果能, 它比當(dāng)前的解決方案強在哪些方面呢?
4.不足或改進之處.
- 以上純屬扯淡
隨著互聯(lián)網(wǎng)的發(fā)展,并發(fā)已經(jīng)是一道繞不開的坎。各種問題都不斷冒出來,那并發(fā)事務(wù)訪問數(shù)據(jù)庫會發(fā)生什么樣的問題呢?
一個一個來看下。
- 臟寫(
Dirty Write)
T1事務(wù)開啟
T1修改:X=5
T2事務(wù)開始
T2修改:X=6
T2事務(wù)提交
T1事務(wù)提交
結(jié)果:X=5
這個時候T2的事務(wù)所做修改就丟失了.
一個事務(wù)修改了另一個未提交事務(wù)修改過的數(shù)據(jù),此為臟寫。
- 臟讀(
Dirty Read)
數(shù)據(jù)狀態(tài):X=5
T1事務(wù)開啟
T2事務(wù)開始
T2修改:X=6
T1讀?。篨=6
T2事務(wù)回滾
T1事務(wù)提交
T1讀到的X=6,庫中X=5
一個事務(wù)讀取了另一個未提交事務(wù)修改過的數(shù)據(jù),此為臟讀。
- 不可重復(fù)讀(
Non-Repeatable Read)
事務(wù)T1開啟
事務(wù)T2開啟
T1讀?。篨=5
T2修改:X=6
事務(wù)T2提交
T1讀?。篨=6
。。。
T1兩次讀取到的同一條記錄的值不一樣。
每次事務(wù)提交后,當(dāng)前事務(wù)都能讀取到記錄的最新值,此為不可重復(fù)讀。
- 幻讀(
Phantom)
庫中數(shù)據(jù): X=6
事務(wù)T1開啟
事務(wù)T2開啟
T1讀?。篨>5 (得到一條X=6)
T2新增:X=7
T1讀?。篨>5 (得到兩條X=6和X=7)
。。。
T1第二次讀取的記錄數(shù)量比第一次讀取到的記錄數(shù)量多。
如果事務(wù)T1根據(jù)條件N查詢數(shù)據(jù),事務(wù)T2添加了滿足條件N的記錄并提交了,T1再次根據(jù)N查詢數(shù)據(jù)能查詢T2新增的記錄,此為幻讀。
注意幾個點:
- 不可重復(fù)讀是針對單條記錄的改動(包括刪除與修改)
- 幻讀是針對查詢條件的范圍內(nèi)記錄的新增
- 幻讀只是針對新增,如果有范圍內(nèi)記錄的刪除或修改,都屬于不可重復(fù)讀
為了解決這些問題,有一幫人提出了一個SQL標(biāo)準(zhǔn),給出了四種隔離級別,用以解決上訴問題:
READ UNCOMMITTEDREAD COMMITTEDREAD REPEATABLESERIALABLE
SQL標(biāo)準(zhǔn)中規(guī)定,
-
READ UNCOMMITTED解決臟寫 -
READ COMMITTED解決臟讀 -
READ REPEATABLE解決臟讀與不可重復(fù)讀 -
SERIALABLE解決幻讀
數(shù)據(jù)庫對臟寫的問題是零容忍,哪怕最低的隔離級別都不允許出現(xiàn)
然而MySQL里的大佬還是牛逼,他們在READ REPEATABLE 級別就已經(jīng)解決了幻讀問題。
實現(xiàn)一般有兩種方式,第一是加鎖,第二是MVCC。
實際上,二者都有用到.
三、千呼萬喚 -- MVCC的原理
ReadView登場
先看下其大致結(jié)構(gòu)

-
m_ids存放當(dāng)前系統(tǒng)中活躍的事務(wù)集合 -
min_trx_id為m_ids中最小的事務(wù)ID -
max_trx_id分配給下一個開啟事務(wù)的事務(wù)ID -
creator_trx_id創(chuàng)建此ReadView的事務(wù)ID
事務(wù)ID注意兩點:
- 只有在對表中的記錄做改動時(執(zhí)行
INSERT、DELETE、UPDATE這些語句時)才會為事務(wù)分配事務(wù)id,否則在一個只讀事務(wù)中的事務(wù)id值都默認為0。- 按分配順序遞增
怎么突然蹦出來一個ReadView? 先看看怎么用.
如果你不是一條魚的話,應(yīng)該還記得前面說過,每條記錄都有一個版本鏈吧,當(dāng)一個事務(wù)要訪問某條記錄時,對著這個ReadView的操作是這樣的:
- 判斷
trx_id與creator_trx_id是否相等,是則意味著此版本正在被當(dāng)前事務(wù)操作,可以訪問 - 判斷
trx_id是否小于min_trx_id,表示此版本的生成事務(wù)已經(jīng)提交,可以訪問 - 判斷
trx_id是否大于等于max_trx_id,表示此版本的生成事務(wù)已經(jīng)提交,可以訪問 - 判斷
trx_id是否在m_ids中,若不在,則意味著此版本的創(chuàng)建事務(wù)已經(jīng)提交,可以訪問;若在,則表明此版本在創(chuàng)建ReadView時還在活動,不能訪問。
若無法訪問則順著版本鏈找下一個版本,如果到最后一個版本(也就是Insert的undo日志)仍無法訪問,那么此記錄對當(dāng)前事務(wù)不可見。
現(xiàn)在知道這玩意有多牛逼了吧~
那跟隔離級別有啥關(guān)系呢?
READ COMMITTED 與 READ REPEATABLE 的最大區(qū)別就是生成ReadView的時機不一樣
-
READ REPEATABLE事務(wù)開啟時生成 -
READ COMMITTED每次查詢前生成
想想
ReadView與其生成時機如何能解決臟讀/幻讀問題~
喊我來加班,到公司都寫完一篇MVCC了,還沒見到人~~~~[允悲]