據(jù)庫(kù)MVCC詳解:從版本鏈到ReadView,一文講透并發(fā)控制)
這個(gè)問(wèn)題的背后其實(shí)是數(shù)據(jù)庫(kù)并發(fā)控制里最精巧的一套機(jī)制在起作用——MVCCMulti-Version Concurrency Control多版本并發(fā)控制。我剛接觸數(shù)據(jù)庫(kù)那會(huì)兒對(duì)“SELECT不阻塞UPDATE”這句話將信將疑直到自己動(dòng)手做了幾組實(shí)驗(yàn)、翻了源碼和官方文檔才真正把這條鏈路理清楚。這篇文章就把我這些年對(duì)MVCC的理解一次性講透包括核心原理、可見(jiàn)性判斷邏輯、實(shí)際場(chǎng)景推演還有生產(chǎn)環(huán)境里容易踩的坑。1. 為什么會(huì)有MVCC先看看沒(méi)有它會(huì)發(fā)生什么1.1 一個(gè)看似簡(jiǎn)單的問(wèn)題先回憶一下你日常寫(xiě)SQL的場(chǎng)景一個(gè)業(yè)務(wù)系統(tǒng)里事務(wù)A在統(tǒng)計(jì)報(bào)表執(zhí)行一條比較耗時(shí)的SELECT與此同時(shí)事務(wù)B在更新同一張表里的某條訂單記錄。在沒(méi)有特殊機(jī)制的情況下這兩條語(yǔ)句會(huì)互不相讓嗎會(huì)的只要在傳統(tǒng)的加鎖并發(fā)模型里這個(gè)問(wèn)題就繞不開(kāi)。數(shù)據(jù)庫(kù)為了保證數(shù)據(jù)一致性和事務(wù)隔離性最簡(jiǎn)單粗暴的辦法就是加鎖讀的時(shí)候加讀鎖寫(xiě)的時(shí)候加寫(xiě)鎖讀鎖和寫(xiě)鎖互斥。結(jié)果是SELECT要等UPDATE提交之后才能讀到數(shù)據(jù)UPDATE也要等SELECT釋放讀鎖之后才能動(dòng)手。對(duì)于高并發(fā)業(yè)務(wù)來(lái)說(shuō)這種串行化等待幾乎致命。那有沒(méi)有辦法讓SELECT和UPDATE各干各的有方向大致分成兩種一種是進(jìn)一步細(xì)分鎖粒度把鎖精確到行、甚至到索引項(xiàng)但鎖沖突還是存在另一種就是今天的主角——讓每一條數(shù)據(jù)記錄同時(shí)保留多個(gè)歷史版本讀的人各自讀取符合自己事務(wù)視角的版本寫(xiě)的人繼續(xù)更新最新版本。讀寫(xiě)之間不僅不需要互相等鎖還都能提交成功。這個(gè)思路就是MVCC。1.2 MVCC的設(shè)計(jì)哲學(xué)空間換時(shí)間MVCC的核心思想概括成一句話就是“以空間換時(shí)間”。傳統(tǒng)鎖機(jī)制把并發(fā)控制的重心放在“阻塞誰(shuí)”上MVCC則把重心放在“給誰(shuí)看哪個(gè)版本”上。每條記錄被更新時(shí)舊版本不會(huì)被立刻刪除而是以某種方式保留在系統(tǒng)里。新版本記錄和舊版本記錄之間通過(guò)指針形成一條版本鏈。當(dāng)某個(gè)事務(wù)執(zhí)行普通SELECT時(shí)數(shù)據(jù)庫(kù)會(huì)依據(jù)該事務(wù)啟動(dòng)時(shí)生成的一份額外信息ReadView即一致性視圖沿著版本鏈找到對(duì)當(dāng)前事務(wù)可見(jiàn)的那個(gè)版本。這個(gè)版本可能是最新數(shù)據(jù)也可能是一兩輪更新之前的數(shù)據(jù)。這就實(shí)現(xiàn)了所謂的一致性快照讀一個(gè)事務(wù)在同一個(gè)快照視角下多次讀取同一條記錄結(jié)果始終一致。而UPDATE這類寫(xiě)操作則直接操作最新版本并為它生成一個(gè)新版本掛到鏈上。用生活里的場(chǎng)景類比MVCC就像是圖書(shū)館里的“預(yù)約借閱機(jī)制”當(dāng)一個(gè)讀者在閱讀某本書(shū)的最新修訂稿時(shí)另一個(gè)讀者完全可以在隔壁看到這本書(shū)的舊版本或草稿版。新舊版本并存各取所需互不干擾。2. 深入MVCC核心機(jī)制隱藏列、版本鏈與ReadView2.1 一行數(shù)據(jù)比你想的多存了三個(gè)字段很多人以為數(shù)據(jù)庫(kù)表里的一行數(shù)據(jù)就只有你建表時(shí)定義的字段。實(shí)際上以InnoDB存儲(chǔ)引擎為例每一行還額外維護(hù)著幾個(gè)隱藏的字段MVCC能運(yùn)轉(zhuǎn)全靠它們。DB_TRX_ID表示最近一次插入或更新這行數(shù)據(jù)的事務(wù)ID。DB_ROLL_PTR回滾指針指向該行的上一個(gè)版本在undo log中的位置版本鏈通過(guò)它串聯(lián)起來(lái)。DB_ROW_ID隱式自增ID當(dāng)表沒(méi)有主鍵時(shí)InnoDB會(huì)自動(dòng)生成聚簇索引這個(gè)字段就作為聚簇索引的鍵值。對(duì)于MVCC來(lái)說(shuō)最關(guān)鍵的是前兩個(gè)字段。DB_TRX_ID相當(dāng)于這行數(shù)據(jù)的“最近修改者標(biāo)簽”ReadView在判斷可見(jiàn)性時(shí)核心就是拿當(dāng)前行記錄的DB_TRX_ID和ReadView里記錄的事務(wù)ID范圍做比較。DB_ROLL_PTR則負(fù)責(zé)把分散在undo log里的歷史版本串成一條完整的鏈保證回溯時(shí)能沿著鏈表逐級(jí)往上找。2.2 版本鏈?zhǔn)窃趺葱纬傻呐e個(gè)例子假設(shè)有一條用戶記錄id1name張三。初始插入事務(wù)ID為10那么DB_TRX_ID10回滾指針指向空。接著事務(wù)20執(zhí)行了UPDATE將name改成李四數(shù)據(jù)庫(kù)會(huì)執(zhí)行兩個(gè)動(dòng)作首先把當(dāng)前行舊內(nèi)容拷貝到undo log生成一個(gè)版本記錄然后在原行上更新name字段并將DB_TRX_ID改成20DB_ROLL_PTR指向剛才生成的undo記錄中。于是版本鏈就變成了兩層新版本DB_TRX_ID20name李四的DB_ROLL_PTR指向舊版本。舊版本DB_TRX_ID10name張三存儲(chǔ)在undo log中回滾指針為空。如果再有一次事務(wù)30執(zhí)行UPDATE把name改成王五版本鏈又會(huì)多一層。每次寫(xiě)操作都在鏈頭追加新版本舊版本依次向后移動(dòng)。查詢時(shí)從鏈頭沿著DB_ROLL_PTR一路往下找直到找到對(duì)當(dāng)前事務(wù)可見(jiàn)的版本為止。這里有個(gè)容易被忽略的細(xì)節(jié)undo log中的舊版本不僅要保留被修改前的字段值還會(huì)保留被修改前對(duì)應(yīng)的DB_TRX_ID和DB_ROLL_PTR。因?yàn)樵赗eadView判斷可見(jiàn)性時(shí)需要逐層讀取這些信息缺一不可。2.3 ReadView每個(gè)事務(wù)的“時(shí)間快照”有了版本鏈還需要一個(gè)判斷依據(jù)——到底哪些版本對(duì)當(dāng)前事務(wù)可見(jiàn)。這個(gè)判斷依據(jù)就是ReadView。ReadView的生成時(shí)機(jī)與隔離級(jí)別相關(guān)這點(diǎn)在后面會(huì)詳細(xì)展開(kāi)。先看ReadView里最重要的四個(gè)字段creator_trx_id創(chuàng)建這個(gè)ReadView的事務(wù)ID。m_ids在創(chuàng)建ReadView時(shí)當(dāng)前系統(tǒng)中所有未提交的讀寫(xiě)事務(wù)ID列表。min_trx_idm_ids中最小的事務(wù)ID。max_trx_id創(chuàng)建ReadView時(shí)系統(tǒng)即將分配給下一個(gè)事務(wù)的ID可以理解為當(dāng)前系統(tǒng)中最大的事務(wù)ID上限。判斷某個(gè)版本是否可見(jiàn)的規(guī)則其實(shí)可以歸納成幾條如果被訪問(wèn)版本的DB_TRX_ID等于creator_trx_id說(shuō)明這個(gè)版本就是當(dāng)前事務(wù)自己修改的肯定可見(jiàn)。如果DB_TRX_ID小于min_trx_id說(shuō)明該版本早在當(dāng)前事務(wù)啟動(dòng)前就已經(jīng)提交可見(jiàn)。如果DB_TRX_ID大于等于max_trx_id說(shuō)明該版本是在當(dāng)前事務(wù)啟動(dòng)之后才被創(chuàng)建不可見(jiàn)。如果DB_TRX_ID落在min_trx_id和max_trx_id之間還要再判斷它是否還在m_ids列表中如果在列表中說(shuō)明事務(wù)尚未提交不可見(jiàn)如果不在列表中說(shuō)明事務(wù)已經(jīng)提交可見(jiàn)。這四條規(guī)則就是MVCC可見(jiàn)性判斷的核心整個(gè)MVCC的“魔法”都濃縮在這幾步判斷里。我在剛學(xué)習(xí)的時(shí)候?yàn)榱税堰@幾條規(guī)則徹底記牢直接把規(guī)則抄在便簽紙上貼在顯示器旁邊做實(shí)驗(yàn)時(shí)對(duì)照著看很快就通了。2.4 RR和RC下ReadView生成時(shí)機(jī)的差異同一個(gè)MVCC機(jī)制在不同隔離級(jí)別下的表現(xiàn)會(huì)有明顯差異最典型的就是可重復(fù)讀RR和讀已提交RC兩種隔離級(jí)別。在可重復(fù)讀RR級(jí)別下事務(wù)第一次執(zhí)行普通SELECT時(shí)會(huì)生成一個(gè)ReadView并一直使用到事務(wù)結(jié)束。后續(xù)所有普通SELECT都復(fù)用這同一個(gè)ReadView所以整個(gè)事務(wù)期間看到的數(shù)據(jù)都是一致的就像事務(wù)開(kāi)始時(shí)拍了張“照片”之后一直看這張照片。在讀已提交RC級(jí)別下事務(wù)中每執(zhí)行一次普通SELECT都會(huì)生成一個(gè)新的ReadView。這樣不同時(shí)間點(diǎn)的SELECT能看到不同的已提交數(shù)據(jù)所以RC級(jí)別下同一個(gè)事務(wù)中兩次SELECT可能讀到不同的結(jié)果也就是不可重復(fù)讀。兩條規(guī)則放在一起就能理解為什么RR能解決一部分“換讀”問(wèn)題以及為什么在RC下MVCC的語(yǔ)義會(huì)有所放寬。這也是面試中經(jīng)常被追問(wèn)的細(xì)節(jié)把這塊弄明白比背十遍八股文都管用。3. 快照讀與當(dāng)前讀SELECT為什么不需要等鎖3.1 快照讀的完整執(zhí)行流程普通SELECT語(yǔ)句在InnoDB里被稱為一致性非鎖定讀Consistent Nonlocking Read也叫快照讀。它不加任何鎖直接生成或復(fù)用ReadView然后沿著版本鏈查找對(duì)當(dāng)前事務(wù)可見(jiàn)的版本。執(zhí)行流程大致如下事務(wù)開(kāi)始根據(jù)隔離級(jí)別確定ReadView的生成策略。定位到目標(biāo)記錄讀取行上的DB_TRX_ID。將DB_TRX_ID與ReadView中的min_trx_id、max_trx_id、m_ids進(jìn)行比較。如果可見(jiàn)直接返回該版本的數(shù)據(jù)。如果不可見(jiàn)通過(guò)DB_ROLL_PTR跳轉(zhuǎn)到鏈表中的下一個(gè)舊版本重復(fù)步驟3的判斷。直到找到可見(jiàn)版本或者遍歷完整個(gè)版本鏈。因?yàn)檎麄€(gè)過(guò)程不涉及鎖所以快照讀永遠(yuǎn)不會(huì)被其他事務(wù)的鎖阻塞也不需要等待其他事務(wù)提交。這就是“SELECT從來(lái)不阻塞UPDATE”的第一層含義SELECT本身不拿鎖UPDATE自然不用等它。3.2 當(dāng)前讀UPDATE為什么必須加鎖與快照讀相對(duì)的是當(dāng)前讀Current Read也叫鎖定讀。UPDATE、DELETE、INSERT以及SELECT ... FOR UPDATE、SELECT ... LOCK IN SHARE MODE 都走當(dāng)前讀。當(dāng)前讀讀取的是記錄的最新版本而不是歷史版本并且在讀取時(shí)需要獲取對(duì)應(yīng)的行鎖或間隙鎖。為什么寫(xiě)操作必須用當(dāng)前讀原因很簡(jiǎn)單寫(xiě)操作要修改數(shù)據(jù)如果基于舊版本去更新那后提交的事務(wù)就會(huì)把先提交的修改覆蓋掉導(dǎo)致丟失更新。所以UPDATE必須先拿到最新版本并加鎖確保在修改期間沒(méi)有其他事務(wù)能同時(shí)寫(xiě)同一條記錄。以UPDATE為例執(zhí)行過(guò)程大致是執(zhí)行器定位到目標(biāo)行。加X(jué)鎖排他鎖防止其他事務(wù)并發(fā)修改同一行。讀取最新版本的數(shù)據(jù)。生成undo log復(fù)制舊版本信息。修改行內(nèi)容更新DB_TRX_ID調(diào)整DB_ROLL_PTR。如果當(dāng)前事務(wù)尚未提交其他事務(wù)的當(dāng)前讀操作會(huì)被阻塞但快照讀不受影響。這里有個(gè)關(guān)鍵認(rèn)知MVCC并沒(méi)有完全取代鎖。它解決的是讀寫(xiě)之間的互斥而寫(xiě)寫(xiě)之間的沖突仍然要靠鎖來(lái)解決。兩個(gè)事務(wù)同時(shí)UPDATE同一條記錄時(shí)后發(fā)起的UPDATE必須等待先獲取鎖的事務(wù)釋放鎖否則就會(huì)一直阻塞。3.3 用一個(gè)時(shí)間線推演SELECT和UPDATE的完整并發(fā)過(guò)程為了更直觀我們用一張表格推演一遍。事務(wù)A執(zhí)行SELECT事務(wù)B執(zhí)行UPDATE隔離級(jí)別采用可重復(fù)讀RR。時(shí)刻事務(wù)ASELECT事務(wù)BUPDATEt1開(kāi)啟事務(wù)生成ReadView開(kāi)啟事務(wù)t2SELECT id1讀取DB_TRX_ID10的舊版本返回結(jié)果暫時(shí)未操作t3SELECT id1再次讀取仍返回DB_TRX_ID10的版本UPDATE id1獲取X鎖新增DB_TRX_ID30的新版本t4SELECT id1繼續(xù)讀取舊版本不受t3 UPDATE影響COMMIT在這個(gè)推演里事務(wù)A在t3時(shí)刻第二次SELECT時(shí)數(shù)據(jù)已經(jīng)變了但它因?yàn)槭褂玫氖鞘聞?wù)開(kāi)始時(shí)生成的ReadView讀到的依然是舊版本。整個(gè)過(guò)程里事務(wù)A從未被阻塞事務(wù)B的UPDATE也從未因?yàn)镾ELECT的存在而停下來(lái)。這正是MVCC在RR隔離級(jí)別下最典型的應(yīng)用場(chǎng)景。那如果事務(wù)A執(zhí)行的是SELECT ... FOR UPDATE呢抱歉當(dāng)前讀才會(huì)被阻塞。事務(wù)B一旦持有X鎖事務(wù)A的FOR UPDATE會(huì)進(jìn)入鎖等待狀態(tài)一直到事務(wù)B提交或回滾。4. 不同數(shù)據(jù)庫(kù)的MVCC實(shí)現(xiàn)差異4.1 InnoDB的undo log方案前面講的主要是InnoDB的實(shí)現(xiàn)。InnoDB把舊版本數(shù)據(jù)統(tǒng)一存放在系統(tǒng)表空間或獨(dú)立undo表空間中行記錄中只保留一個(gè)回滾指針通過(guò)它指向undo log中的舊版本。優(yōu)點(diǎn)是主表記錄體積小、結(jié)構(gòu)固定缺點(diǎn)是版本鏈過(guò)長(zhǎng)時(shí)回溯歷史版本需要頻繁訪問(wèn)undo log產(chǎn)生額外的隨機(jī)IO。在InnoDB中版本鏈的長(zhǎng)度主要由并發(fā)更新次數(shù)決定。一個(gè)被大量更新過(guò)的熱記錄版本鏈可能非常長(zhǎng)遇到長(zhǎng)事務(wù)時(shí)讀操作就需要沿著版本鏈逐級(jí)查找性能會(huì)有所下降。4.2 PostgreSQL的堆內(nèi)多版本方案PostgreSQL采用了另一種思路舊版本直接留在原始數(shù)據(jù)頁(yè)堆表中新的更新會(huì)在同一數(shù)據(jù)頁(yè)上追加一條新的元組tuple。相關(guān)事務(wù)ID信息xmin、xmax直接寫(xiě)進(jìn)元組頭。然后通過(guò)xmax標(biāo)記舊元組是否失效當(dāng)清理進(jìn)程VACUUM運(yùn)行時(shí)再把這些死亡元組從頁(yè)面上物理清除。這種方案的優(yōu)點(diǎn)是讀取舊版本時(shí)不需要訪問(wèn)額外的undo區(qū)域局部性更好缺點(diǎn)是同一數(shù)據(jù)頁(yè)上可能堆積大量死亡元組造成表膨脹bloat而且清理不及時(shí)的話會(huì)顯著影響查詢性能。4.3 其他數(shù)據(jù)庫(kù)的取舍Oracle同樣使用undo表空間方案與InnoDB類似但Oracle的undo管理更成熟支持自動(dòng)調(diào)優(yōu)和保留期設(shè)置。SQL Server默認(rèn)使用基于鎖的隔離機(jī)制但提供了READ_COMMITTED_SNAPSHOT選項(xiàng)和快照隔離級(jí)別開(kāi)啟后也走行版本控制路線在tempdb中保存版本數(shù)據(jù)。這些差異說(shuō)明MVCC沒(méi)有一個(gè)放之四海而皆準(zhǔn)的樣板核心思想一致工程實(shí)現(xiàn)上各有取舍。4.4 實(shí)現(xiàn)方案對(duì)比對(duì)比維度InnoDBundo log方案PostgreSQL堆內(nèi)多版本方案舊版本存儲(chǔ)位置獨(dú)立undo log區(qū)域原數(shù)據(jù)頁(yè)內(nèi)的新增元組行記錄額外信息DB_TRX_ID、DB_ROLL_PTRxmin、xmax等元組頭字段歷史版本回溯代價(jià)可能跨頁(yè)隨機(jī)訪問(wèn)IO成本較高同頁(yè)內(nèi)查找局部性好但頁(yè)內(nèi)易堆積死亡元組空間回收機(jī)制purge線程異步清理undo記錄VACUUM進(jìn)程回收死亡元組典型膨脹風(fēng)險(xiǎn)undo表空間膨脹表和索引膨脹5. 生產(chǎn)環(huán)境中的MVCC問(wèn)題排查與調(diào)優(yōu)5.1 版本膨脹undo失控與表膨脹MVCC在帶來(lái)巨大并發(fā)收益的同時(shí)也埋了一個(gè)隱患——舊版本不會(huì)立即消失。如果清理不及時(shí)就會(huì)出問(wèn)題。對(duì)InnoDB來(lái)說(shuō)undo log不能被purge線程及時(shí)回收。一旦purge線程落后undo表空間會(huì)持續(xù)膨脹磁盤占用暴漲數(shù)據(jù)庫(kù)整體性能下滑。常見(jiàn)場(chǎng)景有超長(zhǎng)事務(wù)長(zhǎng)時(shí)間不提交、高頻UPDATE海量數(shù)據(jù)后事務(wù)不結(jié)束等。對(duì)PostgreSQL來(lái)說(shuō)死亡元組的堆積會(huì)造成表和索引膨脹。這個(gè)更直觀表占用的磁盤空間是實(shí)際數(shù)據(jù)的數(shù)倍但vacuum一直跟不上查詢還要掃描大量死亡元組性能自然變差。5.2 長(zhǎng)事務(wù)是版本膨脹的源頭版本膨脹最核心的根源就是長(zhǎng)事務(wù)。某個(gè)事務(wù)一直不提交就會(huì)導(dǎo)致另一個(gè)關(guān)鍵問(wèn)題被它創(chuàng)建的舊版本對(duì)數(shù)據(jù)庫(kù)來(lái)說(shuō)“未來(lái)可能還需要可見(jiàn)”因此purge線程不敢清理這些版本鏈上的任何內(nèi)容。只要這個(gè)長(zhǎng)事務(wù)存在所有它改動(dòng)過(guò)的記錄以及與之相關(guān)的版本鏈只能一直保留。我見(jiàn)過(guò)一個(gè)具體案例某業(yè)務(wù)系統(tǒng)里一個(gè)后臺(tái)統(tǒng)計(jì)作業(yè)每天運(yùn)行兩個(gè)多小時(shí)期間不做任何提交。結(jié)果那段時(shí)間內(nèi)所有被更新的業(yè)務(wù)數(shù)據(jù)undo log膨脹到平時(shí)幾十倍。更麻煩的是長(zhǎng)事務(wù)還會(huì)導(dǎo)致其他事務(wù)的version鏈路無(wú)法被回收形成連鎖反應(yīng)。5.3 當(dāng)前讀帶來(lái)的阻塞問(wèn)題MVCC能解決的只是普通SELECT和寫(xiě)操作之間的阻塞關(guān)系。一旦業(yè)務(wù)SQL里出現(xiàn)了SELECT ... FOR UPDATE或者UPDATE本身MVCC就管不著了。兩個(gè)業(yè)務(wù)同時(shí)更新同一行照樣會(huì)出現(xiàn)鎖等待鎖等待超時(shí)后會(huì)直接報(bào)錯(cuò)返回。不少同學(xué)在這個(gè)地方踩坑以為有了MVCC就可以在所有場(chǎng)景下不用考慮鎖競(jìng)爭(zhēng)。事實(shí)上FOR UPDATE場(chǎng)景下只要事務(wù)A一直持有X鎖不提交事務(wù)B的當(dāng)前讀就會(huì)一直阻塞直到鎖等待超時(shí)。排查這類問(wèn)題時(shí)要看的是鎖等待狀態(tài)和事務(wù)持有鎖的情況而不是MVCC層面的內(nèi)容。5.4 監(jiān)控視圖與排查命令實(shí)際工作中排查MVCC相關(guān)問(wèn)題主要看幾個(gè)監(jiān)控指標(biāo)和系統(tǒng)視圖。InnoDB場(chǎng)景下可以查看information_schema里的 INNODB_TRX、INNODB_LOCKS、INNODB_LOCK_WAITS 表以及通過(guò) SHOW ENGINE INNODB STATUS 查看事務(wù)和鎖信息。重點(diǎn)關(guān)注是否存在運(yùn)行時(shí)間特別長(zhǎng)的事務(wù)。undo log的增長(zhǎng)速率。有沒(méi)有大量purge線程跟不上或鎖等待事件。PostgreSQL場(chǎng)景下查詢 pg_stat_activity 可以看活躍事務(wù)查 pg_stat_user_tables 可以看n_dead_tup死亡元組數(shù)量。如果n_dead_tup持續(xù)增長(zhǎng)且比活元組數(shù)量還多就該手動(dòng)觸發(fā)VACUUM ANALYZE或者檢查autovacuum配置了。6. 實(shí)戰(zhàn)心得與避坑建議踩過(guò)幾次坑之后我總結(jié)了幾條MVCC相關(guān)的經(jīng)驗(yàn)分享出來(lái)供大家參考。第一正確理解“SELECT不阻塞UPDATE”的適用范圍。普通SELECT走快照讀時(shí)確實(shí)不會(huì)被UPDATE阻塞但開(kāi)發(fā)人員在實(shí)際編寫(xiě)系統(tǒng)時(shí)要清楚FOR UPDATE和UPDATE本身仍存在鎖等待。別把“讀不阻塞寫(xiě)”理解成“所有讀都不阻塞寫(xiě)”。第二隔離級(jí)別的選擇會(huì)直接影響MVCC行為。RR級(jí)別下ReadView只生成一次同一個(gè)事務(wù)內(nèi)多次SELECT結(jié)果一致。如果業(yè)務(wù)不需要這種強(qiáng)一致可以考慮用RC級(jí)別來(lái)降低快照保持時(shí)間也能間接減輕一些版本保留的壓力。第三盡量避免在事務(wù)中做大范圍更新也盡量避免長(zhǎng)事務(wù)。事務(wù)里UPDATE行數(shù)越多、事務(wù)運(yùn)行時(shí)間越長(zhǎng)undo log、版本鏈和各類鎖的占用就越大可能直接拖垮整個(gè)實(shí)例。我個(gè)人的習(xí)慣是把大事務(wù)拆成小批次提交每一步都盡快結(jié)束事務(wù)。第四監(jiān)控一定要落地。數(shù)據(jù)庫(kù)沒(méi)有出現(xiàn)問(wèn)題時(shí)你可能感受不到MVCC的代價(jià)一旦版本開(kāi)始膨脹往往已經(jīng)是事故現(xiàn)場(chǎng)。提前配好undo增長(zhǎng)監(jiān)控、鎖等待監(jiān)控和長(zhǎng)事務(wù)監(jiān)控真的能省很多半夜救火的痛苦。MVCC是數(shù)據(jù)庫(kù)世界里少有的“讀寫(xiě)雙贏”機(jī)制它讓讀操作幾乎無(wú)條件地讓路于寫(xiě)操作又通過(guò)版本隔離保障了事務(wù)的一致性視圖。但理解它、用好它不能只停留在“SELECT不阻塞UPDATE”這一句話上。我希望這篇文章能幫你把MVCC的工作原理、可見(jiàn)性判斷規(guī)則和實(shí)際運(yùn)維中的關(guān)鍵點(diǎn)串起來(lái)下次再有人問(wèn)起這個(gè)問(wèn)題時(shí)你能講得比他期待的更透。