當前位置:網站首頁>『淺入深出』MySQL 中事務的實現

『淺入深出』MySQL 中事務的實現

2022-01-28 15:46:47 面向信仰編程

在關系型數據庫中,事務的重要性不言而喻,只要對數據庫稍有了解的人都知道事務具有 ACID 四個基本屬性,而我們不知道的可能就是數據庫是如何實現這四個屬性的;在這篇文章中,我們將對事務的實現進行分析,嘗試理解數據庫是如何實現事務的,當然我們也會在文章中簡單對 MySQL 中對 ACID 的實現進行簡單的介紹。

Transaction-Basics

事務其實就是並發控制的基本單比特;相信我們都知道,事務是一個序列操作,其中的操作要麼都執行,要麼都不執行,它是一個不可分割的工作單比特;數據庫事務的 ACID 四大特性是事務的基礎,了解了 ACID 是如何實現的,我們也就清楚了事務的實現,接下來我們將依次介紹數據庫是如何實現這四個特性的。

原子性

在學習事務時,經常有人會告訴你,事務就是一系列的操作,要麼全部都執行,要都不執行,這其實就是對事務原子性的刻畫;雖然事務具有原子性,但是原子性並不是只與事務有關系,它的身影在很多地方都會出現。

Atomic-Operation

由於操作並不具有原子性,並且可以再分為多個操作,當這些操作出現錯誤或拋出异常時,整個操作就可能不會繼續執行下去,而已經進行的操作造成的副作用就可能造成數據更新的丟失或者錯誤。

事務其實和一個操作沒有什麼太大的區別,它是一系列的數據庫操作(可以理解為 SQL)的集合,如果事務不具備原子性,那麼就沒辦法保證同一個事務中的所有操作都被執行或者未被執行了,整個數據庫系統就既不可用也不可信。

回滾日志

想要保證事務的原子性,就需要在异常發生時,對已經執行的操作進行回滾,而在 MySQL 中,恢複機制是通過回滾日志(undo log)實現的,所有事務進行的修改都會先記錄到這個回滾日志中,然後在對數據庫中的對應行進行寫入。

Transaction-Undo-Log

這個過程其實非常好理解,為了能够在發生錯誤時撤銷之前的全部操作,肯定是需要將之前的操作都記錄下來的,這樣在發生錯誤時才可以回滾。

回滾日志除了能够在發生錯誤或者用戶執行 ROLLBACK 時提供回滾相關的信息,它還能够在整個系統發生崩潰、數據庫進程直接被殺死後,當用戶再次啟動數據庫進程時,還能够立刻通過查詢回滾日志將之前未完成的事務進行回滾,這也就需要回滾日志必須先於數據持久化到磁盤上,是我們需要先寫日志後寫數據庫的主要原因。

回滾日志並不能將數據庫物理地恢複到執行語句或者事務之前的樣子;它是邏輯日志,當回滾日志被使用時,它只會按照日志邏輯地將數據庫中的修改撤銷掉看,可以理解為,我們在事務中使用的每一條 INSERT 都對應了一條 DELETE,每一條 UPDATE 也都對應一條相反的 UPDATE 語句。

Logical-Undo-Log

在這裏,我們並不會介紹回滾日志的格式以及它是如何被管理的,本文重點關注在它到底是一個什麼樣的東西,究竟解决了、如何解决了什麼樣的問題,如果想要了解具體實現細節的讀者,相信網絡上關於回滾日志的文章一定不少。

事務的狀態

因為事務具有原子性,所以從遠處看的話,事務就是密不可分的一個整體,事務的狀態也只有三種:Active、Commited 和 Failed,事務要不就在執行中,要不然就是成功或者失敗的狀態:

Atomitc-Transaction-State

但是如果放大來看,我們會發現事務不再是原子的,其中包括了很多中間狀態,比如部分提交,事務的狀態圖也變得越來越複雜。

Nonatomitc-Transaction-State

事務的狀態圖以及狀態的描述取自 Database System Concepts 一書中第 14 章的內容。

  • Active:事務的初始狀態,錶示事務正在執行;
  • Partially Commited:在最後一條語句執行之後;
  • Failed:發現事務無法正常執行之後;
  • Aborted:事務被回滾並且數據庫恢複到了事務進行之前的狀態之後;
  • Commited:成功執行整個事務;

雖然在發生錯誤時,整個數據庫的狀態可以恢複,但是如果我們在事務中執行了諸如:向標准輸出打印日志、向外界發出郵件、沒有通過數據庫修改了磁盤上的內容甚至在事務執行期間發生了轉賬匯款,那麼這些操作作為可見的外部輸出都是沒有辦法回滾的;這些問題都是由應用開發者解决和負責的,在絕大多數情况下,我們都需要在整個事務提交後,再觸發類似的無法回滾的操作。

Shutdown-After-Commited

以訂票為例,哪怕我們在整個事務結束之後,才向第三方發起請求,由於向第三方請求並獲取結果是一個需要較長時間的操作,如果在事務剛剛提交時,數據庫或者服務器發生了崩潰,那麼我們就非常有可能丟失發起請求這一過程,這就造成了非常嚴重的問題;而這一點就不是數據庫所能保證的,開發者需要在適當的時候查看請求是否被發起、結果是成功還是失敗。

並行事務的原子性

到目前為止,所有的事務都只是串行執行的,一直都沒有考慮過並行執行的問題;然而在實際工作中,並行執行的事務才是常態,然而並行任務下,卻可能出現非常複雜的問題:

Nonrecoverable-Schedule

當 Transaction1 在執行的過程中對 id = 1 的用戶進行了讀寫,但是沒有將修改的內容進行提交或者回滾,在這時 Transaction2 對同樣的數據進行了讀操作並提交了事務;也就是說 Transaction2 是依賴於 Transaction1 的,當 Transaction1 由於一些錯誤需要回滾時,因為要保證事務的原子性,需要對 Transaction2 進行回滾,但是由於我們已經提交了 Transaction2,所以我們已經沒有辦法進行回滾操作,在這種問題下我們就發生了問題,Database System Concepts 一書中將這種現象稱為不可恢複安排(Nonrecoverable Schedule),那什麼情况下是可以恢複的呢?

A recoverable schedule is one where, for each pair of transactions Ti and Tj such that Tj reads a data item previously written by Ti , the commit operation of Ti appears before the commit operation of Tj .

簡單理解一下,如果 Transaction2 依賴於事務 Transaction1,那麼事務 Transaction1 必須在 Transaction2 提交之前完成提交的操作:

Recoverable-Schedule

然而這樣還不算完,當事務的數量逐漸增多時,整個恢複流程也會變得越來越複雜,如果我們想要從事務發生的錯誤中恢複,也不是一件那麼容易的事情。

Cascading-Rollback

在上圖所示的一次事件中,Transaction2 依賴於 Transaction1,而 Transaction3 又依賴於 Transaction1,當 Transaction1 由於執行出現問題發生回滾時,為了保證事務的原子性,就會將 Transaction2 和 Transaction3 中的工作全部回滾,這種情况也叫做級聯回滾(Cascading Rollback),級聯回滾的發生會導致大量的工作需要撤回,是我們難以接受的,不過如果想要達到絕對的原子性,這件事情又是不得不去處理的,我們會在文章的後面具體介紹如何處理並行事務的原子性。

持久性

既然是數據庫,那麼一定對數據的持久存儲有著非常强烈的需求,如果數據被寫入到數據庫中,那麼數據一定能够被安全存儲在磁盤上;而事務的持久性就體現在,一旦事務被提交,那麼數據一定會被寫入到數據庫中並持久存儲起來。

Compensating-Transaction

當事務已經被提交之後,就無法再次回滾了,唯一能够撤回已經提交的事務的方式就是創建一個相反的事務對原操作進行『補償』,這也是事務持久性的體現之一。

重做日志

與原子性一樣,事務的持久性也是通過日志來實現的,MySQL 使用重做日志(redo log)實現事務的持久性,重做日志由兩部分組成,一是內存中的重做日志緩沖區,因為重做日志緩沖區在內存中,所以它是易失的,另一個就是在磁盤上的重做日志文件,它是持久的。

Redo-Logging

當我們在一個事務中嘗試對數據進行修改時,它會先將數據從磁盤讀入內存,並更新內存中緩存的數據,然後生成一條重做日志並寫入重做日志緩存,當事務真正提交時,MySQL 會將重做日志緩存中的內容刷新到重做日志文件,再將內存中的數據更新到磁盤上,圖中的第 4、5 步就是在事務提交時執行的。

在 InnoDB 中,重做日志都是以 512 字節的塊的形式進行存儲的,同時因為塊的大小與磁盤扇區大小相同,所以重做日志的寫入可以保證原子性,不會由於機器斷電導致重做日志僅寫入一半並留下髒數據。

除了所有對數據庫的修改會產生重做日志,因為回滾日志也是需要持久存儲的,它們也會創建對應的重做日志,在發生錯誤後,數據庫重啟時會從重做日志中找出未被更新到數據庫磁盤中的日志重新執行以滿足事務的持久性。

回滾日志和重做日志

到現在為止我們了解了 MySQL 中的兩種日志,回滾日志(undo log)和重做日志(redo log);在數據庫系統中,事務的原子性和持久性是由事務日志(transaction log)保證的,在實現時也就是上面提到的兩種日志,前者用於對事務的影響進行撤銷,後者在錯誤處理時對已經提交的事務進行重做,它們能保證兩點:

  1. 發生錯誤或者需要回滾的事務能够成功回滾(原子性);
  2. 在事務提交後,數據沒來得及寫會磁盤就宕機時,在下次重新啟動後能够成功恢複數據(持久性);

在數據庫中,這兩種日志經常都是一起工作的,我們可以將它們整體看做一條事務日志,其中包含了事務的 ID、修改的行元素以及修改前後的值。

Transaction-Log

一條事務日志同時包含了修改前後的值,能够非常簡單的進行回滾和重做兩種操作,在這裏我們也不會對重做和回滾日志展開進行介紹,可能會在之後的文章談一談數據庫系統的恢複機制時提到兩種日志的使用。

隔離性

其實作者在之前的文章 『淺入淺出』MySQL 和 InnoDB 就已經介紹過數據庫事務的隔離性,不過為了保證文章的獨立性和完整性,我們還會對事務的隔離性進行介紹,介紹的內容可能稍微有所不同。

事務的隔離性是數據庫處理數據的幾大基礎之一,如果沒有數據庫的事務之間沒有隔離性,就會發生在 並行事務的原子性 一節中提到的級聯回滾等問題,造成性能上的巨大損失。如果所有的事務的執行順序都是線性的,那麼對於事務的管理容易得多,但是允許事務的並行執行卻能能够提昇吞吐量和資源利用率,並且可以减少每個事務的等待時間。

Reasons-for-Allowing-Concurrency

當多個事務同時並發執行時,事務的隔離性可能就會被違反,雖然單個事務的執行可能沒有任何錯誤,但是從總體來看就會造成數據庫的一致性出現問題,而串行雖然能够允許開發者忽略並行造成的影響,能够很好地維護數據庫的一致性,但是卻會影響事務執行的性能。

事務的隔離級別

所以說數據庫的隔離性和一致性其實是一個需要開發者去權衡的問題,為數據庫提供什麼樣的隔離性層級也就决定了數據庫的性能以及可以達到什麼樣的一致性;在 SQL 標准中定義了四種數據庫的事務的隔離級別:READ UNCOMMITEDREAD COMMITEDREPEATABLE READSERIALIZABLE;每個事務的隔離級別其實都比上一級多解决了一個問題:

  • RAED UNCOMMITED:使用查詢語句不會加鎖,可能會讀到未提交的行(Dirty Read);
  • READ COMMITED:只對記錄加記錄鎖,而不會在記錄之間加間隙鎖,所以允許新的記錄插入到被鎖定記錄的附近,所以再多次使用查詢語句時,可能得到不同的結果(Non-Repeatable Read);
  • REPEATABLE READ:多次讀取同一範圍的數據會返回第一次查詢的快照,不會返回不同的數據行,但是可能發生幻讀(Phantom Read);
  • SERIALIZABLE:InnoDB 隱式地將全部的查詢語句加上共享鎖,解决了幻讀的問題;

以上的所有的事務隔離級別都不允許髒寫入(Dirty Write),也就是當前事務更新了另一個事務已經更新但是還未提交的數據,大部分的數據庫中都使用了 READ COMMITED 作為默認的事務隔離級別,但是 MySQL 使用了 REPEATABLE READ 作為默認配置;從 RAED UNCOMMITED 到 SERIALIZABLE,隨著事務隔離級別變得越來越嚴格,數據庫對於並發執行事務的性能也逐漸下降。

Isolation-Performance

對於數據庫的使用者,從理論上說,並不需要知道事務的隔離級別是如何實現的,我們只需要知道這個隔離級別解决了什麼樣的問題,但是不同數據庫對於不同隔離級別的是實現細節在很多時候都會讓我們遇到意料之外的坑。

如果讀者不了解髒讀、不可重複讀和幻讀究竟是什麼,可以閱讀之前的文章 『淺入淺出』MySQL 和 InnoDB,在這裏我們僅放一張圖來展示各個隔離層級對這幾個問題的解决情况。

Transaction-Isolation-Matrix

隔離級別的實現

數據庫對於隔離級別的實現就是使用並發控制機制對在同一時間執行的事務進行控制,限制不同的事務對於同一資源的訪問和更新,而最重要也最常見的並發控制機制,在這裏我們將簡單介紹三種最重要的並發控制器機制的工作原理。

鎖是一種最為常見的並發控制機制,在一個事務中,我們並不會將整個數據庫都加鎖,而是只會鎖住那些需要訪問的數據項, MySQL 和常見數據庫中的鎖都分為兩種,共享鎖(Shared)和互斥鎖(Exclusive),前者也叫讀鎖,後者叫寫鎖。

Shared-Exclusive-Lock

讀鎖保證了讀操作可以並發執行,相互不會影響,而寫鎖保證了在更新數據庫數據時不會有其他的事務訪問或者更改同一條記錄造成不可預知的問題。

時間戳

除了鎖,另一種實現事務的隔離性的方式就是通過時間戳,使用這種方式實現事務的數據庫,例如 PostgreSQL 會為每一條記錄保留兩個字段;讀時間戳中包括了所有訪問該記錄的事務中的最大時間戳,而記錄行的寫時間戳中保存了將記錄改到當前值的事務的時間戳。

Timestamps-Record

使用時間戳實現事務的隔離性時,往往都會使用樂觀鎖,先對數據進行修改,在寫回時再去判斷當前值,也就是時間戳是否改變過,如果沒有改變過,就寫入,否則,生成一個新的時間戳並再次更新數據,樂觀鎖其實並不是真正的鎖機制,它只是一種思想,在這裏並不會對它進行展開介紹。

多版本和快照隔離

通過維護多個版本的數據,數據庫可以允許事務在數據被其他事務更新時對舊版本的數據進行讀取,很多數據庫都對這一機制進行了實現;因為所有的讀操作不再需要等待寫鎖的釋放,所以能够顯著地提昇讀的性能,MySQL 和 PostgreSQL 都對這一機制進行自己的實現,也就是 MVCC,雖然各自實現的方式有所不同,MySQL 就通過文章中提到的回滾日志實現了 MVCC,保證事務並行執行時能够不等待互斥鎖的釋放直接獲取數據。

隔離性與原子性

在這裏就需要簡單提一下在在原子性一節中遇到的級聯回滾等問題了,如果一個事務對數據進行了寫入,這時就會獲取一個互斥鎖,其他的事務就想要獲得改行數據的讀鎖就必須等待寫鎖的釋放,自然就不會發生級聯回滾等問題了。

Shared-Lock-and-Atomicity

不過在大多數的數據庫,比如 MySQL 中都使用了 MVCC 等特性,也就是正常的讀方法是不需要獲取鎖的,在想要對讀取的數據進行更新時需要使用 SELECT ... FOR UPDATE 嘗試獲取對應行的互斥鎖,以保證不同事務可以正常工作。

一致性

作者認為數據庫的一致性是一個非常讓人迷惑的概念,原因是數據庫領域其實包含兩個一致性,一個是 ACID 中的一致性、另一個是 CAP 定義中的一致性。

ACID-And-CAP

這兩個數據庫的一致性說的完全不是一個事情,很多很多人都對這兩者的概念有非常深的誤解,當我們在討論數據庫的一致性時,一定要清楚上下文的語義是什麼,盡量明確的問出我們要討論的到底是 ACID 中的一致性還是 CAP 中的一致性。

ACID

數據庫對於 ACID 中的一致性的定義是這樣的:如果一個事務原子地在一個一致地數據庫中獨立運行,那麼在它執行之後,數據庫的狀態一定是一致的。對於這個概念,它的第一層意思就是對於數據完整性的約束,包括主鍵約束、引用約束以及一些約束檢查等等,在事務的執行的前後以及過程中不會違背對數據完整性的約束,所有對數據庫寫入的操作都應該是合法的,並不能產生不合法的數據狀態。

A transaction must preserve database consistency - if a transaction is run atomically in isolation starting from a consistent database, the database must again be consistent at the end of the transaction.

我們可以將事務理解成一個函數,它接受一個外界的 SQL 輸入和一個一致的數據庫,它一定會返回一個一致的數據庫。

Transaction-Consistency

而第二層意思其實是指邏輯上的對於開發者的要求,我們要在代碼中寫出正確的事務邏輯,比如銀行轉賬,事務中的邏輯不可能只扣錢或者只加錢,這是應用層面上對於數據庫一致性的要求。

Ensuring consistency for an individual transaction is the responsibility of the application programmer who codes the transaction. - Database System Concepts

數據庫 ACID 中的一致性對事務的要求不止包含對數據完整性以及合法性的檢查,還包含應用層面邏輯的正確。

CAP 定理中的數據一致性,其實是說分布式系統中的各個節點中對於同一數據的拷貝有著相同的值;而 ACID 中的一致性是指數據庫的規則,如果 schema 中規定了一個值必須是唯一的,那麼一致的系統必須確保在所有的操作中,該值都是唯一的,由此來看 CAP 和 ACID 對於一致性的定義有著根本性的區別。

總結

事務的 ACID 四大基本特性是保證數據庫能够運行的基石,但是完全保證數據庫的 ACID,尤其是隔離性會對性能有比較大影響,在實際的使用中我們也會根據業務的需求對隔離性進行調整,除了隔離性,數據庫的原子性和持久性相信都是比較好理解的特性,前者保證數據庫的事務要麼全部執行、要麼全部不執行,後者保證了對數據庫的寫入都是持久存儲的、非易失的,而一致性不僅是數據庫對本身數據的完整性的要求,同時也對開發者提出了要求 - 寫出邏輯正確並且合理的事務。

最後,也是最重要的,當別人在講一致性的時候,一定要搞清楚他的上下文,如果對文章的內容有疑問,可以在評論中留言。

References

wechat-account-qrcode

轉載申請

知識共享許可協議
本作品采用 知識共享署名 4.0 國際許可協議進行許可,轉載時請注明原文鏈接,圖片在使用時請保留全部內容,可適當縮放並在引用處附上圖片所在的文章鏈接。

Go 語言設計與實現

各比特讀者朋友,很高興大家通過本博客學習 Go 語言,感謝一路相伴! 《Go語言設計與實現》 的紙質版圖書已經上架京東,有需要的朋友請點擊 鏈接 購買。

文章圖片

你可以在 技術文章配圖指南 中找到畫圖的方法和素材。

版權聲明
本文為[面向信仰編程]所創,轉載請帶上原文鏈接,感謝
https://cht.chowdera.com/2022/01/202201281546472027.html

隨機推薦