0. 前言
數據庫的事務隔離級別是關係型數據庫事務的理論基礎,本文將從資源互斥的角度從上到下依次進行闡釋。
1.數據庫的事務隔離級別
1.1 事務的隔離級別,隔離的是什麼?
在闡述數據庫事務的隔離級別時,我們首先應當明確一下,這個隔離,到底隔離的是什麼。
- 什麼是事務?
從數據庫的事務定義來看,其具備ACID特性(即Atomic,原子性,Consistency一致性,Isolation,隔離性,Duration,持久性)
。
一般意義上講,所謂的事務,指的是一批操作,可以原子性的方式進行,要麼全部成功,要麼全部失敗; - 什麼是隔離性,隔離的是什麼?
隔離性,是指不同的客戶端在做事務操作時,理想狀態下,各個客戶端之間不會有任何相互影響,好像感知不到對方存在一樣。所謂的隔離,真正隔離的對象在實現上是數據庫資源的互斥性訪問
,隔離性就是通過數據庫資源劃分的不同粒度體現的。
接下來,本文將通過數據庫資源的不同粒度的劃分,來闡述隔離性
不同級別的實現。
1.2 - 隔離級別-序列化讀(SERIALIZABLE READ)
1.1.1 將整個數據庫作爲互斥資源
如果將整個數據庫當做互斥資源
的訪問,那麼,這種訪問會有如下性質:
規定同一時間內只能有一個客戶端連接數據庫進行事務操作,在這個客戶端完成事務之前,其他客戶端不能對數據庫進行事務操作。
當客戶端訪問數據庫時,各個客戶端以互斥
的方式進行訪問。交互方式如下圖所示:
這種級別的隔離方式時最理想的,肯定不會存在不同的客戶端事務相應影響的情況,因爲,所有的客戶端在事務操作時,都是以排隊的形式進行的。
數據庫除了在理論上的嚴謹性之外,還要看它的實用性。 下面我們介紹下數據庫性能的一個衡量標準:TPS: 單位時間內的事務數(Transactions Per Second)
,TPS越高,表示數據庫的性能越好。
在後續的介紹中,將會使用這一指標來衡量每一個隔離級別的性能。
假設每個客戶端的每次事務操作耗時爲 T 秒
,並且期間沒有空閒,那麼此時數據庫的最大TPS能力就是1/T
。
最大TPS = 1 / T (T 爲客戶端的平均事務操作時間)
例如:T = 10ms, 那麼數據庫此時的TPS值 爲 1 / 0.01 = 100, 即數據庫每秒能夠完成100個事務操作
合理性討論:使用數據庫級別作爲互斥資源,有這麼必要嗎?
使用數據庫級別作爲互斥資源訪問,確實能夠完全保證事務的隔離性;但是,在實際的應用場景中,使用這種粗粒度的互斥資源沒有必要。
舉例:假設數據庫
mall
中有兩張表:t_user
、t_order
; 而外部共有4個客戶端A
、B
、C
、D
。其中,A
和B
客戶端只操作了t_user
表,C
和D
客戶端只操作了t_order
表。
從互斥資源的角度上來講,客戶端訪問互斥資源的情況,分別有兩對互斥:A
<–t_user
–> B
、C
<–t_order
–>D
,在做事務隔離控制時,沒有必要使用數據庫
作爲互斥資源;可以將互斥資源進行細分,細分到表這一層級。
1.1.2 使用數據庫的表作爲互斥資源
接着上面的例子,我們將數據庫的表作爲互斥資源,細分後的交互方式如下所示:
當我們把鎖級別放到表級別之後,在時序操作上,會有兩個資源互斥組t_user
-[A,B]、t_order
-[C,D], 這兩個互斥組之間不會受到相互影響,可以並行處理,並行的結果如下圖所示:
由於將資源的互斥級別 從數據庫
級別細化到表
級別,數據庫的TPS數量也提升了不少,下面我們簡單估算一下滿負荷狀態下的TPS,還是假設客戶端的平均事務操作的耗時爲T,資源互斥組數量爲N,那麼:
最大TPS = (1 / T)* N
本例中,若T= 10ms ,N = 2,那麼:TPS = (1 / 0.01) * 2 = 200
和將數據庫
作爲互斥資源對比,可以看到,有如互斥粒度降到表
級別,TPS也跟着提高。
注意:在真實的事務操作中,可能一個客戶端事務會操作多張表,那這多張表的任意一張表都會被當做互斥資源。
在目前主流數據庫的實現上,基本上都提供了
鎖表
的方式提供資源互斥資源訪問,通過鎖全表的方式進行的事務隔離處理,在操作時序上,是排隊性質進行的,這種事務隔離的級別最高,即:序列化讀(SERIALIZABLE READ)。
我們可以簡單地來理解序列化讀
的實現方式:鎖全表
鎖全表的方式會導致對同一個表操作的客戶端事務操作變成排隊性質的序列化操作。現在看下另外一個場景:
假設現在有客戶端A
和客戶端B
,在事務操作時,共同使用一張表T_USER
,但是他們操作的行信息有所不同:
上圖中,雖然客戶端A
和客戶端B
以互斥的方式訪問表T_USER
,但是操作的數據並沒有真正的互斥,那我們可以繼續將鎖的粒度細化,從鎖表
這一級,再次細化到鎖行記錄
這一級,這將進一步提高系統的併發處理能力。經過行鎖
細化後,其隔離級別就降到了可重複讀
。
1.2 可重複讀(REPEATABLE_READ)
將上述的例子展開,通過模型的方式體現,如下圖所示:
客戶端A
和客戶端B
同時嘗試訪問相同的行數據;而客戶端C
和客戶端D
也是同時嘗試訪問相同的行數據。在此競爭過程中,可以看到,最多可以有兩個客戶端可以同時訪問表T_USER
,和序列化讀
相比,整個客戶端的併發量又提高了一個量級!
用客戶端時序關係表示如下:
看到這個結果,是不是有這樣的感覺:哇塞,既然使用行鎖併發能力這麼高,爲什麼還要 鎖表方式的序列化讀(SERIALIZABLE READ)
?
解答這個問題之前,我們來看下這種行鎖方式有什麼問題。
通過行鎖的方式,能夠鎖定客戶端鎖操作的行;而在事務進行的過程中,可能會往對應的表中插入新的數據,而這個新的數據,起初並不數據鎖定範圍,使用SQL語句操作數據庫數據時,可能會返回更多的滿足條件的數據,加入新的行鎖,如下圖所示:
如上圖所示:在同一個事務內,完全相同的兩次查詢,返回的記錄數不一致,好像多讀了數據一樣,這種情況,稱爲幻讀(Phantom Read)
使用這種行鎖
的方式進行資源隔離的方式,在數據庫隔離級別上被稱爲 可重複讀 (REPEATABLE READ)
注意:雖然使用行鎖互斥的方式進行數據庫操作,但是會出現
幻讀
的情況,避免幻讀
的方式,可以使用表級鎖
—即提高事務的隔離界別—序列化讀(SERIALIZABLE READ)
1.3 讀已提交(READ_COMMITTED)
實際上,數據庫在實現原子性(Atomic)
時,對於某一表的特定行,其實有兩個狀態:Uncommited
、Commited
,我們將資源在行數據的基礎上繼續細分,如下圖所示:
爲了進一步提高數據庫的併發能力,如上圖所示,將在某一行數據上,使用讀寫分離鎖的機制:客戶端B
和客戶端D
直接使用讀鎖
讀取數據,讀鎖
是共享鎖,所以可以同時進行;而客戶端A
和客戶端C
事務操作上,會存在兩個環節:Uncommited
—> Commited
,在真正 commit
的時候,則使用寫鎖
以互斥的方式完成事務,把互斥訪問資源的時機壓縮的更短。
上述的客戶端B
和客戶端D
只讀取已提交的數據的方式,在隔離級別中,被稱爲讀已提交(READ_COMMITED)
.
通過上述的流程,我們的數據庫的併發能力又能提高一個量級,一切是多麼“美好”!
但是這個只是想象中的美好而已,接下來看它存在的問題。假設我們有如下的數據庫操作:
上述的例子中,reader
在一個事務中,相同的查詢條件,返回的行記錄是同一條,但是這一條的記錄的AGE
列值從18
變成19
,雖然是相同的行記錄,但是內容不一致,這種現象叫做不可重複讀(NO-REPEATABLE-READ)
。
雖然讀已提交(READ COMMITED)
隔離級別的併發讀能力提高了很多個量級,但是在一個事務內,會造成不可重複讀(NO-REPEATABLE-READ)
的情況。
讀已提交
的不可重複讀
現象對開發同學有什麼啓示?
不可重複讀
會導致一條行數據兩次讀取數據可能不一致,這就要求我們在數據庫事務操作上,儘可能少用查詢出來的結果作爲參數執行後續的update
SQL 語句,儘可能使用狀態機來保證數據的完整性。這方面的知識可以單獨開一個課題來討論 :如何使用數據庫來保證業務數據的邏輯完整性?
1.4 讀未提交(READ_UNCOMMITTED)
上述的讀已提交(READ_COMMITTED)
的本質,是將資源互斥訪問的粒度控制到 committed
的行數據上,而實際上,還可以繼續將資源互斥的訪問粒度,細化到未提交(UNCOMMITED)
的行數據上,如下圖所示:
這種方式,由於更細化了資源鎖的粒度,其客戶端的併發能力又得到了進一步的提升。但是,與此同時,會存在新的問題—髒讀現象
,具體流程示例如下圖所示:
如上圖所示:客戶端reader
在事務的過程中,讀取到了其他客戶端updater
尚未提交的數據,之後客戶端reader
可能將其當做已經持久化的數據進行業務操作,而實際上,客戶端updater
可能將其數據回退;在此過程中,客戶端reader
讀取的數據就成了髒數據
,客戶端reader
的讀數據行爲爲:髒讀(Dirty Read)
1.5 小結
對上述的四種事務隔離級別的闡述中,我們使用了從資源互斥訪問的角度做了解釋。資源互斥粒度控制的越細,客戶端事務的併發能力就越高,但是與此同時,會相應地降低數據的一致性。
事務的併發數
和數據數據一致性
這兩個是兩個相反的理想指標。而數據庫研發的方向就是儘可能提高同時提高兩個指標,儘可能減少之間的反作用影響。
2. 數據庫隔離級別和數據一致性的關係
數據庫的隔離級別一般分爲四個級別,從隔離級別由高到低排序的話,分別是:SERIALIZABLE
—> REPEATABLE READ
—> READ_COMMITTED
—>READ_UNCOMMITED
,其分別表示如下幾種含義:
SERIALIZABLE
序列化讀,隔離級別最高,客戶端以互斥的方式訪問數據庫資源,統一時間內,同一個資源只能被一個客戶端訪問,好像客戶端在排隊請求訪問,所以稱爲序列化讀。REPEATABLE_READ
可重複讀,可重複讀能夠保證,一個客戶端在一個事務內,多次訪問同一個資源時,返回結果是一樣的,顧名思義,稱爲可重複讀,這種隔離級別可能會造成幻讀
現象。READ_COMMITTED
讀已提交,即客戶端在一個事務內,每次查詢讀取的數據都是從數據庫讀取最新的已提交的數據;這種隔離界別可能會造成不可重複讀
和幻讀
現象。READ_UNCOMMITTED
讀未提交,即客戶端在一個事務內,可以讀取到其他客戶端事務的尚未提交的數據;這種隔離級別可能會造成髒讀
、不可重複讀
、幻讀
現象。
數據庫之所以有四種隔離級別,是基於對應的併發能力
相關,如下圖所示,隔離級別越高,數據庫的併發處理能力就越低;反之,隔離級別越低,數據庫的併發處理能力就越高。
3. 總結
本文通過資源的角度從上到下分析了數據庫隔離級別設計的基本理念,以及相應的數據一致性的關係,幫助大家對數據庫隔離級別有更清晰的認識。
關注我的公衆號,持續精品好文推送~