還不懂mysql的undo log和mvcc?算我輸!

最後Q淚滴發表於2020-07-27

  最近一直沒啥時間寫點東西,堅持分享真的好難,也不知道該分享點啥,正好有人要問我這些東西,所以騰出點時間,寫一下這個主題。同樣本篇可以給讀者承諾,聽不懂或者沒收穫算我輸,哈哈!

  眾所周知,mysql中讀取方式按照是否需要傳統意義的鎖,分為鎖定讀和非鎖定讀兩種。鎖定讀不用多說,那就一堆演算法了,行鎖,間隙鎖,next-key鎖,無非就是為了保證,一個事務中鎖定讀取一條或者多條資料時,不能讀到別的事務沒有提交的更改(不能髒讀),不能同一個事務兩次讀到的資料內容不一致(應該要可重複讀),不能同一個事務,兩次讀到的資料條數都不一致(不能幻讀)。只要拿到一個鎖定讀查詢,往避免上面三種錯誤情況,就能很輕鬆的區分,針對最常見的RR隔離級別,什麼情況下至少使用什麼型別的鎖(行鎖,gap鎖,next-key鎖)才能避免髒讀,不可重複讀,幻讀。又會聯絡到幾個事務級別,從鎖粒度從小到大,RU(讀未提交),RC(讀已提交),RR(可重複讀),S(序列化)

  RU(讀未提交):英文全稱就留給讀者自己裝B了哈哈),很顯然,這個級別是最無節操的級別,就相當於我在房間,做祕密的事情,我都還沒有覺得別人可以進來,門自己覺得我做完了,然後就給我開了,然後你就進來了,你好歹讓我收拾一下把,再通知一下,我做完了,再進來把。當然也不會像有些人理解的門壓根沒上鎖,總要的事情做到一半,你就可以進來,也不會這麼沒有節操的。在mysql中也就是就算是最低階別RU,也不會讓你讀到一半資料,所以還是有鎖的,只不過這個鎖並不是我們自己主觀去決定開啟的,他會認為每一條資料執行完就可以開鎖了。雖然你還沒手動提交事務,很顯然這種級別,還是會讓你看到一些不該看到的東西。髒讀估計就是從這裡來的把。

  RC(讀已提交):很顯然,只有等我事情做完,並且通知你可以進來了,你才能進來。這樣你總不能看到髒東西了吧,但是有一種情況,門開著你進門上廁所,這時候有一卷新衛生紙,然後我進去用了一點,然後門開啟你又進去發現紙少了一半,你是不是會懷疑這尼瑪怎麼兩次看到的紙不一樣啊,這在生活中很常見,我甚至覺得這個理所當然的沒毛病。但是在資料庫領域它就是覺得有毛病,比如一段程式碼,假設你要買一卷紙100塊(好吧,我承認有點小貴),先查詢你的卡里的錢發現有100,結果這是時候你妹子用微信付款花了50(明顯妹子微信繫結你的卡),然後拿紙,扣錢,結果50 - 100 = -50扣款失敗。這是什麼神仙邏輯,就不能在我交易的過程中不要讓妹子可以用錢嗎,我想假設查賬,扣款要是一個完整的邏輯,biu的一下不需要時間就可以搞定,就不會有這種尷尬了,因為要是每個操作的時間都足夠快,要麼妹子在我交易之前一刻用50,要麼就在我之後一刻50,而不會在我中間一刻用50。我想正是因為我們的系統不可能做到那麼快,才需要認為定義這個東西,才會遇到這種不可重複讀的情況吧

  RR(可重複讀):為了解決上面不可重複讀的問題,誰讓我們條件不夠快呢?很顯然,我開始要用錢的時候,直接把我的卡的錢鎖住,不讓別人用,等我交易完才讓別人用。

  S(序列化):很顯然,鎖好門,排好隊,我的事情確定都做完才放其他人進來

  以上回顧了一下鎖定讀和四種隔離級別,下面進入正題,來說說mvcc和undo log吧。mvcc作為多版本併發控制,使用undo log實現,同樣也可以實現上述四種隔離級別,只不過實現手段不是通過傳統意義上的鎖罷了。當然針對RU(讀未提交)隔離級別,所有更改的語句別的事務都可以直接看到,那根本沒有保留多個版本的必要,用到的就是最新的唯一版本,同樣S(序列化)級別,排隊一個個去讀寫,也根本沒有保留多個版本資料的必要,因為都是用最新的資料就行了。

  到了這裡,要進入正題,其實我想說下為啥,在網上那麼一大堆,並且一大堆mysql的好書,我為啥還死皮賴臉的分享undo log和mvcc。還嫌知識不夠多不夠亂嗎?那是因為我之前學習這些,經歷過很多誤導,可以這麼說網上那麼一大堆,我還沒有看到有一篇部落格或者網路課堂,把這兩個東西說的準確和清楚的,書上那麼一大堆,我想一般人要是不仔細揣摩一陣子,可能永遠都會活在自己的世界裡,並以之為真理,下面我列舉一下網上的那些錯誤或者說不準確的地方。後續再回過頭來看

  1)為了實現mvcc,每行資料會多兩列DATA_TRX_ID和DATA_ROLL_PTR(有些人也不要抬槓,不對可能還有一列DB_ROW_ID,當沒有預設主鍵時會自動加上這列,請不要說於本篇無關的內容),這時候他們的解釋DATA_TRX_ID表示當前資料的事務版本,沒啥毛病,DATA_ROLL_PTR表示事務刪除版本號,納尼!ptr一般不是用來表示指標的嗎,你跟我說時刪除版本號,我書讀的少,你不要忽悠我。我不否認用這樣的理解方式真的可以讓自己感覺理解了,但是面試要問到你這麼答真的沒毛病嗎,除非面試官也這麼理解的(菩薩保佑)。

  2)mvcc裡面使用一個可讀檢視ReadView來輔助判斷那些事務版本號,裡面主要有幾個元素構成,當前活躍事務ID集合mIds,mIds的最小事務ID,mIds的最大事務ID,網路上的資料99%都是這樣描述的,只能說可能那些作者沒有理解清楚或者是人云亦云。正確如何後面再解釋

  3)書上說insert 的undo log區別於delete和update操作的undo log,insert 操作的記錄,只對事務本身可見,對其他事務不可見(這是事務隔離性的要求),故該undo log可以在事務提交後直接刪除。而delete和update的undo log需要等待purge執行緒在合適的條件刪除。為啥insert的undo log就可以直接刪除,為啥會有區別,各位讀者看到這句話是直接死記硬背就滿足了嗎,你能從這句話看懂是為啥嗎,然而可惜的是,至今沒看到有哪本書解釋過,可能是我書讀的少吧。

  4)都說redo是物理日誌(絕大部分是,不糾結),undo是邏輯日誌,並且你新增,undo log會記錄刪除,你更新他會記錄相反的更新,這裡有兩個點,怎麼理解物理日誌和邏輯日誌,怎麼理解是記錄相反操作,新增和刪除相反可以理解,那更新x = x - 10相反難道是x = x + 10,你減去10,相反我就給你加上10,我相信肯定好大一部分人都這麼理解,這麼理解就坑大了。

  5)mvcc能解決幻讀嗎?如果能解決為啥就mysql吹噓RR級別解決了幻讀,難道別的資料庫沒有mvcc,如果不能那怎麼敢吹噓呢?這真的是一個人才問題,後面再談吧!

  我們都知道mysql資料庫是以主鍵ID作為索引使用B+Tree的結構組織整個表的資料,存放在xx.IBD檔案中,資料存放在葉子節點塊中,每一塊都有後一塊葉子節點的指標,當然我們這裡忘了強調,本篇以InnDB引擎來說明的,不然又要有人糾結了。當然這些基本的索引知識,包括上述描述的各種鎖演算法,可以自己看書或者別人家部落格,或者得空我再分享一篇。

  回到正題,MVCC是個什麼鬼?

  1)官方一點的解釋:併發訪問(讀或寫)資料庫時,對正在事務內處理的資料做 多版本的管理。以達到用來避免寫操作的堵塞,從而引發讀操作的併發問題。

  2)無節操解釋,拿廁所的紙巾舉例子,為了讓不同的人多次進來看到的紙巾都是一樣的,那麼每次有人用紙巾,先做個標記版本A,(A能看到的),然後放在一個櫃子裡,然後複製一個一模一樣的紙巾放在紙巾盒裡,標記成當前版本(自己看到的)。然後假定做一個看的規則,每個人進來只能根據規則看到之前看到的那捲紙巾,保證每個人多次進來看到的紙巾是一致的,好吧,紙巾真多,夠麻煩!後面在詳細解釋

  MVCC是做什麼的?

  1)用於事務的回滾

  2)MVCC

  undo log我們關注的型別有哪些?

  1)insert undo log

  2)update undo log

  InnoDB中的MVCC實現原理

  資料表增加兩個隱藏列DATA_TRX_ID和DATA_ROLL_PTR,用於實現mvcc

       

    事務 A 對值 x 進行更新之後,該行即產生一個新版本和舊版本。假設之前插入該行的事務 ID 為 100,事務 A 的 ID 為 200。操作過程如下

    1)對 ID = 1 的記錄加排他鎖,畢竟要修改了,總不能加共享鎖把

    2)把該行原本的值拷貝到 undo log 中

      3)修改改行值並且更新 DATA_TRX_ID,將 DATA_ROLL_PTR 指向剛剛拷貝到 undo log 鏈中的舊版本記錄,記住undo log是個連結串列,如果多個事務多次修改會繼續生成undo log並通過DATA_ROLL_PTR建立指向關係

  上文中的undo log是一個連結串列結構,也就是如果多個事務都修改了這行資料,會根據事務ID的先後,以連結串列形式存放,至於舊版本存放在連結串列的先後順序,這個其實無關緊要,只要方便獲取就好,我傾向於每次修改後把舊版放在連結串列的頭部,這樣可以保證從指標遞迴下來,先找到較新的資料,再找到更舊的資料,一個個版本去判斷是否是自己可以看到的版本。

  那麼現在的核心問題就是當前事務讀取資料的時候如何判斷應該讀取哪個版本?mysql中引入了一個可讀試圖ReadView的概念。主要包含如下屬性

  1)mIds 代表生成ReadView時,當前活躍所有的事務ID,活躍的意思就是事務開啟了還沒提交,這裡可以提一點,事務開啟事務ID會自增,實際上事務ID就是一個全域性自增的數字

  2)min_trx_id 表示當前活躍的mIds中最小的事務ID

  3)max_trx_id 表示生成ReadView時,最大的事務ID,這裡一定不要理解成mIds中最大的ID,這是一個相當錯誤的理解,後面再解釋

  4)creator_trx_id 該ReadView在那個事務裡建立的,

  ReadView有了上面4個屬性後,那麼應該以什麼樣的規則,判斷當前事務到底可以讀取哪個版本的資料呢?

  1)如果被訪問版本的 data_trx_id 小於 m_ids 中的最小值,說明生成該版本的事務在 ReadView 生成前就已經提交了,那麼該版本可以被當前事務訪問。

  2)如果被訪問版本的 data_trx_id大於當前事務的最大值,說明生成該版本資料的事務在生成 ReadView 後才生成,那麼該版本不可以被當前事務訪問。為什麼這裡的最大值不是mIds的最大值,因為事務ID雖然是全域性遞增的,但是並不代表事務ID大的一定要在事務ID小的後面提交,也就是事務開啟有先後,但是事務結束的先後和開啟的先後並不是完全一致的,畢竟事務有長有短。如果此時資料的事務版本是200,而mIds中沒有200,那麼mIds最大值就可能小於200,那麼以規則2判斷就可能讓本該可以訪問到的資料因為這個規則,而訪問不到了,歸根結底就是因為沒有正確找到生成ReadView時的最大事務ID,所以不能肯定的說生成該版本資料的事務在生成 ReadView 後才生成

  3)如果被訪問版本的 data_trx_id屬性值在 最大值和最小值之間(包含),那就需要判斷一下 trx_id 的值是不是在 m_ids 列表中。如果在,說明建立 ReadView 時生成該版本所屬事務還是活躍的,因此該版本不可以被訪問;如果不在,說明建立 ReadView 時生成該版本的事務已經被提交,該版本可以被訪問。

  通俗點來說,也就是ReadView中通過最大事務ID,mIds最小事務ID,mIds活躍事務列表,將當前要讀的資料的事務ID分成了3種情況,要麼小於mIds的最小事務ID,很明顯又在當前活躍的最小事務之前生成,又不在活躍事務中,一定是已提交的事務,這個版本肯定可以訪問;要麼大於生成ReadView的當前的最大事務ID,很明顯在所有活躍事務之後,並且也不可能存在於活躍事務列表中,那麼就說明,該版本在當前活躍事務之後才出現,總不能讀取到未來的版本吧;要麼處於最大最小值之間,這時候就有兩種情況,因為並不是說最大最小值之間就一定是活躍的,畢竟先開啟的事務並不一定會先結束,事務有大小長短,這時候就很簡單,在mIds中就是還沒提交的活躍版本,不可被讀取,不在就是已經提交的版本,可以被讀取。當一個事務要讀取一行資料,首先用上面規則判斷資料的最新版本也就是那行記錄,如果發現可以訪問就直接讀取了,如果發現不能訪問,就通過DATA_ROLL_PTR指標找到undo log,遞迴往下去找每個版本,知道讀取到自己可以讀取的版本為止,如果讀取不到那就返回空唄。

  還有個問題就是MVCC在RC和RR隔離級別下有啥區別?

  

   很明顯,如果是RC級別,那麼事務A兩次讀取到的分別是10和20,如果是RR級別兩次讀取到的都是10,如果同樣由ReadView判斷需要怎麼樣才能區分兩個隔離級別取的版本不一樣呢?先說RC級別,兩個版本不一致,說明可能事務A兩次使用的ReadView裡的內容肯定是有不一樣,結合B事務中間有提交,而提交事務很明顯會影響到mIds當前活躍事務列表,因為事務提交之後就不是活躍事務了不可能再出現在mIds列表中了,這一點很好理解。再來看RR隔離級別事務A,如果要兩次讀取的x值一致,除非兩次用來判定的ReadView沒有啥變化,這不由得讓我們想起了快取的用法,是不是可以在A事務開啟的時候生成一個ReadView,然後在整個A事務期間都用這一份ReadView就行了呢,就像用快取一樣。而RC級別每次查詢都生成一個最新的ReadView,是不是就可以產生區別了,這算是一個比較常規並且巧妙的設計了。

  目前為止,應該基本瞭解了mvcc和undo log是咋回事,那麼接下來就該回到剛開始提到的,網上各種部落格,線上課堂,甚至書上,所講到的錯誤,不準確和模糊的地方了,為了湊字數(開個玩笑,為了方便一個個說清楚),再次copy一下上面的問題。

    1)為了實現mvcc,每行資料會多兩列DATA_TRX_ID和DATA_ROLL_PTR(有些人也不要抬槓,不對可能還有一列DB_ROW_ID,當沒有預設主鍵時會自動加上這列,請不要說於本篇無關的內容),這時候他們的解釋DATA_TRX_ID表示當前資料的事務版本,沒啥毛病,DATA_ROLL_PTR表示事務刪除版本號。

  2)mvcc裡面使用一個可讀檢視ReadView來輔助判斷那些事務版本號,裡面主要有幾個元素構成,當前活躍事務ID集合mIds,mIds的最小事務ID,mIds的最大事務ID,網路上的資料99%都是這樣描述的,只能說可能那些作者沒有理解清楚或者是人云亦云。正確如何後面再解釋

  3)書上說insert 的undo log區別於delete和update操作的undo log,insert 操作的記錄,只對事務本身可見,對其他事務不可見(這是事務隔離性的要求),故該undo log可以在事務提交後直接刪除。而delete和update的undo log需要等待purge執行緒在合適的條件刪除。為啥insert的undo log就可以直接刪除,為啥會有區別,各位讀者看到這句話是直接死記硬背就滿足了嗎,你能從這句話看懂是為啥嗎,然而可惜的是,至今沒看到有哪本書解釋過,可能是我書讀的少吧。

  4)都說redo是物理日誌(絕大部分是,不糾結),undo是邏輯日誌,並且你新增,undo log會記錄刪除,你更新他會記錄相反的更新,這裡有兩個點,怎麼理解物理日誌和邏輯日誌,怎麼理解是記錄相反操作,新增和刪除相反可以理解,那更新x = x - 10相反難道是x = x + 10,你減去10,相反我就給你加上10,我相信肯定好大一部分人都這麼理解,這麼理解就坑大了。

  5)mvcc能解決幻讀嗎?如果能解決為啥就mysql能吹噓RR級別解決了幻讀。

  對於問題1)我想不用說了,這個很明確了,不管看哪本書都不會這麼講,PTR一般都表示指標了,要說刪除版本號,怎麼不叫ROLL_ID呢,從基本的單詞解釋都不可能是刪除版本號吧,不糾結了。

  對於問題2)ReadView中假設那麼最大事務ID是mIds裡的最大事務ID,那當我要讀取的資料版本號大於這個活躍的最大事務ID,就一定認為我這個資料的版本是在生成ReadView之後了嗎,先開啟的事務一定會先提交嗎,當前最大的活躍事務ID,一定是當時最大的事務ID嗎?這不剛生成ReadView的時候好幾個大事務ID提交了,不行嗎?

  對於問題3)insert undo log和update undo log為啥要分開,為啥提交之後insert undo log可以直接刪除了,update undo log還要命苦的等著purge呢?首先insert的特殊性,如果某個事務ID=100新增了一條記錄,那麼在這個事務版本之前這個記錄是不存在的,也就是這條資料要麼就是事務100提交,然後就存在這條資料了,事務100沒有提交,這條資料就是null,那麼請問還需要mvcc多版本控制嗎,這條資料本身不就是一個版本嗎,要麼就是不存在,讀取不到,要麼就是存在,可以讀取,資料是否存在,在RC和RR級別不就看事務有沒有提交嗎,至於RU和S前面早就說了不需要用到MVCC了。不用糾結資料在哪裡讀取出來的,是快取還是磁碟,也不用糾結事務提交後資料是否真的落磁碟了,總之提交後資料可以被讀取到,沒提交資料就讀取不到,我想這就是書上所說的事務隔離性的要求吧。所以根本不需要用到多版本的冗餘,當然事務提交就可以直接刪除insert的undo log了。至於update的undo log可能同時存在事務A,B,C在修改資料,到底是事務A,B或者C提交後就刪除undo log呢,顯然不知道吧,所以要等到purge執行緒事後再決定啥時候刪除了。

  對於問題4)redo確實絕大部分是物理日誌,物理日誌的意思就是有個日誌檔案存放,記錄了每個實體地址目前的值到底是多少,至於undo log,存在於一個特殊的段中,存在於表空間中,很明顯就是和主鍵id組織的資料存在一個檔案中,畢竟每行資料都有個指向undo log的指標了,合併單獨放在一個檔案中呢。如果一個新增操作,undo log記錄的是一個刪除型別,甚至都不需要copy任何資料,當讀到這個版本,發現了刪除標記,就可以直接返回null了,如果是個更新操作,那麼copy一下更新前的值,沒有更新的當然不用copy,也並不需要記錄某個實體地址上是某個特定的值,當你讀到這個undo log,那麼就把讀到的資料根據需要更新成undo log裡對應的資料就行了。如果是一個刪除操作,則將這行記錄copy到undo log中,然後將原始資料標記成已經刪除。這種日誌難道不能看成是一種邏輯日誌嗎,與當前操作相反的一種邏輯日誌,不需要記錄對應實體地址上是些什麼內容的邏輯日誌。

  對於問題5)乍一看很唬人,很容易把你唬懵了。先搞清楚幻讀怎麼產生的,假如事務A中先後讀取了age>10的資料(age加了索引),第一次讀取了一條age=12的,由於緊隨其後事務B又插入了一條13,導致事務A接著第二次查詢發現獲取了兩條資料,說好的一條,怎麼現在是兩條,是不是我喝醉酒眼花了產生了幻覺。而在mysql的鎖定讀場進很明顯通過間隙鎖/next-key鎖解決了幻讀,當我讀取age>10的時候,就把我周圍右邊的間隙的範圍都給鎖住,其它事務休想再插入age>10的資料,然後就解決了幻讀,從源頭上就讓你不能插入。再來說mvcc,在RR隔離級別,當事務A開啟的時候會生成一個事務的快照ReadView,裡面記錄了當前生成的最大事務ID,假定事務A第一次查詢就一條記錄,這時候事務B的事務ID最多存在兩種可能,要麼此時正在執行還沒提交(廢話你要提交了,我怎麼可能就讀到一條),那就一定在mIds列表裡,要麼此時該事務還沒生成,那麼事務B插入的時候,該資料的事務版本必然是大於當前ReadView中的事務最大值的,不管是從那種情況來看根據ReadView的判別規則該資料都不可能讀到。我就不明白為啥網上一大把人義正嚴詞的說單憑mvcc解決不了幻讀,資訊時代網上一大把資料有的說可以解決,有的說不能解決,但是又不給理由,漸漸的就讓人們分成兩個派別了,苦惱啊!其實我覺得mvcc天然就可以解決幻讀,並且基本所有現代關係型資料庫都有mvcc的實現,我有理由相信那些資料庫的快照讀都解決了幻讀(個人猜測,畢竟沒有深入研究過其它資料庫)。我想人們都說mysql的RR可以解決幻讀其它資料庫不行,那只是針對鎖定讀,因為mysql 的RR級別有間隙鎖,其它資料庫沒有這種演算法,所以這麼說把。不相信的人可以多看幾遍上面的推理過程也可以開兩個連線,準備如下兩個語句,上述所得兩種情況分別對應事務A和B先後執行begin,自己去測試下,沒有什麼比自己親自測試讓人相信了。

-- 事務A
begin
; select * from test where age > 10; -- 先執行上面兩句,再去別的連線執行插入 select * from test where age > 10; rollback;
-- 事務B
begin
; insert into test(age) values(13); COMMIT;

  我也看了網上一些測試,其實很多人在事務A中間加入一個更新語句讓以前查不到的資料,第二次可以查到,我想說這種自己事務的操作,自己難道都不能看到嗎?這是幻讀嗎,要是自己事務裡面的修改,自己都看不到,我估計你要懷疑資料庫出毛病了吧,剛修改居然看不到。我說你怎麼不在事務A加一條插入age=13的語句再查詢呢,絕對可以查到,自己插入修改的自己都看不到,那不是幻讀了那是錯誤了。不信可以把事務A兩個語句都加上for update,然後中間修改或者插入一條居於,現象都是一樣的,因為一般都是可重入的,不會鎖自己鎖自己的。

  至此,基本理論知識都告一段落了,如果你們以為這樣就完了,那隻能說你們想多了,哈哈,作為一個專業的碼農,當然是要亮出程式碼,下面我會將自己的理解用java程式碼的方式寫一套簡單的關於MVCC,ReadView和UNDO LOG的邏輯,程式碼是最簡單的流水帳的模式,目的只是為了程式猿們能進一步理解本篇說的所有內容,如有雷同絕對是抄襲我的,哈哈!

package com.mvcc;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 事務類,只是為了方便看懂原理和避免偏離主題,所以這裡省略了本應該需要用到的鎖
 * @author rongdi
 * @date 2020-07-25 20:17
 */
public class Transaction {

    /**
     * 全域性事務id
     */
    private static AtomicInteger globalTrxId = new AtomicInteger();

    /**
     * 當前活躍的事務
     */
    private static Map<Integer,Transaction> currRunningTrxMap = new ConcurrentHashMap<>();

    /**
     * 當前事務id
     */
    private Integer currTrxId = 0;

    /**
     * 事務隔離級別,ru,rc,rr和s
     */
    private String trxMode = "rr";

    /**
     * 只有rc和rr級別非鎖定讀才需要用到mvcc,這個readView是為了方便判斷到底哪個版本的資料可以被
     * 當前事務獲取到的檢視工具
     */
    private ReadView readView;

    /**
     * 開啟事務
     */
    public void begin() {
        /**
         * 根據全域性事務計數器拿到當前事務ID
         */
        currTrxId = globalTrxId.incrementAndGet();
        /**
         * 將當前事務放入當前活躍事務對映中
         */
        currRunningTrxMap.put(currTrxId,this);
        /**
         * 構造或者更新當前事務使用的mvcc輔助判斷檢視ReadView
         */
        updateReadView();
    }

    /**
     * 構造或者更新當前事務使用的mvcc輔助判斷檢視ReadView
     */
    public void updateReadView() {
        /**
         * 構造輔助檢視工具ReadView
         */
        readView = new ReadView(currTrxId);
        /**
         * 設定當前事務最大值
         */
        readView.setMaxTrxId(globalTrxId.get());
        List<Integer> mIds = new ArrayList<>(currRunningTrxMap.keySet());
        Collections.sort(mIds);
        /**
         * 設定當前活躍事務id
         */
        readView.setmIds(new ArrayList<>(currRunningTrxMap.keySet()));
        /**
         * 設定mIds中最小事務ID
         */
        readView.setMinTrxId(mIds.isEmpty()? 0 : mIds.get(0));
        /**
         * 設定當前事務ID
         */
        readView.setCurrTrxId(currTrxId);
    }

    /**
     * 提交事務
     */
    public void commit() {
        currRunningTrxMap.remove(currTrxId);
    }

    public static AtomicInteger getGlobalTrxId() {
        return globalTrxId;
    }

    public static void setGlobalTrxId(AtomicInteger globalTrxId) {
        Transaction.globalTrxId = globalTrxId;
    }

    public static Map<Integer, Transaction> getCurrRunningTrxMap() {
        return currRunningTrxMap;
    }

    public static void setCurrRunningTrxMap(Map<Integer, Transaction> currRunningTrxMap) {
        Transaction.currRunningTrxMap = currRunningTrxMap;
    }

    public Integer getCurrTrxId() {
        return currTrxId;
    }

    public void setCurrTrxId(Integer currTrxId) {
        this.currTrxId = currTrxId;
    }

    public String getTrxMode() {
        return trxMode;
    }

    public void setTrxMode(String trxMode) {
        this.trxMode = trxMode;
    }

    public ReadView getReadView() {
        return readView;
    }

}
package com.mvcc;

import java.util.ArrayList;
import java.util.List;

/**
 * 模擬mysql中的ReadView
 * @author rongdi
 * @date 2020-07-25 20:31
 */
public class ReadView {

    /**
     * 記錄當前活躍的事務ID
     */
    private List<Integer> mIds = new ArrayList<>();

    /**
     * 記錄當前活躍的最小事務ID
     */
    private Integer minTrxId;

    /**
     * 記錄當前最大事務ID,注意並不是活躍的最大ID,包括已提交的,因為有可能最大的事務ID已經提交了
     */
    private Integer maxTrxId;

    /**
     * 記錄當前生成readView時的事務ID
     */
    private Integer currTrxId;

    public ReadView(Integer currTrxId) {
        this.currTrxId = currTrxId;
    }

    public Data read(Data data) {
        /**
         * 先判斷當前最新資料是否可以訪問
         */
        if(canRead(data.getDataTrxId())) {
            return data;
        }
        /**
         * 獲取到該資料的undo log引用
         */
        UndoLog undoLog = data.getNextUndoLog();
        do {
            /**
             * 如果undoLog存在並且可讀,則合併返回
             */
            if(undoLog != null && canRead(undoLog.getTrxId())) {
                return merge(data,undoLog);
            }
            /**
             * 還沒找到可讀版本,繼續獲取下一個更舊版本
             */
            undoLog = undoLog.getNext();
        } while(undoLog != null && undoLog.getNext() != null);

        /**
         * 整個undo log鏈都找不到可讀的,沒辦法了我也幫不鳥你
         */
        return null;
    }

    /**
     * 合併最新資料和目標版本的undo log資料,返回最終可訪問資料
     */
    private Data merge(Data data,UndoLog undoLog) {
        if(undoLog == null) {
            return data;
        }
        /**
         * update 更新 直接把undo儲存的資料替換過來返回
         * add 新增 直接把undo儲存的資料替換過來返回
         * del 刪除 資料當時是不存在的,直接返回null就好了
         */
        if("update".equalsIgnoreCase(undoLog.getOperType())) {
            data.setValue(undoLog.getValue());
            return data;
        } else if("add".equalsIgnoreCase(undoLog.getOperType())) {
            data.setId(undoLog.getRecordId());
            data.setValue(undoLog.getValue());
            return data;
        } else if("del".equalsIgnoreCase(undoLog.getOperType())) {
            return null;
        } else {
            //其餘情況,不管了,直接返回算了
            return data;
        }
    }

    private boolean canRead(Integer dataTrxId) {
        /**
         * 1.如果當前資料的所屬事務正好是當前事務或者資料的事務小於mIds的最小事務ID,
         * 則說明產生該資料的事務在生成ReadView之前已經提交了,該資料可以訪問
         */
        if(dataTrxId == null || dataTrxId.equals(currTrxId) || dataTrxId < minTrxId) {
            return true;
        }
        /**
         * 2.如果當前資料所屬事務大於當前最大事務ID(並不是mIds的最大事務,好多人都覺得是),則
         * 說明產生該資料是在生成ReadView之後,則當前事務不可訪問
         */
        if(dataTrxId > maxTrxId) {
            return false;
        }
        /**
         * 3.如果當前資料所屬事務介於mIds最小事務和當前最大事務ID之間,則需要進一步判斷
         */
        if(dataTrxId >= minTrxId && dataTrxId <= maxTrxId) {
            /**
             * 如果當前資料所屬事務包含在mIds當前活躍事務列表中,則說明該事務還沒提交,
             * 不可訪問,反之表示資料所屬事務已經提交了,可以訪問
             */
            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;

/**
 * 模仿資料庫的資料儲存地方
 * @author rongdi
 * @date 2020-07-25 19:24
 */
public class Data {

    /**
     * 模擬一個存放資料的表
     */
    private static Map<Integer,Data> dataMap = new ConcurrentHashMap<>();

    /**
     * 記錄的ID
     */
    private Integer id;

    /**
     * 記錄的資料
     */
    private String value;

    /**
     * 記錄當前記錄的事務ID
     */
    private Integer dataTrxId;

    /**
     * 指向上個版本的undo log的引用
     */
    private UndoLog nextUndoLog;

    /**
     * 標記資料是否刪除,實際資料庫會根據情況由purge執行緒完成真實資料的清楚
     */
    private boolean isDelete;

    public Data(Integer dataTrxId) {
        this.dataTrxId = dataTrxId;
    }

    /**
     * 模擬資料庫更新操作,這裡就不要自增id,直接指定id了
     * @param id
     * @param value
     * @return
     */
    public Integer update(Integer id,String value) {
        /**
         * 獲取原值,這裡就不判斷是否存在,於本例核心偏離的邏輯了
         */
        Data oldData = dataMap.get(id);
        /**
         * 更新當前資料
         */
        this.id = id;
        this.value = value;
        /**
         * 不要忘了,為了資料的一致性,要準備好隨時失敗回滾的undo log,這裡既然是修改資料,那代表以前
         * 這條資料是就記錄一下以前的舊值,將舊值構造成一個undo log記錄
         */
        UndoLog undoLog = new UndoLog(id,oldData.getValue(),oldData.getDataTrxId(),"update");
        /**
         * 將舊值的undo log掛到當前新的undo log之後,形成一個按從新到舊順序的一個undo log連結串列
         */
        undoLog.setNext(oldData.getNextUndoLog());
        /**
         * 將當前資料的undo log引用指向新生成的undo log
         */
        this.nextUndoLog = undoLog;
        /**
         *  更新資料表當前id的資料
         */
        dataMap.put(id,this);
        return id;
    }

    /**
     * 按照上面更新操作的理解,刪除相當於是把原紀錄修改成標記成已刪除狀態的記錄了
     * @param id
     */
    public void delete(Integer id) {
        /**
         * 獲取原值,這裡就不判斷是否存在,於本例核心偏離的邏輯了
         */
        Data oldData = dataMap.get(id);
        this.id = id;
        /**
         * 將當前資料標記成已刪除
         */
        this.setDelete(true);
        /**
         * 同樣,為了資料的一致性,要準備好隨時失敗回滾的undo log,這裡既然是刪除資料,那代表以前
         * 這條資料存在,就記錄一下以前的舊值,並將舊值構造成一個邏輯上新增的undo log記錄
         */
        UndoLog undoLog = new UndoLog(id,oldData.getValue(),oldData.getDataTrxId(),"add");
        /**
         * 將舊值的undo log掛到當前新的undo log之後,形成一個按從新到舊順序的一個undo log連結串列
         */
        undoLog.setNext(oldData.getNextUndoLog());
        /**
         * 將當前資料的undo log引用指向新生成的undo log
         */
        this.nextUndoLog = undoLog;
        /**
         *  更新資料表當前id的資料
         */
        dataMap.put(id,this);
    }

    /**
     * 按照上面更新操作的理解,新增相當於是把原紀錄原來不存在的記錄修改成了新的記錄
     * @param id
     */
    public void insert(Integer id,String value) {
        /**
         * 更新當前資料
         */
        this.id = id;
        this.value = value;
        /**
         * 同樣,為了資料的一致性,要準備好隨時失敗回滾的undo log,這裡既然是新增資料,那代表以前
         * 這條資料不存在,就記錄一下以前為空值,並將空值構造成一個邏輯上刪除的undo log記錄
         */
        UndoLog undoLog = new UndoLog(id,null,null,"delete");
        /**
         * 將當前資料的undo log引用指向新生成的undo log
         */
        this.nextUndoLog = undoLog;
        /**
         *  更新資料表當前id的資料
         */
        dataMap.put(id,this);
    }

    /**
     * 模擬使用mvcc非鎖定讀,這裡的mode表示事務隔離級別,只有rc和rr級別才需要用到mvcc,同樣為了方便,
     * 使用英文表示隔離級別,rc表示讀已提交,rr表示可重複讀
     */
    public Data select(Integer id) {
        /**
         * 拿到當前事務,然後判斷事務隔離級別,如果是rc,則執行一個語句就要更新一下ReadView,這裡寫的
         * 這麼直接就是為了好理解
         */
        Transaction currTrx = Transaction.getCurrRunningTrxMap().get(this.getDataTrxId());
        String trxMode = currTrx.getTrxMode();
        if("rc".equalsIgnoreCase(trxMode)) {
            currTrx.updateReadView();
        }
        /**
         * 拿到當前事務輔助檢視ReadView
         */
        ReadView readView = currTrx.getReadView();
        /**
         * 模擬根據id取出一行資料
         */
        Data data = Data.getDataMap().get(id);
        /**
         * 使用readView判斷並讀取當前事務可以讀取到的最終資料
         */
        return readView.read(data);
    }


    public static Map<Integer, Data> getDataMap() {
        return dataMap;
    }

    public static void setDataMap(Map<Integer, Data> dataMap) {
        Data.dataMap = dataMap;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getValue() {
        return value;
    }

    public void setValue(String value) {
        this.value = value;
    }

    public Integer getDataTrxId() {
        return dataTrxId;
    }

    public void setDataTrxId(Integer dataTrxId) {
        this.dataTrxId = dataTrxId;
    }

    public UndoLog getNextUndoLog() {
        return nextUndoLog;
    }

    public void setNextUndoLog(UndoLog nextUndoLog) {
        this.nextUndoLog = nextUndoLog;
    }

    public boolean isDelete() {
        return isDelete;
    }

    public void setDelete(boolean delete) {
        isDelete = delete;
    }

    @Override
    public String toString() {
        return "Data{" +
            "id=" + id +
            ", value='" + value + '\'' +
            '}';
    }
}
package com.mvcc;

/**
 * 模仿undo log鏈
 * @author rondi
 * @date 2020-07-25 19:52
 */
public class UndoLog {

    /**
     * 指向上一個undo log
     */
    private UndoLog pre;

    /**
     * 指向下一個undo log
     */
    private UndoLog next;

    /**
     * 記錄資料的ID
     */
    private Integer recordId;

    /**
     * 記錄的資料
     */
    private String value;
    /**
     * 記錄當前資料所屬的事務ID
     */
    private Integer trxId;

    /**
     * 操作型別,感覺用整型好一點,但是如果用整型,又要搞個列舉,麻煩,所以直接用字串了,能表達意思就好
     * update 更新
     * add 新增
     * del 刪除
     */
    private String operType;

    public UndoLog(Integer recordId, String value, Integer trxId, String operType) {
        this.recordId = recordId;
        this.value = value;
        this.trxId = trxId;
        this.operType = operType;
    }

    public UndoLog getPre() {
        return pre;
    }

    public void setPre(UndoLog pre) {
        this.pre = pre;
    }

    public UndoLog getNext() {
        return next;
    }

    public void setNext(UndoLog next) {
        this.next = next;
    }

    public Integer getRecordId() {
        return recordId;
    }

    public void setRecordId(Integer recordId) {
        this.recordId = recordId;
    }

    public String getValue() {
        return value;
    }

    public void setValue(String value) {
        this.value = value;
    }

    public Integer getTrxId() {
        return trxId;
    }

    public void setTrxId(Integer trxId) {
        this.trxId = trxId;
    }

    public String getOperType() {
        return operType;
    }

    public void setOperType(String operType) {
        this.operType = operType;
    }
}

  一共就四個類,敢興趣的讀者可以寫個測試類,然後開幾個執行緒,每個事務使用一個執行緒模擬,並加上睡眠延時去跑一下程式碼。再次強調,本程式碼只是為了讓讀者理解一下上面說的理論知識,並不是mysql的真實實現,可以理解成作者本人認為的一種可行的簡單實現。回顧全文,發現真心不知道如何排版,藝術能力有限,自我感覺只是把問題說清楚了,希望各位見諒!好了,下次再見吧!

  

  

相關文章