眾所周知竹椒,mysql中讀取方式按照是否需要傳統(tǒng)意義的鎖,分為鎖定讀和非鎖定讀兩種米辐。鎖定讀不用多說胸完,那就一堆算法了,行鎖翘贮,間隙鎖赊窥,next-key鎖,無非就是為了保證狸页,一個(gè)事務(wù)中鎖定讀取一條或者多條數(shù)據(jù)時(shí)锨能,不能讀到別的事務(wù)沒有提交的更改(不能臟讀),不能同一個(gè)事務(wù)兩次讀到的數(shù)據(jù)內(nèi)容不一致(應(yīng)該要可重復(fù)讀)芍耘,不能同一個(gè)事務(wù)址遇,兩次讀到的數(shù)據(jù)條數(shù)都不一致(不能幻讀)。只要拿到一個(gè)鎖定讀查詢斋竞,往避免上面三種錯(cuò)誤情況倔约,就能很輕松的區(qū)分,針對最常見的RR隔離級別坝初,什么情況下至少使用什么類型的鎖(行鎖浸剩,gap鎖,next-key鎖)才能避免臟讀脖卖,不可重復(fù)讀乒省,幻讀。又會(huì)聯(lián)系到幾個(gè)事務(wù)級別畦木,從鎖粒度從小到大袖扛,RU(讀未提交),RC(讀已提交)十籍,RR(可重復(fù)讀)蛆封,S(序列化)
RU(讀未提交)
RU(讀未提交):英文全稱就留給讀者自己裝B了哈哈),很顯然勾栗,這個(gè)級別是最無節(jié)操的級別惨篱,就相當(dāng)于我在房間,做秘密的事情围俘,我都還沒有覺得別人可以進(jìn)來砸讳,門自己覺得我做完了琢融,然后就給我開了,然后你就進(jìn)來了簿寂,你好歹讓我收拾一下吧漾抬,再通知一下,我做完了常遂,再進(jìn)來吧纳令。當(dāng)然也不會(huì)像有些人理解的門壓根沒上鎖,總要的事情做到一半克胳,你就可以進(jìn)來平绩,也不會(huì)這么沒有節(jié)操的。在mysql中也就是就算是最低級別RU漠另,也不會(huì)讓你讀到一半數(shù)據(jù)捏雌,所以還是有鎖的,只不過這個(gè)鎖并不是我們自己主觀去決定打開的笆搓,他會(huì)認(rèn)為每一條數(shù)據(jù)執(zhí)行完就可以開鎖了腹忽。雖然你還沒手動(dòng)提交事務(wù),很顯然這種級別砚作,還是會(huì)讓你看到一些不該看到的東西窘奏。臟讀估計(jì)就是從這里來的吧。
RC(讀已提交)
RC(讀已提交):很顯然葫录,只有等我事情做完着裹,并且通知你可以進(jìn)來了,你才能進(jìn)來米同。這樣你總不能看到臟東西了吧骇扇,但是有一種情況,門開著你進(jìn)門上廁所面粮,這時(shí)候有一卷新衛(wèi)生紙少孝,然后我進(jìn)去用了一點(diǎn),然后門打開你又進(jìn)去發(fā)現(xiàn)紙少了一半熬苍,你是不是會(huì)懷疑這尼瑪怎么兩次看到的紙不一樣啊稍走,這在生活中很常見,我甚至覺得這個(gè)理所當(dāng)然的沒毛病柴底。但是在數(shù)據(jù)庫領(lǐng)域它就是覺得有毛病婿脸,比如一段代碼,假設(shè)你要買一卷紙100塊(好吧柄驻,我承認(rèn)有點(diǎn)小貴)狐树,先查詢你的卡里的錢發(fā)現(xiàn)有100,結(jié)果這是時(shí)候你妹子用微信付款花了50(明顯妹子微信綁定你的卡)鸿脓,然后拿紙抑钟,扣錢涯曲,結(jié)果50 - 100 = -50扣款失敗。這是什么神仙邏輯在塔,就不能在我交易的過程中不要讓妹子可以用錢嗎掀抹,我想假設(shè)查賬,扣款要是一個(gè)完整的邏輯心俗,biu的一下不需要時(shí)間就可以搞定,就不會(huì)有這種尷尬了蓉驹,因?yàn)橐敲總€(gè)操作的時(shí)間都足夠快城榛,要么妹子在我交易之前一刻用50,要么就在我之后一刻50态兴,而不會(huì)在我中間一刻用50狠持。我想正是因?yàn)槲覀兊南到y(tǒng)不可能做到那么快,才需要認(rèn)為定義這個(gè)東西瞻润,才會(huì)遇到這種不可重復(fù)讀的情況吧
RR(可重復(fù)讀)
RR(可重復(fù)讀):為了解決上面不可重復(fù)讀的問題喘垂,誰讓我們條件不夠快呢?很顯然绍撞,我開始要用錢的時(shí)候正勒,直接把我的卡的錢鎖住,不讓別人用傻铣,等我交易完才讓別人用章贞。
S(序列化):很顯然,鎖好門非洲,排好隊(duì)鸭限,我的事情確定都做完才放其他人進(jìn)來
以上回顧了一下鎖定讀和四種隔離級別,下面進(jìn)入正題两踏,來說說mvcc和undo log吧败京。mvcc作為多版本并發(fā)控制,使用undo log實(shí)現(xiàn)梦染,同樣也可以實(shí)現(xiàn)上述四種隔離級別赡麦,只不過實(shí)現(xiàn)手段不是通過傳統(tǒng)意義上的鎖罷了。當(dāng)然針對RU(讀未提交)隔離級別帕识,所有更改的語句別的事務(wù)都可以直接看到隧甚,那根本沒有保留多個(gè)版本的必要,用到的就是最新的唯一版本渡冻,同樣S(序列化)級別戚扳,排隊(duì)一個(gè)個(gè)去讀寫,也根本沒有保留多個(gè)版本數(shù)據(jù)的必要族吻,因?yàn)槎际怯米钚碌臄?shù)據(jù)就行了帽借。
到了這里珠增,要進(jìn)入正題,其實(shí)我想說下為啥砍艾,在網(wǎng)上那么一大堆蒂教,并且一大堆mysql的好書,我為啥還死皮賴臉的分享undo log和mvcc脆荷。還嫌知識(shí)不夠多不夠亂嗎凝垛?那是因?yàn)槲抑皩W(xué)習(xí)這些,經(jīng)歷過很多誤導(dǎo)蜓谋,可以這么說網(wǎng)上那么一大堆梦皮,我還沒有看到有一篇博客或者網(wǎng)絡(luò)課堂,把這兩個(gè)東西說的準(zhǔn)確和清楚的桃焕,書上那么一大堆剑肯,我想一般人要是不仔細(xì)揣摩一陣子,可能永遠(yuǎn)都會(huì)活在自己的世界里观堂,并以之為真理让网,下面我列舉一下網(wǎng)上的那些錯(cuò)誤或者說不準(zhǔn)確的地方。后續(xù)再回過頭來看
1)為了實(shí)現(xiàn)mvcc师痕,每行數(shù)據(jù)會(huì)多兩列DATA_TRX_ID和DATA_ROLL_PTR(有些人也不要抬杠溃睹,不對可能還有一列DB_ROW_ID,當(dāng)沒有默認(rèn)主鍵時(shí)會(huì)自動(dòng)加上這列胰坟,請不要說于本篇無關(guān)的內(nèi)容)丸凭,這時(shí)候他們的解釋DATA_TRX_ID表示當(dāng)前數(shù)據(jù)的事務(wù)版本,沒啥毛病腕铸,DATA_ROLL_PTR表示事務(wù)刪除版本號惜犀,納尼!ptr一般不是用來表示指針的嗎狠裹,你跟我說時(shí)刪除版本號虽界,我書讀的少,你不要忽悠我涛菠。我不否認(rèn)用這樣的理解方式真的可以讓自己感覺理解了莉御,但是面試要問到你這么答真的沒毛病嗎,除非面試官也這么理解的(菩薩保佑)俗冻。
2)mvcc里面使用一個(gè)可讀視圖ReadView來輔助判斷那些事務(wù)版本號礁叔,里面主要有幾個(gè)元素構(gòu)成,當(dāng)前活躍事務(wù)ID集合mIds迄薄,mIds的最小事務(wù)ID琅关,mIds的最大事務(wù)ID,網(wǎng)絡(luò)上的資料99%都是這樣描述的讥蔽,只能說可能那些作者沒有理解清楚或者是人云亦云涣易。正確如何后面再解釋
3)書上說insert 的undo log區(qū)別于delete和update操作的undo log画机,insert 操作的記錄,只對事務(wù)本身可見新症,對其他事務(wù)不可見(這是事務(wù)隔離性的要求)步氏,故該undo log可以在事務(wù)提交后直接刪除。而delete和update的undo log需要等待purge線程在合適的條件刪除徒爹。為啥insert的undo log就可以直接刪除荚醒,為啥會(huì)有區(qū)別,各位讀者看到這句話是直接死記硬背就滿足了嗎隆嗅,你能從這句話看懂是為啥嗎界阁,然而可惜的是,至今沒看到有哪本書解釋過榛瓮,可能是我書讀的少吧。
4)都說redo是物理日志(絕大部分是巫击,不糾結(jié))禀晓,undo是邏輯日志,并且你新增坝锰,undo log會(huì)記錄刪除粹懒,你更新他會(huì)記錄相反的更新,這里有兩個(gè)點(diǎn)顷级,怎么理解物理日志和邏輯日志凫乖,怎么理解是記錄相反操作,新增和刪除相反可以理解弓颈,那更新x = x - 10相反難道是x = x + 10,你減去10帽芽,相反我就給你加上10,我相信肯定好大一部分人都這么理解翔冀,這么理解就坑大了导街。
5)mvcc能解決幻讀嗎?如果能解決為啥就mysql吹噓RR級別解決了幻讀纤子,難道別的數(shù)據(jù)庫沒有mvcc搬瑰,如果不能那怎么敢吹噓呢?這真的是一個(gè)人才問題控硼,后面再談吧泽论!
我們都知道m(xù)ysql數(shù)據(jù)庫是以主鍵ID作為索引使用B+Tree的結(jié)構(gòu)組織整個(gè)表的數(shù)據(jù),存放在xx.IBD文件中卡乾,數(shù)據(jù)存放在葉子節(jié)點(diǎn)塊中翼悴,每一塊都有后一塊葉子節(jié)點(diǎn)的指針,當(dāng)然我們這里忘了強(qiáng)調(diào)幔妨,本篇以InnDB引擎來說明的抄瓦,不然又要有人糾結(jié)了潮瓶。當(dāng)然這些基本的索引知識(shí),包括上述描述的各種鎖算法钙姊,可以自己看書或者別人家博客毯辅,或者得空我再分享一篇。
回到正題煞额,MVCC是個(gè)什么鬼思恐?
1)官方一點(diǎn)的解釋:并發(fā)訪問(讀或?qū)?數(shù)據(jù)庫時(shí),對正在事務(wù)內(nèi)處理的數(shù)據(jù)做 多版本的管理膊毁。以達(dá)到用來避免寫操作的堵塞胀莹,從而引發(fā)讀操作的并發(fā)問題。
2)無節(jié)操解釋婚温,拿廁所的紙巾舉例子描焰,為了讓不同的人多次進(jìn)來看到的紙巾都是一樣的,那么每次有人用紙巾栅螟,先做個(gè)標(biāo)記版本A荆秦,(A能看到的),然后放在一個(gè)柜子里力图,然后復(fù)制一個(gè)一模一樣的紙巾放在紙巾盒里步绸,標(biāo)記成當(dāng)前版本(自己看到的)。然后假定做一個(gè)看的規(guī)則吃媒,每個(gè)人進(jìn)來只能根據(jù)規(guī)則看到之前看到的那卷紙巾瓤介,保證每個(gè)人多次進(jìn)來看到的紙巾是一致的,好吧赘那,紙巾真多刑桑,夠麻煩!后面再詳細(xì)解釋
MVCC是做什么的?
1)用于事務(wù)的回滾
2)MVCC
undo log我們關(guān)注的類型有哪些募舟?
1)insert undo log
2)update undo log
InnoDB中的MVCC實(shí)現(xiàn)原理
數(shù)據(jù)表增加兩個(gè)隱藏列DATA_TRX_ID和DATA_ROLL_PTR漾月,用于實(shí)現(xiàn)mvcc
事務(wù) A 對值 x 進(jìn)行更新之后,該行即產(chǎn)生一個(gè)新版本和舊版本胃珍。假設(shè)之前插入該行的事務(wù) ID 為 100梁肿,事務(wù) A 的 ID 為 200。操作過程如下
1)對 ID = 1 的記錄加排他鎖觅彰,畢竟要修改了吩蔑,總不能加共享鎖把
2)把該行原本的值拷貝到 undo log 中
3)修改改行值并且更新 DATA_TRX_ID,將 DATA_ROLL_PTR 指向剛剛拷貝到 undo log 鏈中的舊版本記錄填抬,記住undo log是個(gè)鏈表烛芬,如果多個(gè)事務(wù)多次修改會(huì)繼續(xù)生成undo log并通過DATA_ROLL_PTR建立指向關(guān)系
上文中的undo log是一個(gè)鏈表結(jié)構(gòu),也就是如果多個(gè)事務(wù)都修改了這行數(shù)據(jù),會(huì)根據(jù)事務(wù)ID的先后赘娄,以鏈表形式存放仆潮,至于舊版本存放在鏈表的先后順序,這個(gè)其實(shí)無關(guān)緊要遣臼,只要方便獲取就好性置,我傾向于每次修改后把舊版放在鏈表的頭部,這樣可以保證從指針遞歸下來揍堰,先找到較新的數(shù)據(jù)鹏浅,再找到更舊的數(shù)據(jù),一個(gè)個(gè)版本去判斷是否是自己可以看到的版本屏歹。
那么現(xiàn)在的核心問題就是當(dāng)前事務(wù)讀取數(shù)據(jù)的時(shí)候如何判斷應(yīng)該讀取哪個(gè)版本隐砸?mysql中引入了一個(gè)可讀試圖ReadView的概念。主要包含如下屬性
1)mIds 代表生成ReadView時(shí)蝙眶,當(dāng)前活躍所有的事務(wù)ID季希,活躍的意思就是事務(wù)開啟了還沒提交,這里可以提一點(diǎn)幽纷,事務(wù)開啟事務(wù)ID會(huì)自增式塌,實(shí)際上事務(wù)ID就是一個(gè)全局自增的數(shù)字
2)min_trx_id 表示當(dāng)前活躍的mIds中最小的事務(wù)ID
3)max_trx_id 表示生成ReadView時(shí),最大的事務(wù)ID霹崎,這里一定不要理解成mIds中最大的ID珊搀,這是一個(gè)相當(dāng)錯(cuò)誤的理解冶忱,后面再解釋
4)creator_trx_id 該ReadView在那個(gè)事務(wù)里創(chuàng)建的尾菇,
ReadView有了上面4個(gè)屬性后,那么應(yīng)該以什么樣的規(guī)則囚枪,判斷當(dāng)前事務(wù)到底可以讀取哪個(gè)版本的數(shù)據(jù)呢派诬?
1)如果被訪問版本的 data_trx_id 小于 m_ids 中的最小值,說明生成該版本的事務(wù)在 ReadView 生成前就已經(jīng)提交了链沼,那么該版本可以被當(dāng)前事務(wù)訪問默赂。
2)如果被訪問版本的 data_trx_id大于當(dāng)前事務(wù)的最大值,說明生成該版本數(shù)據(jù)的事務(wù)在生成 ReadView 后才生成括勺,那么該版本不可以被當(dāng)前事務(wù)訪問缆八。為什么這里的最大值不是mIds的最大值,因?yàn)槭聞?wù)ID雖然是全局遞增的疾捍,但是并不代表事務(wù)ID大的一定要在事務(wù)ID小的后面提交奈辰,也就是事務(wù)開啟有先后,但是事務(wù)結(jié)束的先后和開啟的先后并不是完全一致的乱豆,畢竟事務(wù)有長有短奖恰。如果此時(shí)數(shù)據(jù)的事務(wù)版本是200,而mIds中沒有200,那么mIds最大值就可能小于200瑟啃,那么以規(guī)則2判斷就可能讓本該可以訪問到的數(shù)據(jù)因?yàn)檫@個(gè)規(guī)則论泛,而訪問不到了,歸根結(jié)底就是因?yàn)闆]有正確找到生成ReadView時(shí)的最大事務(wù)ID蛹屿,所以不能肯定的說生成該版本數(shù)據(jù)的事務(wù)在生成 ReadView 后才生成
3)如果被訪問版本的 data_trx_id屬性值在 最大值和最小值之間(包含)屁奏,那就需要判斷一下 trx_id 的值是不是在 m_ids 列表中了袁。如果在湿颅,說明創(chuàng)建 ReadView 時(shí)生成該版本所屬事務(wù)還是活躍的载绿,因此該版本不可以被訪問油航;如果不在,說明創(chuàng)建 ReadView 時(shí)生成該版本的事務(wù)已經(jīng)被提交谊囚,該版本可以被訪問怕享。
通俗點(diǎn)來說镰踏,也就是ReadView中通過最大事務(wù)ID奠伪,mIds最小事務(wù)ID绊率,mIds活躍事務(wù)列表,將當(dāng)前要讀的數(shù)據(jù)的事務(wù)ID分成了3種情況脸狸,要么小于mIds的最小事務(wù)ID炊甲,很明顯又在當(dāng)前活躍的最小事務(wù)之前生成卿啡,又不在活躍事務(wù)中耀石,一定是已提交的事務(wù),這個(gè)版本肯定可以訪問炕贵;要么大于生成ReadView的當(dāng)前的最大事務(wù)ID称开,很明顯在所有活躍事務(wù)之后鳖轰,并且也不可能存在于活躍事務(wù)列表中扶镀,那么就說明臭觉,該版本在當(dāng)前活躍事務(wù)之后才出現(xiàn)蝠筑,總不能讀取到未來的版本吧;要么處于最大最小值之間挽封,這時(shí)候就有兩種情況辅愿,因?yàn)椴⒉皇钦f最大最小值之間就一定是活躍的渠缕,畢竟先開啟的事務(wù)并不一定會(huì)先結(jié)束,事務(wù)有大小長短棒坏,這時(shí)候就很簡單遭笋,在mIds中就是還沒提交的活躍版本瓦呼,不可被讀取,不在就是已經(jīng)提交的版本碗啄,可以被讀取稚字。當(dāng)一個(gè)事務(wù)要讀取一行數(shù)據(jù)胆描,首先用上面規(guī)則判斷數(shù)據(jù)的最新版本也就是那行記錄昌讲,如果發(fā)現(xiàn)可以訪問就直接讀取了减噪,如果發(fā)現(xiàn)不能訪問旋廷,就通過DATA_ROLL_PTR指針找到undo log饶碘,遞歸往下去找每個(gè)版本扎运,直到讀取到自己可以讀取的版本為止豪治,如果讀取不到那就返回空唄。
還有個(gè)問題就是MVCC在RC和RR隔離級別下有啥區(qū)別烦衣?
很明顯,如果是RC級別衅澈,那么事務(wù)A兩次讀取到的分別是10和20谬墙,如果是RR級別兩次讀取到的都是10经备,如果同樣由ReadView判斷需要怎么樣才能區(qū)分兩個(gè)隔離級別取的版本不一樣呢弄喘?先說RC級別蘑志,兩個(gè)版本不一致急但,說明可能事務(wù)A兩次使用的ReadView里的內(nèi)容肯定是有不一樣波桩,結(jié)合B事務(wù)中間有提交镐躲,而提交事務(wù)很明顯會(huì)影響到mIds當(dāng)前活躍事務(wù)列表侍筛,因?yàn)槭聞?wù)提交之后就不是活躍事務(wù)了不可能再出現(xiàn)在mIds列表中了匣椰,這一點(diǎn)很好理解禽笑。再來看RR隔離級別事務(wù)A佳镜,如果要兩次讀取的x值一致蟀伸,除非兩次用來判定的ReadView沒有啥變化望蜡,這不由得讓我們想起了緩存的用法脖律,是不是可以在A事務(wù)開啟的時(shí)候生成一個(gè)ReadView小泉,然后在整個(gè)A事務(wù)期間都用這一份ReadView就行了呢微姊,就像用緩存一樣兢交。而RC級別每次查詢都生成一個(gè)最新的ReadView配喳,是不是就可以產(chǎn)生區(qū)別了惫东,這算是一個(gè)比較常規(guī)并且巧妙的設(shè)計(jì)了贴妻。
目前為止只磷,應(yīng)該基本了解了mvcc和undo log是咋回事钮追,那么接下來就該回到剛開始提到的畏陕,網(wǎng)上各種博客惠毁,在線課堂鞠绰,甚至?xí)向谂颍v到的錯(cuò)誤翁巍,不準(zhǔn)確和模糊的地方了灶壶,為了湊字?jǐn)?shù)(開個(gè)玩笑驰凛,為了方便一個(gè)個(gè)說清楚)恰响,再次copy一下上面的問題胚宦。
1)為了實(shí)現(xiàn)mvcc枢劝,每行數(shù)據(jù)會(huì)多兩列DATA_TRX_ID和DATA_ROLL_PTR(有些人也不要抬杠呈野,不對可能還有一列DB_ROW_ID被冒,當(dāng)沒有默認(rèn)主鍵時(shí)會(huì)自動(dòng)加上這列昨悼,請不要說與本篇無關(guān)的內(nèi)容)率触,這時(shí)候他們的解釋DATA_TRX_ID表示當(dāng)前數(shù)據(jù)的事務(wù)版本葱蝗,沒啥毛病两曼,DATA_ROLL_PTR表示事務(wù)刪除版本號悼凑。
2)mvcc里面使用一個(gè)可讀視圖ReadView來輔助判斷那些事務(wù)版本號户辫,里面主要有幾個(gè)元素構(gòu)成渔欢,當(dāng)前活躍事務(wù)ID集合mIds,mIds的最小事務(wù)ID披坏,mIds的最大事務(wù)ID棒拂,網(wǎng)絡(luò)上的資料99%都是這樣描述的帚屉,只能說可能那些作者沒有理解清楚或者是人云亦云攻旦。正確如何后面再解釋
3)書上說insert 的undo log區(qū)別于delete和update操作的undo log牢屋,insert 操作的記錄烙无,只對事務(wù)本身可見截酷,對其他事務(wù)不可見(這是事務(wù)隔離性的要求)迂苛,故該undo log可以在事務(wù)提交后直接刪除三幻。而delete和update的undo log需要等待purge線程在合適的條件刪除赌髓。為啥insert的undo log就可以直接刪除锁蠕,為啥會(huì)有區(qū)別悯搔,各位讀者看到這句話是直接死記硬背就滿足了嗎妒貌,你能從這句話看懂是為啥嗎灌曙,然而可惜的是在刺,至今沒看到有哪本書解釋過蚣驼,可能是我書讀的少吧颖杏。
4)都說redo是物理日志(絕大部分是留储,不糾結(jié))机久,undo是邏輯日志膘盖,并且你新增侠畔,undo log會(huì)記錄刪除软棺,你更新他會(huì)記錄相反的更新喘落,這里有兩個(gè)點(diǎn)瘦棋,怎么理解物理日志和邏輯日志,怎么理解是記錄相反操作沛慢,新增和刪除相反可以理解团甲,那更新x = x - 10相反難道是x = x + 10,你減去10伐庭,相反我就給你加上10,我相信肯定好大一部分人都這么理解集乔,這么理解就坑大了扰路。
5)mvcc能解決幻讀嗎汗唱?如果能解決為啥就mysql能吹噓RR級別解決了幻讀。
對于問題1)我想不用說了际插,這個(gè)很明確了框弛,不管看哪本書都不會(huì)這么講瑟枫,PTR一般都表示指針了,要說刪除版本號景殷,怎么不叫ROLL_ID呢猿挚,從基本的單詞解釋都不可能是刪除版本號吧绩蜻,不糾結(jié)了伊约。
對于問題2)ReadView中假設(shè)那么最大事務(wù)ID是mIds里的最大事務(wù)ID屡律,那當(dāng)我要讀取的數(shù)據(jù)版本號大于這個(gè)活躍的最大事務(wù)ID,就一定認(rèn)為我這個(gè)數(shù)據(jù)的版本是在生成ReadView之后了嗎霍殴,先開啟的事務(wù)一定會(huì)先提交嗎来庭,當(dāng)前最大的活躍事務(wù)ID,一定是當(dāng)時(shí)最大的事務(wù)ID嗎尊搬?這不剛生成ReadView的時(shí)候好幾個(gè)大事務(wù)ID提交了佛寿,不行嗎冀泻?
對于問題3)insert undo log和update undo log為啥要分開,為啥提交之后insert undo log可以直接刪除了肢专,update undo log還要命苦的等著purge呢博杖?首先insert的特殊性哩盲,如果某個(gè)事務(wù)ID=100新增了一條記錄廉油,那么在這個(gè)事務(wù)版本之前這個(gè)記錄是不存在的抒线,也就是這條數(shù)據(jù)要么就是事務(wù)100提交,然后就存在這條數(shù)據(jù)了金吗,事務(wù)100沒有提交十兢,這條數(shù)據(jù)就是null趣竣,那么請問還需要mvcc多版本控制嗎摇庙,這條數(shù)據(jù)本身不就是一個(gè)版本嗎,要么就是不存在卫袒,讀取不到,要么就是存在单匣,可以讀取夕凝,數(shù)據(jù)是否存在,在RC和RR級別不就看事務(wù)有沒有提交嗎户秤,至于RU和S前面早就說了不需要用到MVCC了码秉。不用糾結(jié)數(shù)據(jù)在哪里讀取出來的,是緩存還是磁盤鸡号,也不用糾結(jié)事務(wù)提交后數(shù)據(jù)是否真的落磁盤了转砖,總之提交后數(shù)據(jù)可以被讀取到,沒提交數(shù)據(jù)就讀取不到鲸伴,我想這就是書上所說的事務(wù)隔離性的要求吧府蔗。所以根本不需要用到多版本的冗余,當(dāng)然事務(wù)提交就可以直接刪除insert的undo log了汞窗。至于update的undo log可能同時(shí)存在事務(wù)A姓赤,B,C在修改數(shù)據(jù)仲吏,到底是事務(wù)A不铆,B或者C提交后就刪除undo log呢蝌焚,顯然不知道吧,所以要等到purge線程事后再?zèng)Q定啥時(shí)候刪除了誓斥。
對于問題4)redo確實(shí)絕大部分是物理日志综看,物理日志的意思就是有個(gè)日志文件存放,記錄了每個(gè)物理地址目前的值到底是多少岖食,至于undo log红碑,存在于一個(gè)特殊的段中,存在于表空間中泡垃,很明顯就是和主鍵id組織的數(shù)據(jù)存在一個(gè)文件中析珊,畢竟每行數(shù)據(jù)都有個(gè)指向undo log的指針了,合并單獨(dú)放在一個(gè)文件中呢蔑穴。如果一個(gè)新增操作忠寻,undo log記錄的是一個(gè)刪除類型,甚至都不需要copy任何數(shù)據(jù)存和,當(dāng)讀到這個(gè)版本奕剃,發(fā)現(xiàn)了刪除標(biāo)記,就可以直接返回null了捐腿,如果是個(gè)更新操作纵朋,那么copy一下更新前的值,沒有更新的當(dāng)然不用copy茄袖,也并不需要記錄某個(gè)物理地址上是某個(gè)特定的值操软,當(dāng)你讀到這個(gè)undo log,那么就把讀到的數(shù)據(jù)根據(jù)需要更新成undo log里對應(yīng)的數(shù)據(jù)就行了宪祥。如果是一個(gè)刪除操作聂薪,則將這行記錄copy到undo log中,然后將原始數(shù)據(jù)標(biāo)記成已經(jīng)刪除蝗羊。這種日志難道不能看成是一種邏輯日志嗎藏澳,與當(dāng)前操作相反的一種邏輯日志,不需要記錄對應(yīng)物理地址上是些什么內(nèi)容的邏輯日志耀找。
對于問題5)乍一看很唬人翔悠,很容易把你唬懵了。先搞清楚幻讀怎么產(chǎn)生的涯呻,假如事務(wù)A中先后讀取了age>10的數(shù)據(jù)(age加了索引)凉驻,第一次讀取了一條age=12的,由于緊隨其后事務(wù)B又插入了一條13复罐,導(dǎo)致事務(wù)A接著第二次查詢發(fā)現(xiàn)獲取了兩條數(shù)據(jù)涝登,說好的一條,怎么現(xiàn)在是兩條效诅,是不是我喝醉酒眼花了產(chǎn)生了幻覺胀滚。而在mysql的鎖定讀場進(jìn)很明顯通過間隙鎖/next-key鎖解決了幻讀趟济,當(dāng)我讀取age>10的時(shí)候,就把我周圍右邊的間隙的范圍都給鎖住咽笼,其它事務(wù)休想再插入age>10的數(shù)據(jù)顷编,然后就解決了幻讀,從源頭上就讓你不能插入剑刑。再來說mvcc媳纬,在RR隔離級別,當(dāng)事務(wù)A開啟的時(shí)候會(huì)生成一個(gè)事務(wù)的快照ReadView,里面記錄了當(dāng)前生成的最大事務(wù)ID施掏,假定事務(wù)A第一次查詢就一條記錄钮惠,這時(shí)候事務(wù)B的事務(wù)ID最多存在兩種可能,要么此時(shí)正在運(yùn)行還沒提交(廢話你要提交了七芭,我怎么可能就讀到一條)素挽,那就一定在mIds列表里,要么此時(shí)該事務(wù)還沒生成狸驳,那么事務(wù)B插入的時(shí)候预明,該數(shù)據(jù)的事務(wù)版本必然是大于當(dāng)前ReadView中的事務(wù)最大值的,不管是從那種情況來看根據(jù)ReadView的判別規(guī)則該數(shù)據(jù)都不可能讀到耙箍。
我就不明白為啥網(wǎng)上一大把人義正嚴(yán)詞的說單憑mvcc解決不了幻讀撰糠,信息時(shí)代網(wǎng)上一大把資料有的說可以解決,有的說不能解決究西,但是又不給理由窗慎,漸漸的就讓人們分成兩個(gè)派別了,苦惱奥辈摹!其實(shí)我覺得mvcc天然就可以解決幻讀峦失,并且基本所有現(xiàn)代關(guān)系型數(shù)據(jù)庫都有mvcc的實(shí)現(xiàn)扇丛,我有理由相信那些數(shù)據(jù)庫的快照讀都解決了幻讀(個(gè)人猜測,畢竟沒有深入研究過其它數(shù)據(jù)庫)尉辑。我想人們都說mysql的RR可以解決幻讀其它數(shù)據(jù)庫不行帆精,那只是針對鎖定讀,因?yàn)閙ysql 的RR級別有間隙鎖隧魄,其它數(shù)據(jù)庫沒有這種算法卓练,所以這么說吧。不相信的人可以多看幾遍上面的推理過程也可以開兩個(gè)連接购啄,準(zhǔn)備如下兩個(gè)語句襟企,上述所得兩種情況分別對應(yīng)事務(wù)A和B先后執(zhí)行begin,自己去測試下狮含,沒有什么比自己親自測試讓人相信了顽悼。
-- 事務(wù)A
begin;
select*fromtestwhereage >10;
-- 先執(zhí)行上面兩句曼振,再去別的連接執(zhí)行插入
select*fromtestwhereage >10;
rollback;
-- 事務(wù)B
begin;
insertintotest(age)values(13);
COMMIT;
我也看了網(wǎng)上一些測試,其實(shí)很多人在事務(wù)A中間加入一個(gè)更新語句讓以前查不到的數(shù)據(jù)蔚龙,第二次可以查到冰评,我想說這種自己事務(wù)的操作,自己難道都不能看到嗎木羹?這是幻讀嗎甲雅,要是自己事務(wù)里面的修改,自己都看不到坑填,我估計(jì)你要懷疑數(shù)據(jù)庫出毛病了吧务荆,剛修改居然看不到。我說你怎么不在事務(wù)A加一條插入age=13的語句再查詢呢穷遂,絕對可以查到函匕,自己插入修改的自己都看不到,那不是幻讀了那是錯(cuò)誤了蚪黑。不信可以把事務(wù)A兩個(gè)語句都加上for update盅惜,然后中間修改或者插入一條區(qū)域,現(xiàn)象都是一樣的忌穿,因?yàn)橐话愣际强芍厝氲氖慵牛粫?huì)鎖自己鎖自己的。
至此掠剑,基本理論知識(shí)都告一段落了屈芜,如果你們以為這樣就完了,那只能說你們想多了朴译,哈哈井佑,作為一個(gè)專業(yè)的碼農(nóng),當(dāng)然是要亮出代碼眠寿,下面我會(huì)將自己的理解用java代碼的方式寫一套簡單的關(guān)于MVCC躬翁,ReadView和UNDO LOG的邏輯,代碼是最簡單的流水帳的模式盯拱,目的只是為了程序猿們能進(jìn)一步理解本篇說的所有內(nèi)容盒发,如有雷同絕對是抄襲我的,哈哈狡逢!
package com.mvcc;
importjava.util.ArrayList;
importjava.util.Collections;
importjava.util.List;
importjava.util.Map;
importjava.util.concurrent.ConcurrentHashMap;
importjava.util.concurrent.atomic.AtomicInteger;
/**
* 事務(wù)類,只是為了方便看懂原理和避免偏離主題宁舰,所以這里省略了本應(yīng)該需要用到的鎖
* @author rongdi
* @date 2020-07-25 20:17
*/
publicclassTransaction{
/**
? ? * 全局事務(wù)id
? ? */
privatestaticAtomicInteger globalTrxId =newAtomicInteger();
/**
? ? * 當(dāng)前活躍的事務(wù)
? ? */
privatestaticMap currRunningTrxMap =newConcurrentHashMap<>();
/**
? ? * 當(dāng)前事務(wù)id
? ? */
privateInteger currTrxId =0;
/**
? ? * 事務(wù)隔離級別,ru,rc,rr和s
? ? */
privateString trxMode ="rr";
/**
? ? * 只有rc和rr級別非鎖定讀才需要用到mvcc奢浑,這個(gè)readView是為了方便判斷到底哪個(gè)版本的數(shù)據(jù)可以被
? ? * 當(dāng)前事務(wù)獲取到的視圖工具
? ? */
privateReadView readView;
/**
? ? * 開啟事務(wù)
? ? */
publicvoidbegin(){
/**
? ? ? ? * 根據(jù)全局事務(wù)計(jì)數(shù)器拿到當(dāng)前事務(wù)ID
? ? ? ? */
currTrxId = globalTrxId.incrementAndGet();
/**
? ? ? ? * 將當(dāng)前事務(wù)放入當(dāng)前活躍事務(wù)映射中
? ? ? ? */
currRunningTrxMap.put(currTrxId,this);
/**
? ? ? ? * 構(gòu)造或者更新當(dāng)前事務(wù)使用的mvcc輔助判斷視圖ReadView
? ? ? ? */
updateReadView();
}
/**
? ? * 構(gòu)造或者更新當(dāng)前事務(wù)使用的mvcc輔助判斷視圖ReadView
? ? */
publicvoidupdateReadView(){
/**
? ? ? ? * 構(gòu)造輔助視圖工具ReadView
? ? ? ? */
readView =newReadView(currTrxId);
/**
? ? ? ? * 設(shè)置當(dāng)前事務(wù)最大值
? ? ? ? */
readView.setMaxTrxId(globalTrxId.get());
List mIds =newArrayList<>(currRunningTrxMap.keySet());
Collections.sort(mIds);
/**
? ? ? ? * 設(shè)置當(dāng)前活躍事務(wù)id
? ? ? ? */
readView.setmIds(newArrayList<>(currRunningTrxMap.keySet()));
/**
? ? ? ? * 設(shè)置mIds中最小事務(wù)ID
? ? ? ? */
readView.setMinTrxId(mIds.isEmpty()?0: mIds.get(0));
/**
? ? ? ? * 設(shè)置當(dāng)前事務(wù)ID
? ? ? ? */
readView.setCurrTrxId(currTrxId);
}
/**
? ? * 提交事務(wù)
? ? */
publicvoidcommit(){
currRunningTrxMap.remove(currTrxId);
}
publicstaticAtomicIntegergetGlobalTrxId(){
returnglobalTrxId;
}
publicstaticvoidsetGlobalTrxId(AtomicInteger globalTrxId){
Transaction.globalTrxId = globalTrxId;
}
publicstaticMap getCurrRunningTrxMap() {
returncurrRunningTrxMap;
}
publicstaticvoidsetCurrRunningTrxMap(Map<Integer, Transaction> currRunningTrxMap){
Transaction.currRunningTrxMap = currRunningTrxMap;
}
publicIntegergetCurrTrxId(){
returncurrTrxId;
}
publicvoidsetCurrTrxId(Integer currTrxId){
this.currTrxId = currTrxId;
}
publicStringgetTrxMode(){
returntrxMode;
}
publicvoidsetTrxMode(String trxMode){
this.trxMode = trxMode;
}
publicReadViewgetReadView(){
returnreadView;
}
}
packagecom.mvcc;
importjava.util.ArrayList;
importjava.util.List;
/**
* 模擬mysql中的ReadView
*@authorrongdi
*@date2020-07-25 20:31
*/
publicclassReadView{
/**
? ? * 記錄當(dāng)前活躍的事務(wù)ID
? ? */
privateList mIds = new ArrayList<>();
/**
? ? * 記錄當(dāng)前活躍的最小事務(wù)ID
? ? */
privateInteger minTrxId;
/**
? ? * 記錄當(dāng)前最大事務(wù)ID蛮艰,注意并不是活躍的最大ID,包括已提交的殷费,因?yàn)橛锌赡茏畲蟮氖聞?wù)ID已經(jīng)提交了
? ? */
privateInteger maxTrxId;
/**
? ? * 記錄當(dāng)前生成readView時(shí)的事務(wù)ID
? ? */
privateInteger currTrxId;
publicReadView(Integer currTrxId) {
this.currTrxId = currTrxId;
}
publicData read(Datadata) {
/**
? ? ? ? * 先判斷當(dāng)前最新數(shù)據(jù)是否可以訪問
? ? ? ? */
if(canRead(data.getDataTrxId())) {
returndata;
}
/**
? ? ? ? * 獲取到該數(shù)據(jù)的undo log引用
? ? ? ? */
UndoLog undoLog =data.getNextUndoLog();
do{
/**
? ? ? ? ? ? * 如果undoLog存在并且可讀印荔,則合并返回
? ? ? ? ? ? */
if(undoLog !=null&& canRead(undoLog.getTrxId())) {
returnmerge(data,undoLog);
}
/**
? ? ? ? ? ? * 還沒找到可讀版本低葫,繼續(xù)獲取下一個(gè)更舊版本
? ? ? ? ? ? */
undoLog = undoLog.getNext();
}while(undoLog !=null&& undoLog.getNext() !=null);
/**
? ? ? ? * 整個(gè)undo log鏈都找不到可讀的,沒辦法了我也幫不鳥你
? ? ? ? */
returnnull;
}
/**
? ? * 合并最新數(shù)據(jù)和目標(biāo)版本的undo log數(shù)據(jù)仍律,返回最終可訪問數(shù)據(jù)
? ? */
privateData merge(Datadata,UndoLog undoLog) {
if(undoLog ==null) {
returndata;
}
/**
? ? ? ? * update 更新 直接把undo保存的數(shù)據(jù)替換過來返回
? ? ? ? * add 新增 直接把undo保存的數(shù)據(jù)替換過來返回
? ? ? ? * del 刪除 數(shù)據(jù)當(dāng)時(shí)是不存在的嘿悬,直接返回null就好了
? ? ? ? */
if("update".equalsIgnoreCase(undoLog.getOperType())) {
data.setValue(undoLog.getValue());
returndata;
}elseif("add".equalsIgnoreCase(undoLog.getOperType())) {
data.setId(undoLog.getRecordId());
data.setValue(undoLog.getValue());
returndata;
}elseif("del".equalsIgnoreCase(undoLog.getOperType())) {
returnnull;
}else{
//其余情況,不管了水泉,直接返回算了
? ? ? ? ? ? return data;
? ? ? ? }
? ? }
? ? private boolean canRead(Integer dataTrxId) {
? ? ? ? /**
? ? ? ? * 1.如果當(dāng)前數(shù)據(jù)的所屬事務(wù)正好是當(dāng)前事務(wù)或者數(shù)據(jù)的事務(wù)小于mIds的最小事務(wù)ID善涨,
? ? ? ? * 則說明產(chǎn)生該數(shù)據(jù)的事務(wù)在生成ReadView之前已經(jīng)提交了,該數(shù)據(jù)可以訪問
? ? ? ? */
? ? ? ? if(dataTrxId == null || dataTrxId.equals(currTrxId) || dataTrxId < minTrxId) {
? ? ? ? ? ? return true;
? ? ? ? }
? ? ? ? /**
? ? ? ? * 2.如果當(dāng)前數(shù)據(jù)所屬事務(wù)大于當(dāng)前最大事務(wù)ID(并不是mIds的最大事務(wù)草则,好多人都覺得是)钢拧,則
? ? ? ? * 說明產(chǎn)生該數(shù)據(jù)是在生成ReadView之后,則當(dāng)前事務(wù)不可訪問
? ? ? ? */
? ? ? ? if(dataTrxId > maxTrxId) {
? ? ? ? ? ? return false;
? ? ? ? }
? ? ? ? /**
? ? ? ? * 3.如果當(dāng)前數(shù)據(jù)所屬事務(wù)介于mIds最小事務(wù)和當(dāng)前最大事務(wù)ID之間炕横,則需要進(jìn)一步判斷
? ? ? ? */
? ? ? ? if(dataTrxId >= minTrxId && dataTrxId <= maxTrxId) {
? ? ? ? ? ? /**
? ? ? ? ? ? * 如果當(dāng)前數(shù)據(jù)所屬事務(wù)包含在mIds當(dāng)前活躍事務(wù)列表中源内,則說明該事務(wù)還沒提交,
? ? ? ? ? ? * 不可訪問,反之表示數(shù)據(jù)所屬事務(wù)已經(jīng)提交了份殿,可以訪問
? ? ? ? ? ? */
? ? ? ? ? ? if(mIds.contains(dataTrxId)) {
? ? ? ? ? ? ? ? return false;
? ? ? ? ? ? } else {
? ? ? ? ? ? ? ? return true;
? ? ? ? ? ? }
? ? ? ? }
? ? ? ? return false;
? ? }
? ? public List<Integer> getmIds() {
? ? ? ? return mIds;
? ? }
? ? public void setmIds(List<Integer> mIds) {
? ? ? ? this.mIds = mIds;
? ? }
? ? public Integer getMinTrxId() {
? ? ? ? return minTrxId;
? ? }
? ? public void setMinTrxId(Integer minTrxId) {
? ? ? ? this.minTrxId = minTrxId;
? ? }
? ? public Integer getMaxTrxId() {
? ? ? ? return maxTrxId;
? ? }
? ? public void setMaxTrxId(Integer maxTrxId) {
? ? ? ? this.maxTrxId = maxTrxId;
? ? }
? ? public Integer getCurrTrxId() {
? ? ? ? return currTrxId;
? ? }
? ? public void setCurrTrxId(Integer currTrxId) {
? ? ? ? this.currTrxId = currTrxId;
? ? }
}
package com.mvcc;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 模仿數(shù)據(jù)庫的數(shù)據(jù)存儲(chǔ)地方
* @author rongdi
* @date 2020-07-25 19:24
*/
publicclassData{
/**
? ? * 模擬一個(gè)存放數(shù)據(jù)的表
? ? */
privatestaticMap dataMap =newConcurrentHashMap<>();
/**
? ? * 記錄的ID
? ? */
privateInteger id;
/**
? ? * 記錄的數(shù)據(jù)
? ? */
privateStringvalue;
/**
? ? * 記錄當(dāng)前記錄的事務(wù)ID
? ? */
privateInteger dataTrxId;
/**
? ? * 指向上個(gè)版本的undo log的引用
? ? */
privateUndoLog nextUndoLog;
/**
? ? * 標(biāo)記數(shù)據(jù)是否刪除膜钓,實(shí)際數(shù)據(jù)庫會(huì)根據(jù)情況由purge線程完成真實(shí)數(shù)據(jù)的清楚
? ? */
privateboolean isDelete;
publicData(Integer dataTrxId){
this.dataTrxId = dataTrxId;
}
/**
? ? * 模擬數(shù)據(jù)庫更新操作,這里就不要自增id,直接指定id了
? ? * @param id
? ? * @param value
? ? * @return
? ? */
publicIntegerupdate(Integer id,Stringvalue){
/**
? ? ? ? * 獲取原值,這里就不判斷是否存在卿嘲,于本例核心偏離的邏輯了
? ? ? ? */
Data oldData = dataMap.get(id);
/**
? ? ? ? * 更新當(dāng)前數(shù)據(jù)
? ? ? ? */
this.id = id;
this.value=value;
/**
? ? ? ? * 不要忘了颂斜,為了數(shù)據(jù)的一致性,要準(zhǔn)備好隨時(shí)失敗回滾的undo log拾枣,這里既然是修改數(shù)據(jù)沃疮,那代表以前
? ? ? ? * 這條數(shù)據(jù)是就記錄一下以前的舊值,將舊值構(gòu)造成一個(gè)undo log記錄
? ? ? ? */
UndoLog undoLog =newUndoLog(id,oldData.getValue(),oldData.getDataTrxId(),"update");
/**
? ? ? ? * 將舊值的undo log掛到當(dāng)前新的undo log之后,形成一個(gè)按從新到舊順序的一個(gè)undo log鏈表
? ? ? ? */
undoLog.setNext(oldData.getNextUndoLog());
/**
? ? ? ? * 將當(dāng)前數(shù)據(jù)的undo log引用指向新生成的undo log
? ? ? ? */
this.nextUndoLog = undoLog;
/**
? ? ? ? *? 更新數(shù)據(jù)表當(dāng)前id的數(shù)據(jù)
? ? ? ? */
dataMap.put(id,this);
returnid;
}
/**
? ? * 按照上面更新操作的理解,刪除相當(dāng)于是把原紀(jì)錄修改成標(biāo)記成已刪除狀態(tài)的記錄了
? ? * @param id
? ? */
publicvoiddelete(Integer id){
/**
? ? ? ? * 獲取原值,這里就不判斷是否存在梅肤,于本例核心偏離的邏輯了
? ? ? ? */
Data oldData = dataMap.get(id);
this.id = id;
/**
? ? ? ? * 將當(dāng)前數(shù)據(jù)標(biāo)記成已刪除
? ? ? ? */
this.setDelete(true);
/**
? ? ? ? * 同樣司蔬,為了數(shù)據(jù)的一致性,要準(zhǔn)備好隨時(shí)失敗回滾的undo log凭语,這里既然是刪除數(shù)據(jù)葱她,那代表以前
? ? ? ? * 這條數(shù)據(jù)存在,就記錄一下以前的舊值,并將舊值構(gòu)造成一個(gè)邏輯上新增的undo log記錄
? ? ? ? */
UndoLog undoLog =newUndoLog(id,oldData.getValue(),oldData.getDataTrxId(),"add");
/**
? ? ? ? * 將舊值的undo log掛到當(dāng)前新的undo log之后似扔,形成一個(gè)按從新到舊順序的一個(gè)undo log鏈表
? ? ? ? */
undoLog.setNext(oldData.getNextUndoLog());
/**
? ? ? ? * 將當(dāng)前數(shù)據(jù)的undo log引用指向新生成的undo log
? ? ? ? */
this.nextUndoLog = undoLog;
/**
? ? ? ? *? 更新數(shù)據(jù)表當(dāng)前id的數(shù)據(jù)
? ? ? ? */
dataMap.put(id,this);
}
/**
? ? * 按照上面更新操作的理解,新增相當(dāng)于是把原紀(jì)錄原來不存在的記錄修改成了新的記錄
? ? * @param id
? ? */
publicvoidinsert(Integer id,Stringvalue){
/**
? ? ? ? * 更新當(dāng)前數(shù)據(jù)
? ? ? ? */
this.id = id;
this.value=value;
/**
? ? ? ? * 同樣,為了數(shù)據(jù)的一致性搓谆,要準(zhǔn)備好隨時(shí)失敗回滾的undo log炒辉,這里既然是新增數(shù)據(jù),那代表以前
? ? ? ? * 這條數(shù)據(jù)不存在泉手,就記錄一下以前為空值,并將空值構(gòu)造成一個(gè)邏輯上刪除的undo log記錄
? ? ? ? */
UndoLog undoLog =newUndoLog(id,null,null,"delete");
/**
? ? ? ? * 將當(dāng)前數(shù)據(jù)的undo log引用指向新生成的undo log
? ? ? ? */
this.nextUndoLog = undoLog;
/**
? ? ? ? *? 更新數(shù)據(jù)表當(dāng)前id的數(shù)據(jù)
? ? ? ? */
dataMap.put(id,this);
}
/**
? ? * 模擬使用mvcc非鎖定讀黔寇,這里的mode表示事務(wù)隔離級別,只有rc和rr級別才需要用到mvcc斩萌,同樣為了方便缝裤,
? ? * 使用英文表示隔離級別屏轰,rc表示讀已提交,rr表示可重復(fù)讀
? ? */
publicDataselect(Integer id){
/**
? ? ? ? * 拿到當(dāng)前事務(wù)憋飞,然后判斷事務(wù)隔離級別霎苗,如果是rc,則執(zhí)行一個(gè)語句就要更新一下ReadView,這里寫的
? ? ? ? * 這么直接就是為了好理解
? ? ? ? */
Transaction currTrx = Transaction.getCurrRunningTrxMap().get(this.getDataTrxId());
String trxMode = currTrx.getTrxMode();
if("rc".equalsIgnoreCase(trxMode)) {
currTrx.updateReadView();
}
/**
? ? ? ? * 拿到當(dāng)前事務(wù)輔助視圖ReadView
? ? ? ? */
ReadView readView = currTrx.getReadView();
/**
? ? ? ? * 模擬根據(jù)id取出一行數(shù)據(jù)
? ? ? ? */
Data data = Data.getDataMap().get(id);
/**
? ? ? ? * 使用readView判斷并讀取當(dāng)前事務(wù)可以讀取到的最終數(shù)據(jù)
? ? ? ? */
returnreadView.read(data);
}
publicstaticMapgetDataMap(){
returndataMap;
}
publicstaticvoidsetDataMap(Map<Integer, Data> dataMap){
Data.dataMap = dataMap;
}
publicIntegergetId(){
returnid;
}
publicvoidsetId(Integer id){
this.id = id;
}
publicStringgetValue(){
returnvalue;
}
publicvoidsetValue(Stringvalue){
this.value=value;
}
publicIntegergetDataTrxId(){
returndataTrxId;
}
publicvoidsetDataTrxId(Integer dataTrxId){
this.dataTrxId = dataTrxId;
}
publicUndoLoggetNextUndoLog(){
returnnextUndoLog;
}
publicvoidsetNextUndoLog(UndoLog nextUndoLog){
this.nextUndoLog = nextUndoLog;
}
publicbooleanisDelete(){
returnisDelete;
}
publicvoidsetDelete(boolean delete){
isDelete = delete;
}
@Override
publicStringtoString(){
return"Data{"+
"id="+ id +
", value='"+value+'\''+
'}';
}
}
package com.mvcc;
/**
* 模仿undo log鏈
* @author rondi
* @date 2020-07-25 19:52
*/
publicclassUndoLog{
/**
? ? * 指向上一個(gè)undo log
? ? */
privateUndoLog pre;
/**
? ? * 指向下一個(gè)undo log
? ? */
privateUndoLog next;
/**
? ? * 記錄數(shù)據(jù)的ID
? ? */
privateInteger recordId;
/**
? ? * 記錄的數(shù)據(jù)
? ? */
privateStringvalue;
/**
? ? * 記錄當(dāng)前數(shù)據(jù)所屬的事務(wù)ID
? ? */
privateInteger trxId;
/**
? ? * 操作類型榛做,感覺用整型好一點(diǎn)唁盏,但是如果用整型,又要搞個(gè)枚舉检眯,麻煩厘擂,所以直接用字符串了,能表達(dá)意思就好
? ? * update 更新
? ? * add 新增
? ? * del 刪除
? ? */
privateString operType;
publicUndoLog(Integer recordId, Stringvalue, Integer trxId, String operType){
this.recordId = recordId;
this.value=value;
this.trxId = trxId;
this.operType = operType;
}
publicUndoLoggetPre(){
returnpre;
}
publicvoidsetPre(UndoLog pre){
this.pre = pre;
}
publicUndoLoggetNext(){
returnnext;
}
publicvoidsetNext(UndoLog next){
this.next = next;
}
publicIntegergetRecordId(){
returnrecordId;
}
publicvoidsetRecordId(Integer recordId){
this.recordId = recordId;
}
publicStringgetValue(){
returnvalue;
}
publicvoidsetValue(Stringvalue){
this.value=value;
}
publicIntegergetTrxId(){
returntrxId;
}
publicvoidsetTrxId(Integer trxId){
this.trxId = trxId;
}
publicStringgetOperType(){
returnoperType;
}
publicvoidsetOperType(String operType){
this.operType = operType;
}
}
一共就四個(gè)類锰瘸,感興趣的讀者可以寫個(gè)測試類刽严,然后開幾個(gè)線程,每個(gè)事務(wù)使用一個(gè)線程模擬避凝,并加上睡眠延時(shí)去跑一下代碼舞萄。再次強(qiáng)調(diào),本代碼只是為了讓讀者理解一下上面說的理論知識(shí)恕曲,并不是mysql的真實(shí)實(shí)現(xiàn)鹏氧,可以理解成作者本人認(rèn)為的一種可行的簡單實(shí)現(xiàn)。