<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          MySQL 是如何實(shí)現(xiàn) ACID 的?

          共 5449字,需瀏覽 11分鐘

           ·

          2021-09-18 11:54

          點(diǎn)擊關(guān)注公眾號(hào),Java干貨及時(shí)送達(dá)

          來(lái)源:https://llc687.top/131.html

          本文主要探討MySQL InnoDB 引擎下ACID的實(shí)現(xiàn)原理,對(duì)于諸如什么是事務(wù),隔離級(jí)別的含義等基礎(chǔ)知識(shí)不做過(guò)多闡述。

          ACID

          MySQL 作為一個(gè)關(guān)系型數(shù)據(jù)庫(kù),以最常見的 InnoDB 引擎來(lái)說(shuō),是如何保證 ACID 的。

          • (Atomicity原子性: 事務(wù)是最小的執(zhí)行單位,不允許分割。原子性確保動(dòng)作要么全部完成,要么完全不起作用;
          • (Consistency)一致性: 執(zhí)行事務(wù)前后,數(shù)據(jù)保持一致;
          • (Isolation)隔離性: 并發(fā)訪問(wèn)數(shù)據(jù)庫(kù)時(shí),一個(gè)事務(wù)不被其他事務(wù)所干擾。
          • (Durability)持久性: 一個(gè)事務(wù)被提交之后。對(duì)數(shù)據(jù)庫(kù)中數(shù)據(jù)的改變是持久的,即使數(shù)據(jù)庫(kù)發(fā)生故障。

          隔離性

          先說(shuō)說(shuō)隔離性,首先是四種隔離級(jí)別。

          隔離級(jí)別說(shuō)明
          讀未提交一個(gè)事務(wù)還沒(méi)提交時(shí),它做的變更就能被別的事務(wù)看到
          讀提交一個(gè)事務(wù)提交之后,它做的變更才會(huì)被其他事務(wù)看到
          可重復(fù)讀一個(gè)事務(wù)中,對(duì)同一份數(shù)據(jù)的讀取結(jié)果總是相同的,無(wú)論是否有其他事務(wù)對(duì)這份數(shù)據(jù)進(jìn)行操作,以及這個(gè)事務(wù)是否提交。InnoDB默認(rèn)級(jí)別
          串行化事務(wù)串行化執(zhí)行,每次讀都需要獲得表級(jí)共享鎖,讀寫相互都會(huì)阻塞,隔離級(jí)別最高,犧牲系統(tǒng)并發(fā)性。

          不同的隔離級(jí)別是為了解決不同的問(wèn)題。也就是臟讀、幻讀、不可重復(fù)讀。

          隔離級(jí)別臟讀不可重復(fù)讀幻讀
          讀未提交可以出現(xiàn)可以出現(xiàn)可以出現(xiàn)
          讀提交不允許出現(xiàn)可以出現(xiàn)可以出現(xiàn)
          可重復(fù)讀不允許出現(xiàn)不允許出現(xiàn)可以出現(xiàn)
          序列化不允許出現(xiàn)不允許出現(xiàn)不允許出現(xiàn)

          那么不同的隔離級(jí)別,隔離性是如何實(shí)現(xiàn)的,為什么不同事物間能夠互不干擾?答案是 鎖 和 MVCC。

          先來(lái)說(shuō)說(shuō)鎖, MySQL 有多少鎖。

          粒度

          從粒度上來(lái)說(shuō)就是表鎖、頁(yè)鎖、行鎖。表鎖有意向共享鎖、意向排他鎖、自增鎖等。行鎖是在引擎層由各個(gè)引擎自己實(shí)現(xiàn)的。但并不是所有的引擎都支持行鎖,比如 MyISAM 引擎就不支持行鎖。

          行鎖的種類

          在 InnoDB 事務(wù)中,行鎖通過(guò)給索引上的索引項(xiàng)加鎖來(lái)實(shí)現(xiàn)。這意味著只有通過(guò)索引條件檢索數(shù)據(jù),InnoDB才使用行級(jí)鎖,否則將使用表鎖。行級(jí)鎖定同樣分為兩種類型:共享鎖排他鎖,以及加鎖前需要先獲得的意向共享鎖和意向排他鎖。

          • 共享鎖:讀鎖,允許其他事務(wù)再加S鎖,不允許其他事務(wù)再加X(jué)鎖,即其他事務(wù)只讀不可寫。select...lock in share mode 加鎖。
          • 排它鎖:寫鎖,不允許其他事務(wù)再加S鎖或者X鎖。insert、update、delete、for update加鎖。

          行鎖是在需要的時(shí)候才加的,但并不是不需要了就立刻釋放,而是要等到事務(wù)結(jié)束時(shí)才釋放。這個(gè)就是兩階段鎖協(xié)議。

          行鎖的實(shí)現(xiàn)算法

          Record Lock

          單個(gè)行記錄上的鎖,總是會(huì)去鎖住索引記錄。

          Gap Lock

          間隙鎖,想一下幻讀的原因,其實(shí)就是行鎖只能鎖住行,但新插入記錄這個(gè)動(dòng)作,要更新的是記錄之間的“間隙”。所以加入間隙鎖來(lái)解決幻讀。

          Next-Key Lock

          Gap Lock + Record Lock, 左開又閉。

          鎖之于隔離性

          大致介紹了下鎖,可以看到。有了鎖,當(dāng)某事務(wù)正在寫數(shù)據(jù)時(shí),其他事務(wù)獲取不到寫鎖,就無(wú)法寫數(shù)據(jù),一定程度上保證了事務(wù)間的隔離。但前面說(shuō),加了寫鎖,為什么其他事務(wù)也能讀數(shù)據(jù)呢,不是獲取不到讀鎖嗎

          MVCC

          前面說(shuō)到,有了鎖,當(dāng)前事務(wù)沒(méi)有寫鎖就不能修改數(shù)據(jù),但還是能讀的,而且讀的時(shí)候,即使該行數(shù)據(jù)其他事務(wù)已修改且提交,還是可以重復(fù)讀到同樣的值。這就是MVCC,多版本的并發(fā)控制,Multi-Version Concurrency Control。

          版本鏈

          Innodb 中行記錄的存儲(chǔ)格式,有一些額外的字段:DATA_TRX_ID和DATA_ROLL_PTR

          • DATA_TRX_ID:數(shù)據(jù)行版本號(hào)。用來(lái)標(biāo)識(shí)最近對(duì)本行記錄做修改的事務(wù) id。
          • DATA_ROLL_PTR:指向該行回滾段的指針。該行記錄上所有舊版本,在 undo log 中都通過(guò)鏈表的形式組織。

          undo log : 記錄數(shù)據(jù)被修改之前的日志,后面會(huì)詳細(xì)說(shuō)。

          ReadView

          在每一條 SQL 開始的時(shí)候被創(chuàng)建,有幾個(gè)重要屬性:

          • trx_ids: 當(dāng)前系統(tǒng)活躍(未提交)事務(wù)版本號(hào)集合。
          • low_limit_id: 創(chuàng)建當(dāng)前 read view 時(shí)“當(dāng)前系統(tǒng)最大事務(wù)版本號(hào)+1”。
          • up_limit_id: 創(chuàng)建當(dāng)前read view 時(shí)“系統(tǒng)正處于活躍事務(wù)最小版本號(hào)”
          • creator_trx_id: 創(chuàng)建當(dāng)前read view的事務(wù)版本號(hào);

          開始查詢

          現(xiàn)在開始查詢,一個(gè) select 過(guò)來(lái)了,找到了一行數(shù)據(jù)。

          • DATA_TRX_ID <up_limit_id :說(shuō)明數(shù)據(jù)在當(dāng)前事務(wù)之前就存在了,顯示。

          • DATA_TRX_ID >= low_limit_id:

            說(shuō)明該數(shù)據(jù)是在當(dāng)前read view 創(chuàng)建后才產(chǎn)生的,數(shù)據(jù)不顯示。

            • 不顯示怎么辦,根據(jù) DATA_ROLL_PTR 從 undo log 中找到歷史版本,找不到就空。
          • up_limit_id <DATA_TRX_ID <low_limit_id :就要看隔離級(jí)別了。

          點(diǎn)擊關(guān)注公眾號(hào),Java干貨及時(shí)送達(dá)

          RR 級(jí)別的幻讀

          有了鎖和 MVCC , 事務(wù)的隔離性得到解決。這里要引申一下,默認(rèn)的 RR 的級(jí)別,解決了幻讀嗎?幻讀通常針對(duì)的是 INSERT, 不可重復(fù)度則針對(duì) UPDATE 。

          事物 1事物 2
          beginbegin
          select * from dept
          -insert into dept(name) values("A")
          -commit
          update dept set name="B"
          commit

          我們期望是

          id  name
          1   A
          2   B

          實(shí)際卻是

          id  name
          1   B
          2   B

          其實(shí)在 MySQL 可重復(fù)讀的隔離級(jí)別中并不是完全解決了幻讀的問(wèn)題,而是解決了讀數(shù)據(jù)情況下的幻讀問(wèn)題。而對(duì)于修改的操作依舊存在幻讀問(wèn)題,就是說(shuō) MVCC 對(duì)于幻讀的解決時(shí)不徹底的。另外,MySQL 系列面試題和答案全部整理好了,微信搜索Java技術(shù)棧,在后臺(tái)發(fā)送:面試,可以在線閱讀。

          原子性

          接著說(shuō)說(shuō)原子性。前文有提到 undo log ,回滾日志。隔離性的MVCC其實(shí)就是依靠它來(lái)實(shí)現(xiàn)的,原子性也是。實(shí)現(xiàn)原子性的關(guān)鍵,是當(dāng)事務(wù)回滾時(shí)能夠撤銷所有已經(jīng)成功執(zhí)行的sql語(yǔ)句。

          最新 Java 核心技術(shù)推薦:https://github.com/javastacks/javastack

          當(dāng)事務(wù)對(duì)數(shù)據(jù)庫(kù)進(jìn)行修改時(shí),InnoDB會(huì)生成對(duì)應(yīng)的 undo log;如果事務(wù)執(zhí)行失敗或調(diào)用了 rollback,導(dǎo)致事務(wù)需要回滾,便可以利用 undo log 中的信息將數(shù)據(jù)回滾到修改之前的樣子。undo log 屬于邏輯日志,它記錄的是sql執(zhí)行相關(guān)的信息。當(dāng)發(fā)生回滾時(shí),InnoDB 會(huì)根據(jù) undo log 的內(nèi)容做與之前相反的工作:

          • 對(duì)于每個(gè) insert,回滾時(shí)會(huì)執(zhí)行 delete;
          • 對(duì)于每個(gè) delete,回滾時(shí)會(huì)執(zhí)行insert;
          • 對(duì)于每個(gè) update,回滾時(shí)會(huì)執(zhí)行一個(gè)相反的 update,把數(shù)據(jù)改回去。

          以u(píng)pdate操作為例:當(dāng)事務(wù)執(zhí)行update時(shí),其生成的undo log中會(huì)包含被修改行的主鍵(以便知道修改了哪些行)、修改了哪些列、這些列在修改前后的值等信息,回滾時(shí)便可以使用這些信息將數(shù)據(jù)還原到update之前的狀態(tài)。

          持久性

          Innnodb有很多 log,持久性靠的是 redo log。

          一條SQL更新語(yǔ)句怎么運(yùn)行

          持久性肯定和寫有關(guān),MySQL 里經(jīng)常說(shuō)到的 WAL 技術(shù),WAL 的全稱是 Write-Ahead Logging,它的關(guān)鍵點(diǎn)就是先寫日志,再寫磁盤。就像小店做生意,有個(gè)粉板,有個(gè)賬本,來(lái)客了先寫粉板,等不忙的時(shí)候再寫賬本。點(diǎn)擊在線刷題,看看你可以打多少分了。

          redo log

          redo log 就是這個(gè)粉板,當(dāng)有一條記錄要更新時(shí),InnoDB 引擎就會(huì)先把記錄寫到 redo log(并更新內(nèi)存),這個(gè)時(shí)候更新就算完成了。在適當(dāng)?shù)臅r(shí)候,將這個(gè)操作記錄更新到磁盤里面,而這個(gè)更新往往是在系統(tǒng)比較空閑的時(shí)候做,這就像打烊以后掌柜做的事。

          redo log 有兩個(gè)特點(diǎn):

          • 大小固定,循環(huán)寫
          • crash-safe

          對(duì)于redo log 是有兩階段的:commit 和 prepare 如果不使用“兩階段提交”,數(shù)據(jù)庫(kù)的狀態(tài)就有可能和用它的日志恢復(fù)出來(lái)的庫(kù)的狀態(tài)不一致. 好了,先到這里,看看另一個(gè)。

          Buffer Pool

          InnoDB還提供了緩存,Buffer Pool 中包含了磁盤中部分?jǐn)?shù)據(jù)頁(yè)的映射,作為訪問(wèn)數(shù)據(jù)庫(kù)的緩沖:

          • 當(dāng)讀取數(shù)據(jù)時(shí),會(huì)先從Buffer Pool中讀取,如果Buffer Pool中沒(méi)有,則從磁盤讀取后放入Buffer Pool;
          • 當(dāng)向數(shù)據(jù)庫(kù)寫入數(shù)據(jù)時(shí),會(huì)首先寫入Buffer Pool,Buffer Pool中修改的數(shù)據(jù)會(huì)定期刷新到磁盤中。

          Buffer Pool 的使用大大提高了讀寫數(shù)據(jù)的效率,但是也帶了新的問(wèn)題:如果MySQL宕機(jī),而此時(shí) Buffer Pool 中修改的數(shù)據(jù)還沒(méi)有刷新到磁盤,就會(huì)導(dǎo)致數(shù)據(jù)的丟失,事務(wù)的持久性無(wú)法保證。

          所以加入了 redo log。當(dāng)數(shù)據(jù)修改時(shí),除了修改Buffer Pool中的數(shù)據(jù),還會(huì)在redo log記錄這次操作;

          當(dāng)事務(wù)提交時(shí),會(huì)調(diào)用fsync接口對(duì)redo log進(jìn)行刷盤。

          如果MySQL宕機(jī),重啟時(shí)可以讀取redo log中的數(shù)據(jù),對(duì)數(shù)據(jù)庫(kù)進(jìn)行恢復(fù)。

          redo log采用的是WAL(Write-ahead logging,預(yù)寫式日志),所有修改先寫入日志,再更新到Buffer Pool,保證了數(shù)據(jù)不會(huì)因MySQL宕機(jī)而丟失,從而滿足了持久性要求。而且這樣做還有兩個(gè)優(yōu)點(diǎn):

          • 刷臟頁(yè)是隨機(jī) IO,redo log 順序 IO
          • 刷臟頁(yè)以Page為單位,一個(gè)Page上的修改整頁(yè)都要寫;而redo log 只包含真正需要寫入的,無(wú)效 IO 減少。

          binlog

          說(shuō)到這,可能會(huì)疑問(wèn)還有個(gè) bin log 也是寫操作并用于數(shù)據(jù)的恢復(fù),有啥區(qū)別呢。

          binlog 和 redo log

          對(duì)于語(yǔ)句 update T set c=c+1 where ID=2;

          1. 執(zhí)行器先找引擎取 ID=2 這一行。ID 是主鍵,直接用樹搜索找到。如果 ID = 2 這一行所在數(shù)據(jù)頁(yè)就在內(nèi)存中,就直接返回給執(zhí)行器;否則,需要先從磁盤讀入內(nèi)存,再返回。
          2. 執(zhí)行器拿到引擎給的行數(shù)據(jù),把這個(gè)值加上 1,N+1,得到新的一行數(shù)據(jù),再調(diào)用引擎接口寫入這行新數(shù)據(jù)。
          3. 引擎將這行新數(shù)據(jù)更新到內(nèi)存中,同時(shí)將這個(gè)更新操作記錄到 redo log 里面,此時(shí) redo log 處于 prepare 狀態(tài)。然后告知執(zhí)行器執(zhí)行完成了,隨時(shí)可以提交事務(wù)。
          4. 執(zhí)行器生成這個(gè)操作的 binlog,并把 binlog 寫入磁盤。
          5. 執(zhí)行器調(diào)用引擎的提交事務(wù)接口,引擎把剛剛寫入的 redo log 改成提交(commit)狀態(tài),更新完成

          為什么先寫 redo log 呢 ?

          • 先 redo 后 bin : binlog 丟失,少了一次更新,恢復(fù)后仍是0。
          • 先 bin 后 redo : 多了一次事務(wù),恢復(fù)后是1。

          一致性

          一致性是事務(wù)追求的最終目標(biāo),前問(wèn)所訴的原子性、持久性和隔離性,其實(shí)都是為了保證數(shù)據(jù)庫(kù)狀態(tài)的一致性。當(dāng)然,上文都是數(shù)據(jù)庫(kù)層面的保障,一致性的實(shí)現(xiàn)也需要應(yīng)用層面進(jìn)行保障。

          也就是你的業(yè)務(wù),比如購(gòu)買操作只扣除用戶的余額,不減庫(kù)存,肯定無(wú)法保證狀態(tài)的一致。

          總結(jié)

          MySQL 都很熟, ACID 也知道是個(gè)啥,但 MySQL 的 ACID 怎么實(shí)現(xiàn)的?

          有時(shí)候,就像你知道了有 undo log、redo log 但可能并不太清楚為什么有,當(dāng)知道了設(shè)計(jì)的目的,了解起來(lái)就會(huì)更加清晰了。另外,關(guān)注公眾號(hào)Java技術(shù)棧,在后臺(tái)回復(fù):面試,可以獲取我整理的 Java/ MySQL 系列面試題和答案,非常齊全。

          參考:

          https://zhuanlan.zhihu.com/p/52977862
          https://learnku.com/articles/39212
          https://www.cnblogs.com/rjzheng/p/10841031.html
          https://www.cnblogs.com/kismetv/p/10331633.html






          關(guān)注Java技術(shù)棧看更多干貨



          獲取 Spring Boot 實(shí)戰(zhàn)筆記!

          瀏覽 82
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  无码一区一区 | 青青草在线超碰 | 天天日天天操天天摸 | 高清无码一区在线 | 亚洲激情蜜芽 |