高并發場景下鎖的使用技巧
如何確保一個方法,或者一塊代碼在高并發情況下,同一時間只能被一個執行緒執行,單體應用可以使用并發處理相關的 API 進行控制,但單體應用架構演變為分布式微服務架構后,跨行程的實體部署,顯然就沒辦法通過應用層鎖的機制來控制并發了,那么鎖都有哪些型別,為什么要使用鎖,鎖的使用場景有哪些?今天我們來聊一聊高并發場景下鎖的使用技巧,
鎖類別
不同的應用場景對鎖的要求各不相同,我們先來看下鎖都有哪些類別,這些鎖之間有什么區別,
- 悲觀鎖(synchronize)
- Java 中的重量級鎖 synchronize
- 資料庫行鎖
- 樂觀鎖
- Java 中的輕量級鎖 volatile 和 CAS
- 資料庫版本號
- 分布式鎖(Redis鎖)
樂觀鎖
就好比說是你是一個生活態度樂觀積極向上的人,總是往最好的情況去想,比如你每次去獲取共享資料的時候會認為別人不會修改,所以不會上鎖,但是在更新的時候你會判斷這期間有沒有人去更新這個資料,
樂觀鎖使用在前,判斷在后,我們看下偽代碼:
reduce()
{
select total_amount from table_1
if(total_amount < amount ){
return failed.
}
//其他業務邏輯
update total_amount = total_amount - amount where total_amount > amount; }
- 資料庫的版本號屬于樂觀鎖;
- 通過CAS演算法實作的類屬于樂觀鎖,
悲觀鎖
悲觀鎖是怎么理解呢?相對樂觀鎖剛好反過來,總是假設最壞的情況,假設你每次拿資料的時候會被其他人修改,所以你在每次共享資料的時候會對他加一把鎖,等你使用完了再釋放鎖,再給別人使用資料,
悲觀鎖判斷在前,使用在后,我們也看下偽代碼:
reduce()
{
//其他業務邏輯
int num = update total_amount = total_amount - amount where total_amount > amount;
if(num ==1 ){
//業務邏輯.
}
}
- Java中的的synchronize是重量級鎖 ,屬于悲觀鎖;
- 資料庫行鎖屬于悲觀鎖;
扣減操作案例
這里舉一個非常常見的例子,在高并發情況下余額扣減,或者類似商品庫存扣減,也可以是資金賬戶的余額扣減,扣減操作會發生什么問題呢?很容易可以看到,可能會發生的問題是扣減導致的超賣,也就是扣減成了負數,
舉個例子,比如我的庫存資料只有100個,并發情況下第1筆請求賣出100個,第2批賣出100元,導致當前的庫存數量為負數,遇到這種場景應該如何破解呢?這里列舉四種方案,
方案1:同步排它鎖
這時候很容易想到最簡單的方案:同步排它鎖(synchronize),但是排他鎖的缺點很明顯:
- 其中一個缺點是,執行緒串行導致的性能問題,性能消耗比較大,
- 另一個缺點是無法解決分布式部署情況下跨行程問題;
方案2:資料庫行鎖
第二我們可能會想到,那用資料庫行鎖來鎖住這條資料,這種方案相比排它鎖解決了跨行程的問題,但是依然有缺點,
- 其中一個缺點就是性能問題,在資料庫層面會一直阻塞,直到事務提交,這里也是串行執行;
- 第二個需要注意設定事務的隔離級別是Read Committed,否則并發情況下,另外的事務無法看到提交的資料,依然會導致超賣問題;
- 缺點三是容易打滿資料庫連接,如果事務中有第三方介面互動(存在超時的可能性),會導致這個事務的連接一直阻塞,打滿資料庫連接,
- 最后一個缺點,容易產生交叉死鎖,如果多個業務的加鎖控制不好,就會發生AB兩條記錄的交叉死鎖,
方案3:redis分布式鎖
前面的方案本質上是把資料庫當作分布式鎖來使用,所以同樣的道理,redis,zookeeper都相當于資料庫的一種鎖,其實當遇到加鎖問題,代碼本身無論是synchronize或者各種lock使用起來都比較復雜,所以思路是把代碼處理一致性的問難題交給一個能夠幫助你處理一致性的問題的專業組件,比如資料庫,比如redis,比如zookeeper等,
這里我們分析下分布式鎖的優缺點:
- 優點:
- 可以避免大量對資料庫排他鎖的征用,提高系統的回應能力;
- 缺點:
- 設定鎖和設定超時時間的原子性;
- 不設定超時時間的缺點;
- 服務宕機或執行緒阻塞超時的情況;
- 超時時間設定不合理的情況;
加鎖和過期設定的原子性
redis加鎖的命令setnx,設定鎖的過期時間是expire,解鎖的命令是del,但是2.6.12之前的版本中,加鎖和設定鎖過期命令是兩個操作,不具備原子性,如果setnx設定完key-value之后,還沒有來得及使用expire來設定過期時間,當前執行緒掛掉了或者執行緒阻塞,會導致當前執行緒設定的key一直有效,后續的執行緒無法正常使用setnx獲取鎖,導致死鎖,
針對這個問題,redis2.6.12以上的版本增加了可選的引數,可以在加鎖的同時設定key的過期時間,保證了加鎖和過期操作原子性的,
但是,即使解決了原子性的問題,業務上同樣會遇到一些極端的問題,比如分布式環境下,A獲取到了鎖之后,因為執行緒A的業務代碼耗時過長,導致鎖的超時時間,鎖自動失效,后續執行緒B就意外的持有了鎖,之后執行緒A再次恢復執行,直接用del命令釋放鎖,這樣就錯誤的將執行緒B同樣Key的鎖誤洗掉了,代碼耗時過長還是比較常見的場景,假如你的代碼中有外部通訊介面呼叫,就容易產生這樣的場景,
設定合理的時長
剛才講到的執行緒超時阻塞的情況,那么如果不設定時長呢,當然也不行,如果執行緒持有鎖的程序中突然服務宕機了,這樣鎖就永遠無法失效了,同樣的也存在鎖超時時間設定是否合理的問題,如果設定所持有時間過長會影響性能,如果設定時間過短,有可能業務阻塞沒有處理完成,是否可以合理的設定鎖的時間?
續命鎖
這是一個很不容易解決的問題,不過有一個辦法能解決這個問題,那就是續命鎖,我們可以先給鎖設定一個超時時間,然后啟動一個守護執行緒,讓守護執行緒在一段時間之后重新去設定這個鎖的超時時間,續命鎖的實作程序就是寫一個守護執行緒,然后去判斷物件鎖的情況,快失效的時候,再次進行重新加鎖,但是一定要判斷鎖的物件是同一個,不能亂續,
同樣,主執行緒業務執行完了,守護執行緒也需要銷毀,避免資源浪費,使用續命鎖的方案相對比較而言更復雜,所以如果業務比較簡單,可以根據經驗類比,合理的設定鎖的超時時間就行,
方案4:資料庫樂觀鎖
資料庫樂觀鎖加鎖的一個原則就是盡量想辦法減少鎖的范圍,鎖的范圍越大,性能越差,資料庫的鎖就是把鎖的范圍減小到了最小,我們看下面的偽代碼
reduce()
{
select total_amount from table_1
if(total_amount < amount ){
return failed.
}
//其他業務邏輯
update total_amount = total_amount - amount;
}
我們可以看到修改前的代碼是沒有where條件的,修改后,再加where條件判斷:總庫存大于將被扣減的庫存,
update total_amount = total_amount - amount where total_amount > amount
如果更新條數回傳0,說明在執行程序中被其他執行緒搶先執行扣減,并且避免了扣減為負數,
但是這種方案還會涉及一個問題,如果在之前的update代碼中,以及其他的業務邏輯中還有一些其他的資料庫寫操作的話,那這部分資料如何回滾呢?
我的建議是這樣的,你可以選擇下面這兩種寫法:
- 利用事務回滾寫法:
我們先給業務方法增加事務,方法在扣減庫存影響條數為零的時候扔出一個例外,這樣對他之前的業務代碼也會回滾,
reduce()
{
select total_amount from table_1
if(total_amount < amount ){
return failed.
}
//其他業務邏輯
int num = update total_amount = total_amount - amount where total_amount > amount; if(num==0) throw Exception;}
- 第二種寫法
reduce()
{
//其他業務邏輯
int num = update total_amount = total_amount - amount where total_amount > amount; if(num ==1 ){
//業務邏輯.
} else{ throw Exception; }
}
首先執行update業務邏輯,如果執行成功了再去執行邏輯操作,這種方案是我相對比較建議的方案,在并發情況下對共享資源扣減操作可以使用這種方法,但是這里需要引出一個問題,比如說萬一其他業務邏輯中的業務,因為特殊原因失敗了該怎么辦呢?比如說在扣減程序中服務OOM了怎么辦?
我只能說這些非常極端的情況,比如突然宕機中間資料都丟了,這種極少數的情況下只能人工介入,如果所有的極端情況都考慮到,也不現實,我們討論的重點是并發情況下,共享資源的操作如何加鎖的問題,
總結
最后我來給你總結一下,如果你可以非常熟練的解決這類問題,第一時間肯定想到的是:資料庫版本號解決方案或者分布式鎖的解決方案;但是如果你是一個初學者,相信你一定會第一時間考慮到Java中提供的同步鎖或者資料庫行鎖,
今天討論的目的就是希望把這幾種場景中的鎖放到一個具體的場景中,逐步去對比和分析,讓你能夠更加全面體系的了解使用鎖這個問題的來龍去脈
歡迎關注公眾號 【碼農開花】一起學習成長
我會一直分享Java干貨,也會分享免費的學習資料課程和面試寶典
回復:【計算機】【設計模式】【面試】有驚喜哦
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/236894.html
標籤:Java
上一篇:Java基礎之:例外及例外處理
