事務是現代關系型數據庫的核心之一。在多個事務並發操作數據庫(多線程、網絡並發等)的時候,如果沒有有效的避免機制,就會出現以下幾種問題:
(
第一類丟失更新
A事務撤銷時,把已經提交的B事務的更新數據覆蓋了。這種錯誤可能造成很嚴重的問題,通過下面的賬戶取款轉賬就可以看出來:
時間 |
取款事務A |
轉賬事務B |
T1 |
開始事務 |
|
T2 |
|
開始事務 |
T3 |
查詢賬戶余額為1000元 |
|
T4 |
|
查詢賬戶余額為1000元 |
T5 |
|
匯入100元把余額改為1100元 |
T6 |
|
提交事務 |
T7 |
取出100元把余額改為900元 |
|
T8 |
撤銷事務 |
|
T9 |
余額恢復為1000 元(丟失更新) |
|
A事務在撤銷時,“不小心”將B事務已經轉入賬戶的金額給抹去了。
SQL92沒有定義這種現象,標准定義的所有隔離界別都不允許第一類丟失更新發生。
第二類丟失更新
A事務覆蓋B事務已經提交的數據,造成B事務所做操作丟失:
時間 |
轉賬事務A |
取款事務B |
T1 |
|
開始事務 |
T2 |
開始事務 |
|
T3 |
|
查詢賬戶余額為1000元 |
T4 |
查詢賬戶余額為1000元 |
|
T5 |
|
取出100元把余額改為900元 |
T6 |
|
提交事務 |
T7 |
匯入100元 |
|
T8 |
提交事務 |
|
T9 |
把余額改為1100 元(丟失更新) |
|
上面的例子里由於支票轉賬事務覆蓋了取款事務對存款余額所做的更新,導致銀行最后損失了100元,相反如果轉賬事務先提交,那么用戶賬戶將損失100元。
)
第一類丟失更新(Lost Update)
在完全未隔離事務的情況下,兩個事務更新同一條數據資源,某一事務完成,另一事務異常終止,回滾造成第一個完成的更新也同時丟失 。這個問題現代關系型數據庫已經不會發生,就不在這里占用篇幅,有興趣的可以自行百度。
臟讀(Dirty Read)
A事務執行過程中,B事務讀取了A事務的修改。但是由於某些原因,A事務可能沒有完成提交,發生RollBack了操作,則B事務所讀取的數據就會是不正確的。這個未提交數據就是臟讀(Dirty Read)。臟讀產生的流程如下:
不可重復讀(Nonrepeatable Read)
B事務讀取了兩次數據,在這兩次的讀取過程中A事務修改了數據,B事務的這兩次讀取出來的數據不一樣。B事務這種讀取的結果,即為不可重復讀(Nonrepeatable Read)。不可重復讀的產生的流程如下:
不可重復讀有一種特殊情況,兩個事務更新同一條數據資源,后完成的事務會造成先完成的事務更新丟失。這種情況就是大名鼎鼎的第二類丟失更新。主流的數據庫已經默認屏蔽了第一類丟失更新問題(即:后做的事務撤銷,發生回滾造成已完成事務的更新丟失),但我們編程的時候仍需要特別注意第二類丟失更新。它產生的流程如下:
幻讀(Phantom Read)
B事務讀取了兩次數據,在這兩次的讀取過程中A事務添加了數據,B事務的這兩次讀取出來的集合不一樣。幻讀產生的流程如下:
這個流程看起來和不可重復讀差不多,但幻讀強調的集合的增減,而不是單獨一條數據的修改。
數據庫隔離級別
為了解決上面提及的並發問題,主流關系型數據庫都會提供四種事務隔離級別。
讀未提交(Read Uncommitted)
在該隔離級別,所有事務都可以看到其他未提交事務的執行結果。本隔離級別是最低的隔離級別,雖然擁有超高的並發處理能力及很低的系統開銷,但很少用於實際應用。因為采用這種隔離級別只能防止第一類更新丟失問題,不能解決臟讀,不可重復讀及幻讀問題。
讀已提交(Read Committed)
這是大多數數據庫系統的默認隔離級別(但不是MySQL默認的)。它滿足了隔離的簡單定義:一個事務只能看見已經提交事務所做的改變。這種隔離級別可以防止臟讀問題,但會出現不可重復讀及幻讀問題。
可重復讀(Repeatable Read)
這是MySQL的默認事務隔離級別,它確保同一事務的多個實例在並發讀取數據時,會看到同樣的數據行。這種隔離級別可以防止除幻讀外的其他問題。
可串行化(Serializable)
這是最高的隔離級別,它通過強制事務排序,使之不可能相互沖突,從而解決幻讀、第二類更新丟失問題。在這個級別,可以解決上面提到的所有並發問題,但可能導致大量的超時現象和鎖競爭,通常數據庫不會用這個隔離級別,我們需要其他的機制來解決這些問題:樂觀鎖和悲觀鎖。
這四種隔離級別會產生的問題如下(網上到處都有,懶得畫了):
如何使用數據庫的隔離級別
很多文章博客在介紹完這些隔離級別以后,就沒有以后了。讀的人一般會覺得,嗯,是這么回事,我知道了!
學習一個知識點,是需要實踐的。比如下面這個常見而又異常嚴重的情況:
圖中是典型的第二類丟失更新問題,后果異常嚴重。我們這里就以讀已提交(Read Committed)及以下隔離級別中會出現不可重復讀現象為例。從上面的表格可以看出,當事務隔離級別為可重復讀(Repeatable Read)時可以避免。(事務隔離級別)
轉自:https://www.cnblogs.com/yubaolee/p/10398633.html
悲觀鎖:
顧名思義,很悲觀,就是每次拿數據的時候都認為別的線程會修改數據,所以在每次拿的時候都會給數據上鎖。上鎖之后,當別的線程想要拿數據時,就會阻塞,直到給數據上鎖的線程將事務提交或者回滾。傳統的關系型數據庫里就用到了很多這種鎖機制,比如行鎖,表鎖,共享鎖,排他鎖等,都是在做操作之前先上鎖。
行鎖:
下面演示行鎖,打開兩個mysql命令行界面,兩個線程分別執行如下操作:(左邊先執行)
左邊的線程,在事務中通過select for update語句給sid = 1的數據行上了鎖。右邊的線程此時可以使用select語句讀取數據,但是如果也使用select for update語句,就會阻塞,使用update,add,delete也會阻塞。
當左邊的線程將事務提交(或者回滾),右邊的線程就會獲取鎖,線程不再阻塞:
此時,右邊的線程獲取鎖,左邊的線程如果執行類似操作,也會被阻塞:
表鎖:
上述例子中,如果使用如下語句就是使用的表鎖:
select * from student for update;
頁鎖:
行鎖鎖指定行,表鎖鎖整張表,頁鎖是折中實現,即一次鎖定相鄰的一組記錄。
共享鎖:
共享鎖又稱為讀鎖,一個線程給數據加上共享鎖后,其他線程只能讀數據,不能修改。
排他鎖:
排他鎖又稱為寫鎖,和共享鎖的區別在於,其他線程既不能讀也不能修改。
樂觀鎖:
樂觀鎖其實不會上鎖。顧名思義,很樂觀,它默認別的線程不會修改數據,所以不會上鎖。只是在更新前去判斷別的線程在此期間有沒有修改數據,如果修改了,會交給業務層去處理。
常用的實現方式是使用版本戳,例如在一張表中添加一個整型字段version,每更新version++,比如某個時刻version=1,線程A讀取了此version=1,線程B也讀取了此version=1,當線程A更新數據之前,判斷version仍然為1,更新成功,version++變為2,但是當線程B再提交更新時,發現version變為2了,與之前讀的version=1不一致,就知道有別的線程更新了數據,這個時候就會進行業務邏輯的處理。
通常情況下,寫操作較少時,使用樂觀鎖,寫操作較多時,使用悲觀鎖。
轉載自:https://blog.csdn.net/xiangwanpeng/article/details/55106732
丟失更新: 當兩個事物或多個事務都更新了同一條數據,但是這些事務彼此之間都不知道其他事務進行的修改,因此第二個更改覆蓋了第一次的更改,說白了,就是事務A還沒有提交之后,但是這個時候事務B更新了數據,那么事務A就丟失更新了。
解決方案: 樂觀鎖+悲觀鎖
悲觀鎖: 數據庫的一種鎖機制,悲觀鎖分成兩種,分別是共享鎖和排它鎖
添加共享鎖方式:select * from account lock in share mode ;
添加排它鎖方式:select * from account for update;
下面說一下共享鎖: 共享鎖就是,例如 我在客戶端A 給數據C 添加了共享鎖,此時我在客戶端B只能添加共享鎖進行查看,沒有修改的權利,如果我想要在客戶端B進行修改,我只能在A處commit才能進行修改。
下面說一下排它鎖:排它鎖就是我在客戶端A 給數據C添加了排它鎖,那么我在客戶端B只能在客戶端 A commit之后,才能select數據,換句話說,只要我在客戶端B用鎖進行了查詢,那我我都需要等待Acommit之后,如果此時我客戶端B不加鎖,我是可以查詢到的。這個排它鎖很像數據庫隔離級別中的最高的隔離級別。但是排它鎖是鎖住了一條數據,而排它鎖是鎖上了這條數據。
說完了悲觀鎖,那么我們在說說樂觀鎖。
樂觀鎖:就是假設丟失更新不存在,它使用的是數據庫的字段進行加測。 例如我在我的字段中添加一個字段,字段的類型是 timestamp 在插入和修改時 都會自動更新為當前時間 ,我根據我的sql條件進行判斷,如果我的時間不符合,那么我的更新失敗。