在關係型數據庫中,事務的重要性不言而喻,只要對數據庫稍有了解的人都知道事務具備 ACID 四個基本屬性,而咱們不知道的可能就是數據庫是如何實現這四個屬性的;mysql
在這篇文章中,咱們將對事務的實現進行分析,嘗試理解數據庫是如何實現事務的,固然咱們也會在文章中簡單對 MySQL 中對 ACID 的實現進行簡單的介紹。sql
事務其實就是併發控制的基本單位;相信咱們都知道,事務是一個序列操做,其中的操做要麼都執行,要麼都不執行,它是一個不可分割的工做單位;數據庫事務的 ACID 四大特性是事務的基礎,瞭解了 ACID 是如何實現的,咱們也就清楚了事務的實現,接下來咱們將依次介紹數據庫是如何實現這四個特性的。數據庫
在學習事務時,常常有人會告訴你,事務就是一系列的操做,要麼所有都執行,要都不執行,這其實就是對事務原子性的刻畫;雖然事務具備原子性,可是原子性並非只與事務有關係,它的身影在不少地方都會出現。緩存
因爲操做並不具備原子性,而且能夠再分爲多個操做,當這些操做出現錯誤或拋出異常時,整個操做就可能不會繼續執行下去,而已經進行的操做形成的反作用就可能形成數據更新的丟失或者錯誤。安全
事務其實和一個操做沒有什麼太大的區別,它是一系列的數據庫操做(能夠理解爲 SQL)的集合,若是事務不具有原子性,那麼就沒辦法保證同一個事務中的全部操做都被執行或者未被執行了,整個數據庫系統就既不可用也不可信。服務器
想要保證事務的原子性,就須要在異常發生時,對已經執行的操做進行回滾,而在 MySQL 中,恢復機制是經過回滾日誌(undo log)實現的,全部事務進行的修改都會先記錄到這個回滾日誌中,而後在對數據庫中的對應行進行寫入。網絡
這個過程其實很是好理解,爲了可以在發生錯誤時撤銷以前的所有操做,確定是須要將以前的操做都記錄下來的,這樣在發生錯誤時才能夠回滾。併發
回滾日誌除了可以在發生錯誤或者用戶執行 ROLLBACK
時提供回滾相關的信息,它還可以在整個系統發生崩潰、數據庫進程直接被殺死後,當用戶再次啓動數據庫進程時,還可以馬上經過查詢回滾日誌將以前未完成的事務進行回滾,這也就須要回滾日誌必須先於數據持久化到磁盤上,是咱們須要先寫日誌後寫數據庫的主要緣由。app
回滾日誌並不能將數據庫物理地恢復到執行語句或者事務以前的樣子;它是邏輯日誌,當回滾日誌被使用時,它只會按照日誌邏輯地將數據庫中的修改撤銷掉看,能夠理解爲,咱們在事務中使用的每一條 INSERT
都對應了一條 DELETE
,每一條 UPDATE
也都對應一條相反的 UPDATE
語句。分佈式
在這裏,咱們並不會介紹回滾日誌的格式以及它是如何被管理的,本文重點關注在它究竟是一個什麼樣的東西,究竟解決了、如何解決了什麼樣的問題,若是想要了解具體實現細節的讀者,相信網絡上關於回滾日誌的文章必定很多。
由於事務具備原子性,因此從遠處看的話,事務就是密不可分的一個總體,事務的狀態也只有三種:Active、Commited 和 Failed,事務要不就在執行中,要否則就是成功或者失敗的狀態:
可是若是放大來看,咱們會發現事務再也不是原子的,其中包括了不少中間狀態,好比部分提交,事務的狀態圖也變得愈來愈複雜。
事務的狀態圖以及狀態的描述取自 Database System Concepts 一書中第 14 章的內容。
雖然在發生錯誤時,整個數據庫的狀態能夠恢復,可是若是咱們在事務中執行了諸如:向標準輸出打印日誌、向外界發出郵件、沒有經過數據庫修改了磁盤上的內容甚至在事務執行期間發生了轉帳匯款,那麼這些操做做爲可見的外部輸出都是沒有辦法回滾的;這些問題都是由應用開發者解決和負責的,在絕大多數狀況下,咱們都須要在整個事務提交後,再觸發相似的沒法回滾的操做。
以訂票爲例,哪怕咱們在整個事務結束以後,才向第三方發起請求,因爲向第三方請求並獲取結果是一個須要較長時間的操做,若是在事務剛剛提交時,數據庫或者服務器發生了崩潰,那麼咱們就很是有可能丟失發起請求這一過程,這就形成了很是嚴重的問題;而這一點就不是數據庫所能保證的,開發者須要在適當的時候查看請求是否被髮起、結果是成功仍是失敗。
到目前爲止,全部的事務都只是串行執行的,一直都沒有考慮過並行執行的問題;然而在實際工做中,並行執行的事務纔是常態,然而並行任務下,卻可能出現很是複雜的問題:
當 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 提交以前完成提交的操做:
然而這樣還不算完,當事務的數量逐漸增多時,整個恢復流程也會變得愈來愈複雜,若是咱們想要從事務發生的錯誤中恢復,也不是一件那麼容易的事情。
在上圖所示的一次事件中,Transaction2 依賴於 Transaction1,而 Transaction3 又依賴於 Transaction1,當 Transaction1 因爲執行出現問題發生回滾時,爲了保證事務的原子性,就會將 Transaction2 和 Transaction3 中的工做所有回滾,這種狀況也叫作級聯回滾(Cascading Rollback),級聯回滾的發生會致使大量的工做須要撤回,是咱們難以接受的,不過若是想要達到絕對的原子性,這件事情又是不得不去處理的,咱們會在文章的後面具體介紹如何處理並行事務的原子性。
既然是數據庫,那麼必定對數據的持久存儲有着很是強烈的需求,若是數據被寫入到數據庫中,那麼數據必定可以被安全存儲在磁盤上;而事務的持久性就體如今,一旦事務被提交,那麼數據必定會被寫入到數據庫中並持久存儲起來。
當事務已經被提交以後,就沒法再次回滾了,惟一可以撤回已經提交的事務的方式就是建立一個相反的事務對原操做進行『補償』,這也是事務持久性的體現之一。
與原子性同樣,事務的持久性也是經過日誌來實現的,MySQL 使用重作日誌(redo log)實現事務的持久性,重作日誌由兩部分組成,一是內存中的重作日誌緩衝區,由於重作日誌緩衝區在內存中,因此它是易失的,另外一個就是在磁盤上的重作日誌文件,它是持久的。
當咱們在一個事務中嘗試對數據進行修改時,它會先將數據從磁盤讀入內存,並更新內存中緩存的數據,而後生成一條重作日誌並寫入重作日誌緩存,當事務真正提交時,MySQL 會將重作日誌緩存中的內容刷新到重作日誌文件,再將內存中的數據更新到磁盤上,圖中的第 四、5 步就是在事務提交時執行的。
在 InnoDB 中,重作日誌都是以 512 字節的塊的形式進行存儲的,同時由於塊的大小與磁盤扇區大小相同,因此重作日誌的寫入能夠保證原子性,不會因爲機器斷電致使重作日誌僅寫入一半並留下髒數據。
除了全部對數據庫的修改會產生重作日誌,由於回滾日誌也是須要持久存儲的,它們也會建立對應的重作日誌,在發生錯誤後,數據庫重啓時會從重作日誌中找出未被更新到數據庫磁盤中的日誌從新執行以知足事務的持久性。
到如今爲止咱們瞭解了 MySQL 中的兩種日誌,回滾日誌(undo log)和重作日誌(redo log);在數據庫系統中,事務的原子性和持久性是由事務日誌(transaction log)保證的,在實現時也就是上面提到的兩種日誌,前者用於對事務的影響進行撤銷,後者在錯誤處理時對已經提交的事務進行重作,它們能保證兩點:
在數據庫中,這兩種日誌常常都是一塊兒工做的,咱們能夠將它們總體看作一條事務日誌,其中包含了事務的 ID、修改的行元素以及修改先後的值。
一條事務日誌同時包含了修改先後的值,可以很是簡單的進行回滾和重作兩種操做,在這裏咱們也不會對重作和回滾日誌展開進行介紹,可能會在以後的文章談一談數據庫系統的恢復機制時提到兩種日誌的使用。
其實做者在以前的文章 『淺入淺出』MySQL 和 InnoDB 就已經介紹過數據庫事務的隔離性,不過爲了保證文章的獨立性和完整性,咱們還會對事務的隔離性進行介紹,介紹的內容可能稍微有所不一樣。
事務的隔離性是數據庫處理數據的幾大基礎之一,若是沒有數據庫的事務之間沒有隔離性,就會發生在 並行事務的原子性 一節中提到的級聯回滾等問題,形成性能上的巨大損失。若是全部的事務的執行順序都是線性的,那麼對於事務的管理容易得多,可是容許事務的並行執行卻能可以提高吞吐量和資源利用率,而且能夠減小每一個事務的等待時間。
當多個事務同時併發執行時,事務的隔離性可能就會被違反,雖然單個事務的執行可能沒有任何錯誤,可是從整體來看就會形成數據庫的一致性出現問題,而串行雖然可以容許開發者忽略並行形成的影響,可以很好地維護數據庫的一致性,可是卻會影響事務執行的性能。
因此說數據庫的隔離性和一致性實際上是一個須要開發者去權衡的問題,爲數據庫提供什麼樣的隔離性層級也就決定了數據庫的性能以及能夠達到什麼樣的一致性;在 SQL 標準中定義了四種數據庫的事務的隔離級別:READ UNCOMMITED
、READ COMMITED
、REPEATABLE READ
和 SERIALIZABLE
;每一個事務的隔離級別其實都比上一級多解決了一個問題:
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,隨着事務隔離級別變得愈來愈嚴格,數據庫對於併發執行事務的性能也逐漸降低。
對於數據庫的使用者,從理論上說,並不須要知道事務的隔離級別是如何實現的,咱們只須要知道這個隔離級別解決了什麼樣的問題,可是不一樣數據庫對於不一樣隔離級別的是實現細節在不少時候都會讓咱們遇到意料以外的坑。
若是讀者不瞭解髒讀、不可重複讀和幻讀到底是什麼,能夠閱讀以前的文章 『淺入淺出』MySQL 和 InnoDB,在這裏咱們僅放一張圖來展現各個隔離層級對這幾個問題的解決狀況。
數據庫對於隔離級別的實現就是使用併發控制機制對在同一時間執行的事務進行控制,限制不一樣的事務對於同一資源的訪問和更新,而最重要也最多見的併發控制機制,在這裏咱們將簡單介紹三種最重要的併發控制器機制的工做原理。
鎖是一種最爲常見的併發控制機制,在一個事務中,咱們並不會將整個數據庫都加鎖,而是隻會鎖住那些須要訪問的數據項, MySQL 和常見數據庫中的鎖都分爲兩種,共享鎖(Shared)和互斥鎖(Exclusive),前者也叫讀鎖,後者叫寫鎖。
讀鎖保證了讀操做能夠併發執行,相互不會影響,而寫鎖保證了在更新數據庫數據時不會有其餘的事務訪問或者更改同一條記錄形成不可預知的問題。
除了鎖,另外一種實現事務的隔離性的方式就是經過時間戳,使用這種方式實現事務的數據庫,例如 PostgreSQL 會爲每一條記錄保留兩個字段;讀時間戳中包括了全部訪問該記錄的事務中的最大時間戳,而記錄行的寫時間戳中保存了將記錄改到當前值的事務的時間戳。
使用時間戳實現事務的隔離性時,每每都會使用樂觀鎖,先對數據進行修改,在寫回時再去判斷當前值,也就是時間戳是否改變過,若是沒有改變過,就寫入,不然,生成一個新的時間戳並再次更新數據,樂觀鎖其實並非真正的鎖機制,它只是一種思想,在這裏並不會對它進行展開介紹。
經過維護多個版本的數據,數據庫能夠容許事務在數據被其餘事務更新時對舊版本的數據進行讀取,不少數據庫都對這一機制進行了實現;由於全部的讀操做再也不須要等待寫鎖的釋放,因此可以顯著地提高讀的性能,MySQL 和 PostgreSQL 都對這一機制進行本身的實現,也就是 MVCC,雖然各自實現的方式有所不一樣,MySQL 就經過文章中提到的回滾日誌實現了 MVCC,保證事務並行執行時可以不等待互斥鎖的釋放直接獲取數據。
在這裏就須要簡單提一下在在原子性一節中遇到的級聯回滾等問題了,若是一個事務對數據進行了寫入,這時就會獲取一個互斥鎖,其餘的事務就想要得到改行數據的讀鎖就必須等待寫鎖的釋放,天然就不會發生級聯回滾等問題了。
不過在大多數的數據庫,好比 MySQL 中都使用了 MVCC 等特性,也就是正常的讀方法是不須要獲取鎖的,在想要對讀取的數據進行更新時須要使用 SELECT ... FOR UPDATE
嘗試獲取對應行的互斥鎖,以保證不一樣事務能夠正常工做。
做者認爲數據庫的一致性是一個很是讓人迷惑的概念,緣由是數據庫領域其實包含兩個一致性,一個是 ACID 中的一致性、另外一個是 CAP 定義中的一致性。
這兩個數據庫的一致性說的徹底不是一個事情,不少不少人都對這二者的概念有很是深的誤解,當咱們在討論數據庫的一致性時,必定要清楚上下文的語義是什麼,儘可能明確的問出咱們要討論的究竟是 ACID 中的一致性仍是 CAP 中的一致性。
數據庫對於 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 輸入和一個一致的數據庫,它必定會返回一個一致的數據庫。
而第二層意思實際上是指邏輯上的對於開發者的要求,咱們要在代碼中寫出正確的事務邏輯,好比銀行轉帳,事務中的邏輯不可能只扣錢或者只加錢,這是應用層面上對於數據庫一致性的要求。
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,尤爲是隔離性會對性能有比較大影響,在實際的使用中咱們也會根據業務的需求對隔離性進行調整,除了隔離性,數據庫的原子性和持久性相信都是比較好理解的特性,前者保證數據庫的事務要麼所有執行、要麼所有不執行,後者保證了對數據庫的寫入都是持久存儲的、非易失的,而一致性不只是數據庫對自己數據的完整性的要求,同時也對開發者提出了要求 - 寫出邏輯正確而且合理的事務。