數據庫(五),事務
為什麽需要事務呢?
在數據庫(二),數據庫起源裏面我們提到了事務。
數據庫除了對查詢等操作進行了抽象,另外一個重要的功能就是事務了。為什麽需要事務呢?因為我們在操作數據的時候,可能遇到多個線程同時操作數據的問題,也可能遇到突然數據庫故障了的問題,這些都可能造成數據的不一致。所以事務要保證的就是一致性。
保證一致性的第一重意思是鎖,這是為了應對多個連接同時連到數據庫的時候。因為我們可能為每個連接分配一個線程,而這些線程有可能同時操作同一塊數據,這樣將會發生不一致。所以我們只好在寫的時候加上鎖,也就是強行保證只有一個線程可以訪問到這塊數據。
另外我們還會遇到數據庫崩潰的問題,所以我們要求一個事務一定是原子
對於單機事務而言,需要保證
- 原子性
- 一致性
- 隔離性
- 持久性
也就是所謂的ACID,下面我們依次介紹他們是怎麽實現的。
原子性
Undo日誌
所謂原子性指的是要麽同時成功,要麽同時失敗。比如Bob賬戶裏面有100塊,而Smith賬戶裏面有0元,現在我們希望Bob轉100塊給Smith。
所謂原子性就是要麽Bob成功轉給了Smith100塊,此時Bob有0元、Smith有100塊。要麽失敗了,Bob仍然有100塊,Smith為0元。不會存在Bob把錢轉出去了,而Smith卻沒有拿到錢的情況。
現在我們來想想要實現這個事務,應該怎麽做
鎖定Bob賬戶
鎖定Smith賬戶
查看Bob是否有100塊錢,如果有,則從賬號裏面減少100塊
給Smith賬戶裏面增加100塊
依次解鎖Bob和Smith
但是執行事務不會永遠是一帆風順的,可能出現意外,比如Bob或者Smith賬戶不存在怎麽辦?沒關系,我們可以回滾到上一個狀態。
但是數據庫不可能把每個狀態都記錄下來,這就需要我們在轉賬之前把之前的狀態記錄下來。
比如我們看剛剛那個轉賬操作的中間狀態
- Bob:100,Smith:0
- Bob:0,Smith :0 (此時正在轉賬)
- Bob : 0 , Smith :100(轉賬成功)
我們可以在插入兩個undo段,他們記錄在日誌中。
- Bob:100,Smith:0
- Bob:0,Smith :0 (此時正在轉賬)
- 上一個狀態為:Bob:100,Smith:0
- Bob : 0 , Smith :100(轉賬成功)
- 上一個狀態為: Bob:0,Smith :0
這樣如果要回滾,只需要回溯日誌即可實現。這
另外還有一種可能就是事務並沒有進行完,系統就崩潰了怎麽辦?那系統重啟之後就得做恢復操作啊。那怎麽恢復了,同樣也是通過日誌。我們可以在進行真正的操作之前,需要把要做的事寫下來,
我們會在事務開始之前寫下:
Bob原有100元,Smith原有0元
如果事務執行到一半就斷電,那麽重啟之後我們就可以按照日誌來恢復,然後仍然是** Bob有100元,Smith有0元。即使恢復100次,仍然是這個結果,這就叫冪等性**,所以恢復過程中也斷電了,我們仍然可以按照日誌來進行恢復。
現在還有個一問題沒有解決,那就是怎麽知道一個事務沒有完成呢?
同樣我們可以通過記錄日誌的方式來完成。比如我們在記錄的時候,不但把余額記上,還把事務開始了和結束這兩個動作打上標記。
比如
[開始事務T1][事務T1:Bob原有100]
[事務T2:Smith原有0][提交事務T1]
這樣,如果在日誌中看到了提交事務T1或者回滾事務T1,我們就知道這個事務已經結束了。如果只看到開始事務T1,那就得恢復。比如下面這個就得恢復
[開始事務T1][事務T1:Bob原有100]
[事務T2:Smith原有0]
而且,在恢復之後,需要在日誌文件中加上一行回滾事務T1,這樣下次恢復就不用再考慮T1這個事務呢,因為現在早已經回到上一個狀態去了呢。
Undo日誌寫入文件的時機
上面的討論其實我們都故意忽略了一個問題,那就是Undo日誌也需要加載到內存中才能讀寫,但是如果日誌還沒寫好就斷電了怎麽辦?
其實我們只要掌握好把日誌寫入文件的時機就OK了。
最容易想到的就是在一開始就把日誌寫入文件,就好比寫作文前把草稿打好,後面只管按著草稿謄抄一遍就可以了。
然而,現實是,一開始的時候,我們都不知道程序要操作哪個字段,怎麽記錄日誌呢,當然也不能寫入文件呢。所以肯定是一邊在內存中操作Undo日誌,一邊找時機寫入磁盤中。
比如上面的轉賬操作,我們其實可以這樣來修改和寫日誌。
操作 | 數據緩沖區 | 日誌緩沖區 |
---|---|---|
開始事務T1 | [開始事務T1] | |
Bob = Bob - 100 | Bob新余額為0 | [事務T1,Bob原有余額為100] |
把日誌寫入文件 | 註意,日誌寫入文件後,緩沖區會清空 | |
把Bob余額寫入文件 | ||
Smith = Smith + 100 | [事務T1,Smith原有余額0] | |
把日誌寫入文件 | 註意,日誌寫入文件後,緩沖區會清空 | |
把Smith余額寫入文件 | Smith新余額為100 | |
提交事務T1 | [提交事務T1] | |
把日誌寫入文件 | 註意,日誌寫入文件後,緩沖區會清空 |
總結一下就是,
當余額發生改變的時候,記錄之前的余額
在余額要寫入硬盤之前,需要把日誌先寫入文件,然後日誌緩沖區會清空。
提交事務的日誌一定是在所有余額都寫入硬盤之後才寫入
也就是說事務過程中,余額發生改變,在余額正式寫入了硬盤以後,相當於木已成舟,所以我們也需要把日誌寫入硬盤。
當所有余額都穩穩當當的落到磁盤上了,我們自然也應該把日誌落到磁盤上
那麽我們可以攻防演練一下。
如果Bob的余額寫到了硬盤,但是Smith還沒修改。此時日誌中落盤的只有Bob原有的余額也就是:
[開始事務T1][事務T1:Bob原有100]
恢復的時候,發現事務沒有結束,所以還會把Bob的余額給恢復了。
同理,如果Bob和Smith的余額都落盤了,但是沒有提交事務,此時日誌是
[開始事務T1][事務T1:Bob原有100]
[事務T2:Smith原有0]
依然可以恢復兩個賬戶的余額。
即使兩個賬戶的最新余額都落盤了,也提交了事務,但是只要在日誌寫入磁盤之前崩潰,則Undo日誌還是
[開始事務T1][事務T1:Bob原有100]
[事務T2:Smith原有0]
同樣會把余額恢復成原樣。
原子性做不到的地方
現在可算是把原子性說完了,但是只有原子性是不夠的,為什麽呢?因為它無法保證多個線程訪問數據時的一致性。
比如在第2步的時候,另一個事務把把smith賬戶加到了300塊錢,
- Bob:100,Smith:0
- Bob:0,Smith :0 ------------->Bob:0,Smith :300(另一個事務幹的)
- 上一個狀態為:Bob:100,Smith:0
- Bob : 0 , Smith :100(轉賬成功)
- 上一個狀態為: Bob:0,Smith :0
如果有另一個事務在進行到步驟2的時候把smith賬戶加到了300塊錢,此時如果回滾,會把smith改為0,那加上的300塊就丟失了。 那麽我們還需要一致性。
一致性
上一章我們提到了如果在事務中間,有另一個事務突然插手對數據進行修改,則如果出現回退,將會出現數據不一致的問題。
那怎麽解決這個問題呢?如果我們一個事務對數據操作完了以後,另一個事務再進入,這樣就不會發生爭搶和數據不一致了。所以核心就在於加鎖。
比如
- Lock Bob , Smith
- Bob:100,Smith:0
- Bob:0,Smith :0 ------------->Bob:0,Smith :300(另一個事務幹的)
- 上一個狀態為:Bob:100,Smith:0
- Bob : 0 , Smith :100(轉賬成功)
- 上一個狀態為: Bob:0,Smith :0
- unLock Bob and Smith
在事務的開始和結束分別進行加鎖和解鎖。這樣,其他的事務並不可知事務內部的事情。只有在事務單元內部完全成功了以後才對外可見。
到現在我們“仿佛”已經解決了並發、一致兩個大問題了,但是新的問題也來了,加鎖以後,其他的事務無法對數據進行訪問,那麽系統的並發度是上不來的,這就是下面的隔離性要解決的問題。
隔離性
所謂隔離性,其實是以性能作為理由,在破壞一致性。何以見得?因為如果要保證強一致性,最好的方法就是不管讀寫,統統排隊進行,這樣一定不會出現數據不一致的情況。
然而此時就做不到高的並發,性能也就上不去。所以我們只要做一些妥協,比如只加寫鎖,不加讀鎖。
我們首先需要看看,兩個事務單元對同一個數據,有哪幾種並發模式,然後定義不同的隔離級別,看每種隔離級別可以實現哪些並發模式。
4種可能
同樣我們以一個例子來說明
現在 T1 :Bob要給Smith 100塊,然後T2 : Smith要給Joe 100塊。
這就是兩個事務,如下圖所示,為了保證一致性,Smith賬戶會被兩個事務單元鎖定。也就是兩個事務有共享數據,Bob在給Smith轉錢的時候,另一個事務無法對Smith賬戶進行操作了,並發就上不去。
此時兩個事務單元T1,T2之間只有讀寫並發、寫讀並發、讀讀並發、寫寫並發4種可能。
寫寫並行
什麽時候能寫寫並行,只有當兩個事務的數據完全沒有重疊的情況下,比如如下的情況。
因為沒有共享數據,所以完全可以寫寫並行,也就是寫寫都不加鎖。
讀讀並行
也就是讀操作不加鎖,這樣讀與讀可以並行操作,因為讀不會修改數據,所以讀讀可以放心的並行,而不用擔心一致性的問題。
讀寫並行
也就是讀的時候,可以並發寫。我們知道,寫操作會修改數據,但是寫是加鎖的,所以我們無法讀到寫未提交的結果。所以雖然兩次讀到的數據是不一樣的,不可重復讀,但是每次讀到的數據都是正確的,不存在不一致。寫讀並行
也就是寫的時候,還可以並發讀。因為數據是在不斷改變的,很可能讀到中間的狀態,如果系統在此時崩潰了,重啟的時候會恢復到修改前的值,此時自然會出現錯亂。
那麽我們是否無法實現寫讀並行了嗎?並不是,可以通過Copy on Write。具體怎麽做呢?每次寫操作之前都把數據復制一份到log裏面,在log裏面進行修改。其實就是把原來的數據復制一份,然後修改。這樣讀操作作用的就是原來的數據,而寫作用的是備份的數據,互不幹擾。
這種方法又叫(MVCC,Multi Version content control,多版本內容控制)。那麽多版本是什麽意思。
我們知道數據被復制出去了一份以後,可能會被修改多次,那麽下一次讀應該讀修改後哪個版本的數據呢?這個時候,我們可以在日誌裏面加上版本號。比如說,現在寫入的數據版本號是10,如果要讀取版本號為5的數據,則可以往前一直找,直到找到對應的位置。
所以如果讀發生在寫操作之後,讀的版本號一定要大於寫的版本號。這樣就可以保證讀到想要的數據。
四種隔離級別
上面講了兩個事務單元針對一塊數據其實有4種並發的可能,接下來我們繼續討論隔離級別。不同的隔離級別可以實現讀寫並行、寫讀並行、讀讀並行、寫寫並行的一種或者幾種。
串行化:
就是讀的時候不允許寫,寫的時候不允許讀,這樣可以保證數據強一致,但是性能最低。SQLite默認采用這種方式。
可重復讀,也就是只能實現讀讀並行,讀寫、寫讀、寫寫等不能實現。
所以在兩個都是讀的時候,不加讀鎖,其他情況均需要加鎖。
MySQL默認是這種方式。
讀已提交(Read Committed):
此時當數據被加上讀鎖了以後,一個寫進來,寫鎖替換掉讀鎖,也就是可以將讀鎖升級為寫鎖。那麽如果事務T1讀取了數據,然後事務T2把這個數據修改了,因為事務T2也是加鎖的,所以它會提交,那麽事務T1再讀取這個數據時,原來的數據已經發生變化了。這就是不可重復讀。
此時可以做到讀寫並行、讀讀並行,做不了寫讀並行
Oracle , PostgreSQL, SQL Server都是使用的這種模式。讀未提交:顧名思義,就是可以讀到未提交的內容
最低級別的隔離,此時只加上寫和讀是不加鎖的。因為數據是在不斷改變的,很可能讀到中間的狀態,如果系統在此時崩潰了,重啟的時候會恢復到修改前的值,此時自然會出現錯亂。
要解決寫讀並行的問題,可以使用上面說過的Copy on write,這種方法最大的好處在於可以保證寫讀並行,同時隔離級別還很高
持久性
現在我們來討論最後ACID的持久性,也就是只要事務提交了,不管是崩潰還是出錯,數據一定要寫到磁盤上
那麽數據什麽情況下會丟失呢?
首先是磁盤損壞。所以我們可以使用RAID冗余磁盤陣列來保證可靠性。詳見【大話存儲】學習筆記(4,5章),RAID
還有就是內存如果掉電,裏面的數據就必然丟失,持久性得不到保證。但是如果每一次提交操作完成以後,都將內存中的數據同步到硬盤上,則會造成頻繁寫硬盤,性能將下降。所以持久性和延遲無法兼得
我們只要進行折中,比如只要把數據提交到內存,就立刻返回成功,然後將一段時間的請求打包送到磁盤上。這樣就避免了每次提交都寫磁盤
參考
- 慕課網
- 如果有人問你數據庫的原理,叫他看這篇文章如果有人問你數據庫的原理,叫他看這篇文章
數據庫(五),事務