數據庫內核雜談:事務、隔離、併發(1)

這篇文章,我們要分享一個很重要的概念:事務及其相關實現。

事務 (transaction) 和 ACID

事務的定義是:一個事務是一組對數據庫中數據操作的集合。無論集合中有多少操作,對於用戶來說,只是對數據庫狀態的一個原子改變。

單從概念定義來理解,可能有些晦澀難懂,我們舉個例子來講解:數據庫中有兩個用戶的銀行賬戶 A:100 元 ; B:200 元。假設事務是 A 轉賬 50 元到 B,可以理解為這個事務由兩個操作組成:1) A-= 50; 2) B+=50。對於用戶來說,數據庫對於這個事務只有兩個狀態:執行事務前的初始狀態,即 A:100 元 ; B:200 元,以及執行事務後的轉賬成功狀態:A:50 元 ;B:250 元,不會有中間狀態,比如錢從 A 已經扣除,卻還沒轉到 B 上:A:50 元 ; B:200 元。

一個事務的所有操作要麼全部執行,要麼一個都不執行。如果在執行事務的過程中,因為任何原因導致事務失敗,已經執行的操作都要被回滾 (rollback)。這種“all-or-none" 的屬性就是所謂的事務的原子性 (atomicity)。

當一個事務被認定執行成功後,即代表這個事務的操作被數據庫持久化。因此,即使數據庫在此時奔潰了,比如進程被殺死了,甚至是服務器斷電了,這個事務的操作依然有效,這就是事務的另一個屬性,持久性 (durability)。

假定數據庫的初始狀態是穩定的,或者說對用戶來說是一致的。由於事務執行的原子性,即執行失敗就回滾到執行前的狀態,執行成功就變成一個新的穩定狀態。因此,事務的執行會保持數據庫狀態的一致性 (consistency)。

數據庫系統是多用戶系統。多個用戶可能在同一時間執行不同的事務,稱為併發。如果想要做到事務的原子性,那麼數據庫就必須做到併發的事務互不影響。從事務的角度出發,在執行它本身的過程中,不會感知到其他事務的存在。從數據庫的角度出發,即使同一時間有多個事務併發,從微觀尺度上看,它們之間也有先來後到,必須等一個事務完成後,另一個事務才開始。這種併發事務之間的不感知就是所謂的事務隔離性 (isolation)。

總之,一個事務是一組對數據庫中數據操作的集合。事務,對於數據庫系統,具有原子性 (atomicity),一致性 (consistency),隔離性 (isolation),以及持久性 (durability)。曾經聽過這樣一個觀點,事務的出現主要是針對併發。其實不然,ACID 屬性中只有隔離性是針對併發事務的。所以,即使數據庫系統是一個單用戶系統,我們依然希望事務具有原子性、一致性和持久性。

隔離級別 (Isolation Level)

如果讓你來實現事務的隔離性,最容易的辦法,你會想到什麼?我想絕大部分的讀者都會想到,給數據庫加一個全局的操作鎖,在同一時間裡只允許一個用戶對數據庫進行操作,這就保證了隔離性。

的確,這樣可以保證隔離性,但也限制了併發性,對數據庫的性能產生了極大的影響。在實際情況中,沒有數據庫會這麼去實現。並且這個世界並非非黑即白,隔離性也並不是有或者沒有。數據庫一般會提供多種隔離性的級別,供用戶選擇:越嚴格的隔離級別越接近全局鎖,越寬鬆的隔離級別越能提高併發。天下沒有免費的午餐,寬鬆的隔離級別也會隨之帶來一些問題。

我們結合併發事務可能帶來的問題,來講述一下不同的隔離級別。

首先,我們定義一個相對簡單的事務模型,方便後續討論各種隔離級別和可能遇到的數據問題。雖然數據庫支持各種複雜的操作,但歸根到底就是對數據基本單元的讀寫操作,對於任一給定數據單元 A,我們定義 read(A),write(A, val) 分別為讀取和寫入操作。 同時,對於事務,提供 begin(開啟事務), commit(提交事務), rollback(回滾事務) 操作。

先從最寬鬆的隔離級別開始,read uncommitted(讀未提交)。顧名思義,讀未提交就是在一個事務中,允許讀取其他事務未提交的數據。下圖示例很清晰地詮釋了讀未提交:

數據庫內核雜談:事務、隔離、併發(1)

在事務 T1 中,讀取 A 得到結果是 5,是因為事務 T2 修改了 A 的值,雖然當時 T2 還未提交,甚至最後 T2 回滾了。讀未提交導致的問題就是 dirty read(髒讀)。髒讀的定義就是,一個事務讀取了另一個事務還未提交的修改。雖然可能大多數情況下,我們都會認為髒讀產生了不正確的結果。但是,拋開業務談正確性都是耍流氓。或許,某些用戶的某些業務,為了支持更大地併發,允許髒讀的出現。因為,對於讀未提交,完全不需要對操作進行加鎖,自然併發性更高。

如何避免髒讀呢?數據庫引入了第二層的隔離級別,read committed(讀提交)。讀提交就是指在一個事務中,只能夠讀取到其他事務已經提交的數據。

在讀提交的隔離級別下,再回看上面的例子,T1 中讀取 A 的值就應該還是 10,因為當時 T2 還沒有提交。沿著上面的例子,接著往下看,如果最後 T2 提交了事務,而 T1 在之後又讀取了一次 A,這時候的值就變為 5 了。

數據庫內核雜談:事務、隔離、併發(1)

這又出現了什麼問題呢?在 T1 事務中,先後讀取了兩次 A,兩次的值不一樣了。回顧最早提及的事務的隔離性,兩次讀取同一數據的值不一樣,其實違反了隔離性。因為隔離性定義了一個事務不需要感知其他事務的存在,但顯然,由於值不同,說明在這個過程中另一個事務提交了數據。這類問題就被定義為 nonrepeatable read(不可重複度讀):在一個事務過程中,可能出現多次讀取同一數據但得到的值不同的現象。

如何避免不可重複度這個問題呢?數據庫引入了第三層隔離級別,根據上面的經驗,你可能已經猜出來了,名稱就叫做 repeatable read(可重複讀)。可重複讀指的是在一個事務中,只能讀取已經提交的數據,且可以重複查詢這些數據,並且,在重複查詢之間,不允許其他事務對這些數據進行寫操作。雖然我們還沒講到實現,但不難想象,對讀數據加讀鎖鎖就能實現。

對於可重複讀級別來說,上述例子中的兩次讀取都會得到數據是 10。讀者可能會有疑問,那彼時 T2 的 commit 會失敗嗎?如果是加鎖實現的可重複讀,那 T2 的 commit 就會 hold 在那,直至 T1 結束,取決於 T1 最後有沒有更新 A,如果有,T2 就會失敗。

可重複讀,似乎看上去很完美,解決了所有並行事務帶來的不確定性。其實不然,我們通過下面這個 SQL 語句的例子來看:

複製代碼

<code>T1:BEGIN;SELECT * FROM students WHERE class_id = 1;  // (1)... SELECT * FROM students WHERE class_id = 1;  // (2)...COMMIT;/<code>

上面示例中的查詢語句 (1) 和 (2),在可重複讀隔離級別下,應該返回相同的結果嗎?乍一看,應該覺得,沒錯啊。但可重複讀隔離級別只是規定對被已經讀取的數據,禁止其他事務進行修改。那如果是下面這個事務呢?

複製代碼

<code>T2:BEGIN;INSERT INTO students (1 /* class_id */, ...);COMMIT; /<code>

T2 事務並沒有修改現有數據,而是新增了一條新數據,恰巧 class_id = 1。如果這條插入介於 (1) 和 (2) 之間,(2) 的結果會改變嗎?答案是,會的。語句 (2) 會比 (1) 多顯示一條記錄,即 T2 插入的。這個問題被稱為 phantom read(幻讀),指的是,在一個事務中,當查詢了一組數據後,再次發起相同查詢,卻發現滿足條件的數據被另一個提交的事務改變了。

如何才能避免幻讀呢?數據庫系統只能推出最保守的隔離機制,serializable(可有序化),即所有的事務必須按照一定順序執行,直接避免了不同事務併發帶來的各種問題。

數據庫系統針對不同需求,推出了不同的隔離級別,由寬到緊分別是:

1)讀未提交:在一個事務中,允許讀取其他事務未提交的數據。

2)讀提交:在一個事務中,只能夠讀取到其他事務已經提交的數據。

3)可重複讀:在一個事務中,只能讀取已經提交的數據,且可以重複查詢這些數據,並且,在重複查詢之間,不允許其他事務對這些數據進行寫操作。

4)可有序化:所有的事務必須按照一定順序執行。

而後三種隔離級別分別為了解決前一種隔離級別遇到的問題:

1)髒讀:一個事務讀取了另一個事務還未提交的修改。

2)不可重複度:在一個事務過程中,可能出現多次讀取同一數據但得到不同值的現象。

3)幻讀:在一個事務中,當查詢了一組數據後,再次發起相同查詢,卻發現滿足條件的數據被另一個提交的事務改變了。

下方列出了一張表格,更直觀地展現它們之間的關係。

隔離級別髒讀不可重複度幻讀讀未提交可能出現可能出現可能出現讀提交不能可能出現可能出現可重複讀不能不能可能出現可有序化不能不能不能

總結

這篇文章主要覆蓋了事務的定義、ACID 屬性以及對於隔離性,數據庫推出的不同隔離級別。雖然並沒有提到很多的實現,不過,理清這些概念對於理解和學習事務的實現是很有必要的。預告一下,下篇文章我們會分享事務的實現。


分享到:


相關文章: