1. 程式人生 > >數據庫(五),事務

數據庫(五),事務

日誌文件 是把 還需要 解決 也不能 實現 損壞 可見 哪些

為什麽需要事務呢?

在數據庫(二),數據庫起源裏面我們提到了事務。

數據庫除了對查詢等操作進行了抽象,另外一個重要的功能就是事務了。為什麽需要事務呢?因為我們在操作數據的時候,可能遇到多個線程同時操作數據的問題,也可能遇到突然數據庫故障了的問題,這些都可能造成數據的不一致。所以事務要保證的就是一致性

保證一致性的第一重意思是,這是為了應對多個連接同時連到數據庫的時候。因為我們可能為每個連接分配一個線程,而這些線程有可能同時操作同一塊數據,這樣將會發生不一致。所以我們只好在寫的時候加上,也就是強行保證只有一個線程可以訪問到這塊數據。

另外我們還會遇到數據庫崩潰的問題,所以我們要求一個事務一定是原子

的,也就是 要麽全部發生, 要麽根本不發生。比如Bob給Smith轉100塊,要麽Bob有100塊,要麽Smith有100塊,不存在中間狀態。

對於單機事務而言,需要保證

  • 原子性
  • 一致性
  • 隔離性
  • 持久性

也就是所謂的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賬戶不存在怎麽辦?沒關系,我們可以回滾到上一個狀態。

但是數據庫不可能把每個狀態都記錄下來,這就需要我們在轉賬之前把之前的狀態記錄下來。

比如我們看剛剛那個轉賬操作的中間狀態

  1. Bob:100,Smith:0
  2. Bob:0,Smith :0 (此時正在轉賬)
  3. Bob : 0 , Smith :100(轉賬成功)

我們可以在插入兩個undo段,他們記錄在日誌中。

  1. Bob:100,Smith:0
  2. Bob:0,Smith :0 (此時正在轉賬)
    • 上一個狀態為:Bob:100,Smith:0
  3. 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塊錢,

  1. Bob:100,Smith:0
  2. Bob:0,Smith :0 ------------->Bob:0,Smith :300(另一個事務幹的)
    • 上一個狀態為:Bob:100,Smith:0
  3. Bob : 0 , Smith :100(轉賬成功)
    • 上一個狀態為: Bob:0,Smith :0

如果有另一個事務在進行到步驟2的時候把smith賬戶加到了300塊錢,此時如果回滾,會把smith改為0,那加上的300塊就丟失了。 那麽我們還需要一致性。
技術分享圖片

一致性

上一章我們提到了如果在事務中間,有另一個事務突然插手對數據進行修改,則如果出現回退,將會出現數據不一致的問題。

那怎麽解決這個問題呢?如果我們一個事務對數據操作完了以後,另一個事務再進入,這樣就不會發生爭搶和數據不一致了。所以核心就在於加鎖

比如

  • Lock Bob , Smith
  1. Bob:100,Smith:0
  2. Bob:0,Smith :0 ------------->Bob:0,Smith :300(另一個事務幹的)
    • 上一個狀態為:Bob:100,Smith:0
  3. 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

  • 還有就是內存如果掉電,裏面的數據就必然丟失,持久性得不到保證。但是如果每一次提交操作完成以後,都將內存中的數據同步到硬盤上,則會造成頻繁寫硬盤,性能將下降。所以持久性和延遲無法兼得

    我們只要進行折中,比如只要把數據提交到內存,就立刻返回成功,然後將一段時間的請求打包送到磁盤上。這樣就避免了每次提交都寫磁盤

技術分享圖片

參考

  • 慕課網
  • 如果有人問你數據庫的原理,叫他看這篇文章如果有人問你數據庫的原理,叫他看這篇文章

數據庫(五),事務