1. MySQL 是如何實現(xiàn) ACID 的?

        共 6100字,需瀏覽 13分鐘

         ·

        2021-09-30 21:01

        相關閱讀

        300本計算機編程的經(jīng)典書籍下載

        AI全套:Python3+TensorFlow打造人臉識別智能小程序

        最新人工智能資料-Google工程師親授 Tensorflow-入門到進階

        Java架構全階段七期完整

        黑馬頭條項目 - Java Springboot2.0(視頻、資料、代碼和講義)14天完整版

        Spring核心編程思想

        來源:https://llc687.top/131.html

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


        ACID

        MySQL 作為一個關系型數(shù)據(jù)庫,以最常見的 InnoDB 引擎來說,是如何保證 ACID 的。

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


        隔離性

        先說說隔離性,首先是四種隔離級別。

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

        不同的隔離級別是為了解決不同的問題。也就是臟讀、幻讀、不可重復讀。

        隔離級別臟讀不可重復讀幻讀
        讀未提交可以出現(xiàn)可以出現(xiàn)可以出現(xiàn)
        讀提交不允許出現(xiàn)可以出現(xiàn)可以出現(xiàn)
        可重復讀不允許出現(xiàn)不允許出現(xiàn)可以出現(xiàn)
        序列化不允許出現(xiàn)不允許出現(xiàn)不允許出現(xiàn)
        那么不同的隔離級別,隔離性是如何實現(xiàn)的,為什么不同事物間能夠互不干擾?答案是 鎖 和 MVCC。

        先來說說鎖, MySQL 有多少鎖。

        粒度

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

        行鎖的種類

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

        • 共享鎖:讀鎖,允許其他事務再加S鎖,不允許其他事務再加X鎖,即其他事務只讀不可寫。select...lock in share mode 加鎖。

        • 排它鎖:寫鎖,不允許其他事務再加S鎖或者X鎖。insert、update、delete、for update加鎖。

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

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

        Record Lock

        單個行記錄上的鎖,總是會去鎖住索引記錄。

        Gap Lock

        間隙鎖,想一下幻讀的原因,其實就是行鎖只能鎖住行,但新插入記錄這個動作,要更新的是記錄之間的“間隙”。所以加入間隙鎖來解決幻讀。

        Next-Key Lock

        Gap Lock + Record Lock, 左開又閉。

        鎖之于隔離性

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

        MVCC

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

        版本鏈

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

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

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

        ReadView

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

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

        開始查詢

        現(xiàn)在開始查詢,一個 select 過來了,找到了一行數(shù)據(jù)。

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

        • DATA_TRX_ID >= low_limit_id:

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

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

        RR 級別的幻讀

        有了鎖和 MVCC , 事務的隔離性得到解決。這里要引申一下,默認的 RR 的級別,解決了幻讀嗎?幻讀通常針對的是 INSERT, 不可重復度則針對 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

        實際卻是

        id  name
        1   B
        2   B
        其實在 MySQL 可重復讀的隔離級別中并不是完全解決了幻讀的問題,而是解決了讀數(shù)據(jù)情況下的幻讀問題。而對于修改的操作依舊存在幻讀問題,就是說 MVCC 對于幻讀的解決時不徹底的。另外,MySQL 系列面試題和答案全部整理好了,微信搜索互聯(lián)網(wǎng)架構師,在后臺發(fā)送:2T,可以在線閱讀。


        原子性

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

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

        • 對于每個 insert,回滾時會執(zhí)行 delete;
        • 對于每個 delete,回滾時會執(zhí)行insert;
        • 對于每個 update,回滾時會執(zhí)行一個相反的 update,把數(shù)據(jù)改回去。
        以update操作為例:當事務執(zhí)行update時,其生成的undo log中會包含被修改行的主鍵(以便知道修改了哪些行)、修改了哪些列、這些列在修改前后的值等信息,回滾時便可以使用這些信息將數(shù)據(jù)還原到update之前的狀態(tài)。


        持久性

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

        一條SQL更新語句怎么運行

        持久性肯定和寫有關,MySQL 里經(jīng)常說到的 WAL 技術,WAL 的全稱是 Write-Ahead Logging,它的關鍵點就是先寫日志,再寫磁盤。就像小店做生意,有個粉板,有個賬本,來客了先寫粉板,等不忙的時候再寫賬本。

        redo log

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

        redo log 有兩個特點:

        • 大小固定,循環(huán)寫
        • crash-safe
        對于redo log 是有兩階段的:commit 和 prepare 如果不使用“兩階段提交”,數(shù)據(jù)庫的狀態(tài)就有可能和用它的日志恢復出來的庫的狀態(tài)不一致. 好了,先到這里,看看另一個。

        Buffer Pool

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

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

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

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

        當事務提交時,會調(diào)用fsync接口對redo log進行刷盤。

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

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

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

        binlog

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

        binlog 和 redo log

        對于語句 update T set c=c+1 where ID=2;

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


        一致性

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

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


        總結

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

        有時候,就像你知道了有 undo log、redo log 但可能并不太清楚為什么有,當知道了設計的目的,了解起來就會更加清晰了。另外,關注公眾號互聯(lián)網(wǎng)架構師,在后臺回復:2T,可以獲取我整理的 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

        全棧架構社區(qū)交流群

         「全棧架構社區(qū)」建立了讀者架構師交流群,大家可以添加小編微信進行加群。歡迎有想法、樂于分享的朋友們一起交流學習。

        掃描添加好友邀你進架構師群,加我時注明姓名+公司+職位】

        看完本文有收獲?請轉發(fā)分享給更多人


        往期資源:


        Flutter 移動應用開發(fā)實戰(zhàn) 視頻(開發(fā)你自己的抖音APP)
        Java面試進階訓練營 第2季(分布式篇)
        Java高級 - 分布式系統(tǒng)開發(fā)技術視頻
        瀏覽 32
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
          
          

            1. 香港三级毛片 | 欧美成人在线第一页 | 我与亲女的性关系自述视频 | 大香蕉伊在线观看视频 | 亚洲黄色免费电影 |