MySQL是如何實現事務的ACID

紀莫發表於2020-08-19

前言

最近在面試,有被問到,MySQL的InnoDB引擎是如何實現事務的,又或者說是如何實現ACID這幾個特性的,當時沒有答好,所以自己總結出來,記錄一下。

事務的四大特性ACID

事務的四大特性ACID分別是,A-原子性(Atomicity),C-一致性(Consistency),I-隔離性(Isolation),D-永續性(Durability)。一致性是最終目的,原子性、隔離性、永續性是為了保證一致性所做的措施。所以我寫的順序並不是按照ACID來寫的,將一致性放到了最後,順序就變成了,ADIC。

原子性(A)

原子性是指一個事務就是一個不可分割的工作單位,要麼全部都執行成功,要麼全部都執行失敗,沒有中間狀態或是隻執行一部分。
MySQL的InnoDB引擎是靠undo log(回滾日誌)來實現的,undo log能夠保證在事務回滾時,能夠撤銷所有已經執行成功的SQL。
undo log 屬於邏輯日誌,它記錄的是SQL執行相關的資訊。當事務對資料庫進行修改時,InnoDB會生成與之對應的undo log。如果事務執行失敗或者呼叫的rollback,導致事務需要回滾,InnoDB引擎會根據undo log中的記錄,將資料回滾到之前的樣子。
例如在執行insert語句時會生成相關的delete語句的undo log。反之執行delete語句也會生成相關的insert語句的undo log。執行update語句時也是如此,不過update語句在執行undo log回滾時有可能會涉及到MVCC。主要是為了保證在執行undo log的時候的select能看到哪個版本的資料。

永續性(D)

永續性是指事務一旦提交,對資料庫的操作就是永久性的,接下來的其他操作和異常故障不應該對它有任何影響。
我們都知道MySQL的資料最終是存放在磁碟中的,所以才會有磁碟的容量大小決定資料容量的大小。但是如果對MySQL的操作都是通過讀寫磁碟來進行的話,那麼光是磁碟的I/O就夠把效率大大的拉低了。
所以InnoDB為MySQL提供了緩衝池(Buffer Pool),Buffer Pool中包含了磁碟中部分資料頁的對映。
當從資料庫讀取資料時,會先從Buffer Pool中讀取資料,如果Buffer Pool中沒有,則從磁碟讀取後放入到Buffer Pool中。
當向資料庫寫入資料時,會先寫入到Buffer Pool中,Buffer Pool中更新的資料會定期重新整理到磁碟中(此過程稱為刷髒)。
雖然Buffer Pool為MySQL的讀寫提高了效率,但是卻也帶來了新的問題,那就是如果資料剛更新到Buffer Pool中還沒來得及重新整理到磁碟中時,MySQL突然當機了,這就會導致資料丟失,造成事務的永續性無法保證了
為了解決這個快取的一致性問題,redo log就出現了。在對Buffer Pool中的資料進行修改的時候通過redo log記錄這次操作,當事務提交時會通過fsync介面對redo log進行刷盤。
redo log是記錄在磁碟中的,所以當MySQL出現當機時,可以從磁碟中讀取redo log進行資料的恢復,從而保證了事務的永續性。
redo log 採用的預寫的方式記錄日誌,即先記錄日誌,再更新Buffer Pool,這樣就強行的保證了,資料只要儲存在了redo log中就一定會儲存到磁碟中了。

這要解釋一下,redo log 也是寫磁碟,刷髒也是寫磁碟,為啥要先記錄redo log而不是直接刷髒?

主要原因就是redo log比刷髒快很多。
第一點是,redo log是追加操作日誌,是順序IO;而刷髒是隨機IO,因為每次更新的資料不一定是挨著的,也就是隨機的。
第二點是,刷髒是以資料頁(Page)為單位的,MySQL預設頁大小是16KB,對一個頁上的修改,都要整個頁都刷到磁碟中;而redo log只包含真正的需要寫入磁碟的操作日誌。

MySQL還有一個記錄操作的日誌,叫binlog ,那麼redo log和binlog又有什麼區別呢?
  • 第一點作用上的區別
    redo log是用來記錄更新快取的,為了保證MySQL就算當機也不會影響事務的永續性;binlog是用來記錄什麼時間操作了什麼,主要有時間點,可以保證將資料恢復到某個時間點,也有用於主從同步資料的。
  • 第二點層次上的區別
    redo log是儲存引擎InnoDB實現的(MyISAM就沒有redo log),而binlog是在MySQL伺服器層面存在的任何其他儲存引擎也有binlog。
    儲存內容上,redo log是物理日誌,基於磁碟的資料頁,binlog是邏輯日誌,儲存的一條執行SQL。
  • 第三點寫入時機的區別
    redo log 在預設情況下是在事務提交時,進行刷盤的;可以通過引數:innodb_flush_log_at_trx_commit 來改變策略,可以不用等到事務提交時才進行刷盤。
    如:可以設定成每秒提交一次。
    binlog是在事務提交時寫入。

隔離性(I)

原子性和永續性都是基於單個事務內部的措施,而隔離性是隻多個事務之間相互隔離,互不影響的特性。
我們都知道事務的隔離級別中最嚴謹的是序列化(Serializable),但是隔離性越高,效能就越低,所以一般不使用序列化這個隔離級別。
對於隔離性的,我們要分兩種情況進行討論:

  • 一個事務中的寫操作對另一個事務中的寫操作的影響;
  • 一個事務中的寫操作對另一個事務中的讀操作的影響;

首先,事務間的寫操作其實是靠MySQL的鎖機制來實現隔離的,而事務間的寫和讀操作是靠MVCC機制來實現的。

鎖機制

MySQL中的鎖主要有
按照功能分:讀鎖和寫鎖;按照作用範圍分:表級鎖和行級鎖;
還有意向鎖,間隙鎖等。
讀鎖:又稱“共享鎖”,是指多個事務可以共享一把鎖,都只能訪問資料,並不能修改。
寫鎖:又稱“排他鎖”,是不能和其他事務共享資料的,如果一個事務獲取到了一個資料的排他鎖,那麼其他事務就不能再獲取該行的其他鎖,包括共享鎖和排他鎖。
表級鎖:是指會將整個表進行鎖定,效能較差,不同儲存引擎支援的鎖的粒度不同,MyISAM引擎支援表級鎖,InnoDB引擎支援表級鎖也支援行級鎖。
行級鎖:會將需要操作的相應行進行鎖定,效能好。
意向鎖:意向鎖是表級鎖,如果在一個事務已經對一個表中的某個資料加上了排他鎖或共享鎖,那麼就可以加上意向鎖,這樣當下一個事務來進行鎖表的時候發現已經存在意向鎖了,就會先被阻塞,如果不加意向鎖的話,第二個事務來鎖表的時候需要一行一行的遍歷檢視是否有資料已經被鎖住了。
間隙鎖:間隙鎖是為了防止產生幻讀而加的鎖,加在不存在的空閒空間,可以是兩個索引記錄之間,也可能是第一個索引記錄之前或最後一個索引之後的空間(但是並不包含當前記錄)。這樣就保證了在間隙鎖執行的時候,新增的資料會阻塞,保證了一個事務中的兩次查詢獲得的記錄數都是一致的。
Next-Key Lock:Next-Key Lock是行級鎖和間隙鎖的結合產生的鎖,因為間隙鎖是不會鎖住當前記錄的而Next-Key Lock是會將當前記錄也鎖住的。
例如:如果一個表中有三條資料分別是:

id name number
1 小明 16
2 小紅 17
3 小張 20
4 小王 20

那麼在執行SQL:select * from table where number = 17 for update 時間隙鎖會鎖住,number的區間是(16,17),(17,20),但是Next-Key Lock的鎖住的是:
16,17),(17,20)區間加間隙鎖,同時number=17加記錄鎖。

鎖機制保障了多個事務間的寫操作的隔離,而多個事務間的讀和寫操作的保證是需要通過MVCC機制來保證的。

MVCC機制

MVCC全稱是【Multi-Version ConCurrency Control】即多版本控制協議。

MVCC的主要是靠在每行記錄上增加隱藏列和使用undo log來實現的,隱藏列主要包括,改行資料建立的版本號(遞增的),刪除時間,指向undo log的指標等。
那麼MVCC是如何保證讀寫隔離的呢?主要是通過快照讀當前讀兩個操作。

  • 快照讀
    MVCC為了保證併發的效率,在進行讀取資料的時候是不加鎖的,在執行select的時候(不帶鎖的普通select),會先讀取當前資料的版本號,如果在select還沒返回結果時,有事務將此行資料進行了修改,那麼版本號就會比執行select的時候的大,所以為了保證select讀取資料的一致性,就只會讀取小於或等於當前版本的資料,這個歷史版本的資料就是從undo log中獲取到的。
  • 當前讀
    當執行insert、update、delete的時候,是讀取的當前最新的版本資料,並且會給當前記錄加上鎖,用來保證在操作的時候不會被別的事務將版本號進行修改。

像普通的select就是快照讀即讀取的有可能就是資料的歷史版本。
insert、update、delete、select ... lock in share mode 和select ... for update 讀取的就是當前讀,即讀取的都是資料的最新版本。

其實將隔離級別設定為Serializable也是可以實現讀寫隔離的,但是併發效率會比低很多,所以一般用的很少,但是MVCC是讀不加鎖的,只有在寫的時候才會加鎖,從而提高的併發的效率。

通過MVCC機制保證了多個事務間的讀寫隔離,從而實現了事務的隔離性。

一致性(C)

一致性是指在事務執行前後,資料的一致性,事務前後資料完整性沒有破壞,並且都是合法的資料狀態。

  • 其中一致性的指標有:
    索引的完整(唯一索引,不重複等),資料列的完成(欄位型別,長度,大小符合要求),外來鍵約束等。
  • 實現一致性的措施:
    保證原子性,永續性,隔離性,如果這些特性都無法保證,那麼一致性就也無法保證了。從資料庫層面來看,除了前面那幾個特性的保證外,對欄位的一致性是有保證措施的,例如整型的字元不能傳入,字串、時間等格式,字串的長度不能超過列的限制。但是在應用層面也是需要開發者自己來保證的,
    例如:從A轉賬給B一部分金額,那麼就要保證,從A從將金額扣除多少就要去給B增加多少金額,如果只扣除A的金額,而沒有增加B的金額,是無法保證一致性的。

總結

MySQL事務的ACID,一致性是最終目的。
保證一致性的措施有:
A原子性:靠undo log來保證(異常或執行失敗後進行回滾)。
D永續性:靠redo log來保證(保證當MySQL當機或停電後,可以通過redo log最終將資料儲存至磁碟中)。
I隔離性:事務間的讀寫靠MySQL的鎖機制來保證隔離,事務間的寫操作靠MVCC機制(快照讀、當前讀)來保證隔離性。
C一致性:事務的最終目的,即需要資料庫層面保證,又需要應用層面進行保證。

相關文章