如何確保一個方法,或者一塊代碼在高併發狀況下,同一時間只能被一個線程執行,單體應用可使用併發處理相關的 API 進行控制,但單體應用架構演變爲分佈式微服務架構後,跨進程的實例部署,顯然就沒辦法經過應用層鎖的機制來控制併發了。那麼鎖都有哪些類型,爲何要使用鎖,鎖的使用場景有哪些?今天咱們來聊一聊高併發場景下鎖的使用技巧。java
不一樣的應用場景對鎖的要求各不相同,咱們先來看下鎖都有哪些類別,這些鎖之間有什麼區別。面試
就比如說是你是一個生活態度樂觀積極向上的人,老是往最好的狀況去想,好比你每次去獲取共享數據的時候會認爲別人不會修改,因此不會上鎖,可是在更新的時候你會判斷這期間有沒有人去更新這個數據。redis
樂觀鎖使用在前,判斷在後。咱們看下僞代碼:算法
reduce() { select total_amount from table_1 if(total_amount < amount ){ return failed. } //其餘業務邏輯 update total_amount = total_amount - amount where total_amount > amount; }
悲觀鎖是怎麼理解呢?相對樂觀鎖恰好反過來,老是假設最壞的狀況,假設你每次拿數據的時候會被其餘人修改,因此你在每次共享數據的時候會對他加一把鎖,等你使用完了再釋放鎖,再給別人使用數據。數據庫
悲觀鎖判斷在前,使用在後。咱們也看下僞代碼:設計模式
reduce() { //其餘業務邏輯 int num = update total_amount = total_amount - amount where total_amount > amount; if(num ==1 ){ //業務邏輯. } }
這裏舉一個很是常見的例子,在高併發狀況下餘額扣減,或者相似商品庫存扣減,也能夠是資金帳戶的餘額扣減。扣減操做會發生什麼問題呢?很容易能夠看到,可能會發生的問題是扣減致使的超賣,也就是扣減成了負數。架構
舉個例子,好比個人庫存數據只有100個。併發狀況下第1筆請求賣出100個,第2批賣出100元,致使當前的庫存數量爲負數。遇到這種場景應該如何破解呢?這裏列舉四種方案。併發
這時候很容易想到最簡單的方案:同步排它鎖(synchronize)。可是排他鎖的缺點很明顯:分佈式
第二咱們可能會想到,那用數據庫行鎖來鎖住這條數據,這種方案相比排它鎖解決了跨進程的問題,可是依然有缺點。微服務
前面的方案本質上是把數據庫看成分佈式鎖來使用,因此一樣的道理,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的鎖誤刪除了。代碼耗時過長仍是比較常見的場景,假如你的代碼中有外部通信接口調用,就容易產生這樣的場景。
設置合理的時長
剛纔講到的線程超時阻塞的狀況,那麼若是不設置時長呢,固然也不行,若是線程持有鎖的過程當中忽然服務宕機了,這樣鎖就永遠沒法失效了。一樣的也存在鎖超時時間設置是否合理的問題,若是設置所持有時間過長會影響性能,若是設置時間太短,有可能業務阻塞沒有處理完成,是否能夠合理的設置鎖的時間?
續命鎖
這是一個很不容易解決的問題,不過有一個辦法能解決這個問題,那就是續命鎖,咱們能夠先給鎖設置一個超時時間,而後啓動一個守護線程,讓守護線程在一段時間以後從新去設置這個鎖的超時時間,續命鎖的實現過程就是寫一個守護線程,而後去判斷對象鎖的狀況,快失效的時候,再次進行從新加鎖,可是必定要判斷鎖的對象是同一個,不能亂續。
一樣,主線程業務執行完了,守護線程也須要銷燬,避免資源浪費,使用續命鎖的方案相對比較而言更復雜,因此若是業務比較簡單,能夠根據經驗類比,合理的設置鎖的超時時間就行。
數據庫樂觀鎖加鎖的一個原則就是儘可能想辦法減小鎖的範圍。鎖的範圍越大,性能越差,數據庫的鎖就是把鎖的範圍減少到了最小。咱們看下面的僞代碼
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乾貨,也會分享免費的學習資料課程和麪試寶典 回覆:【計算機】【設計模式】【面試】有驚喜哦