
MVCC多版本并發(fā)控制是 MySQL InnoDB 實(shí)現(xiàn)并發(fā)讀的底層機(jī)制事務(wù)隔離級別是數(shù)據(jù)庫定義的事務(wù)之間可見性的規(guī)則標(biāo)準(zhǔn)。InnoDB 使用 MVCC 鎖 共同實(shí)現(xiàn) 4 種隔離級別。一、MVCC 核心原理InnoDBMVCC 的核心思想每行數(shù)據(jù)保存多個版本讀的時候讀快照版本寫的時候新建版本不加鎖實(shí)現(xiàn)讀寫不阻塞??煺毡举|(zhì)就是某一個時間點(diǎn)數(shù)據(jù)庫的數(shù)據(jù)邏輯鏡像。它并不是把整份數(shù)據(jù)復(fù)制一份存起來而是依靠Read View undo log 版本鏈拼接出來的虛擬視圖。1. 隱藏列每條記錄自帶DB_TRX_ID最近修改這條記錄的事務(wù) ID新增 / 更新時寫入DB_ROLL_PTR回滾指針指向undo log里的舊版本數(shù)據(jù)形成版本鏈DB_ROW_ID隱藏主鍵沒有主鍵時使用undo log作用保存數(shù)據(jù)修改前的舊版本用于回滾 構(gòu)建快照讀。undo log回滾日志邏輯日志記錄的是數(shù)據(jù)變更的邏輯內(nèi)容不是磁盤上的物理字節(jié)。undo log 記錄的是反向操作比如執(zhí)行update t set nameb where id1;undo log 里存的是id1name原來的值是a當(dāng)事務(wù)回滾時直接把舊值寫回去。它不關(guān)心數(shù)據(jù)頁在磁盤上長什么樣只關(guān)心行數(shù)據(jù)的邏輯變更所以叫邏輯日志。2. Read View讀視圖MVCC 核心Read View快照的「可見性規(guī)則」記錄生成快照瞬間所有活躍事務(wù) ID用來判斷版本能不能看見。Read View 四個核心字段m_ids當(dāng)前系統(tǒng)里所有活躍未提交事務(wù) ID 集合開啟了但還沒 commit 的事務(wù)min_trx_idm_ids里面最小的事務(wù) IDmax_trx_id下一個將要分配的事務(wù) ID不是 m_ids 最大值是全局事務(wù) ID 計(jì)數(shù)器creator_trx_id創(chuàng)建這個 Read View 的當(dāng)前事務(wù) ID可見性判斷規(guī)則判斷某行記錄的trx_id記錄行上的trx_id創(chuàng)建這行版本的事務(wù) ID如果trx_id min_trx_id可見事務(wù)早已提交如果trx_id max_trx_id不可見這個事務(wù)是創(chuàng)建 Read View 之后才開啟的如果min_trx_id trx_id max_trx_id若trx_id在m_ids集合中不可見事務(wù)還沒提交不在m_ids可見事務(wù)已經(jīng)提交如果當(dāng)前版本不可見就順著 undo log 往前找歷史版本重復(fù)判斷直到找到可見版本或者沒有歷史版本返回空??煺兆xSnapshot Read不加鎖的普通 SELECT 查詢就是快照讀。讀取的是快照版本的數(shù)據(jù)依靠 MVCCRead View undo log 版本鏈實(shí)現(xiàn)不加行鎖。核心特點(diǎn)不加鎖讀寫不阻塞。寫操作修改最新行讀操作去讀 undo log 里的歷史版本互不阻塞。讀到的不是當(dāng)前最新數(shù)據(jù)而是快照可見版本。底層依賴Read View undo log 版本鏈。什么時候生成 Read ViewRR可重復(fù)讀InnoDB 默認(rèn)隔離級別事務(wù)中第一次快照讀的時候生成 1 個 Read View整個事務(wù)復(fù)用這一個。? 所以 RR 可以解決幻讀MVCC 層面因?yàn)檎麄€事務(wù)快照不變。RC讀已提交每一次快照讀都會重新生成一個新的 Read View。? 所以 RC 可以讀到別的事務(wù)已提交的數(shù)據(jù)無法防止幻讀。?? 重點(diǎn)區(qū)分 RC每次 select 都新建 ReadView RR事務(wù)第一次 select 才創(chuàng)建 ReadView后續(xù) select 復(fù)用同一個總結(jié)Read View 就是快照讀的一個時間快照記錄生成快照那一刻所有活躍事務(wù)用來過濾 undo log 版本鏈決定哪一行舊數(shù)據(jù)能被當(dāng)前事務(wù)讀到。事務(wù)執(zhí)行快照讀普通 select時生成一個 Read View用來判斷這條記錄的版本對當(dāng)前事務(wù)是否可見。 Read View 4 個字段m_ids當(dāng)前活躍未提交事務(wù) ID 集合min_trx_idm_ids 里最小事務(wù) IDmax_trx_id下一個將要分配的事務(wù) IDcreator_trx_id當(dāng)前事務(wù)自己的 ID可見性判斷規(guī)則重點(diǎn)拿到記錄的DB_TRX_ID和 Read View 對比DB_TRX_ID min_trx_id事務(wù)已經(jīng)提交 →可見DB_TRX_ID max_trx_id事務(wù)在 Read View 之后開啟 →不可見min_trx_id DB_TRX_ID max_trx_id在活躍區(qū)間如果DB_TRX_id在m_ids里事務(wù)還沒提交 →不可見不在 m_ids已經(jīng)提交 →可見如果當(dāng)前版本不可見順著roll_ptr去 undo log 找版本鏈上更早版本繼續(xù)判斷。數(shù)據(jù)最新版本物理行磁盤上當(dāng)前這條記錄DB_TRX_ID 是最后修改它的事務(wù)這是當(dāng)前版本update/delete/select ... for update當(dāng)前讀就讀這個??煺瞻姹臼聞?wù)在某個時間點(diǎn)生成 Read View 之后根據(jù)這個視圖規(guī)則沿著 undo log 版本鏈找到對當(dāng)前事務(wù)可見的那一條歷史版本就是快照版本。?? 重點(diǎn)快照版本不會復(fù)制一份新數(shù)據(jù)到磁盤只是復(fù)用 undo log 里舊記錄靠版本鏈跳轉(zhuǎn)讀取所以開銷很小。3. 快照讀 vs 當(dāng)前讀快照讀普通 SELECT走 MVCC讀取快照版本不加行鎖讀寫不阻塞。select * from table;當(dāng)前讀加鎖讀讀取最新已提交版本會加行鎖不走 MVCC 快照。select ... for update / lock in share mode; update / delete / insert? MVCC 解決的是快照讀的并發(fā)問題當(dāng)前讀靠鎖。二、SQL 標(biāo)準(zhǔn) 4 種隔離級別 InnoDB 實(shí)現(xiàn)方式隔離級別臟讀不可重復(fù)讀幻讀InnoDB 實(shí)現(xiàn)方案讀未提交 Read Uncommitted?存在?存在?存在不使用 MVCC直接讀最新數(shù)據(jù)讀已提交 Read CommittedRC?解決?存在?存在每次快照讀都生成新 Read View可重復(fù)讀 Repeatable ReadRRMySQL 默認(rèn)?解決?解決?? 快照讀解決當(dāng)前讀仍存在靠間隙鎖部分抑制事務(wù)第一次快照讀時生成 Read View整個事務(wù)復(fù)用這一個 Read View串行化 Serializable?解決?解決?解決關(guān)閉 MVCC所有 select 自動轉(zhuǎn)為當(dāng)前讀加共享鎖完全串行逐個拆解1. 讀未提交 RU事務(wù)可以讀到其他事務(wù)未提交的數(shù)據(jù)。 InnoDB 這個級別不使用 MVCC直接讀最新行數(shù)據(jù)。幾乎不會使用。2. 讀已提交 RC只能讀到已經(jīng)提交的數(shù)據(jù)杜絕臟讀每次 select 快照讀都會生成全新 Read View現(xiàn)象同一個事務(wù)先后兩次 select中間別的事務(wù)提交修改兩次讀到不一樣數(shù)據(jù) →不可重復(fù)讀RC 沒有間隙鎖所以幻讀問題也存在。3. 可重復(fù)讀 RRMySQL InnoDB 默認(rèn)隔離級別核心事務(wù)第一次快照讀的時候創(chuàng)建 Read View后續(xù)快照讀全程復(fù)用這一個視圖所以同一個事務(wù)多次快照讀看到的數(shù)據(jù)和第一次一致解決不可重復(fù)讀。幻讀說明面試高頻 幻讀一個事務(wù)內(nèi)同樣的查詢第二次查出多 / 少了新插入的數(shù)據(jù)??煺兆xMVCC 快照里不包含新插入記錄快照讀看不到幻讀當(dāng)前讀for update/updateMVCC 快照無效會讀到新插入行仍然存在幻讀InnoDB RR 通過間隙鎖 臨鍵鎖阻止其他事務(wù)插入抑制幻讀不是徹底消除RC vs RR MVCC 最大區(qū)別一句話 RC每次 select 新建 ReadViewRR事務(wù)第一次 select 才創(chuàng)建 ReadView復(fù)用。4. 串行化 Serializable最高隔離級別MVCC 失效。 普通select自動等價于select ... lock in share mode全部變成加鎖當(dāng)前讀讀寫互相阻塞完全串行執(zhí)行性能最差。臟讀、不可重復(fù)讀、幻讀全部解決。三、經(jīng)典現(xiàn)象舉例RR事務(wù) ARRbegin; select * from user where id1; -- 第一次快照讀生成ReadViewversion100 -- 此時事務(wù)B開啟修改id1并commit select * from user where id1; -- 復(fù)用舊ReadView仍然讀到version100不會讀到B提交的數(shù)據(jù) → 可重復(fù)讀同樣場景RC 下第二次 select 會新建 ReadView讀到事務(wù) B 提交后的新數(shù)據(jù)出現(xiàn)不可重復(fù)讀。四、引入 Seata AT 之后數(shù)據(jù)庫隔離級別推薦是什么?生產(chǎn)推薦MySQL 使用 RC讀已提交口述版 Seata AT 模式官方推薦數(shù)據(jù)庫隔離級別為 RC。 原因Seata AT 的核心原理一階段執(zhí)行業(yè)務(wù) SQL生成 undo_log 快照提交本地事務(wù)二階段提交直接刪 undo_log回滾時通過 undo_log 恢復(fù)數(shù)據(jù)。如果用 RR可重復(fù)讀MySQL RR 是基于 MVCC 快照。事務(wù)開啟時就拿到快照。一階段本地事務(wù)提交了但其他事務(wù)讀到的還是舊快照此時如果發(fā)生二階段回滾通過 undo_log 恢復(fù)數(shù)據(jù)別的事務(wù)的 MVCC 快照看不到回滾后的數(shù)據(jù)會讀到臟數(shù)據(jù)出現(xiàn)分布式臟讀。RC 下每次查詢都會獲取最新快照一旦二階段回滾完成其他事務(wù)馬上讀到最新數(shù)據(jù)規(guī)避上面這個分布式臟讀問題。補(bǔ)充RR 不是不能用只是坑多不推薦生產(chǎn)Seata 本身不會改變 MySQL 底層隔離級別隔離級別是數(shù)據(jù)庫層面配置Seata 只是協(xié)調(diào)分布式事務(wù)。一句話記憶Seata AT 推薦 RC避免 RR 的 MVCC 快照長時間持有造成分布式臟讀注意區(qū)分MySQL 隔離級別控制庫內(nèi)事務(wù)可見性Seata 全局事務(wù)隔離級別是 Seata 自己的概念有讀未提交、讀已提交和數(shù)據(jù)庫隔離級別不是一回事。 Seata 全局隔離級別默認(rèn)是讀未提交可以配置為讀已提交但性能會下降。五、引入 Seata 之后MVCC 會帶來什么問題重點(diǎn)講AT 模式下 MySQL MVCC 帶來的 2 個核心問題面試高頻問題 1RR 隔離級別下的分布式臟讀最重要場景全局事務(wù) G1AT 模式G1 一階段本地事務(wù)執(zhí)行 update寫入 undo_log本地事務(wù)提交此時數(shù)據(jù)庫數(shù)據(jù)已經(jīng)改了undo_log 保留此時全局事務(wù)還沒二階段提交屬于全局未提交狀態(tài)另一個全局事務(wù) G2MySQL 隔離級別 RRG2 事務(wù)開啟時拿到 MVCC 舊快照G2 查詢這行拿到的是快照里舊數(shù)據(jù)G1 修改前的數(shù)據(jù)這時 G1 發(fā)生回滾二階段通過 undo_log 把數(shù)據(jù)恢復(fù)回去G2 繼續(xù)查詢?nèi)匀皇撬聞?wù)一開始的快照讀到的還是舊數(shù)據(jù)。現(xiàn)象全局事務(wù)已經(jīng)回滾但 G2 讀到了被回滾之前的數(shù)據(jù)分布式臟讀。根源RR 的 MVCC 快照是事務(wù)啟動時固定不受全局事務(wù)二階段回滾影響。解決數(shù)據(jù)庫改成 RCRC 每次查詢重新拿最新快照回滾完成后查詢就能看到最新值。問題 2undo_log 快照 和 MySQL MVCC 快照不一致問題Seata 的 undo_log 是業(yè)務(wù) SQL 執(zhí)行前手動記錄的數(shù)據(jù)快照是 Seata 自己的快照MySQL MVCC 是數(shù)據(jù)庫自動維護(hù)的 undo 版本鏈兩套快照體系。RC 模式每次讀最新版本業(yè)務(wù)查詢走 MySQL 最新數(shù)據(jù)和 Seata undo_log 的時序更容易對齊RR 模式MySQL 讀的是 MVCC 快照有可能讀到和 Seata undo_log 不一致的數(shù)據(jù)版本增加數(shù)據(jù)一致性風(fēng)險。延伸坑RC 下也有小問題RC 雖然解決上面臟讀但 RC 本身會出現(xiàn)不可重復(fù)讀。在 Seata 長全局事務(wù)場景同一個分支事務(wù)內(nèi)多次查詢可以讀到其他已提交事務(wù)的數(shù)據(jù)業(yè)務(wù)代碼需要考慮這個特性。三、擴(kuò)展Q1Seata 全局隔離級別和 MySQL 隔離級別區(qū)別MySQL 隔離級別單機(jī)數(shù)據(jù)庫事務(wù)可見性依賴 MVCC、鎖。Seata 全局隔離級別分布式事務(wù)之間的數(shù)據(jù)可見性Seata AT 默認(rèn)全局讀未提交。如果設(shè)置 Seata 全局讀已提交Seata 會加鎖其他全局事務(wù)不能讀到當(dāng)前全局事務(wù)一階段修改但未二階段提交的數(shù)據(jù)代價是性能下降并發(fā)降低。Q2Seata TCC 模式也要 RC 嗎TCC 沒有 undo_log不需要依賴 MySQL undo 快照TCC 對數(shù)據(jù)庫隔離級別沒有強(qiáng)制要求RC/RR 都可以。上面的 MVCC 臟讀問題主要是 AT 模式特有。Q3為什么 AT 一階段本地事務(wù)要提交AT 一階段業(yè)務(wù) SQL 寫入 undo_log 在同一個本地事務(wù)然后提交本地事務(wù)。 本地提交釋放數(shù)據(jù)庫鎖提升并發(fā)但數(shù)據(jù)已經(jīng)落庫只是 undo_log 保留等待二階段決定提交 / 回滾。這也是為什么會出現(xiàn)中間狀態(tài)需要處理可見性問題。Q4Seata AT 默認(rèn)全局讀未提交但本地是 RC數(shù)據(jù)庫本地隔離級別MySQLRCRead CommittedSeata AT【全局事務(wù)隔離級別】默認(rèn)Read Uncommitted全局讀未提交這兩個不是一個維度。原理AT 模式一階段分支本地事務(wù)直接提交到數(shù)據(jù)庫本地 RC別的普通 SQL 能讀到這個分支修改的數(shù)據(jù)但是全局事務(wù)還沒提交TC 上的全局鎖還沒釋放隨時可能二階段回滾undo_log 會把數(shù)據(jù)改回去 現(xiàn)象 別的普通select可以讀到分支已經(jīng)本地提交但全局還沒提交的數(shù)據(jù)。 這就是 Seata 文檔說的全局臟讀也就是全局層面的讀未提交。數(shù)據(jù)庫 RC 只保證看不到同一個數(shù)據(jù)庫本地事務(wù)未提交的數(shù)據(jù)RC 管不了「本地已經(jīng)提交但分布式全局事務(wù)還沒提交后續(xù)可能回滾」這個場景。舉例子全局事務(wù) G1調(diào)用分支 B1 更新余額B1 本地事務(wù)提交MySQL RC別的 SQL 能讀到 B1 修改但是 G1 還沒全局提交隨時會觸發(fā)回滾undo_log 恢復(fù)舊數(shù)據(jù)此時另一個普通 select 讀到了 B1 修改的值讀到了全局未提交事務(wù)的數(shù)據(jù)全局臟讀。exG1 全局事務(wù)分支 B1 扣余額B1 本地事務(wù)已經(jīng)提交DB 層面可見但 G1 全局還沒提交。此時業(yè)務(wù)查詢讀到扣減后的余額如果后續(xù) G1 發(fā)生回滾undo_log 把余額恢復(fù)回去。結(jié)果 查詢在一段時間內(nèi)讀到了一個最終會被撤銷的數(shù)據(jù)。Q5 : 如何解決讀到全局未提交事務(wù)的數(shù)據(jù)這個問題答案升級成【全局讀已提交Global Read Committed】想要全局 RC兩種寫法SQL 寫SELECT ... FOR UPDATESeata 會代理這條 SQL去 TC 校驗(yàn)全局鎖如果數(shù)據(jù)被別的全局事務(wù)持有全局鎖就阻塞重試直到拿到全局鎖代表對方全局事務(wù)已經(jīng)完成才返回?cái)?shù)據(jù)。保證讀到的數(shù)據(jù)一定是全局已提交的。注解GlobalLock select for update效果一樣。?? 只有select for update會被 Seata RM 代理、檢查全局鎖普通 select 不會攔截會出現(xiàn)全局臟讀。