江湖傳說:不瞭解數據庫事務的程序員不是一個好的DBA。閱遍網上無數關於數據庫事務的文章,都感受雲裏霧裏,不知所云。因而乎拍案而起,麻蛋,仍是本身寫吧。最後便有了這篇文章,它試圖用通俗的文字來講明單機事務的ACID特性及其大體的實現原理。html
數據庫事務(簡稱:事務)是數據庫管理系統執行過程當中的一個邏輯單位,由一個有限的數據庫操做序列構成。—— 維基百科程序員
好吧,你沒怎麼看明白?對於應用程序來講,事務就是一系列對數據庫的數據進行讀或寫的操做,在本文中,把一個讀或者寫操做稱爲事務單元。同一時刻,可能有多個應用程序同時向數據庫發送讀寫請求,因此對於數據庫管理系統(如:MySQL、Oracle等)來講,一個事務包含一系列事務單元。數據庫
舉個栗子,Bob向Smith轉帳100塊這樣一個動做,包含多個對數據庫的讀寫操做,咱們把這一系列操做稱爲一個事務,具體操做以下表所示。編程
Bob向Smith轉帳的整個流程緩存
只要是對數據庫的一個操做就是一個事務單元,事務單元也並不是只有讀寫操做,創建索引、刪除表等等都是事務單元,例以下面對數據庫的操做都是事務單元。性能優化
事務做爲一個總體被執行,包含在其中的對數據庫的操做要麼所有被執行,要麼都不執行。服務器
仍是以Bob向Smith轉帳100塊錢爲例,整個事務包含以下操做:多線程
轉帳操做併發
A
,B
,C
3個操做,要麼所有成功,要麼所有失敗。能夠看到,若是可以保證這點,那麼對於上層的應用程序就再也不須要去作各類中間狀態的維持工做,而只要關注業務邏輯便可。好比在C
操做開始以前,發現Smith的帳戶被鎖定了沒法進行加款操做,那麼數據庫可以自動的將A
和B
兩個操做進行回滾,從讓上層的應用程序只關注具體的業務流程實現,而不須要關注事務自己的實現流程。異步
2.一、數據庫如何實現原子性?
實現原子性的核心是要記錄下每個變動的中間狀態或者是記錄變動的具體過程。這樣咱們就能夠在發現問題時,直接把老數據替換回去,從而實現回滾操做,保證原子性。
以轉帳的實例進行細節分析,在執行A以前,數據庫中的數據大概是這樣(version1
):
執行A操做:檢查Bob帳戶是否有100塊,執行的SQL:select money from T where pk=1
,一個簡單的查詢操做並不涉及對數據的修改,所以不會記錄變動數據。
執行B操做:Bob帳戶減去100塊,執行SQL:update T set money=money-100 where pk=1
,執行完這個操做,數據庫應該是這樣(version2
)
注:除了當前運行事務的這個進程,其餘的進程只要不是在讀未提交狀態,徹底看不見這些中間狀態
接着執行C
操做,忽然發現Smith的帳戶出現未知異常,致使加款的操做沒法進行,那麼整個事務單元執行失敗,須要回滾前面的操做,因爲A
操做不涉及數據的修改,所以只須要回滾B
操做。要回滾B
操做,就須要知道PK=1這一行在version1
版本時的數據:money=100
,在回滾時用version1
版本的記錄替換當前版本(version2
)據便可。
2.二、那數據庫是如何實現回滾的呢?
首先要明確的是回滾必須按照順序進行,不然會出現不符合預期的狀況。
這個很容易理解,若是兩條update語句按照不一樣的順序執行,那麼其結果確定不一致,同理,若是回滾時,不按照執行順序的反序執行,那麼回滾的結果也確定不一致。因此咱們必須讓回滾自己按照執行順序的反序執行。通常而言,實現方式就是把數據按照順序記錄到文件裏,而後將這個文件按照FILO(先入後出)的方式讀取出來,這樣能夠保證按照執行序列的反序來回滾了。
除了記錄中間狀態的數據外,回滾還要考慮的一個重要因素:併發。
若是數據庫系統將全部針對他的讀寫請求都按照順序執行,那麼徹底不用考慮併發因素,回滾也能夠很簡單的實現。但系統須要更高效的利用CPU和各類物理資源,且不少數據在物理上就是須要被共享的。因此,處理併發和同步就成了一個數據庫系統必須面對的實際問題。
若是進行不恰當的併發處理,那麼多線程執行回滾操做會致使最終數據出現錯亂,好比A進程優先進行了兩個操做並記錄了回滾段,B進程緊接着進行了一個操做並記錄了回滾段,這時候A進程要回滾,那麼他用本身記錄的回滾中間狀態恢復了數據,然而B也要進行回滾,就會發現數據自己已經沒法回滾到最初的狀態去了。
若是要切實的解決這個問題,咱們只能把每一個事務所影響的數據所有都加上鎖,這樣,在這個事務沒有完成以前,其餘進程不能進入到這些加鎖的數據中對這個數據進行修改。從而保證了儘量細顆粒度的併發控制,同時也解決了回滾中會出現的回滾時序衝突問題。
固然這種方式的代價就是回滾隱含了對事務鎖的要求,而事務只要加鎖,就存在對加鎖數據的讀寫請求,就須要等待,進而下降併發性能。
事務應確保數據庫的狀態從一個一致狀態轉變爲另外一個一致狀態。
怎麼理解這句話?仍是以轉帳的示例來講明,在整個轉帳的事務單元中,數據庫中的數據有3個版本:
A操做(查詢)後獲得version1
:Bob=100,Smith=0
undo日誌:無
B操做(減款)後獲得version2
:Bob=0,Smith=0
undo日誌:Bob=100,Smith=0
C操做(加款)後獲得version3
:Bob=0,Smith=100
undo日誌:Bob=0,Smith=0
其中version1
是初始的一致狀態,version3
是最終的一致狀態,version2
爲中間狀態,一致性要保證的是應用程序只能看到初始的一致狀態或者最終的一致狀態,而不能看見中間狀態。
這裏咱們須要注意一致性和原子性的區別。
原子性的語義只保證數據庫記錄了回滾段,如上面的undo日誌
,它能夠保證在事務單元執行出現異常時,可根據回滾段(undo日誌
)回滾到以前的版本。
而一致性則保證上層應用程序不看到中間狀態,雖然原子性和一致性常常一塊兒出現,但它們沒有任何須然的聯繫。原子性只保證整個事務單元那麼所有執行成功,要麼所有執行失敗。它不保證你看不到中間狀態。我舉個簡單的栗子說明。
原子性與一致性區別示意圖
如今請只考慮原子性的語義,線程1執行Bob向Smith轉帳100塊,線程2執行向Smith帳戶加款200塊。線程1和線程2同時執行到向Smith轉帳,線程2執行成功後,Smith帳戶有200塊,這時若是線程1執行成功,那麼Smith帳戶應該有300塊,但遺憾的時,線程1的操做失敗,那麼線程1的事務必須回滾,根據上文的分析,這時候線程1的undo日誌
必定是Bob=100,Smith=0
,回滾結束後,你會發現線程2給Smith加款的200塊錢莫名其妙的消失了。出現這種狀況是確定不能接受,因此一致性就是爲了防止這種狀況。一致性保證線程2只能看到兩種狀態,即Bob=100,Smith=0
或者Bob=0,Smith=100
,而不會看到Bob=0,Smith=0
這種狀態,更不會在中間狀態時就向Smith帳戶加款。
那數據庫是如何實現一致性呢?答案很簡單,就是鎖。
一致性保證
線程1在執行Bob向Smith轉帳,同時線程2執行向Smith加款時,發現Smith帳戶已經被鎖定,那麼線程2等待,直到Smith帳戶解除鎖定爲止。
多個事務併發執行時,一個事務的執行不該影響其餘事務的執行
簡單的理解就是一個事務內部的操做以及正在操做的數據必須封鎖起來,不被其餘企圖修改這些數據的事務看到。那麼如何保證事物的隔離性?就如同上面保持一致性所講的那樣,只須要在每一個事務執行以前加一把排他鎖,事務執行結束後釋放鎖,而後再執行下一個事務,直到執行完成全部的事務單元便可,就如同這樣:
數據庫依次執行事務單元
將全部的事務排隊,利用排他鎖的方式,將事務鎖住,單位時間內,只有一個事務進來,這就是事務隔離級別中的可序列化(Serializable)級別。
可序列化級別是事務的最高隔離級別,它強制事務排序,使事務間不可能相互衝突。但很明顯的,這種方式有一個很嚴重的問題:並行度過低,致使性能很是差。性能太差就意味着大多數狀況下不可用,就須要想辦法提升性能。
經過仔細分析,咱們能夠發現最核心的問題就是一把大鎖堵住了全部的請求。一種行之有效的方法就是利用鎖分離 + 讀寫鎖來提高並行度。
鎖分離是指使用多個鎖來控制沒有衝突的事務,每一個事務都有本身的鎖,這樣就可讓沒有衝突的事務並行的執行。請看下面的小例子:
鎖分離示例
有三個事務,事務1爲Bob向Smith轉帳,涉及3個數據庫操做、事務2爲查詢Joe帳戶餘額,只有一個讀操做、事務3爲Jack向LILei轉帳。若是事務的隔離級別爲可序列化級別,那麼事務的執行順序應該是這樣的:
串行事務單元
但很明顯,三個事務之間徹底沒有衝突,使用鎖分離技術後,他們的執行順序就變成了這樣:
利用鎖分離提升並行度
採用鎖分離技術能夠提升並行度,但我還想要再提升速度呢?咱們能夠把控制事務的鎖拆分紅讀寫鎖。使用讀寫鎖後,事務內部的全部讀操做均可以並行,就如同這樣:
讀寫鎖 - 讀讀並行
這就是事務隔離級別中的可重複讀(Repeatable Read)級別。可重複讀級別在序列化級別上的基礎上,讓兩個讀操做能夠並行執行,提升並行度。可重複讀保證了同一個事務裏,全部讀操做的結果都是事務開始時的狀態(一致性)。可是,因爲當前的事務(A事務)對已經存在的行加讀或寫鎖,不能阻止另外一個事務(B事務)插入
新數據,因此當A事務再次查詢時可能會查出更多的結果,這就是幻讀現象。
舉一個很是簡單的例子,在工資表中,事務A第一次查詢全部工資爲2000的用戶,結果有10人,但同一時刻事務B新增了若干條工資數據,致使事務A再次查詢工資爲2000的用戶時,結果變成了15人,這就是幻讀。
可重複讀只能作到讀讀並行,並不能完美的提高性能,這個時候就產生了另一個事務隔離級別讀已提交(Read Commited)。」讀已提交「與」可重複讀「的區別就在於讀鎖能不能被寫鎖升級。
怎樣理解這句話?
數據庫對當前的讀操做加鎖,這時來了一個寫操做,咱們要不要放寫操做進來呢?若是不放,那麼只能讀讀並行,就是可重複讀的隔離級別。若是放進來,新的寫請求會將原來的讀鎖升級爲寫鎖,這樣除了讀讀可並行,讀寫也可並行,進一步提高了並行度,原來的讀讀並行就變成了這個樣子:
讀鎖被寫鎖升級
固然性能的提升,確定是要付出代價的,讀已提交的事務隔離級別除了可能會出現「幻讀」的狀況,還會出現「不可重複讀」。
一個事務執行一個查詢,讀取了大量的數據行。因爲讀鎖能夠被寫鎖升級,因此在它結束讀取以前,另外一個事務可能完成了對數據行的更改。當第一個事務試圖再次執行同一個查詢,服務器就會返回不一樣的結果,這就是「不可重複讀」。
仍是舉一個很是簡單的例子,好比事務A是Bob向Smith轉帳100塊,事務B是向Bob收取管理費10塊。事務A在檢查Bob是否有100塊時,查詢後發現有100,同一時刻,事務B發起扣手續費的操做,當事務B達到時,發現數據被事務A的讀鎖鎖住的,因爲事務的隔離級別是「讀已提交」,讀鎖直接被升級,B事務順利扣款10塊,Bob帳戶還剩90塊,B事務結束後,A事務再進行轉帳操做時就會發現餘額已經不夠100了。
既然讀鎖能夠被寫鎖升級,那若是乾脆不要讀鎖呢?這樣的話讀讀、讀寫、寫讀均可以並行,只有寫寫仍是串行,這樣又能夠更進一步提高並行度,就如同這樣:
寫讀並行
這就是4種事務隔離級別的最後一種:讀未提交(Read Uncommitted),隔離級別最低,同時也是並行度最高、性能最好的隔離級別。固然也存在很大的問題,就是「髒讀」。所謂「髒讀」就是事務A修改了一行,另外一個事務B也能夠讀到該行。若是第一個事務A執行了回滾,那麼事務B讀取的就是歷來沒有正式出現過的值,也就是前面提到的讀取到中間狀態數據,這確定是不可以接受的,因此大部分狀況都不該該使用這個隔離級別。
最後作個小結,回顧這4種隔離級別,咱們能夠看到隔離級別越高,性能越差,越能保持一致性;隔離級別越低,性能越好,對一致性的破壞也就更完全,出現的問題也就越多。因此咱們能夠用一句話來總結:事務的隔離性就是以性能爲理由,對強一致性的破壞。
大多數數據庫的默認事務隔離級別是「讀已提交」,MySQL的默認事務隔離級別是「可重複讀」。像「可重複讀」這種事務隔離級別併發性能是很是低的,那MySQL又是如何在「可重複讀」的隔離級別下達到很高的性能的?答案請參考第六部分。
終於說到ACID的最後一個字母了,所謂事務的持久性就是指:
已被提交的事務對數據庫的修改應該永久保存在數據庫中
也就是說:若是一個事務一旦提交,它對數據庫中數據的改變就應該是永久性的,接下來的其餘操做或故障不該該對其有任何影響。
其實在不少數據庫系統中,因爲性能的緣由,事務操做時,並非數據每次被修改後當即被寫入磁盤,而是採用異步刷盤的模式。持久性就是爲了保證這些在緩存中的數據,在故障(硬件損壞或者斷電等)恢復後,仍然可以正確的寫入磁盤。
這裏就存在兩種狀況,若是在事務提交以前發生故障,那麼緩存的數據丟失,修改的信息也就丟失了,數據庫只能根據日誌作回滾。若是在事務提交以後發生故障,即便緩存中的數據丟失,仍然能夠根據日誌將事務單元繼續提交,整個事務仍然是成功的,不會致使任何數據丟失。固然這裏日誌的持久化又是另一個話題了,簡單的說,就是在對數據庫更新時,必定要保證日誌已經寫入磁盤,若是日誌沒有寫入磁盤,故障發生後,數據只能丟失。
因此持久化的語義更多的體如今數據庫發生故障時,確保提交的事務不丟失。
在前文已經說到,讀未提交級別下會出現髒讀,而在可序列化隔離級別下,事務只能串行執行,性能過低。大多數狀況下,這兩個事務隔離級別都是不能接受的,通常狀況下會在可重複讀與讀已提交兩個隔離級別下對系統性能進行優化。
其中可重複讀能夠作到讀讀並行,讀已提交寫鎖能夠將讀鎖升級。在這兩個事務隔離級別下,若是當前事務正在寫,那麼其餘全部的讀都將被阻塞(這裏的讀寫事務是針對相同的資源,或者說是針對數據庫的同一行數據),因此優化的點也在這裏,有沒有辦法讓寫不阻塞讀呢?這樣的話,能夠大大提高數據庫讀的性能,尤爲是在讀多寫少的場景下,這樣的性能優化尤爲重要。
MVCC(多版本併發控制)模型爲解決這個問題提供了思路。數據庫爲了支持事務,在每一個寫事務(更新數據)時,都會記錄undo log
以便在事務執行出現異常時能夠回滾到事務初始的狀態,就如同這樣:
undo log
事務A爲Bob向Smith轉帳的操做,假設目前數據正在進行事務A,這時候正好來了一個讀事務B,傳統狀況下,讀事務B是須要在此等待的,直到事務A執行完成,就如同這樣:
事務B被事務A阻塞
在MVCC模型下,每行數據具備多個版本,假設事務A下數據的當前版本爲版本1
,那麼這一時刻其回滾段中對應的數據版本爲版本0
,當事務B到達時,發現事務A爲寫事務,且數據當前版本爲版本1
,那麼事務B自動到回滾段中讀取版本爲版本0
的數據,版本0
的數據也稱爲快照數據,就如同下圖這樣。
MVCC
如上示例,事務A正在轉帳,整個轉帳的過帳中Bob和Smith帳戶數據有3個版本,其對應回滾段中的數據有兩個版本,因此事務B讀到的數據始終是初始狀態的值,也就是回滾段中version2
的數據,其對應的是事務A中version1
的數據。
這裏請你們考慮一個問題,假如如今有兩個一樣的A事務:A一、A2,還有一個事務B,這3個事務幾乎同時達到,那麼B事務是應該讀取A1以前的數據,仍是讀取A2以前的數據呢?這裏引伸出來的問題就是:一個讀請求應該讀哪個寫以後的數據?
不一樣的數據庫有不一樣的實現方式,可是大體的原理都是在系統內部維護一個邏輯時間戳,好比:根據時間前後順序在內部維持一個全局的自增號,每來一個請求加1,利用這個自增號來維持前後順序,好比Oracle中的SCN,Innodb中的Trx_id(事務ID)。這樣就能夠肯定讀事務應該讀取那個版本的快照數據。
不一樣的數據庫,實現MVCC的方式不一樣,甚至是同一數據庫,不一樣隔離級別下的實現方式也不一樣。好比MySQL的InnoDB存儲引擎下,在讀已提交(READ COMMITTED)事務隔離級別下,老是讀取被鎖定行的最新一份快照數據。而在可重複讀(REPEATABLE READ)事務隔離級別下,老是讀取事務開始時的行數據版本。這二者之間的不一樣,請看下圖:
不一樣事務隔離級別下,實現MVCC的方式也不一樣
數據表Table在事務AB開始以前查詢id=1的這行數據的結果是amount=1
時刻1:開始AB事務
時刻2:事務B更新amount的值爲3,同一時刻A事務並未結束
時刻3:事務A再次查詢,讀取事務B開始以前的數據,在兩種隔離級別下amount均爲1
時刻4:事務B提交,同一時刻A事務並未結束
時刻5:事務A再次查詢,在讀已提交事務隔離級別下,讀取被鎖定行的最新一份快照數據即amount=3,在可重複讀事務隔離級別下,老是讀取事務開始時的行數據即amout=1
還有一點須要說明的是在MVCC併發控制中,讀操做能夠分紅兩類:快照讀 (snapshot read)與當前讀 (current read)。快照讀,讀取的是記錄的可見版本 (有多是歷史版本),不用加鎖。當前讀,讀取的是記錄的最新版本,而且當前讀返回的記錄,都會加上鎖,保證其餘事務不會再併發修改這條記錄。
那哪些讀操做是快照讀?哪些操做又是當前讀呢?以MySQL InnoDB爲例:
快照讀:簡單的select操做,屬於快照讀,不加鎖:
select * from table where A=?;
當前讀:特殊的讀操做,插入/更新/刪除操做,屬於當前讀,須要加鎖。
select * from table where A=? lock in share mode; select * from table where A=? for update; insert into table values (…); update table set A=? where B=?; delete from table where A=?;
全部以上的語句,都屬於當前讀,讀取記錄的最新版本。而且,讀取以後,還須要保證其餘併發事務不能修改當前記錄,對讀取記錄加鎖。其中,除了第一條語句,對讀取記錄加S鎖 (共享鎖)外,其餘的操做,都加的是X鎖 (排它鎖)。
說明:關於MVCC,我想寫的也就這麼多,也就一些最基本的原理,其實網上關於MVCC的介紹不是不少,而已有的大多數文章分析也不是很透徹,總感受雲裏霧裏的,也但願能夠看到更多深刻淺出介紹MVCC的文章或者書籍,你們也能夠留言推薦給我。
寫了這麼多,總結起來,什麼是事務?
事務的核心是鎖和併發
怎麼優化事務?
在鎖和併發之間找到一個平衡值
你以爲這句話太抽象了?那換種方式,在優化事務時,你能夠儘可能減小鎖的覆蓋範圍,好比:MyISAM
使用表鎖,它的鎖範圍就大於Innodb
的行鎖,因此若是寫多讀少且須要支持事務的話,請使用Innodb
存儲引擎,若是讀多寫少的話,可使用MyISAM
存儲引擎。固然如今大部分數據庫都參考MVCC模型來實現以提升併發性能,可是仍然須要設置合理的事務隔離級別。
除了這個你還能夠增長鎖上可並行的線程數,將讀寫鎖分離,並行讀取數據。具體實現也就是儘可能把大事務拆分紅小事務,這樣在縮小鎖範圍同時,能夠將讀寫鎖分離開,何樂而不爲呢?最後還要使用正確的鎖類型,好比:悲觀鎖就適合併發爭搶比較嚴重場景,而樂觀鎖則適合併發爭搶不太嚴重場景。
還有就是MVCC實現的核心思路就是:無鎖編程 + copy on write,本質就是可以作到寫不阻塞讀。
最後一句話:容易理解的模型性,實現簡單,但性能都很差,性能好的模型都不容易理解,且實現困難。
文字資料:
事務原子性
視頻資料:
單機事務原理與實現1
單機事務原理與實現2
單機事務原理與實現3