Java面試題 Part11 樂觀鎖與悲觀鎖
面試的時候遇到過問Redis是如何解決“競態條件”的,相關知識點總結一下。
樂觀鎖
所謂競態條件,舉個例子,一個代表點擊數的數值hitcount,每個客戶點擊一次則+1。
沒有事務的時候,假設我們的操作如下:
hc=GET hitcount;
hc=hc+1;
SET hitcount $hc;
非併發狀態下,這樣做是OK的,但是併發狀態下會出現的問題是:
A和B兩個客戶端分別從Redis處取值,並+1,值都是11。
Redis是單線程模型,所以A和B的SET命令只能先執行1個,此處先執行A,hitcount更新為11。
接著執行B的SET命令,hitcount依然是11,這就是明顯的因為競態而產生的錯誤,hitcount應該為12才是。
Redis的事務其實是通過MULTI命令開啟事務,將後續一系列的命令放在一個隊列裡,不立即執行,直到EXEC命令,隊列中的命令才會依次執行。
命令類似:
MULTI;
set val1 111;
set val2 222;
EXEC;
在實際工作中,我們也會經常遇到這種問題:我們必須先拿到數據,根據數據做出判斷,進行一些處理之後,再更新數據,這時候我們就沒法保證取數據、更新數據在同一個隊列事務中了。
這就需要樂觀鎖。簡單說,我們每取一個數據的時候,Redis不僅返回數值,還會返回這個數值的版本號。
當我們執行更新命令時,Redis會拿你要SET值的版本號與庫裡現在值的版本號進行比對,如果相同,則更新,版本號變更。
如果版本號不同,則說明在我們執行更新命令之前,有其他客戶端修改了這條數據,我們的更新操作失敗。
Redis裡是通過WATCH命令來監控版本號的。
WATCH hitcount;
hc=GET hitcount;
hc=hc+1;
MULTI;
SET hitcount $hc;
EXEC;
因為通過WATCH監控了hitcount這個Key,那麼在事務中SET的時候,一旦發現版本號不對,執行就失敗。
悲觀鎖
樂觀鎖是CAS——Check And Set,先檢查(版本號)再設置更新,那麼悲觀鎖就是先鎖,再查,最後更新。
以MySQL為例,我們要實現悲觀鎖:
SELECT * FROM tabA AS t WHERE t.id=1 FOR UPDATE;
這樣,因為主鍵是明確指定的(id=1),所以這一行記錄就被鎖定了——行鎖定,在本事務被commit之前,這條數據都只會被本事務的SQL語句進行修改,其他事務的加鎖操作,更新操作都會在本事務提交之後再執行,單純的查詢則不受影響。
如果沒有指定明確地主鍵,則鎖定的是表——表鎖定。