MySQL知識點總結(重點分析事務)

範式

數據庫三種範式如下。

範式 描述 反例
第一範式 每個字段都是原子的,不能再分解 某個字段是JSON串,或者是數組
第二範式 1) 表必須有主鍵,可以是多個順序屬性的組合。2) 非主屬性必須完全依賴主屬性(這裏指的是組合主鍵),而不能部分依賴。 好友關係表中,主鍵是關注人ID和被關注人ID,表中存儲的姓名等字段只依賴主鍵中的一個屬性,不完全依賴主鍵
第三範式 沒有傳遞依賴(非主屬性必須直接依賴主屬性,不能間接依賴主屬性) 在員工表中,有部門ID和部門名稱等,部門名稱直接依賴於部門ID,而不是員工ID

一般工程中,對於數據庫的設計要求達到第三範式,但這不是一定要遵守的,所以在開發中,爲了性能或便於開發,出現了很多違背範式的設計。如冗餘字段、字段中存一個JSON串,分庫分表之後數據多維度冗餘存儲、寬表等。

B+樹

關於B樹和B+樹的概念,請見漫畫算法:什麼是 B+ 樹?

B+樹的邏輯結構

下面來看一下數據庫中主鍵索引對應的B+樹的邏輯結構。

indsd

該結構有幾個關鍵特徵(與一般的B+樹有點不同)

  • 在葉子節點一層,所有記錄的主鍵從小到大的順序排列,並且形成了一個雙向鏈表,葉子節點的每一個Key指向一條記錄。
  • 在非葉子節點一層,每個非葉子節點都指向葉子節點中值最小的Key(但非葉子節點不存儲記錄),同層的非葉子節點也形成一個雙向鏈表。

基於B+樹的這幾個特徵,就可以很容易的實現範圍查詢、前綴匹配模糊查詢、排序和分頁(查詢條件應是索引)。這裏有幾個要注意的問題。

  • 模糊查詢不應該使用後綴匹配或者中間匹配,因爲索引的排序是按照從小到大排序的,只有前綴相同的纔會被排列在一起,否則就用不上索引了,只能逐個遍歷。
  • 對於ofset這種特性,其實是用不到索引的。比如select *** where *** limit 100, 10,數據庫需要遍歷前面100條數據才知道offset=100的位置在哪。合理的分頁方法就是不使用offset,把offset變成條件來查詢。比如變成select *** where *** and id > 100 limit 10,這樣才能利用索引的遍歷,快速定位id=100的位置。

事務與鎖

事務的四大特性ACID。

  • 原子性(A):事務要麼不執行,要麼完全執行。(如果執行到一半機器宕機了,已執行的部分需要回滾回去)。
  • 一致性©:各種約束條件,比如主鍵不能爲空,參照完整性等。
  • 隔離性(I):只要事務不是串行的,就需要隔離(一般都是並行的,效率更高嘛)。
  • 持久性(D):一旦事務提交了,數據不能丟失。

事務與事務併發地操作數據庫的表記錄,可能會導致下面幾類問題。

問題 描述
髒讀 一個事務A讀取了另一個未提交的事務B的數據,但是事務A提交之前,事務B又回滾了,導致事務A剛剛讀到的就是一個髒數據(RC隔離級別可解決)。
不可重複讀 同一個事務兩次查詢同一行記錄,得到的結果不一樣。因爲另一個事務對該行記錄進行了修改操作(行排它鎖可解決)。
幻讀 同一個事務兩次查詢某一範圍,得到的記錄數不一樣,因爲另一個事務在這個範圍內進行了增加或刪除操作(臨鍵鎖可解決)。
丟失更新 兩個事務同時修改同一行記錄,事務A的修改被後面的事務B覆蓋了(需要自己加鎖來解決)。

下面來看一下InnoDB的事務隔離級別。可解決上面的三個問題,最後一個問題只能在業務代碼中解決。

名稱 描述
READ_UNCOMMITTED(RU) 跟沒有一樣,幾乎不使用。
READ_COMMITTED(RC) 只能讀取另一個事務已提交的事務,能防止髒讀。
REPEATABLE_READ(RR) 可重複讀(在一個事務內多次查詢的結果相同,其它事務不可修改該查詢條件範圍內的數據,會根據條件上Gap 鎖)
SERIALIZABLE 所有的事務依次逐個執行,相當於串行化了,效率太低,一般也不使用。

關於數據庫的鎖,請詳見這篇。MySQL中的“鎖”事

事務實現原理1(Redo Log)

Write-Ahead

爲了保證數據的持久性,需要每提交一個事務就刷一次磁盤,但是這樣效率太低了,所以就有了Write-Ahead。
Write-Ahead:先在內存中提交事務,然後寫日誌(在InnoDB中就是redo log,日誌是爲了防止宕機導致內存數據丟失),然後再後臺任務中把內存中的數據異步刷到磁盤。

Redo Log的邏輯與物理結構

從邏輯上來講,日誌就是一個無限延長的字節流,從數據庫啓動開始,日誌就一直在增加,直到數據庫關閉。
在邏輯上,日誌是按照時間順序從小到大用LSN(是一個64位的數)來編號的,因爲事務有大有小,所以日誌是個變長記錄(每一段數據量都不一樣)。
image.png

從物理上來講,日誌不可能是一個無限延長的字節流,因爲每個文件有大小限制。在物理上是整塊的讀取和寫入(這裏就是Redo Log 塊,一個塊就是512字節),而不是按字節流來處理的。而且日誌是可以被覆寫的,因爲當數據被刷到磁盤上後,這些日誌也就沒有用了,所以他們是可以被覆蓋的。可循環使用,一個固定大小的文件,每512字節一個塊。
image.png

Physiological Logging

在InnoDB中,Redo Log採用先以Page爲單位記錄日誌(物理記法),每個Page裏再採用邏輯記法(記錄Page裏的哪一行修改了)。這種記法就叫做Physiological Logging。
之所以採用這種記法,是邏輯日誌和物理日誌的對應關係決定的。

  • 一條邏輯日誌可能會產生多個Page的物理日誌。因爲一個表可能有多個索引,每個索引都是一個B+樹,更新一條記錄(一個邏輯日誌),但這可能會同時更新多個索引,導致產生了多個Page的物理日誌。
  • 就算一條邏輯日誌對應一個Page,也可能會修改這個Page的多個位置(在中間插入一條記錄,需要修改Page的多個位置)。

事務崩潰恢複分析

未提交的事務日誌也在Redo Log中

因爲不同事務的日誌在Redo Log中是交叉存在的,所以沒法把未提交的事務與已提交的事務分開。ARIES算法的做法就是,不管事務有沒有提交,它的日誌都會被記錄到Redo Log上並刷到磁盤中。當崩潰恢復的時候,會把Redo Log全部重放一遍(不管是提交的還是未提交都都重做了,也就是完全恢復到崩潰之前的狀態),然後再把未提交的事務給找出來,做回滾處理。

Rollback轉化爲Commit

其實事務的回滾都是反向提交。也就是根據事務中的SQL語句生成反向對應的SQL語句執行,然後Commit(這種逆向的SQL語句也會被記錄到Redo Log中,防止恢復中宕機,但是會與正常的日誌區分開),所以回滾是邏輯層面上的回滾,在物理層面其實是個提交。
image.png

ARIES算法

如下圖,有六個事務,每個事務所在的線段表示事務在Redo Log中的起始位置和結束位置。發生宕機時,需要回滾事務T3、T4、T5。
在圖中,綠線表示兩個Checkpoint點和Crash(宕機)點。藍線表示三個階段工作的起始位置。
image.png

階段1:分析階段

在分析階段,要解決兩個問題。
1)確定哪些數據頁是髒頁,爲階段2的Redo做準備(找出從最近的Checkpoint到Crash之間所有未刷盤的Page)。
2)確定哪些事務未提交,未階段3的Undo做準備(因爲未提交的事務也寫進了Redo Log中,需要將這些事務找出來,並做回滾)。

ARIES的Checkpoint機制,一般使用的是Fuzzy Checkpoint,它在內存中維護了兩個表,活躍事務表和髒頁表。
1)活躍事務表:當前所有未提交事務的集合,每個事務維護了一個關鍵變量lastLSN(該事務產生的日誌中最後一條日誌的LSN)。
2)髒頁表:當前所有未刷到磁盤上得Page的集合(包括未提交事務和已提交事務),recoveryLSN是導致該頁爲髒頁的最早LSN(最近一次刷盤後最早開始的事務產生的日誌的LSN)。

每次Fuzzy Checkpoint,就是把這兩個表的數據生成一個快照,形成一條checkponit日誌,記入Redo Log中。
下圖展示了事務的開始標誌(S表示Start transaction),結束標誌(C表示Commit)以及Checkpoint在Redo Log中的位置(這裏只展示了活躍事務表,髒頁表也是類似的,唯一不同的就是髒頁集合只會增加,不會減少,髒頁集合中可能有些頁是乾淨的,但由於Redo Log是冪等的,所以不影響)。
image.png

階段2:進行Redo

階段1中已經準備好了髒頁集合,取集合中髒頁的recoveryLSN的最小值(也就是最早開始髒的那一頁),得到firstLSN,在Redo Log中從firstLSN開始遍歷到末尾,把每條Redo Log對應的Page全部重新刷到磁盤中。但是這些髒頁中可能有些頁並不是髒的,所以這裏要做冪等。也就是利用Page中的一個PageLSN字段(它記錄了當前Page刷盤時最後一次修改它的日誌對應的LSN),在Redo重放的時候,判斷如果日誌的LSN比磁盤中得PageLSN要小,那就直接略過(這點非常類似TCP的超時重發中的判重機制)。在Redo完成後,保證了所有的髒頁都刷到了磁盤中,並且未提交事務也寫入了磁盤中,這時需要對未提交事務進行回滾,也就是階段3。

階段3:進行Undo

階段1中已經準備好了未提交事務集合,從最後一條日誌逆向遍歷(每條日誌都有一個preLSN字段,指向前一條日誌),直到未提交事務中的第一條日誌。
從後往前開始回滾,每遇到一條屬於未提交事務集合中事務的日誌,就生成一條對應的逆向SQL(這裏需要用到對應的歷史版本數據)執行,這條逆向SQL也會被記錄到Redo Log中,但與一般的日誌有所不同,稱爲Compensation Log Record(CLR),逆向執行完事務後(遇到事務的開始標誌)就提交,這也就完成了回滾。

事務實現原理2(Undo Log)

Undo Log功能

上面在宕機回滾中,提到了生成逆向SQL,這個是需要使用到歷史版本數據的。Undo Log就是用於記錄和維護歷史版本數據的(事務的每一次修改,就是一個版本)。其實這是用到了CopyOnWrite的思想,每次事務在修改記錄之前,都會把該記錄拷貝一份出來(將它備份在Undo Log中)再進行修改操作(這個思想類似JDK中CopyOnWriteArratList)。事務的RC、RR隔離級別就是通過CopyOnWrite實現的。

併發的事務,要同時讀寫同一行數據,只能讀取數據的歷史版本,而不能讀取當前正在被修改的數據(所以這樣就有了丟失更新的問題,當然這個可以通過加鎖等方式解決)。這種機制稱爲Multiversion concurrency control 多版本併發控制(MVCC)。基於MVCC的這種特性,通常select語句都是不加鎖的,因爲他們讀到的都是歷史版本的數據,這種讀,叫做“快照讀”。

Undo Log結構

Undo Log並不是log,而是數據(所以Undo Log也會被記錄到Redo Log中,在宕機後用Redo Log來恢復Undo Log),它記錄的不是事務執行的日誌,而是數據的歷史版本。一旦事務提交後,就不需要Undo Log了(它只在事務提交過程中有用)。
所以Undo Log應該叫做記錄的備份數據,也就是在事務提交之前的備份數據(因爲可能有其它事務還在引用歷史版本數據),事務提交後它就沒有用了。
Undo Log的結構除了主鍵ID和數據外,還有兩個字段。一個是修改該記錄的事務ID,一個是rollback_ptr(指向之前的一個版本,所以它用來串聯所有的歷史版本)。
image.png

BinLog與主從複製

Binlog與Redo Log

  • Redo Log主要是用於實現事務的日誌,而Binlog主要是用於主從複製的日誌(也可以用於實現主動更新緩存)。
  • Redo Log是InnoDB引擎中的,並且事務在日誌中是交叉排列(可並行寫入)的。而Binlog是MySQL層面的,事務在日誌中是連續排列(只能串行寫入)的,並且全局只有一份。
  • Redo Log可以並行寫入,而且未提交的事務也會被寫入。Binlog只能串行寫入(可通過Group Commit解決效率問題,類似pipeline機制),並且Binlog中只記錄已提交的事務。

內部XA

內部XA就是內部的分佈式事務,也就是Redo Log與Binlog之間的事務,一般使用2階段提交方案。
內部XA
Binlog中只記錄已提交的事務,所以以Binlog刷盤成功來判斷一個事務是否被提交。如果宕機了,分以下三種情況討論。

  • 在階段1宕機,此時Binlog全在內存中或還沒寫入內存,Redo Log可以自己回滾,沒有影響。
  • 在階段2宕機,此時Binlog還沒刷盤完,Binlog可以通過類似Checksum的方法來判斷不完整的部分,並把這段截斷。Redo Log同上。
  • 在階段2宕機,此時Binlog刷盤結束,InnoDB還沒提交,因爲Binlog已經持久化了,可以根據Binlog來恢復事務的提交。

主從複製

MySQL有三種主從複製的方式。同步複製因爲效率太低,一般不會使用,而異步複製雖然快,但是容易丟失數據,所以一般使用半同步複製。

複製方式 描述
同步複製 等待所有的slave都接收到了Binlog,才向客戶端返回事務提交成功
異步複製 只要Master事務提交成功,立刻向客戶端返回事務提交成功,然後通過後臺線程異步地向slave同步Binlog。
半同步複製 Master事務提交成功後,向slave同步Binlog,只要有部分slave(默認是1)接收到了Binlog,就向客戶端返回事務提交成功。

並行複製

下圖展示了MySQL的主從複製的原理,其中階段2使用了並行回放。這就是並行複製。
並行複製
所謂的並行回放,其實就是一次性從RelayLog中取出多個事務,然後通過多個線程並行的執行。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章