工做中常常會遇到爭搶共享資源的場景,好比用戶搶購秒殺商品,若是不對商品庫存進行保護,可能會形成超賣的狀況。超賣現象在售賣火車票的場景下更加明顯,兩我的購買到同一天同一輛列車,相同座位的狀況是不容許出現的。交易系統中的退款一樣如此,因爲網絡延遲和重複提交極端時間差的狀況下,可能會形成同一個用戶重複的退款請求。以上不管是超賣,仍是重複退款,都是沒有對須要保護的資源或業務進行完善的保護而形成的,從設計方面必定要避免這種狀況的發生。
php
本文以退款交易場景入手,引入分佈式鎖,嘗試分析分佈式鎖須要考慮關注點,包括如下內容:html
鎖是一種控制共享資源爭搶的機制,採用互斥方式防止多線程(或多進程)間形成的衝突。鎖是一種獲取保護資源的憑證,就像公園門票,只有持有門票纔有資格入園;鎖是使得對同一類共享資源的訪問串行化。沒有得到鎖只能排隊等待,直到其餘線程釋放掉鎖。這裏須要對「同一類共享資源」正確理解,好比訂單系統中的同一種商品庫存,退款系統中同一個用戶。git
在多線程中,Java 已經提供了很好原生鎖(包括synchronized,lock),前面的其餘文章中也已經講到了內置鎖和顯示鎖的理解和使用,在此再也不贅述。可是(是否是已經料到了我要說可是了呢?),在分佈式系統中,由於要跨進程或者跨服務器 ,這種場景下JDK原生鎖已經沒法知足咱們的需求,須要一種可以分佈式系統中保護共享資源的方式,分佈式鎖在這種狀況下產生了。github
不少事情每每都是如此,爲了解決一個問題,引入了新方案,而新方案卻會帶來其餘的問題,又須要用更多的時間去解決新方案帶來的問題。沒有一個完美的方案,所以對方案的取捨,就是具體場景中應該重點關注哪些問題,忽略哪些問題的選擇。redis
分佈式鎖是一個在分佈式環境中很重要的原語,它代表不一樣進程間採用互斥的方式操做共享資源。如何才稱得上分佈式鎖呢?分佈式鎖須要知足三個基本的條件:json
外部存儲
顧名思義,分佈式鎖是在分佈式部署環境中給多個主機提供鎖服務。Java具備天生的多線程優點,在同一個進程的線程中能夠經過互斥鎖住共享資源來保證多線程之間干擾,鎖的載體是堆中共享變量,使用JDK原生鎖synchronized和lock能夠很方便的解決,可是將問題擴展到分佈式環境中,就超出了JDK原生鎖做用範疇。須要另外的存儲載體,能夠是共享內存或者磁盤文件。考慮到分佈式鎖的高可用性,避免單點問題,所以共享內存中數據是須要持久化的,這點內容會在下文中的分佈式鎖的高可用中涉及到。服務器
全局惟一標識
與JDK原生鎖相似,分佈式鎖一樣須要標記爲全局惟一。在多線程環境中,鎖可使一個對象引用,也能夠是基本類型變量,都有惟一的標識來區分鎖保護的不一樣資源。仍然以上面的退款爲例,爲了保護用戶的帳戶資金,不容許同一個用戶併發退款。所以同一個用戶退款操做採用互斥鎖保護起來,不一樣用戶之間不須要互斥操做。具體方法一種能夠經過鎖用戶帳戶的方式,另外一種對用戶userId設置不一樣的狀態標識,這兩種方式都是採用對堆中變量的原子操做保證互斥的。
分佈式環境中上述第一種方法就不適用了,舉個例子,小明的帳戶能夠同時在A、B兩個不一樣實例中加鎖。那麼能夠採用第二種方法,自定義一個標識,使其全局惟一便可,每次申請退款時,首先嚐試獲取該標識,若是該標識已經被其餘佔用,則須要等待,直到釋放該標識(是否是與synchronized很類似)。對於交易而言,全局惟一的標識很簡單:業務+userId便可惟一標識。網絡
至少有兩種狀態
鎖至少須要兩種狀態:加鎖(lock)和解鎖(unlock)。用狀態區分當前嘗試獲取的鎖是否已經被其餘操做佔用,被佔用只有等待鎖釋放後才能嘗試獲取鎖並加鎖,保護共享資源。多線程
爲解決共享資源在分佈式環境下併發訪問帶來的問題,引入分佈式鎖採用互斥訪問的方式將併發訪問串行化。下文中以Redis爲例,分析使用分佈式鎖時重點須要考慮的狀況。併發
獲取鎖操做的原子性
從讀取鎖的狀態,到設置鎖狀態爲加鎖(獲取鎖的過程),不是原子性的操做,若是不能保證這兩步做爲一個的原子操做,可能存在競態條件,在極端的時間差的狀況下,會有多個服務同時獲取到同一個鎖,從而獲取操做工做資源的憑證,這是不容許的。幸運的是Redis提供了CAS原子性功能SETNX,是「SET if Not eXists」的縮寫,也就是隻有不存在的時候才設置。
鎖與保護共享資源的數據一致性
獲取鎖與開始操做共享資源必須保證一致性,結束操做共享資源和釋放鎖必須保證一致性。共享資源操做結束後必須釋放鎖,退出臨界區,不然會形成鎖飢餓;開始操做共享資源,必須是在獲取鎖以後,不然鎖就沒法保護共享資源。
分佈式鎖的性能
分佈式鎖須要考慮網絡傳輸時間,超時時間一樣須要考慮網絡時間消耗。
可重入
某個請求試圖得到一個已經由它本身持有的鎖,那麼這個請求就會成功,這是重入。當重入時須要將計數器加一,釋放鎖時,計數器相應減一,通常分佈式鎖一樣支持可重入,所以須要設計標記不一樣的請求。
公平鎖和非公平鎖
公平鎖設定按照請求的順序獲取鎖,不容許插隊。公平是個好東西,不過大多數狀況下非公平鎖的性能要高於公平鎖。
正常狀況下,加鎖,執行保護資源,釋放鎖。若是沒有異常,那這世界就太美好了。那麼生產環境中,使用分佈式鎖時應該注意哪些容錯的問題。
鎖沒法釋放
以退款爲例,退款服務宕機,分佈式鎖服務正常。此時鎖保護的資源(或部分)已沒法對外提供服務,沒法通知鎖自身運行狀況,爲避免鎖服務一直沒法釋放,能夠爲鎖設置超時時間,當鎖執行時間超過了超時時間,鎖會過時,從而保證鎖與保護服務的最終一致性。
固然鎖設置超時時間又會引出另外一個問題:好比鎖的超時時間是500ms,而部分退款服務可能因爲網絡等緣由執行時間爲800ms(退款服務沒有宕機,僅僅是執行時間相比平均執行時間較長而已),這種狀況下,鎖已通過期,而退款服務仍在執行,鎖做爲保護資源的功能失效了。有一個辦法能夠兼顧超時時間和鎖失效的問題,退款服務保持心跳通知鎖服務,鎖服務收到心跳後延長鎖的超時時間,不足在於即便退款服務已經宕機,鎖服務仍然須要到達超時時間後纔會解鎖。Redisson分佈式鎖就是採用這種方式。
分佈式鎖時效設置的必要性:確保在將來的必定時間內,不管得到鎖的節點發生了什麼問題,最終鎖都能被釋放掉。
性能
針對訪問量大的共享資源,嘗試自旋方式獲取鎖時的長時間等待,即容易形成CPU空轉性能的消耗,又容易形成節點阻塞;而每隔一段時間嘗試獲取鎖,便沒法保證資源的高效利用。基於以上兩種解決方案的弊端,能夠採用嘗試獲取鎖必定次數後,加入到等待隊列中,當鎖釋放後,通知等待隊列中的下一個等待節點獲取鎖。既能夠避免CPU空轉帶來的性能消耗,又能夠及時響應,保證系統的性能和穩定性,避免毛刺的出現。設計上能夠參考Java併發包中AQS。
鎖飢餓
一個線程在嘗試獲取鎖的過程當中一直沒法獲取鎖,這種狀況就是鎖飢餓,好比體弱的狼很難在一羣強壯的狼羣中搶到食物,不少狀況下鎖飢餓是因爲優先級較低形成的。發生鎖飢餓時,沒法獲取鎖便沒法進行鎖保護資源的操做。爲避免鎖飢餓狀況的發生,設計時須要將鎖設計成公平鎖。
監控
監聽鎖的運行狀況,掌握鎖持有者的動態,若判斷鎖持有者處於不活動狀態,要可以強制釋放其持有的鎖,引入第三方監控系統。
固然,分佈式鎖還有一些其餘的問題:好比頻繁獲取鎖釋放鎖帶來的系統穩定和性能問題,如何保證鎖的高可用,分佈式鎖的持久化,分佈式鎖單點問題,分佈式鎖網絡傳輸性能等,還有分佈式鎖主節點宕機,從節點還沒同步到鎖,鎖的惟一性被破壞,多個客戶端能夠得到同一個鎖…
寫做不易,痛並快樂着;理解可能存在誤差,句句斟酌推敲;抵制抄襲,踐行原創技術之路。若是本文能對您有所幫助,實爲榮幸,我是葛一凡。
原文連接:http://geyifan.cn/2017/02/11/what-problems-when-using-distributed-locks/