現在我們簡單聊一下數據庫中的悲觀鎖和樂觀鎖。
悲觀鎖
悲觀鎖正如其名稱,比較悲觀。總會認為:每當修改數據時,會有其他線程也會同時修改該數據。所以針對這種情況悲觀鎖的做法是:讀取數據之后就加鎖
(eg: select...for update)
,這樣別的線程讀取該數據的時候就需要等待當前線程釋放鎖,獲得到鎖的線程才能獲得該數據的讀寫權限。從而保證了並發修改數據錯誤的問題。但是由於阻塞原因,所以導致吞吐量不高。悲觀鎖更適用於多寫少讀的情況。
場景: 同學A和同學B都要給你轉500塊錢(開心壞了吧,這樣最終你能得到1000塊錢)。
使用悲觀鎖的流程:
- 同學A獲取到你的賬戶余額
balance = 0
並對該條記錄加鎖。 - 同學B獲取你的賬戶余額。由於同學A已經對這條記錄加鎖了,所以同學B需要等同學A轉帳完成(釋放鎖)才能獲得余額。
- 同學A轉賬完成並釋放鎖,此時你的賬戶余額
balance=balance + 500 = 500
- 同學B獲取到你的賬戶余額
balance = 500
,並對該條記錄加鎖(如果你人緣好,此時同學C給你轉賬也是需要等待同學B轉賬完成才可以轉賬哦) - 同學B轉賬完成並釋放鎖(如果有同學C想給你轉賬,此時同學C就可以獲得鎖並轉賬了)。此時你的賬戶余額為
balance = balance + 500 = 1000
- 最終你開開心心的得到了1000塊錢。
假設轉賬過程沒有鎖,我們看看會發生什么:
- 同學A獲取到你的賬戶余額
balance_a = 0
(沒有加鎖,此時同學B也可以獲取到賬戶余額) - 同學B獲取到你的賬戶余額
balance_b = 0
- 同學A轉賬完成,此時你的賬戶余額為
balance = balance_a + 500 = 500
- 同學B轉賬完成,此時你的賬戶余額為
balance = balance_b + 500 = 500
- 最終同學A和同學B都轉了500,但是你最終只獲得了500。這一定是不能接受的吧。
丟失的500塊去哪里了呢?從第2步可以看到同學B獲取到的賬戶余額是0,而不是同學A轉帳之后的余額500。所以問題出在這里,這是高並發場景的常見問題。所以加鎖是非常必須的。但是加了悲觀鎖,同學都要排隊給我轉賬,對於沒有耐心的同學就直接不轉帳了,我豈不是錯失了發財的好機會。那有什么好辦法呢?答案就是下面的樂觀鎖
樂觀鎖
樂觀鎖顧名思義比較樂觀,他只有在更新數據的時候才會檢查這條數據是否被其他線程更新了(這點與悲觀鎖一樣,悲觀鎖是在讀取數據的時候就加鎖了)。如果更新數據時,發現這條數據被其他線程更新了,則此次更新失敗。如果數據未被其他線程更新,則更新成功。由於樂觀鎖沒有了鎖等待,提高了吞吐量,所以樂觀鎖適合多讀少寫的場景。
常見的樂觀鎖實現方式是:版本號version和CAS(compare and swap)。此處只介紹版本號方式。
要采用版本號,首先需要在數據庫表中新增一個字段version,表示此條記錄的更新版本,記錄每變動一次,版本號加1。依舊使用上面轉賬的例子說明:
- 同學A獲取到你的賬戶余額
balance = 0
和版本號version_a = 0
- 同學B獲取到你的賬戶余額
balance = 0
和版本號version_b = 0
- 同學A轉賬完成
update table set balance = ${balance}, version = version + 1 and version = 0
。(此時版本號為0,所以更新成功) - 同學B轉賬完成
update table set balance = ${balance}, version = version + 1 and version = 0
。(此時版本號為1,所以更新失敗,更新失敗之后同學B再轉一次即可) - 同學B重新轉帳之后,你還是美滋滋的獲得了1000。
總結
悲觀鎖:讀取時加鎖,更新完釋放鎖,再此過程中會造成其他線程阻塞,導致吞吐量低,適用於多寫場景。
樂觀鎖:不加鎖,只有在更新時驗證數據是否被其他線程更新,吞吐量較高,適用於多讀場景。