帶你理解事務(上)、基本概念

語言: CN / TW / HK

簡介和特性

對於程序猿來説,編程就是將大千世界的各種業務用代碼表示出來,所以代碼輸出的最終結果也要和現實世界能夠對應的上。我們用銀行轉賬來舉例子,比如我給我的朋友A轉100元,最終的結果無非就是有兩種,一是成功,我的賬户餘額減少100,我朋友的賬户餘額增加100,二是失敗,我們兩賬户餘額都沒有變。肯定是不允許出現我的賬户餘額減少了但是我朋友的賬户餘額沒有增加,或者是我朋友的賬户餘額增加了我的賬户餘額卻沒有減少,出現這兩種現象都會讓我們和銀行的關係變得極不和諧。

例子中轉賬這件事情雖然看起來很簡單,但是當變成代碼的時候就比較麻煩了,我們來看看變成代碼之後會是什麼樣子:

image.png

乍看之下感覺很簡單好像沒有什麼問題,但是如果在2和3中間增加了一步檢查朋友賬户是否為合法賬户的步驟變成如下:

image.png

我們假定,第3步這步出了問題,我朋友的賬户是個洗錢賬户,這時候轉賬流程要結束,但是有個問題是我的賬户已經扣掉了100元,如果不還我100的話那我不虧死,這樣估計銀行招牌早就被砸了。所以這個轉賬流程必須有如下幾個特點:

1、原子性(Atomic)

要麼轉賬成功,要不就沒轉,不能存在轉了一半的這種情況。換句話説,轉賬這個過程的所有環節是不可分割的,不管轉賬的過程如何複雜,最後的結果是要麼成功,我的賬户少了錢,我朋友賬户錢增加了對應數量的錢,要麼是我的賬户和我朋友的賬户的錢都沒有改變。這個特點我們稱它為原子性。

2、一致性(Consistency)

這一點網上的很多博客解釋的都不正確。這個特點其實指的是,轉賬這個過程中的數據是要能和現實世界的規則對應起來,或者説是需要滿足一定的規則約束,比如,我的賬户錢必須大於0,並且扣完錢也是必須大於0的。不滿足這樣的約束的話則説明數據是有問題的

3、隔離性(Isolation)

現實世界中的同一時刻,是有多個轉賬同時發生的,這同時發生的多個轉賬之間的數據不能相互影響,要把這個問題説清楚會稍微複雜一些,我們慢慢來説,我們把我們之前提的轉賬流程拆的更細一點如下:

  • (1)讀取我賬户上的金額,假如此時金額101
  • (2)將餘額扣除100
  • (3)把剩餘的餘額寫到磁盤上
  • (4)讀取朋友賬户金額
  • (5)給朋友賬户金額增加100
  • (6)將新的餘額寫到磁盤上 假設現在有兩個這樣的轉賬過程同時發生,在計算機中,由於多處理器的原因,這兩個轉賬並不會嚴格的按照徹底做完一個

| 時間線 |轉賬1 |轉賬2 | | --- | --- |--- | | 1| 讀取我賬户上的金額,此時金額101| | | 2| | 讀取我賬户上的金額,此時金額101| | 3| 將餘額扣除100| | | 4| 把剩餘的餘額寫到磁盤上| | | 5| | 將餘額扣除1| | 6| | 把剩餘的餘額寫到磁盤上| | 7| 讀取朋友賬户金額| | | 8| | 讀取朋友賬户金額| | 9| 給朋友賬户金額增加100| | | 10| 將新的餘額寫到磁盤上| | | 11| | 給朋友賬户金額增加1| | 12| | 將新的餘額寫到磁盤上|

相信不少人已經看出問題了吧,由於在時間線2處讀到的錢還是101,因此在第6處寫到磁盤上的我的餘額是100,這時候銀行不得虧死,因此,正確的流程是時間線134這三個事情必須先做,然後時間線2處的才允許發生,正確流程如下:

| 時間線 |轉賬1 |轉賬2 | | --- | --- |--- | | 1| 讀取我賬户上的金額,假如此時金額101| | | 2| 將餘額扣除100| | | 3| 把剩餘的餘額寫到磁盤上| | | 4| | 讀取我賬户上的金額,此時金額是1| | 5| | 將餘額扣除1| | 6| | 把剩餘的餘額寫到磁盤上| | 7| 讀取朋友賬户金額| | | 8| | 讀取朋友賬户金額| | 9| 給朋友賬户金額增加100| | | 10| 將新的餘額寫到磁盤上| | | 11| | 給朋友賬户金額增加1| | 12| | 將新的餘額寫到磁盤上|

4、持久性(Durability)

一旦轉賬完成之後,數據就永久的被持久化到磁盤上不會丟失。

我們所説的事務(transcation)必須具有這四種特性,否則就會出現問題。

事務併發讀寫會遇到的問題

我們用修改數據庫中我的賬户的餘額為例子,來看看多個事務同時發生都有可能出現什麼問題:

髒寫

| 時間線 |事務1 |事務2 | | --- | --- |--- | |1 | start | | |2 | | start| |3 | update user set money=100 where id=1 | | |4 | | update user set money=50 where id=1| |5 | | commit| |6 |select money from user where id=1 (此時查出來money是50) | | |7 | commit | |

這個案例中,事務2修改了事務1還沒有提交的數據,事務1在時間線6的重新查詢的時候一臉懵逼,明明自己的錢應該是100的,卻不知道為什麼變成了50,就這樣白白的損失了50塊錢,和銀行的關係變得極其不和諧。

髒讀

| 時間線 |事務1 |事務2 | | --- | --- |--- | |1 |start | | |2 | | start| |3 |update user set money=100 where id=1 | | |4 | |select money from user where id=1 (查出來的money是100)| |5 |update user set money=50 where id=1 | | |6 |commit | | |7 | |commit |

這個案例中,事務1先把數據庫中我的賬户餘額更新為了100,然後事務2來查詢發現餘額是100,就告訴我餘額是100,這種讀到了別的事務未提交的數據就叫做髒讀。有人不理解這個“髒”字體現在了哪裏,因為事務1還是有可能繼續修改money字段的,比如在上面的時間線6的時候事務1修改了money,這樣的話事務2讀到的數據其實是個錯誤的數據。

不可重複讀

| 時間線 |事務1 |事務2 | | --- | --- |--- | |1 |start | | |2 | | start| |3 | select money from user where id=1 (此時查出來的money是100)| | |4 | | update user set money=50 where id=1| |5 |select money from user where id=1(此時查出來的money是50) | | |6 |commit | | |7 | | commit|

事務1在時間線5的時候讀到了事務2還沒有提交的數據,並且和自己在時間線3讀到的數據不一樣,這時候我們稱這種現象稱為不可重複讀。

幻讀

| 時間線 |事務1 |事務2 | | --- | --- |--- | |1 | start | | |2 | | start| |3 | select * from user where money>100 (假設查到了一條數據) | | |4 | | insert into user (money,id) values (200,2) | |5 | select * from user where money>100 (此時會查到兩條數據) | | |6 | commit | | |7 | | commit|

事務1在時間線3和時間線5查到的數據不一樣,並且時間線5讀到了時間線3沒有讀到的數據,我們稱這種現象為幻讀。

四種現象的嚴重程度排序 髒寫 > 髒讀 > 不可重複讀 > 幻讀

隔離級別

注意,這裏是以MySQL為例子,説的是MySQL中使用Innodb時候的事務隔離級別,不同的數據庫對隔離級別的支持是不一樣的。在MySQL中,支持下面四種事務隔離級別: * READ UNCOMMITTED: 讀未提交 * READ COMMITTED: 讀已提交 * REPEATABLE READ: 可重複讀 * SERIALIZABLE: 串行化

事實上這是sql標準中的四種隔離級別,sql標準中還規定了不同隔離級別中可以發生什麼問題和不可以發生什麼問題,具體如下:

|隔離級別|髒寫|髒讀|不可重複讀|幻讀| | --- | --- |--- |--- |--- | |READ UNCOMMITTED |❎ |可能出現 |可能出現 |可能出現 | |READ COMMITTED |❎ |❎ |可能出現 |可能出現 | |REPEATABLE READ |❎ |❎ |❎ |可能出現 | |SERIALIZABLE |❎ |❎ |❎ |❎ |

我們可以看到,由於髒寫太過於嚴重,所以在哪個事務隔離級別下都不允許發生。