『MySQL』深入理解事務的來龍去脈

天澄發表於2019-04-21

MySQL系列文章:

『MySQL』搞懂 InnoDB 鎖機制 以及 高併發下如何解決超賣問題

「MySQL」高效能索引優化策略

『MySQL』揭開索引神祕面紗

『MySQL』MySQL執行流程

一張思維導圖看完系列文章:

『MySQL』深入理解事務的來龍去脈

距離上一篇MySQL的文章已經過去一個月了,終於有時間來寫寫關於MySQL的事務了。本文內容預設是針對 MySQL InnoDB 引擎。

先來一張思維導圖讀全文內容:

『MySQL』深入理解事務的來龍去脈

1. 為什麼需要有事務

瞭解事務之前,先來看看資料庫為什麼需要有事務,假設沒有事務會有什麼影響?

舉一個轉賬的例子,假設你朋友向你借10000元,你開啟APP,樂呵呵的把錢轉了,你的卡里已經少了10000元,但是你打電話給朋友時,你朋友說沒有收到啊,你這時候肯定賣銀行怎麼不靠譜,沒到賬怎麼把我卡里的錢給扣了。

我們來捋一捋上述銀行發生的過程,簡單的分三步:

A發起轉賬10000給B -> A銀行卡減10000元 -> B銀行卡增加10000元。

上述案例是第三步出現了問題,如果有事務,則不會發生案例中的事情,可以理解為事務就是這三個步驟是一根繩子上的螞蚱,要麼都成功,要麼都失敗。

所以資料庫引入事務的主要目的是事務會把資料庫會從一種一致狀態轉換到另一種一致狀態,資料庫提交工作時可以確保要麼所有修改都儲存,要麼所有修改都不儲存。

瞭解事務,還需要了解事務的理論依據ACID,也可以說事務的幾個特性。

1.1 ACID

1.1.1 A(Atomicity) 原子性

還是上面轉賬的例子,原子性強調轉賬從A-B的三個步驟必須要麼都成功,要麼都不成功。

原子性是整個資料庫事務是不可分割的工作單位,只有事務中的所有的資料庫操作都執行成功,才算整個事務成功。事務中任何一個SQL執行失敗,已經執行成功的SQL語句也必須撤銷,回到執行事務的之前的狀態。

1.1.2 C(Consistency) 一致性

一致性是指事務將資料庫從一種一致性狀態變為下一種一致性狀態。在事務開始之前和之後,資料庫的完整性約束沒有被破壞。

上面轉賬的例子,無論轉賬成功或者失敗,A和B加起來變化就是10000元,不會多也不會少。

1.1.3 I(Isolation) 隔離性

隔離性要求每個讀寫事務對其他事務的操作物件能相互分離。

比如A轉賬的銀行是工商銀行,那麼別人在工商銀行轉賬不能干擾A的轉賬行為。

1.1.4 D(Durability) 永續性

永續性指事務一旦提交,其結果就是永久性的。

2. 事務的實現

事務的實現就是如何實現ACID特性,下面一圖下概況下:

『MySQL』深入理解事務的來龍去脈

由上圖看,事務的實現通過 redo_log 和 undo_log, 以及鎖實現,鎖實現事務的隔離,上一篇已經講解InnoDB的鎖,需要了解的朋友可以檢視上一篇文章。

redo_log 實現持久化和原子性,而undo_log實現一致性,二種日誌均可以視為一種恢復操作,redo_log是恢復提交事務修改的頁操作,而undo_log是回滾行記錄到特定版本。二者記錄的內容也不同,redo_log是物理日誌,記錄頁的物理修改操作,而undo_log是邏輯日誌,根據每行記錄進行記錄。

2.1 redo log 重做日誌

redo_log 重做日誌上面已經提到實現持久化和原子性,重做日誌由兩部分組成,一是記憶體中的重做日誌快取(redo log buffer),這部分是容易丟失的。二是重做日誌檔案(redo log file),這部分是持久的。

知道redo_log是什麼?還需要了解其更新流程以及redo log存的是什麼內容和恢復機制。

2.1.1 更新流程

先來了解第一個問題,redo log的更新流程如下圖,以一次Update 操作為例。

『MySQL』深入理解事務的來龍去脈

  • 執行update操作。
  • 先將原始資料從磁碟讀取到記憶體,修改記憶體中的資料。
  • 生成一條重做日誌寫入redo log buffer,記錄資料被修改後的值。
  • 當事務提交時,需要將redo log buffer中的內容重新整理到redo log file。
  • 事務提交後,也會將記憶體中修改資料的值寫入磁碟。

為了確保每次日誌都寫入重做日誌檔案,InnoDB儲存引擎會呼叫一次fsync操作。

2.1.2 儲存格式內容

瞭解redo log儲存格式和內容之前,先來對比一下跟binlog二進位制日誌由什麼不同,binlog主要是主從複製和進行POINT-IN-TIME的恢復,想必大家對它不陌生。

binlog只有在事務提交的時候才會寫入,且是資料庫的上層產生的。redo log是Innodb引擎層產生的。

對比一二者的寫入方式:

『MySQL』深入理解事務的來龍去脈

binlog是每次事務才寫入,所以每個事務只會有一條日誌,記錄的邏輯日誌,也可以說記錄的就是SQL語句。

redo log是事務開始就開始寫入,*T1表示事務提交。記錄的是物理格式日誌,即每個頁的修改。

redo log預設是以block(塊)的方式為單位進行儲存,每個塊是512個位元組。不同的資料庫引擎有對應的重做日誌格式,Innodb的儲存管理是基於頁的,所以其重做日誌也是基於頁的。

redo log格式:

『MySQL』深入理解事務的來龍去脈

  • redo_log_type 重做日誌型別
  • space 表空間的ID
  • page_no 頁的偏移量
  • redo_log_body 儲存內容

執行一條插入語句,重做日誌大致為:

INSERT INTO user SELECT 1,2;
           |
page(2,3), offset 32, value 1,2 # 主鍵索引
page(2,4), offset 64, value 2   # 輔助索引
複製程式碼

可以看到重做日誌儲存的格式有點看不太懂,看不懂沒有關係,主要是告訴大家,重做日誌儲存物理格式日誌,也就是基於儲存頁的修改。

2.1.3 恢復機制

再來了解一下 redo log的恢復機制:

『MySQL』深入理解事務的來龍去脈

上圖概況了重做日誌的恢復機制,先來解釋一下圖中出現的 LSN 是什麼?

LSN(Log Sequence Number) 日誌序列號,Innodb裡,LSN佔8個位元組,且是單調遞增的,代表的含義有: 重做日誌寫入的總量、checkpoint的位置、頁的版本。

假設在LSN=10000的時候資料庫出現故障,磁碟中checkpoint為10000,表示磁碟已經重新整理到10000這個序列號,當前redolog的checkpoint是13000,則需要恢復10000-13000的資料。

再來想想,redo log為什麼可以實現事務的原子性和永續性。

  • 原子性,是redo log記錄了事務期間操作的物理日誌,事務提交之前,並沒有寫入磁碟,儲存在記憶體裡,如果事務失敗,資料庫磁碟不會有影響,回滾掉事務記憶體部分即可。
  • 永續性,redo log 會在事務提交時將日誌儲存到磁碟redo log file,保證日誌的永續性。

2.2 undo log

redo log一旦提交意味著持久化了,但是有時候需要對其進行rollback操作,那就需要undo log。

undo log是邏輯日誌,只是將資料庫邏輯的恢復到原來的樣子。並不能將資料庫物理地恢復到執行語句或者事務之前的樣子。雖然所有的邏輯修改均被取消了,但是資料結構和頁本身在回滾前後可能不一樣了。

既然是邏輯日誌,可以理解為它儲存的是SQL, 在事務中使用的每一條 INSERT 都對應了一條 DELETE,每一條 UPDATE 也都對應一條相反的 UPDATE 語句。

『MySQL』深入理解事務的來龍去脈

undo log 存放在資料庫內部的一個特殊段(segment)中,也叫undo段,存在於共享表空間中。

undo log實現了事務的一致性,可以通過undo log恢復到事務之前的邏輯狀態,保證一致性。

undo log 還可以實現MVCC(Multi-Version Concurrency Control ,多版本併發控制),多版本併發控制其實可以通過 undo log 形成一個事務執行過程中的版本鏈,每一個寫操作會產生一個版本,資料庫發生讀的併發訪問時,讀操作訪問版本鏈,返回最合適的結果直接返回。從而讀寫操作之間沒有衝突,提高了效能。

3. 事務控制語句

『MySQL』深入理解事務的來龍去脈

上圖列出了事務的一些控制語句,start transaction/begin、commit、rollback相信大家都有用過。

savepoint identifier 可以建立事務的一個儲存點,執行回滾操作時可以回滾到指定儲存點,不需要回滾整個事務。

打個比例,假設你去旅遊到目標地需要三個行程,第一程 深圳到廣州高鐵,第二程 從廣州飛到雅加達,第三程 雅加達飛到某島。 如果再第三程 飛機取消行程,事務要回滾,如果要你再會深圳,你肯定會心理一萬個草泥馬。因為再進入事務,第一步和第二步是不變的,所以不需要回滾,直接回滾第三步即可。

set transaction 修改事務隔離級別,比如修改會話級別的事務:

set session transaction isolation level read committed;

4. 事務隔離級別

『MySQL』深入理解事務的來龍去脈

事務的隔離性是通過鎖來實現,上一篇也提到事務的隔離級別,這篇簡單回顧一下。

四種隔離級別,按READ-UNCOMMITTED、READ-COMMITTED、REPEATABLE-READ、SERIALIZABLE順序,隔離級別是從低到高,InnoDB預設是REPEATABLE-READ級別,此級別在其餘資料庫中是會引起幻讀問題,InnoDB採用Next-Key Lock鎖演算法避免了此問題,什麼是幻讀問題,請參考上一篇文章。

隔離級別越低,則事務請求的鎖和保持鎖的時間就越短。

4.1 READ-UNCOMMITTED

READ-UNCOMMITTED 中文叫未提交讀,即一個事務讀到了另一個未提交事務修改過的資料,整個過程如下圖:

『MySQL』深入理解事務的來龍去脈

如上圖,SessionA和SessionB分別開啟一個事務,SessionB中的事務先將id為1的記錄的name列更新為'lisi',然後Session 中的事務再去查詢這條id為1的記錄,那麼在未提交讀的隔離級別下,查詢結果由'zhangsan'變成了'lisi',也就是說某個事務讀到了另一個未提交事務修改過的記錄。但是如果SessionB中的事務稍後進行了回滾,那麼SessionA中的事務相當於讀到了一個不存在的資料,這種現象也稱為髒讀。

可見READ-UNCOMMITTED是非常不安全。

4.2 READ COMMITTED

READ COMMITTED 中文叫已提交讀,或者叫不可重複讀。即一個事務能讀到另一個已經提交事務修改後的資料,如果其他事務均對該資料進行修改並提交,該事務也能查詢到最新值。如下圖:

『MySQL』深入理解事務的來龍去脈

在第4步 SessionB 修改後,如果未提交,SessionA是讀不到,但SessionB一旦提交後,SessionA即可讀到SessionB修改的內容。

從某種程度上已提交讀是違反事務的隔離性的。

4.3 REPEATABLE READ

REPEATABLE READ 中文叫可重複讀,即事務能讀到另一個已經提交的事務修改過的資料,但是第一次讀過某條記錄後,即使後面其他事務修改了該記錄的值並且提交,該事務之後再讀該條記錄時,讀到的仍是第一次讀到的值,而不是每次都讀到不同的資料。如下圖:

『MySQL』深入理解事務的來龍去脈

InnoDB預設是這種隔離級別,SessionB無論怎麼修改id=1的值,SessionA讀到依然是自己開啟事務第一次讀到的內容。

4.4 SERIALIZABLE

SERIALIZABLE 叫序列化, 上面三種隔離級別可以進行 讀-讀 或者 讀-寫、寫-讀三種併發操作,而SERIALIZABLE不允許讀-寫,寫-讀的併發操作。 如下圖:

『MySQL』深入理解事務的來龍去脈

SessionB 對 id=1 進行修改的時候,SessionA 讀取id=1則需要等待 SessionB 提交事務。可以理解SessionB在更新的時候加了X鎖。

5. 分散式事務

分散式事務指允許多個獨立的事務資源參與到一個全域性的事務中。全域性事務要求在其中的所有參與的事務要麼都提交,要麼都回滾。

5.1 InnoDB 分散式事務

InnoDB 是支援分散式事務,由一個或多個資源管理器(Resource Managers),一個事務管理器(Transaction Manager),以及一個應用程式(Application Program)組成。

  • 資源管理器(Resource Managers),提供訪問事務資源的方法,一般一個資料庫就是一個資源管理器。
  • 事務管理器(Transaction Manager),協調參與全域性事務中的各個事務,需要和參與全域性事務的所有資源管理器進行通訊。
  • 應用程式(Application Program) 定義事務的邊界,指定全域性事務中的操作。

如下圖:

『MySQL』深入理解事務的來龍去脈

應用程式向一個或多個資料庫執行事務操作,事務管理器進行管理事務,通過二段式提交,第一階段所有參與的全域性事務的節點都開始準備,告訴事務管理器都準備好了,可以提交了。第二階段,事務管理器告訴每一個資源管理器是執行Commit 還是 Rollback。如果任何一個節點顯示不能提交,則所有的節點被告知需要回滾。

5.2 TCC分散式事務

InnoDB的分散式是資料庫實現的,看看資料庫外如何分散式事務,比較常見的是TCC分散式事務。

『MySQL』深入理解事務的來龍去脈

上圖描述了TCC分散式事務的流程,假設電商業務中,支付後需要修改庫存,積分,物流倉儲的資料,如果一個失敗則全部回滾。

TCC分散式事務,有三個階段,Try,Confirm, Cancel。也就是說每個參與事務的服務都需要實現這三個介面,庫存、積分、倉儲都需要實現這三個介面。

第一階段,Try,業務應用調取各個服務的Try介面,告訴他們給我預留一個商品,有人要購買,可以理解為凍結,每一步都不執行成功,只是標記更新狀態。

第二階段,Confirm,確認階段,即事務協調器調取每個服務Confirm執行事務操作,如果某一個服務的Confirm失敗,則有第三個階段。如果成功則結束事務。

第三個階段,Cancel,如果在第二個階段有一個事務提交失敗,則事務協調器調取所有業務的Cancel介面,回滾事務,將第一階段凍結的商品恢復。

思考題:

在MQ中介軟體連線的上游服務和下游服務中如何實現分散式事務了?

歡迎大家留言討論。

更多MySQL相關文章和討論,請關注公眾號:『 天澄技術雜談 』

『MySQL』深入理解事務的來龍去脈

相關文章