事務是數據庫系統中的重要概念,瞭解這一律念是以正確的方式開發和數據庫交互的應用程序的前提。可是不少開發者對於事務的認識相對片面和膚淺,僅僅把它等同於ACID,不瞭解數據庫系統引入事務的真正動機,ACID對於事務意味着什麼以及最重要的:數據庫系統是如何保證事務的ACID特性的?mysql
由於最近在作一個微服務和分佈式事務相關的項目,做爲必要的前置準備,須要加深下對單機事務的認識並理解其實現原理。這篇文章是在我給組內的小夥伴分享的PPT的基礎上改動而成,旨在幫助你們創建關於事務的相對體系的認識。因爲時間緣由,因此寫的比較倉促,裏面確定有理解不到位甚至出錯的地方,但願看到的小夥伴可以指出來。同時還需強調幾點:sql
如下全部內容都是針對單機事務而言,不涉及分佈式事務相關的東西!數據庫
關於事務原理的講解不針對具體的某個數據庫實現,因此某些地方可能和你的實踐經驗不符。併發
1.1 爲何須要數據庫事務mvc
轉帳是生活中常見的操做,好比從A帳戶轉帳100元到B帳號。站在用戶角度而言,這是一個邏輯上的單一操做,然而在數據庫系統中,至少會分紅兩個步驟來完成:分佈式
1.將A帳戶的金額減小100元微服務
2.將B帳戶的金額增長100元。性能
在這個過程當中可能會出現如下問題:spa
1.轉帳操做的第一步執行成功,A帳戶上的錢減小了100元,可是第二步執行失敗或者未執行便發生系統崩潰,致使B帳戶並無相應增長100元。3d
2.轉帳操做剛完成就發生系統崩潰,系統重啓恢復時丟失了崩潰前的轉帳記錄。
3.同時又另外一個用戶轉帳給B帳戶,因爲同時對B帳戶進行操做,致使B帳戶金額出現異常。
爲了便於解決這些問題,須要引入數據庫事務的概念。
1.2 什麼是數據庫事務
定義:數據庫事務是構成單一邏輯工做單元的操做集合
一個典型的數據庫事務以下所示
BEGIN TRANSACTION //事務開始 SQL1 SQL2 COMMIT/ROLLBACK //事務提交或回滾
關於事務的定義有幾點須要解釋下:
1.數據庫事務能夠包含一個或多個數據庫操做,但這些操做構成一個邏輯上的總體。
2.構成邏輯總體的這些數據庫操做,要麼所有執行成功,要麼所有不執行。
3.構成事務的全部操做,要麼全都對數據庫產生影響,要麼全都不產生影響,即無論事務是否執行成功,數據庫總能保持一致性狀態。
4.以上即便在數據庫出現故障以及併發事務存在的狀況下依然成立。
1.3 事務如何解決問題
對於上面的轉帳例子,能夠將轉帳相關的全部操做包含在一個事務中
BEGIN TRANSACTION A帳戶減小100元 B帳戶增長100元 COMMIT
1.當數據庫操做失敗或者系統出現崩潰,系統可以以事務爲邊界進行恢復,不會出現A帳戶金額減小而B帳戶未增長的狀況。
2.當有多個用戶同時操做數據庫時,數據庫可以以事務爲單位進行併發控制,使多個用戶對B帳戶的轉帳操做相互隔離。
事務使系統可以更方便的進行故障恢復以及併發控制,從而保證數據庫狀態的一致性。
1.4 事務的ACID特性以及實現原理概述
原子性(Atomicity):事務中的全部操做做爲一個總體像原子同樣不可分割,要麼所有成功,要麼所有失敗。
一致性(Consistency):事務的執行結果必須使數據庫從一個一致性狀態到另外一個一致性狀態。一致性狀態是指:1.系統的狀態知足數據的完整性約束(主碼,參照完整性,check約束等) 2.系統的狀態反應數據庫本應描述的現實世界的真實狀態,好比轉帳先後兩個帳戶的金額總和應該保持不變。
隔離性(Isolation):併發執行的事務不會相互影響,其對數據庫的影響和它們串行執行時同樣。好比多個用戶同時往一個帳戶轉帳,最後帳戶的結果應該和他們按前後次序轉帳的結果同樣。
持久性(Durability):事務一旦提交,其對數據庫的更新就是持久的。任何事務或系統故障都不會致使數據丟失。
在事務的ACID特性中,C即一致性是事務的根本追求,而對數據一致性的破壞主要來自兩個方面
1.事務的併發執行
2.事務故障或系統故障
數據庫系統是經過併發控制技術和日誌恢復技術來避免這種狀況發生的。
併發控制技術保證了事務的隔離性,使數據庫的一致性狀態不會由於併發執行的操做被破壞。
日誌恢復技術保證了事務的原子性,使一致性狀態不會因事務或系統故障被破壞。同時使已提交的對數據庫的修改不會因系統崩潰而丟失,保證了事務的持久性。
2.1 常見的併發異常
在講解併發控制技術前,先簡單介紹下數據庫常見的併發異常。
髒寫
髒寫是指事務回滾了其餘事務對數據項的已提交修改,好比下面這種狀況
在事務1對數據A的回滾,致使事務2對A的已提交修改也被回滾了。
丟失更新
丟失更新是指事務覆蓋了其餘事務對數據的已提交修改,致使這些修改好像丟失了同樣。
事務1和事務2讀取A的值都爲10,事務2先將A加上10並提交修改,以後事務2將A減小10並提交修改,A的值最後爲,致使事務2對A的修改好像丟失了同樣
髒讀
髒讀是指一個事務讀取了另外一個事務未提交的數據
在事務1對A的處理過程當中,事務2讀取了A的值,但以後事務1回滾,致使事務2讀取的A是未提交的髒數據。
不可重複讀
不可重複讀是指一個事務對同一數據的讀取結果先後不一致。髒讀和不可重複讀的區別在於:前者讀取的是事務未提交的髒數據,後者讀取的是事務已經提交的數據,只不過由於數據被其餘事務修改過致使先後兩次讀取的結果不同,好比下面這種狀況
因爲事務2對A的已提交修改,事務1先後兩次讀取的結果不一致。
幻讀
幻讀是指事務讀取某個範圍的數據時,由於其餘事務的操做致使先後兩次讀取的結果不一致。幻讀和不可重複讀的區別在於,不可重複讀是針對肯定的某一行數據而言,而幻讀是針對不肯定的多行數據。於是幻讀一般出如今帶有查詢條件的範圍查詢中,好比下面這種狀況:
事務1查詢A<5的數據,因爲事務2插入了一條A=4的數據,致使事務1兩次查詢獲得的結果不同
2.2 事務的隔離級別
事務具備隔離性,理論上來講事務之間的執行不該該相互產生影響,其對數據庫的影響應該和它們串行執行時同樣。
然而徹底的隔離性會致使系統併發性能很低,下降對資源的利用率,於是實際上對隔離性的要求會有所放寬,這也會必定程度形成對數據庫一致性要求下降
SQL標準爲事務定義了不一樣的隔離級別,從低到高依次是
讀未提交(READ UNCOMMITTED)
讀已提交(READ COMMITTED)
可重複讀(REPEATABLE READ)
串行化(SERIALIZABLE)
事務的隔離級別越低,可能出現的併發異常越多,可是一般而言系統能提供的併發能力越強。
不一樣的隔離級別與可能的併發異常的對應狀況以下表所示,有一點須要強調,這種對應關係只是理論上的,對於特定的數據庫實現不必定準確,好比mysql
的Innodb存儲引擎經過Next-Key Locking技術在可重複讀級別就消除了幻讀的可能。
全部事務隔離級別都不容許出現髒寫,而串行化能夠避免全部可能出現的併發異常,可是會極大的下降系統的併發處理能力。
2.3 事務隔離性的實現——常見的併發控制技術
併發控制技術是實現事務隔離性以及不一樣隔離級別的關鍵,實現方式有不少,按照其對可能衝突的操做採起的不一樣策略能夠分爲樂觀併發控制和悲觀併發控制兩大類。
樂觀併發控制:對於併發執行可能衝突的操做,假定其不會真的衝突,容許併發執行,直到真正發生衝突時纔去解決衝突,好比讓事務回滾。
悲觀併發控制:對於併發執行可能衝突的操做,假定其一定發生衝突,經過讓事務等待(鎖)或者停止(時間戳排序)的方式使並行的操做串行執行。
2.3.1 基於封鎖的併發控制
核心思想:對於併發可能衝突的操做,好比讀-寫,寫-讀,寫-寫,經過鎖使它們互斥執行。
鎖一般分爲共享鎖和排他鎖兩種類型
1.共享鎖(S):事務T對數據A加共享鎖,其餘事務只能對A加共享鎖但不能加排他鎖。
2.排他鎖(X):事務T對數據A加排他鎖,其餘事務對A既不能加共享鎖也不能加排他鎖
基於鎖的併發控制流程:
事務根據本身對數據項進行的操做類型申請相應的鎖(讀申請共享鎖,寫申請排他鎖)
申請鎖的請求被髮送給鎖管理器。鎖管理器根據當前數據項是否已經有鎖以及申請的和持有的鎖是否衝突決定是否爲該請求授予鎖。
若鎖被授予,則申請鎖的事務能夠繼續執行;若被拒絕,則申請鎖的事務將進行等待,直到鎖被其餘事務釋放。
可能出現的問題:
死鎖:多個事務持有鎖並互相循環等待其餘事務的鎖致使全部事務都沒法繼續執行。
飢餓:數據項A一直被加共享鎖,致使事務一直沒法獲取A的排他鎖。
對於可能發生衝突的併發操做,鎖使它們由並行變爲串行執行,是一種悲觀的併發控制。
2.3.2 基於時間戳的併發控制
核心思想:對於併發可能衝突的操做,基於時間戳排序規則選定某事務繼續執行,其餘事務回滾。
系統會在每一個事務開始時賦予其一個時間戳,這個時間戳能夠是系統時鐘也能夠是一個不斷累加的計數器值,當事務回滾時會爲其賦予一個新的時間戳,先開始的事務時間戳小於後開始事務的時間戳。
每個數據項Q有兩個時間戳相關的字段:
W-timestamp(Q):成功執行write(Q)的全部事務的最大時間戳
R-timestamp(Q):成功執行read(Q)的全部事務的最大時間戳
時間戳排序規則以下:
假設事務T發出read(Q),T的時間戳爲TS
a.若TS(T)<W-timestamp(Q),則T須要讀入的Q已被覆蓋。此
read操做將被拒絕,T回滾。
b.若TS(T)>=W-timestamp(Q),則執行read操做,同時把
R-timestamp(Q)設置爲TS(T)與R-timestamp(Q)中的最大值
假設事務T發出write(Q)
a.若TS(T)<R-timestamp(Q),write操做被拒絕,T回滾。
b.若TS(T)<W-timestamp(Q),則write操做被拒絕,T回滾。
c.其餘狀況:系統執行write操做,將W-timestamp(Q)設置
爲TS(T)。
基於時間戳排序和基於鎖實現的本質同樣:對於可能衝突的併發操做,以串行的方式取代併發執行,於是它也是一種悲觀併發控制。它們的區別主要有兩點:
基於鎖是讓衝突的事務進行等待,而基於時間戳排序是讓衝突的事務回滾。
基於鎖衝突事務的執行次序是根據它們申請鎖的順序,先申請的先執行;而基於時間戳排序是根據特定的時間戳排序規則。
2.3.3 基於有效性檢查的併發控制
核心思想:事務對數據的更新首先在本身的工做空間進行,等到要寫回數據庫時才進行有效性檢查,對不符合要求的事務進行回滾。
基於有效性檢查的事務執行過程會被分爲三個階段:
讀階段:數據項被讀入並保存在事務的局部變量中。全部write操做都是對局部變量進行,並不對數據庫進行真正的更新。
有效性檢查階段:對事務進行有效性檢查,判斷是否能夠執行write操做而不違反可串行性。若是失敗,則回滾該事務。
寫階段:事務已經過有效性檢查,則將臨時變量中的結果更新到數據庫中。
有效性檢查一般也是經過對事務的時間戳進行比較完成的,不過和基於時間戳排序的規則不同。
該方法容許可能衝突的操做併發執行,由於每一個事務操做的都是本身工做空間的局部變量,直到有效性檢查階段發現了衝突纔回滾。於是這是一種樂觀的併發策略。
2.3.4 基於快照隔離的併發控制
快照隔離是多版本併發控制(mvcc)的一種實現方式。
其核心思想是:數據庫爲每一個數據項維護多個版本(快照),每一個事務只對屬於本身的私有快照進行更新,在事務真正提交前進行有效性檢查,使得事務正常提交更新或者失敗回滾。
因爲快照隔離致使事務看不到其餘事務對數據項的更新,爲了不出現丟失更新問題,能夠採用如下兩種方案避免:
先提交者獲勝:對於執行該檢查的事務T,判斷是否有其餘事務已經將更新寫入數據庫,是則T回滾不然T正常提交。
先更新者獲勝:經過鎖機制保證第一個得到鎖的事務提交其更新,以後試圖更新的事務停止。
事務間可能衝突的操做經過數據項的不一樣版本的快照相互隔離,到真正要寫入數據庫時才進行衝突檢測。於是這也是一種樂觀併發控制。
2.3.5 關於併發控制技術的總結
以上只是對常見的幾種併發控制技術進行了介紹,不涉及特別複雜的原理的講解。之因此這麼作一是要真的把原理和實現細節講清楚須要涉及的東西太多,篇幅太長,從做者和讀者角度而言都不是一件輕鬆的事,因此只對其實現的核心思想和實現要點進行了簡單的介紹,其餘部分就一筆帶過了。二是併發控制的實現的方式太過多樣,基於封鎖的實現就有不少變體,mvcc多版本併發控制的實現方式就更是多樣,並且不少時候會和其餘併發控制方式好比封鎖的方式結合起來使用。
3.1 爲何須要故障恢復技術
數據庫運行過程當中可能會出現故障,這些故障包括事務故障和系統故障兩大類
事務故障:好比非法輸入,系統出現死鎖,致使事務沒法繼續執行。
系統故障:好比因爲軟件漏洞或硬件錯誤致使系統崩潰或停止。
這些故障可能會對事務和數據庫狀態形成破壞,於是必須提供一種技術來對各類故障進行恢復,保證數據庫一致性,事務的原子性以及持久性。數據庫一般以日誌的方式記錄數據庫的操做從而在故障時進行恢復,於是能夠稱之爲日誌恢復技術。
3.2 事務的執行過程以及可能產生的問題
事務的執行過程能夠簡化以下:
系統會爲每一個事務開闢一個私有工做區
事務讀操做將從磁盤中拷貝數據項到工做區中,在執行寫操做前全部的更新都做用於工做區中的拷貝.
事務的寫操做將把數據輸出到內存的緩衝區中,等到合適的時間再由緩衝區管理器將數據寫入到磁盤。
因爲數據庫存在當即修改和延遲修改,因此在事務執行過程當中可能存在如下狀況:
在事務提交前出現故障,可是事務對數據庫的部分修改已經寫入磁盤數據庫中。這致使了事務的原子性被破壞。
在系統崩潰前事務已經提交,但數據還在內存緩衝區中,沒有寫入磁盤。系統恢復時將丟失這次已提交的修改。這是對事務持久性的破壞。
3.3 日誌的種類和格式
<T,X,V1,V2>:描述一次數據庫寫操做,T是執行寫操做的事務的惟一標識,X是要寫的數據項,V1是數據項的舊值,V2是數據項的新值。
<T,X,V1>:對數據庫寫操做的撤銷操做,將事務T的X數據項恢復爲舊值V1。在事務恢復階段插入。
<T start>: 事務T開始
<T commit>: 事務T提交
<T abort>: 事務T停止
關於日誌,有如下兩條規則
1.系統在對數據庫進行修改前會在日誌文件末尾追加相應的日誌記錄。
2.當一個事務的commit日誌記錄寫入到磁盤成功後,稱這個事務已提交,但事務所作的修改可能並未寫入磁盤
3.4 日誌恢復的核心思想
撤銷事務undo:將事務更新的全部數據項恢復爲日誌中的舊值,事務撤銷完畢時將插入一條<T abort>記錄。
重作事務redo:將事務更新的全部數據項恢復爲日誌中的新值。
事務正常回滾/因事務故障停止將進行redo
系統從崩潰中恢復時將先進行redo再進行undo。
如下事務將進行undo:日誌中只包括<T start>記錄,但既不包括<T commit>記錄也不包括<T abort>記錄.
如下事務將進行redo:日誌中包括<T start>記錄,也包括<T commit>記錄或<T abort>記錄。
假設系統從崩潰中恢復時日誌記錄以下
<T0 start> <T0,A,1000,950> <T0,B,2000,2050> <T0 commit> <T1 start> <T1,C,700,600>
因爲T0既有start記錄又有commit記錄,將會對事務T0進行重作,執行相應的redo操做。
因爲T1只有start記錄,將會對T1進行撤銷,執行相應的undo操做,撤銷完畢將寫入一條abort記錄。
3.5 事務故障停止/正常回滾的恢復流程
從後往前掃描日誌,對於事務T的每一個形如<T,X,V1,V2>的記錄,將舊值V1寫入數據項X中。
往日誌中寫一個特殊的只讀記錄<T,X,V1>,表示將數據項恢復成舊值V1,
這是一個只讀的補償記錄,不須要根據它進行undo。
一旦發現了<T start>日誌記錄,就中止繼續掃描,並往日誌中寫一個
<T abort>日誌記錄。
3.6 系統崩潰時的恢復過程(帶檢查點)
檢查點是形如<checkpoint L>的特殊的日誌記錄,L是寫入檢查點記錄時還未提交的事務的集合,系統保證在檢查點以前已經提交的事務對數據庫的修改已經寫入磁盤,不須要進行redo。檢查點能夠加快恢復的過程。
系統奔潰時的恢復過程分爲兩個階段:重作階段和撤銷階段。
重作階段:
系統從最後一個檢查點開始正向的掃描日誌,將要重作的事務的列表undo-list設置爲檢查點日誌記錄中的L列表。
發現<T,X,V1,V2>的更新記錄或<T,X,V>的補償撤銷記錄,就重作該操做。
發現<T start>記錄,就把T加入到undo-list中。
發現<T abort>或<T commit>記錄,就把T從undo-list中去除。
撤銷階段:
系統從尾部開始反向掃描日誌
發現屬於undo-list中的事務的日誌記錄,就執行undo操做
發現undo-list中事務的T的<T start>記錄,就寫入一條<T abort>記錄,
並把T從undo-list中去除。
4.undo-list爲空,則撤銷階段結束
總結:先將日誌記錄中全部事務的更新按順序重作一遍,在針對須要撤銷的事務按相反的順序執行其更新操做的撤銷操做。
3.6.1 一個系統崩潰恢復的例子
恢復前的日誌以下,寫入最後一條日誌記錄後系統崩潰
<T0 start> <T0,B,2000,2050> <T2 commit> <T1 start> <checkpoint {T0,T1}> //以前T2已經commit,故不用重作 <T1,C,700,600> <T1 commit> <T2 start> <T2,A,500,400> <T0,B,2000> <T0 abort> //T0回滾完成,插入該記錄後系統崩潰
事務是數據庫系統進行併發控制的基本單位,是數據庫系統進行故障恢復的基本單位,從而也是保持數據庫狀態一致性的基本單位。ACID是事務的基本特性,數據庫系統是經過併發控制技術和日誌恢復技術來對事務的ACID進行保證的,從而能夠獲得以下的關於數據庫事務的概念體系結構。