柚子快報激活碼778899分享:數(shù)據(jù)庫 【MySQL】事務(wù)二
事務(wù)二
1.數(shù)據(jù)庫并發(fā)的場景2.讀-寫2.1 3個記錄隱藏字段2.2 undo日志2.3 模擬 MVCC2.4 Read View2.5 RR 與 RC的本質(zhì)區(qū)別
3.讀-讀4.寫-寫
點贊??收藏??關(guān)注?? 你的支持是對我最大的鼓勵,我們一起努力吧!??
關(guān)于事務(wù)的所有知識上篇博客我們都說過了,今天這篇博客主要是為了解密,RC和RR隔離級別,它怎么做到一個事務(wù)提交了,其他事務(wù)還看不到。數(shù)據(jù)不是只有一份嗎,他改了我怎么看不到,這個原理是怎么樣的?
之前說的隔離性和隔離級別的話題,前提是多事務(wù)進行并發(fā)運行,所以我們應(yīng)該想明白的是一個數(shù)據(jù)庫在被并發(fā)訪問時它的場景有那些,因為只有知道場景才能針對不同場景提供不同方案。
1.數(shù)據(jù)庫并發(fā)的場景
數(shù)據(jù)庫并發(fā)的場景有三種:
讀-讀 :不存在任何問題,也不需要并發(fā)控制,因為沒有人去修改讀-寫 :有線程安全問題,可能會造成事務(wù)隔離性問題,可能遇到臟讀,幻讀,不可重復(fù)讀寫-寫 :數(shù)據(jù)庫只會被人寫,事務(wù)都是寫事務(wù),一定通過加鎖來保證數(shù)據(jù)安全。否則有線程安全問題,事務(wù)不是有回滾嗎,有可能一個事務(wù)在更新另一個事務(wù)回滾了彼此交叉運行,可能會存在更新丟失問題,比如第一類更新丟失,第二類更新丟失(后面補充),
2.讀-寫
就像之前做的實驗,讀到的數(shù)據(jù)和寫的數(shù)據(jù)是不一樣的,其實這已經(jīng)證明讀寫的數(shù)據(jù)不是同一份。后面再說。在讀寫并發(fā)實現(xiàn)很好的隔離性采用的核心技術(shù)之一多版本并發(fā)控制。
多版本并發(fā)控制( MVCC ) 是一種用來解決 讀-寫沖突 的無鎖并發(fā)控制
歷史上在說事務(wù)的時候一直強調(diào)事務(wù)是原子的,但是事務(wù)在執(zhí)行一定是有執(zhí)行中,mysql為了解決執(zhí)行中對應(yīng)的并發(fā)問題,也一定要讓事務(wù)在執(zhí)行的時候有先有后,保證事務(wù)那些數(shù)據(jù)能看到那些數(shù)據(jù)看不到,所以它一定要判定事務(wù)的先后問題!事務(wù)在怎么同時到來一定有先有后。那問題是怎么區(qū)分事務(wù)的先后問題?
mysql為事務(wù)分配單向增長的事務(wù)ID,事務(wù)與事務(wù)ID是一對一的關(guān)系。事務(wù)ID越小代表來的越早,ID越大代表來的越晚,所以可以通過ID來判定事務(wù)的先后順序。為每個修改保存一個版本,版本與事務(wù)ID關(guān)聯(lián),讀操作只讀該事務(wù)開始前的數(shù)據(jù)庫的快照。 所以 MVCC 可以為數(shù)據(jù)庫解決以下問題
在并發(fā)讀寫數(shù)據(jù)庫時,可以做到在讀操作時不用阻塞寫操作,寫操作也不用阻塞讀操作,提高了數(shù)據(jù)庫并發(fā)讀寫的性能同時還可以解決臟讀,幻讀,不可重復(fù)讀等事務(wù)隔離問題,但不能解決更新丟失問題
每個事務(wù)都要有自己的事務(wù)ID,可以根據(jù)事務(wù)ID的大小,來決定事務(wù)到來的先后順序。
一個事務(wù)可以交給mysql運行,兩個十個都可以由多個客戶端并發(fā)的交給mysql,也就是說mysqld可能會面臨同時處理多個事務(wù)的情況, 事務(wù)在使用mysql的人看來它是原子的,但在mysql內(nèi)部它一定要有個執(zhí)行的過程,所以它的執(zhí)行過程就證明mysql中事務(wù)也有自己的生命周期,事務(wù)要被創(chuàng)建,要被放到某個等待隊列里,要被執(zhí)行,執(zhí)行出錯要被回滾,執(zhí)行完畢事務(wù)要被消除,這些都指向一點mysqld要對多個事務(wù)進行管理,先描述,在組織! 換句話說事務(wù)在我看來,mysqld中一定是對應(yīng)的一個或者一套結(jié)構(gòu)體對象/類對象,事務(wù)也要有自己的結(jié)構(gòu)體那每個事務(wù)都要有自己的事務(wù)ID是不是就好理解了。來一個事務(wù)就new一個事務(wù)對象,對事務(wù)的管理就變成了對某種數(shù)據(jù)結(jié)構(gòu)的增刪查改。
有了這個概念,我們再來談事務(wù)隔離級別具體的解決方案MVCC。不過在談MVCC之前我們要先知道三個前提知識:
3個記錄隱藏字段undo 日志Read View
2.1 3個記錄隱藏字段
其實我們在建表的時候指明有多少列,你以為有4列、5列等等,可實際上mysql都要默認給添加上3個隱藏字段。
DB_TRX_ID :6 byte,最近修改( 修改/插入 )事務(wù)ID,記錄這條記錄/最后一次修改該記錄的事務(wù)ID。
比如說未來在表中插入任何數(shù)據(jù),插入的這條記錄是那個事務(wù)插入的,事務(wù)ID是誰,要把事務(wù)ID放在表中。無論是手動啟動事務(wù)還是單SQL由系統(tǒng)默認封裝的事務(wù),最終在數(shù)據(jù)庫中所以操作的SQL必須以事務(wù)的方式讓mysql統(tǒng)一執(zhí)行。每一個事務(wù)都有ID,所以所有表的操作都要和事務(wù)ID關(guān)聯(lián)起來保存到表里。
DB_ROLL_PTR : 7 byte,回滾指針,指向這條記錄的上一個版本(簡單理解成,指向歷史版本就行,這些數(shù)據(jù)一般在 undo log 中)
實際上你對表中某一行記錄做修改,mysql在特定的隔離級別下,不是讓直接去改表中的數(shù)據(jù),它會把你要改的這條記錄先保存一份,讓你改最新表中的數(shù)據(jù),這樣的話就可以在改之后也可以知道歷史的數(shù)據(jù)是什么,這種策略特別想像 寫時拷貝。增加、修改、刪除都是要先把數(shù)據(jù)保存一份,然后改最新的數(shù)據(jù)。然后最新記錄要能找到它歷史的最新信息,所以有了這個 回滾指針。指向被修改之前的上一個版本。
DB_ROW_ID : 6 byte,隱含的自增ID(隱藏主鍵),如果數(shù)據(jù)表沒有主鍵, InnoDB 會自動以DB_ROW_ID 產(chǎn)生一個聚簇索引補充:實際還有一個刪除flag隱藏字段, 記錄當前記錄的狀態(tài),是被更新過還是被刪除了。一般刪除并不是把這條記錄真的刪除了,只是把flag變了。我們建立的表結(jié)構(gòu)是在聚簇索引的葉子節(jié)點中以page方式存在,它是內(nèi)存級的。所以刪除的時候我們并不需要把數(shù)據(jù)情況還要做各自表結(jié)構(gòu)的移動那太麻煩了。所以我只需要把它清掉就可以,清掉之后只需要最終維持page是臟的或者干凈的,后面刷盤的時候在把數(shù)據(jù)排列到磁盤中。下次在不就連續(xù)了嘛。
建一個學(xué)生表
create table if not exists student(
name varchar(11) not null,
age int not null
);
我們查的時候只能看到兩列,自動提交是被打開的,實際上insert就是一個事務(wù),在插入張三 28后面也一定會有這個數(shù)據(jù)是那個事務(wù)插入的,沒有指明主鍵mysql會有一個默認主鍵,因為歷史上沒有數(shù)據(jù)所以回滾指針為null。
namegaeDB_TRX_ID(創(chuàng)建該記錄的事務(wù)ID)DB_ROW_ID(隱式主鍵)DB_ROLL_PTR(回滾指針)張三28null1null
我們目前并不知道創(chuàng)建該記錄的事務(wù)ID,隱式主鍵,我們就默認設(shè)置成null,1。第一條記錄也沒有其他版本,我們設(shè)置回滾指針為null。
2.2 undo日志
以前說過mysql中有很多日志,其中undo日志是mysql中比較重要的模板。這個模塊是什么東西呢
mysql在啟動的的時候會申請對應(yīng)的緩存區(qū),實際上msyql中還有一大堆日志緩存區(qū),其中有一塊叫做undo log,從名字上看 undo 是撤銷的意思 log 是日志的意思,關(guān)于它我們今天給它就一個結(jié)論,它是我們在應(yīng)用層由Mysql維護的內(nèi)存空間!
MySQL 將來是以服務(wù)進程的方式,在內(nèi)存中運行。我們之前所講的所有機制:索引,事務(wù),隔離性,日志等,都是在內(nèi)存中完成的,即在 MySQL 內(nèi)部的相關(guān)緩沖區(qū)中,保存相關(guān)數(shù)據(jù),完成各種判斷操作。然后在合適的時候,將相關(guān)數(shù)據(jù)刷新到磁盤當中的。
所以,我們這里理解undo log,簡單理解成,就是 MySQL 中的一段內(nèi)存緩沖區(qū),用來保存日志數(shù)據(jù)的就行。
有了上面兩個預(yù)備知識,一個是3個隱藏字段,一個是undo log,下面我們來模擬一下多版本并發(fā)控制( MVCC )是怎么做的。
2.3 模擬 MVCC
現(xiàn)在假設(shè)我們目前表中就一條張三的數(shù)據(jù)。是事務(wù)9將它insert進來的。這個記錄在B+數(shù)的葉子節(jié)點存著。
我們的場景是有一個事務(wù)10(僅僅為了好區(qū)分),對student表中記錄進行修改(update):將name(張三)改成name(李四)。
因為事務(wù)10要對數(shù)據(jù)進行修改,所以一定要先給對應(yīng)記錄先加行鎖。修改前,先將這個記錄拷貝到undo log中,所以,undo log中就有了一行副本數(shù)據(jù)。(原理就是寫時拷貝),只要放到undo log里這個記錄就一定在undo log有起始地址。假設(shè)是0xaa, 這個原始數(shù)據(jù)里面有隱藏字段,其中有一個DB_ROLL_PTR 回滾指針,初始默認為null因為它沒有歷史版本,但是現(xiàn)在不是已經(jīng)把老版本已經(jīng)在undo log里保存一份了嗎,然后在這個回滾指針里填入0xaa保存起來。然后這條最新記錄不就指向了undo log里面的叫做歷史版本。嚴格起來說應(yīng)該是版本列。
所以現(xiàn)在 MySQL 中有兩行同樣的記錄。然后我們不是要做name張三改李四嗎, 所以我們直接把原始記錄的張三改成李四。改完之后,你不也是事務(wù)嗎,也有自己的事務(wù)ID,所以修改原始記錄的隱藏字段 DB_TRX_ID 為當前 事務(wù)10 的ID。換句話說我們就可以記錄下來這個記錄被誰修改。
這個事務(wù)完了就提交,然后對這個記錄釋放鎖。
整個過程是在加鎖的環(huán)境下進行的,所以意味著當你在做update的時候,其他事務(wù)也對這條記錄修改它一定是要等你把這個update操作做完的。這就串行起來了。所以寫寫并發(fā)加鎖是常見的。至此我們就完成了一次對記錄修改的操作。此時在mysql表里最新葉子節(jié)點記錄就是這個被修改的數(shù)據(jù)。undo log里面的是歷史數(shù)據(jù)。
現(xiàn)在又有一個事務(wù)11,對student表中記錄進行修改(update):將age(28)改成age(38)。
它要改的話,要改那條數(shù)據(jù)呢? 歷史數(shù)據(jù)決定不能改,你沒有資格去改歷史數(shù)據(jù)。你只能去改最新的數(shù)據(jù)。
事務(wù)11,因為也要修改,所以要先給該記錄加行鎖。修改前,先將老記錄拷貝到undo log中,所以,undo log中就又有了一行副本數(shù)據(jù)。此時,新的副本,我們采用頭插方式,插入undo log。這條記錄拷貝到undo log里一定也有自己的地址 0xbb。然后這個新拷貝到undo log記錄里的回滾指針一定是指向之前上一條老記錄。還沒有完,因為要做修改,所以最新記錄要填充自己的回滾指針。所以這個最新記錄里的 DB_ROLL_PTR 回滾指針要指向它自己修改之前它自己任務(wù)的老的版本 0xbb。 然后修改最新記錄中的age,改成 38。改的是當前最新記錄,不是歷史版本,歷史版本就不能更改!并且修改最新記錄的隱藏字段 DB_TRX_ID 為當前 事務(wù)11 的ID。 事務(wù)11提交,釋放鎖。
如果事務(wù)11、12、13都要對這條記錄修改沒關(guān)系,只要一直在被訪問就會一直形成版本鏈。所以此時外面是最新版,undo log是歷史版,它們是用指針的方式形成了一個鏈表。這樣,我們就有了一個基于鏈表記錄的歷史版本鏈。所謂的回滾,無非就是把undo log里的歷史數(shù)據(jù)拿出來,覆蓋當前最新記錄。 還有做插入操作除了形成版本鏈為了支持事務(wù)隔離,mysql還做了一件工作,我們insert它就會在日志里記錄一條相反的sql delete,如果是delete 就會在日志里面記錄insert ,所以回滾的時候直接逆向的把歷史里的新增的sql全部再跑一遍,數(shù)據(jù)就恢復(fù)起來了。
這些多版本數(shù)據(jù)肯定是由mysql幫我們維護,它我們就稱之為MVCC多版本控制。 上面的一個一個版本,我們可以稱之為一個一個的快照。
當前外面的記錄就是最新記錄,undo log里的是歷史記錄。如果一直對一個表的信息進行修改,難道要一直給我形成版本鏈嗎。undo log是不是就有可能被塞滿了?
首先undo log是一個臨時緩存區(qū),它里面保存的歷史版本通常指的是這個事務(wù)運行期間,但是這個事務(wù)一旦提交了,這個undo log里面的對于這個事務(wù)的歷史版本就會free掉。那什么時候undo log里的歷史數(shù)據(jù)還要呢,有的人要訪問當前數(shù)據(jù),有的人要訪問歷史數(shù)據(jù),所以訪問當前數(shù)據(jù)的事務(wù)結(jié)束了并不代表歷史數(shù)據(jù)就要被清掉。換句話說undo log里面的數(shù)據(jù)有進就有出,出的時候沒有人用我的時候undo log就會被mysql自動清理,所以不用擔心打滿。
一些思考
如果一個事務(wù)已經(jīng)提交了是不能被回滾的,因為undo log被清理了!事務(wù)沒有被提交,不斷被修改時就不斷形成新的版本,這樣的話可以定點回滾或者整體回滾。
上面是以更新(upadte)主講的,如果是delete刪一條數(shù)據(jù)呢?一樣的,別忘了,刪數(shù)據(jù)不是清空,而是把被刪除的數(shù)據(jù)flag置為刪除。其實刪的時候也是可以把老的數(shù)據(jù)形成版本,然后再把當前版本的flag置為刪除。也可有自己的版本鏈。
如果是insert呢?因為insert是插入,也就是之前沒有數(shù)據(jù),那么insert也就沒有歷史版本。但是一般為了回滾操作,mysql內(nèi)部除了要把insert里的數(shù)據(jù)也放入undo log中也要記錄一下insert對于的語句delete,所以回滾的時候就把delete執(zhí)行一下。如果當前事務(wù)commit了,那么這個undolog 的歷史insert記錄就可以被清空了。
總結(jié)一下,也就是我們可以理解成,update和delete可以形成版本鏈,insert暫時不考慮。
那么select呢?
對數(shù)據(jù)做更新刪除插入肯定要加鎖因為要保證數(shù)據(jù)的安全??墒亲x寫并不會阻塞是可以同時跑的。但update、delete、insert一定修改的時最新數(shù)據(jù),歷史版本的數(shù)據(jù)沒有資格修改所以加鎖。select不會對數(shù)據(jù)做任何修改,所以,為select維護多版本,沒有意義。不過,此時有個問題,
就是: select讀取,是讀取最新的版本呢?還是讀取歷史版本?
當前讀:讀取最新的記錄,就是當前讀。增刪改,都叫做當前讀,select也有可能當前讀,比如:selectlock in share mode(共享鎖), select for update
快照讀:讀取歷史版本不讀最新記錄,就叫做快照讀。
歷史經(jīng)驗告送我,讀寫并發(fā)不管是在RC還是在RR級別下讀寫都可以并發(fā),寫寫要相互阻塞。讀寫并發(fā)并且根據(jù)隔離性的不同我們確實發(fā)現(xiàn),一個事務(wù)提交修改其他事務(wù)都有可能看不到,那么就注定了讀寫一定是不同的數(shù)據(jù)。為什么讀寫可以并發(fā)呢?
因為寫是寫的當前最新數(shù)據(jù),讀是讀的歷史版本,所以不會出現(xiàn)訪問同一個位置,就不需要加鎖,不需要加鎖就不會出現(xiàn)互相阻塞的情況,訪問不同的位置就沒有加鎖,我們就可以并發(fā)進行讀寫操作。
那一個事務(wù)把數(shù)據(jù)改了,但是另一個事務(wù)讀的還是老數(shù)據(jù),你告送我是有隔離性的體現(xiàn),所以隔離性本質(zhì)上在數(shù)據(jù)層面上隔離,再本質(zhì)是在版本上隔離!所以在不同隔離級別下看到的數(shù)據(jù)不一樣。因為有了MVCC有了多版本,所以我們可以理解讀寫并發(fā)的原因,我們也能理解隔離性它是怎么做到讓我們看到不同的數(shù)據(jù)。然后才有了那一個事物具體應(yīng)該看到那些版本,看到歷史的那些版本,要不要看到最新版本,那歷史有很多版應(yīng)該看那些版,這完完全全是由隔離級別決定。我們應(yīng)該看到那些版本。隔離性隔離版本是用MVCC實現(xiàn)的,回滾也是用MVCC來完成事務(wù)回滾的。
我們可以看到,在多個事務(wù)同時刪改查的時候,都是當前讀,是要加鎖的。那同時有select過來,如果也要讀取最新版(當前讀),那么也就需要加鎖,這就是串行化。
但如果select是快照讀,讀取歷史版本的話,是不受加鎖限制的 。不讓增刪改碰歷史版本只讓它們訪問最新版。而select只需要關(guān)心歷史版本,因為select都是讀沒有人改,所以讀歷史版本完全不用加鎖也就是可以并行執(zhí)行!換言之,提高了效率,即MVCC的意義所在。
那么,是什么決定了,select是當前讀,還是快照讀呢? 隔離級別! 就如RU讀未提交,一定讀的最新數(shù)據(jù)。RC/RR 讀的是歷史數(shù)據(jù)。
那為什么要有隔離級別呢?也就是說為什么要讓不同事務(wù)看到不同的版本? 事務(wù)都是原子的。所以,無論如何,事務(wù)到來時一定是有先有后。
但是經(jīng)過上面的操作我們發(fā)現(xiàn),事務(wù)從begin->CURD->commit,是有一個階段的。也就是事務(wù)有執(zhí)行前,執(zhí)行中,執(zhí)行后的階段。但,不管怎么啟動多個事務(wù),總是在啟動時有先有后的。
那么多個事務(wù)在執(zhí)行中,CURD操作是會交織在一起的。那么,為了保證事務(wù)的“有先有后”,比如后來的可以看到先來的數(shù)據(jù)等等,是不是應(yīng)該讓不同的事務(wù)看到它該看到的內(nèi)容,這就是所謂的隔離性與隔離級別要解決的問題。根據(jù)先后順序,讓不同事務(wù)看到不同內(nèi)容的問題。
最終總結(jié)一下:所謂隔離性就是讀取的時候看的是那些版本,看的歷史版本不一樣最終看到的數(shù)據(jù)也不一樣。最后應(yīng)該看到那些版本由隔離級別決定。
那為什么隔離級別RC和RR會看到不同的結(jié)果呢?所以我們要進入第三個預(yù)備知識 read view。
2.4 Read View
Read View就是事務(wù)進行 快照讀 操作的時候生產(chǎn)的 讀視圖 (Read View),在該事務(wù)執(zhí)行的快照讀的那一刻,會生成數(shù)據(jù)庫系統(tǒng)當前的一個快照,記錄并維護系統(tǒng)當前活躍事務(wù)的ID(當每個事務(wù)開啟時,都會被分配一個ID, 這個ID是遞增的,所以最新的事務(wù),ID值越大)
Read View 在 MySQL 源碼中,就是一個類,本質(zhì)是用來進行可見性判斷的。 即當我們某個事務(wù)執(zhí)行快照讀的時候,對該記錄創(chuàng)建一個 Read View 讀視圖,把它比作條件,用來判斷當前事務(wù)能夠看到哪個版本的數(shù)據(jù),既可能是當前最新的數(shù)據(jù),也有可能是該行記錄的 undo log 里面的某個版本的數(shù)據(jù)。
下面是 ReadView 結(jié)構(gòu),但為了減少負擔,我們簡化一下
class ReadView {
// 省略...
private:
/** 高水位,大于等于這個ID的事務(wù)均不可見*/
trx_id_t m_low_limit_id
/** 低水位:小于這個ID的事務(wù)均可見 */
trx_id_t m_up_limit_id;
/** 創(chuàng)建該 Read View 的事務(wù)ID*/
trx_id_t m_creator_trx_id;
/** 創(chuàng)建視圖時的其他活躍事務(wù)id列表*/
ids_t m_ids;
/** 配合purge,標識該視圖不需要小于m_low_limit_no的UNDO LOG,
* 如果其他視圖也不需要,則可以刪除小于m_low_limit_no的UNDO LOG*/
trx_id_t m_low_limit_no;
/** 標記視圖是否被關(guān)閉*/
bool m_closed;
// 省略...
};
上面的字段,我們僅需要關(guān)注下面四個
m_ids; //一張列表,用來維護Read View生成時刻,系統(tǒng)正活躍的事務(wù)ID
up_limit_id; //記錄m_ids列表中事務(wù)ID最小的ID(沒有寫錯)
low_limit_id; //ReadView生成時刻系統(tǒng)尚未分配的下一個事務(wù)ID,也就是目前已出現(xiàn)過的事務(wù)ID的最大值+1(也沒有寫錯)。并不是m_ids最大值+1。
比如說我是5號事務(wù),當我到來的時候我看到3,4,6號事務(wù)在運行,可能在我到來的時候整個系統(tǒng)早就有7、8、9早就跑完了。所以整個系統(tǒng)中分配最大的事務(wù)ID是9。系統(tǒng)尚未分配的下一個事務(wù)ID就是9+1 也就是10。
creator_trx_id //創(chuàng)建該ReadView的事務(wù)ID
現(xiàn)在我們既有了事務(wù)所對應(yīng)的read view,又有對數(shù)據(jù)進行修改而會生成的版本鏈。在實際讀取數(shù)據(jù)版本鏈的時候,是能讀取到每一個版本對應(yīng)的事務(wù)ID的,即:當前記錄的DB_TRX_ID 。
那么,我們現(xiàn)在手里面有的東西就有,當前快照讀的 ReadView 和 版本鏈中的某一個記錄的DB_TRX_ID 。
所以現(xiàn)在的工作就是怎么去根據(jù)當前事務(wù)的ReadView 里面字段的事務(wù)ID和當前版本鏈每一條記錄事務(wù)ID做對比,來確認該版本鏈中某條記錄應(yīng)不應(yīng)該看到。做這個不就是在做可見性判斷嗎。
所以現(xiàn)在的問題就是,當前快照讀,應(yīng)不應(yīng)該讀到版本鏈中某一個版本。一張圖,解決所有問題!
下面來解釋一下這個圖,上面是對表中數(shù)據(jù)進行修改形成的版本鏈,橫著的先代表時間的流逝,在時間的流逝中一定會存在很多的事務(wù),有的事務(wù)是已經(jīng)提交的,有的事務(wù)是正常操作的,有的是已經(jīng)操作完形成版本鏈后面才來的新事務(wù)。
我們的事務(wù)只要select快照都就會生成一個快照,里面會存著一些字段,我們重要關(guān)注m_ids,up_limit_id, low_limit_id,creator_trx_id這四個字段。只要creator_trx_id(創(chuàng)建該ReadView的事務(wù)ID) == DB_TRX_ID(版本鏈中事務(wù)的ID),意思就是我在遍歷這個版本鏈的時候發(fā)現(xiàn)版本鏈中事務(wù)的ID和我自己ReadView的事務(wù)ID是一樣的,這說明我現(xiàn)在正在查看的記錄就是我自己增加修改刪除更新,我自己做修改的我自己應(yīng)該看到!還有up_limit_id是我這個事務(wù)到來時我所看到活躍事務(wù)ID中最小的ID,如果遍歷版本鏈發(fā)現(xiàn)歷史記錄對應(yīng)的事務(wù)ID比我看到的所有活躍事務(wù)ID中最小的ID還要小,DB_TRX_ID < up_limit_id,我來了我看到的正在活躍事務(wù)列表m_ids中最小事務(wù)ID是up_limit_id,而現(xiàn)在版本鏈中更改記錄的事務(wù)ID比我所看到的正在運行事務(wù)的事務(wù)ID還要小,說明 這個DB_TRX_ID對應(yīng)的事務(wù)早就結(jié)束了早就提交了!因為它如果還在運行它也要被我看到,所以一個早就已經(jīng)提交早就結(jié)束的事務(wù),別人已經(jīng)結(jié)束我這個事務(wù)才來的,所以我應(yīng)該看到! 換句話只要版本鏈中記錄的事務(wù)ID比我看到正在和我以前跑的事務(wù)列表中事務(wù)ID最小值還要小,說明早就結(jié)結(jié)束了,我和你這個事務(wù)是串行執(zhí)行的沒有交叉,一定是你先跑完我才跑的,所以在這種條件判斷下,我一定能看到!
下面再看最右側(cè)快照后的事務(wù)。有可能我自己已經(jīng)來了,當我們來了之后形成的Read View,這里有個細節(jié),Read View是一個對象,new出來后值初始化之后,值就不變了 (這是一次的情況),相當于就是給它照了一個相??赡墚斘覄傂纬赏闞ead View就有新的事務(wù)來了,新的事務(wù)來的比我晚,而我所看的是low_limit_id是系統(tǒng)已經(jīng)分配的最大事務(wù)ID值+1,也就是還沒有分配的事務(wù)ID值,如果DB_TRX_ID (歷史版本中記錄的事務(wù)ID) >= low_limit_id(系統(tǒng)未分配的事務(wù)ID),也就是說這個記錄所對應(yīng)的事務(wù)ID比我自己所形成的Read view中我所看到的目前事務(wù)ID值還要大,那就證明當我在形成快照Read View的時候,這個事務(wù)還沒有它還沒有跑起來,如果跑起來了就會被我看到。說明它比我晚到,說明是形成Read View快照之后才提交的事務(wù),所以不應(yīng)該看到!
還有當我們在形成快照的時候,還有一些和我并發(fā)一塊跑的事務(wù)。我們的核心就是想根據(jù)事務(wù)ID判斷誰先誰后,根據(jù)先來后來判斷能不能看到。先來的不應(yīng)該看到后來的數(shù)據(jù)修改,后來應(yīng)該看到先來的數(shù)據(jù)修改。 就比如你是學(xué)弟你可以看到你學(xué)長找工作的情況,但你是學(xué)長你就看到你學(xué)弟找工作的情況。那正在和我并發(fā)運行的事務(wù)它們對數(shù)據(jù)進行的修改我應(yīng)不應(yīng)該看到呢?m_ids 是一張列表,用來維護Read View生成時刻,系統(tǒng)正活躍的事務(wù)ID。我們已經(jīng)把和我一塊并發(fā)跑的事務(wù)ID放到這個m_ids集合里了,這里就有個問題這里看到的事務(wù)ID一定是連續(xù)的嗎? 我們要記住記住一句話,事務(wù)到來一定有先有后,但事務(wù)不一定同時結(jié)束! 事務(wù)有常事務(wù)和短事務(wù),晚到的可能也早走。早來可能也晚走等。 比如:我我們有11、12、13、14、15號事務(wù),在快照前12、14提交了,那么快照到的:m_ids就是11、13、15,即:我們快照到的事務(wù)ID可以不連續(xù)! 如果版本鏈中的記錄的DB_TRX_ID不在m_ids列表中,說明這個事務(wù)在我形成Read View時已經(jīng)提交!可以看到. 如果在,說明該事務(wù)和我們的事務(wù)一樣都是活躍事務(wù),沒有commit。不應(yīng)該看到
所以我們能看到的事務(wù)有兩種場景,一、版本鏈中記錄的事務(wù)ID要小于我所看到的最小事務(wù)ID,說明你早就提交了。二、只要和我同時并發(fā)運行的事務(wù)ID,它如果不在我的m_ids中,就說明在我形成Read View的時候,它已經(jīng)提交了,所以我就能看到。
還有兩種是看不到的,一、版本鏈中記錄的事務(wù)ID比我所看到的最大事務(wù)ID還要大。二、版本鏈中記錄的事務(wù)ID在我m_ids里,說明和我在并發(fā)運行,此時就不應(yīng)該看到。
所以我們就可以使用Read View來進行來判斷那些事務(wù)能看到那些事務(wù)看不到。
對應(yīng)源碼策略:
如果查到不應(yīng)該看到當前版本,接下來就是遍歷下一個版本,直到符合條件,即可以看到。上面的readview 是當你進行select的時候,會自動形成。
read view是事務(wù)可見性的一個類,不是事務(wù)創(chuàng)建出來,就會有read view,而是當這個事務(wù)(已經(jīng)存在),首次進行快照讀的時候,mysql形成read view! 換句話說事務(wù)的建立和給這個事務(wù)形成read view是有一個時間窗口的,不一定立馬有,只有當快照讀的時候才有。
接下來read view具體的流程。 假設(shè)當前有條記錄:
nameageDB_TRX_ID(創(chuàng)建該記錄的事務(wù)ID)DB_ROW_ID(隱式主鍵)DB_ROLL_PTR(回滾指針)張三28null1null
事務(wù)操作:
事務(wù)1 [id=1]事務(wù)2 [id=2]事務(wù)3 [id=3]事務(wù)4 [id=4]事務(wù)開始事務(wù)開始事務(wù)開始事務(wù)開始………修改且已提交進行中快照讀進行中………
事務(wù)4:修改name(張三) 變成name(李四) 當 事務(wù)2 對某行數(shù)據(jù)執(zhí)行了 快照讀 ,數(shù)據(jù)庫為該行數(shù)據(jù)生成一個 Read View 讀視圖,然后初始化對應(yīng)字段
事務(wù)2的 Read View m_ids; // 1,3 up_limit_id; // 1 low_limit_id; // 4 + 1 = 5,原因:ReadView生成時刻,系統(tǒng)尚未分配的下一個事務(wù)ID creator_trx_id // 2
此時版本鏈是:
只有事務(wù)4修改過該行記錄,并在事務(wù)2執(zhí)行快照讀前,就提交了事務(wù)
我們的事務(wù)2在快照讀該行記錄的時候,就會拿版本鏈中的該行記錄的 DB_TRX_ID 去跟up_limit_id,low_limit_id和活躍事務(wù)ID列表(trx_list) 進行比較,判 斷當前事務(wù)2能看到該記錄的版本。
事務(wù)2的 Read View m_ids; // 1,3 up_limit_id; // 1 low_limit_id; // 4 + 1 = 5,原因:ReadView生成時刻,系統(tǒng)尚未分配的下一個事務(wù)ID creator_trx_id // 2 事務(wù)4提交的記錄對應(yīng)的事務(wù)ID DB_TRX_ID=4 比較步驟 DB_TRX_ID(4)< up_limit_id(1) ? 不小于,說明這個事務(wù)就不是我來前就提交的,下一步 DB_TRX_ID(4)>= low_limit_id(5) ? 不大于,說明這是事務(wù)也不是我形成Read view之后才來的事務(wù), 下一步 m_ids.contains(DB_TRX_ID) ? 不包含,說明,事務(wù)4不在當前的活躍事務(wù)中。 結(jié)論 故,事務(wù)4的更改,應(yīng)該看到。 所以事務(wù)2能讀到的最新數(shù)據(jù)記錄是事務(wù)4所提交的版本,而事務(wù)4提交的版本也是全局角度上最新的版本
2.5 RR 與 RC的本質(zhì)區(qū)別
下面先看當前讀和快照讀在RR級別下的區(qū)別
以加共享鎖方式進行讀取,對應(yīng)的就是當前讀。
select * from user lock in share mode
快照讀,讀的歷史版本,所以讀寫可以并發(fā)。
select * from user;
下面測試一下看看,設(shè)置RR模式下測試
set global transaction isolation level REPEATABLE READ;
依舊用之前的表
create table if not exists user(
id int primary key,
age int not null,
name varchar(16) not null
);
插入一條記錄,用來測試
insert into user (id, age, name) values (1, 15,'黃蓉');
測試用例1-表1:
事務(wù)A和事務(wù)B并發(fā)運行,都進行快照讀,快照讀讀的是歷史版本可是當前沒有歷史版本,沒有就讀的是最新數(shù)據(jù),不影響。但是事務(wù)A更新age=18,然后commit。因為改了數(shù)據(jù)所以一定會形成版本鏈。因為事務(wù)B是快照讀并且是RR級別所以只能讀到歷史版本。沒有讀到age=18,當我把查的過程強制改成當前讀,所以我應(yīng)該會讀到age=18。
事務(wù)A操作事務(wù)A描述事務(wù)B描述事務(wù)B操作begin開啟事務(wù)開啟事務(wù)beginselect * from user快照讀(無影響)查詢快照讀查詢select * from userupdate user setage=18 where id=1;更新age=18--commit提交事務(wù)--select 快照讀 ,沒有讀到age=18select * from userselect lock in share mode當前讀 , 讀到age=18select * from userlock in share mode
根據(jù)之前說的RR級別,你改了我看不到。 即使commit提交了,我也看不到
可是我們今天就想讀,之前都是select快照讀,今天改成select當前讀,讀到的不就是最新記錄了
其實如果在隔離性這里想讀到最新數(shù)據(jù)也是可以的。不過正常情況下select讀到的就是歷史版本。
那到底想說什么呢,我們把下面實驗也做一下,再說。
測試用例2-表2: 還是啟動事務(wù)A和事務(wù)B,先讓事務(wù)A把更新做了然后提交,讓事務(wù)B在事務(wù)A提交之后,然后事務(wù)B在查。
事務(wù)A操作事務(wù)A描述事務(wù)B描述事務(wù)B操作begin開啟事務(wù)開啟事務(wù)beginselect * from user快照讀,查到age=18--update user setage=28 where id=1;更新age=28--commit提交事務(wù)--select 快照讀 age=28select * from userselect lock in sharemode當前讀 age=28select * from userlock in share mode
啟動事務(wù)A和事務(wù)B,事務(wù)A在跑的時候事務(wù)B沒有做select。直到事務(wù)A提交了。
當事務(wù)A提交結(jié)束之后,事務(wù)Bselect看到了最新的修改。
說好的不是可重復(fù)讀呢?說好的隔離性呢? 修改了就看到了。 但你憑什么說你看到了?人家的隔離級叫可重復(fù)讀,可重復(fù)讀只需要保證第一次讀和最后一次和中間讀的數(shù)據(jù)只要是一樣就好了。你怎么知道你現(xiàn)在讀的數(shù)據(jù)就是最新的或者是最老的,你確定不了 ,所以只要保證前后數(shù)據(jù)讀的是一樣的不就好了嗎。RR級別是遵守的。換句話說上面的兩個例子,事務(wù)A操作沒編號,事務(wù)B僅僅是在事務(wù)Aupdate之前少做了一個select。那為什么會出現(xiàn)這樣大的差別,兩個事務(wù)同時跑,為什么上面就看不到更新,下面能看到呢?
上面看不到更新的原因是,事務(wù)B在和事務(wù)A同時運行時,事務(wù)B在select快照讀的時候,mysql就已經(jīng)給事務(wù)B形成一個Read View,形成Read View之后,進行快照讀,讀的時事務(wù)B快照的對象填的值任務(wù)事務(wù)A是和它一塊運行的,事務(wù)A就在事務(wù)B的m_ids列表中,所以事務(wù)B就看不到事務(wù)A提交的修改了。
下面看到的原因時,事務(wù)B和事務(wù)A同時起來,但是事務(wù)B并沒有在事務(wù)A運行的時候進行快照讀,并沒有形成Read View 對象,所以事務(wù)B并沒有記錄系統(tǒng)中任何并發(fā)事務(wù)的情況,當事務(wù)A把數(shù)據(jù)更新了提交了已經(jīng)結(jié)束了,事務(wù)B才select快照讀才形成Read View,形成Read View的時候事務(wù)B看和自己同時并發(fā)運行的事務(wù)時,事務(wù)A已經(jīng)不存在了,那么此時事務(wù)B看到m_ids列表中最小值都比事務(wù)A ID大,說明事務(wù)A在事務(wù)B來之前就已經(jīng)提交了,所以事務(wù)B此時就能看到事務(wù)A的修改。
換句話說, RR級別下Read View形成的時機不同,會影響事務(wù)的可見性! 可見性看的數(shù)據(jù)更新還是數(shù)據(jù)更老這個其實不重要,在RR級別下保證讀到的內(nèi)容是一致的這才重要。
結(jié)論:
事務(wù)中快照讀的結(jié)果是非常依賴該事務(wù)首次出現(xiàn)快照讀的地方,即某個事務(wù)中首次出現(xiàn)快照讀,決定該事務(wù)后續(xù)快照讀結(jié)果的能力delete同樣如此
RR 與 RC的本質(zhì)區(qū)別
正是Read View生成時機的不同,從而造成RC,RR級別下快照讀的結(jié)果的不同在RR級別下的某個事務(wù)的對某條記錄的第一次快照讀會創(chuàng)建一個快照及Read View, 將當前系統(tǒng)活躍的其他事務(wù)記錄起來此后在調(diào)用快照讀的時候,還是使用的是同一個Read View,所以只要當前事務(wù)在其他事務(wù)提交更新之前使用過快照讀,那么之后的快照讀使用的都是同一個Read View,既然是同一個Read View意味著Read View不變,Read View里面看到的并發(fā)事務(wù)ID情況也是不變的,也就意味著RR級別下可見性不變了。所以對之后的修改不可見;即RR級別下,快照讀生成Read View時,Read View會記錄此時所有其他活動事務(wù)的快照,這些事務(wù)的修改對于當前事務(wù)都是不可見的。因為跟我是同時并發(fā)的,只有不在m_ids列表我才認為我能看到,也就是早于Read View創(chuàng)建的事務(wù)所做的修改均是可見
一句話RR級別就只有一個Read View,而且不更新。在首次調(diào)用快照讀時形成。因為Read View不變,所以可見性不變,所以隨便怎么玩,看歷史版本時對于我當前RR級別你對數(shù)據(jù)做任何修改,改完提交,我都看不到。
而在RC級別下的,事務(wù)中,每次快照讀都會新生成一個快照和Read View, 也就是每一次在RC級別下快照讀的時候,mysql都要給我們重新形成Read View,因為每次都是新的,這就是我們在RC級別下的事務(wù)中可以看到別的事務(wù)提交的更新的原因。
因為每一次select快照讀都要形成Read View,而時間是一直往后走的,只要我一直向后不斷select,那么每次形成的Read View在時間上總是比較新的,你一個事務(wù)只要被提交了注定要被釋放掉放在歷史的版本鏈中,所以我的Read View在不斷時間線往后移的時候,我總是能看到你的提交的。這就是我們在RC級別下的事務(wù)中可以看到別的事務(wù)提交的更新的原因。
可能別的事務(wù)在這個時候是我和并發(fā)運行的,當它commit提交之后,然后我這個事務(wù)可能沒有結(jié)束可能會不斷select不斷形成新的Read View,可能上一次Read View我在和你并發(fā),下一次Read View你這個事務(wù)就提交了,我當然就可以看到你的提交了
總之在RC隔離級別下,是每個快照讀都會生成并獲取最新的Read View;而在RR隔離級別下,則是同一個事務(wù)中的第一個快照讀才會創(chuàng)建Read View, 之后的快照讀獲取的都是同一個Read View。所以RR級別下它的可見性就不變了,所以就讀不到別的事務(wù)的修改了。正是RC每次快照讀,都會形成Read View,所以,每次讀都有可能讀到別的事務(wù)的修改,所以,每次讀都有可能讀到不同的東西,所以,RC才會有不可重復(fù)讀問題。而RR級別它用的同一個Read View,它看到版本鏈該看多少就看多少,不會變了,所以在重復(fù)select不會出現(xiàn)數(shù)據(jù)變化,這就是可重復(fù)讀
所以RC和RR就是一層窗戶紙的關(guān)系,無非就是每次select快照讀要不要重新形成Read View,不形成就是RR,一直都在更新就是RC。
所以現(xiàn)在就可以理解為什么兩個不同的事務(wù),為什么可以進行讀寫并發(fā)好像不加鎖去訪問數(shù)據(jù)同一個數(shù)據(jù)呢。那是因為有歷史版本的存在,寫(增刪改)是當前讀,讀的都是當前數(shù)據(jù)。select是快照讀,讀的是歷史版本。MySQL底層用MVCC維護多版本,所以我們兩個訪問的根本就是不同版本的數(shù)據(jù),那就不需要加鎖。所以讀寫就可以直接并發(fā)。
還有為什么并發(fā)事務(wù),一個事務(wù)更新其他事務(wù)看不到,同理也是因為MVCC多版本的支持,因為讀到的是不同的版本。老版本歷史版本不變,數(shù)據(jù)不是放在版本鏈中,所以你當前做的任何更新,我讀歷史版本,你更新最新的,我怎么能看到你的結(jié)果,所以表現(xiàn)出一種隔離性。
為什么在RR和RC級別下能或者不能看到別人對應(yīng)的提交呢。取決于要不要重新給事務(wù)形成新的Read View。
如果在一個事務(wù)內(nèi)部如果操作成功了提交好說,失敗了回滾,憑什么回滾,不就是因為相反操作被記錄下來歷史版本鏈中有數(shù)據(jù),所以可以盡可能的做事務(wù)回滾。所謂回滾做兩件事情,第一事務(wù)內(nèi)部對應(yīng)的結(jié)構(gòu)體Read View對象釋放,第二將事務(wù)曾經(jīng)修改過的數(shù)據(jù)恢復(fù)成最開始。
讀未提交 都是當前讀也不要Read View,寫完我就能讀。RC和RR 已經(jīng)搞定了。串行化更不用說了,也都是當前讀只不過要加鎖。
3.讀-讀
不討論
4.寫-寫
現(xiàn)階段,直接理解成都是當前讀,當前不做深究
推薦閱讀 【MySQL筆記】正確的理解MySQL的MVCC及實現(xiàn)原理 詳細分析MySQL事務(wù)日志(redo log和undo log) 【MySQL】InnoDB 如何避免臟讀和不可重復(fù)讀
柚子快報激活碼778899分享:數(shù)據(jù)庫 【MySQL】事務(wù)二
好文推薦
本文內(nèi)容根據(jù)網(wǎng)絡(luò)資料整理,出于傳遞更多信息之目的,不代表金鑰匙跨境贊同其觀點和立場。
轉(zhuǎn)載請注明,如有侵權(quán),聯(lián)系刪除。