并發問題可歸納為以下幾類:
A.丟失更新:撤銷一個事務時,把其他事務已提交的更新數據覆蓋(A和B事務并發執行,A事務執行更新后,提交;B事務在A事務更新后,B事務結束前也做了對該行數據的更新操作,然后回滾,則兩次更新操作都丟失了)。
B.臟讀:一個事務讀到另一個事務未提交的更新數據(A和B事務并發執行,B事務執行更新后,A事務查詢B事務沒有提交的數據,B事務回滾,則A事務得到的數據不是數據庫中的真實數據。也就是臟數據,即和數據庫中不一致的數據)。
C.不可重復讀:一個事務讀到另一個事務已提交的更新數據(A和B事務并發執行,A事務查詢數據,然后B事務更新該數據,A再次查詢該數據時,發現該數據變化了)。
D. 覆蓋更新:這是不可重復讀中的特例,一個事務覆蓋另一個事務已提交的更新數據(即A事務更新數據,然后B事務更新該數據,A事務查詢發現自己更新的數據變了)。
E.虛讀(幻讀):一個事務讀到另一個事務已提交的新插入的數據(A和B事務并發執行,A事務查詢數據,B事務插入或者刪除數據,A事務再次查詢發現結果集中有以前沒有的數據或者以前有的數據消失了)。
數據庫系統提供了四種事務隔離級別供用戶選擇:
A.Serializable(串行化):一個事務在執行過程中完全看不到其他事務對數據庫所做的更新(事務執行的時候不允許別的事務并發執行。事務串行化執行,事務只能一個接著一個地執行,而不能并發執行。)。
B.Repeatable Read(可重復讀):一個事務在執行過程中可以看到其他事務已經提交的新插入的記錄,但是不能看到其他其他事務對已有記錄的更新。
C.Read Commited(讀已提交數據):一個事務在執行過程中可以看到其他事務已經提交的新插入的記錄,而且能看到其他事務已經提交的對已有記錄的更新。
D.Read Uncommitted(讀未提交數據):一個事務在執行過程中可以看到其他事務沒有提交的新插入的記錄,而且能看到其他事務沒有提交的對已有記錄的更新。
| 丟失更新 | 臟讀 | 非重復讀 | 覆蓋更新 | 幻像讀 |
未提交讀 | Y | Y | Y | Y | Y |
已提交讀 | N | N | Y | Y | Y |
可重復讀 | N | N | N | N | Y |
串行化 | N | N | N | N | N |
隔離級別
數據庫系統有四個隔離級別(大多數數據庫默認級別為read commited)。對數據庫使用何種隔離級別要審慎分析,因為
1. 維護一個最高的隔離級別雖然會防止數據的出錯,但是卻導致了并行度的損失,以及導致死鎖出現的可能性增加。
2. 然而,降低隔離級別,卻會引起一些難以發現的bug。
SERIALIZABLE(序列化)
添加范圍鎖(比如表鎖,頁鎖等,關于range lock,我也沒有很深入的研究),直到transaction A結束。以此阻止其它transaction B對此范圍內的insert,update等操作。
幻讀,臟讀,不可重復讀等問題都不會發生。
REPEATABLE READ(可重復讀)
對于讀出的記錄,添加共享鎖直到transaction A結束。其它transaction B對這個記錄的試圖修改會一直等待直到transaction A結束。
可能發生的問題:當執行一個范圍查詢時,可能會發生幻讀。
READ COMMITTED(提交讀)
在transaction A中讀取數據時對記錄添加共享鎖,但讀取結束立即釋放。其它transaction B對這個記錄的試圖修改會一直等待直到A中的讀取過程結束,而不需要整個transaction A的結束。所以,在transaction A的不同階段對同一記錄的讀取結果可能是不同的。
可能發生的問題:不可重復讀。
READ UNCOMMITTED(未提交讀)
不添加共享鎖。所以其它transaction B可以在transaction A對記錄的讀取過程中修改同一記錄,可能會導致A讀取的數據是一個被破壞的或者說不完整不正確的數據。
另外,在transaction A中可以讀取到transaction B(未提交)中修改的數據。比如transaction B對R記錄修改了,但未提交。此時,在transaction A中讀取R記錄,讀出的是被B修改過的數據。
可能發生的問題:臟讀。
問題
我們看到,當執行不同的隔離級別時,可能會發生各種各樣不同的問題。下面對它們進行總結并舉例說明。
幻讀
幻讀發生在當兩個完全相同的查詢執行時,第二次查詢所返回的結果集跟第一個查詢不相同。
發生的情況:沒有范圍鎖。
例子:
事務1 | 事務2 |
SELECT * FROM users WHERE age BETWEEN 10 AND 30 | |
INSERT INTO users VALUES ( 3 , 'Bob' , 27 ); | |
SELECT * FROM users WHERE age BETWEEN 10 AND 30; |
如何避免:實行序列化隔離模式,在任何一個低級別的隔離中都可能會發生。
不可重復讀
在基于鎖的并行控制方法中,如果在執行select時不添加讀鎖,就會發生不可重復讀問題。
在多版本并行控制機制中,當一個遇到提交沖突的事務需要回退但卻被釋放時,會發生不可重復讀問題。
事務1 | 事務2 |
SELECT * FROM users WHERE id = 1; | |
UPDATE users SET age = 21 WHERE id = 1 ; COMMIT; /* in multiversion concurrency*/ control, or lock-based READ COMMITTED * | |
SELECT * FROM users WHERE id = 1; | |
COMMIT; /* lock-based REPEATABLE READ */ |
在上面這個例子中,事務2提交成功,它所做的修改已經可見。然而,事務1已經讀取了一個其它的值。在序列化和可重復讀的隔離級別中,數據庫管理系統會返回舊值,即在被事務2修改之前的值。在提交讀和未提交讀隔離級別下,可能會返回被更新的值,這就是“不可重復讀”。
有兩個策略可以防止這個問題的發生:
1. 推遲事務2的執行,直至事務1提交或者回退。這種策略在使用鎖時應用。(悲觀鎖機制,比如用select for update為數據行加上一個排他鎖)
2. 而在多版本并行控制中,事務2可以被先提交。而事務1,繼續執行在舊版本的數據上。當事務1終于嘗試提交時,數據庫會檢驗它的結果是否和事務1、事務2順序執行時一樣。如果是,則事務1提交成功。如果不是,事務1會被回退。(樂觀鎖機制)
臟讀
臟讀發生在一個事務A讀取了被另一個事務B修改,但是還未提交的數據。假如B回退,則事務A讀取的是無效的數據。這跟不可重復讀類似,但是第二個事務不需要執行提交。
事務1 | 事務2 |
SELECT * FROM users WHERE id = 1; | |
UPDATE users SET age = 21 WHERE id = 1 | |
SELECT FROM users WHERE id = 1; | |
COMMIT; /* lock-based DIRTY READ */ |