分佈式鎖的一些理解
在多線程並發的情況下,單個節點內的線程安全可以通過synchronized關鍵字和Lock接口來保證。
synchronized和lock的區別
Lock是一個接口,是基於在語言層面實現的鎖,而synchronized是Java中的關鍵字,是基於JVM實現的內置鎖,Java中的每一個對象都可以使用synchronized添加鎖。
synchronized在發生異常時,會自動釋放線程佔有的鎖,因此不會導致死鎖現象發生;而Lock在發生異常時,如果沒有主動通過unLock()去釋放鎖,則很可能造成死鎖現象,因此使用Lock時需要在finally塊中釋放鎖;
Lock可以提高多個線程進行讀操作的效率。(可以通過readwritelock實現讀寫分離,一個用來獲取讀鎖,一個用來獲取寫鎖。)
當開發的應用程序處於一個分佈式的集群環境中,涉及到多節點,多進程共同完成時,如何保證線程的執行順序是正確的。比如在高並發的情況下,很多企業都會使用Nginx反向代理服務器實現負載均衡的目的,這個時候很多請求會被分配到不同的Server上,一旦這些請求涉及到對統一資源進行修改操作時,就會出現問題,這個時候在分佈式系統中就需要一個全局鎖實現多個線程(不同進程中的線程)之間的同步。
常見的處理辦法有三種:數據庫、緩存、分佈式協調系統。數據庫和緩存是比較常用的,但是分佈式協調系統是不常用的。
常用的分佈式鎖的實現包含:
Redis分佈式鎖、Zookeeper分佈式鎖、Memcached
Redis提供的三種方法:
redis> SETNX job "programmer" # job 設置成功 (integer) 1 redis> SETNX job "code-farmer" # 嘗試覆蓋 job ,失敗
(3)鎖超時 EXPIRE: 為給定 key
設置生存時間,當 key
過期時(生存時間為 0
),它會被自動刪除。
每次當一個節點想要去操作臨界資源的時候,我們可以通過redis來的鍵值對來標記一把鎖,每一進程首先通過Redis訪問同一個key,對於每一個進程來說,如果該key不存在,則該線程可以獲取鎖,將該鍵值對寫入redis,如果存在,則說明鎖已經被其他進程所佔用。具體邏輯的偽代碼如下:
try{ if(SETNX(key, 1) == 1){ //do something ...... }finally{ DEL(key); }
但是此時,又會出現問題,因為SETNX和DEL操作並不是原子操作,如果程序在執行完SETNX後,而並沒有執行EXPIRE就已經宕機了,這樣一來,原先的問題依然存在,整個系統都將被阻塞。
幸虧
try{ if(SET(key, 1, 30, timeout, NX) == 1){ //do something ...... } }finally{ DEL(key); }
解決了原子操作,仍然還有一點需要注意,例如,A節點的進程獲取到鎖的時候,A進程可能執行的很慢,在do something未完成的情況下,30秒的時間片已經使用完,此時會將該key給深處掉,此時B進程發現這個key不存在,則去訪問,並成功的獲取到鎖,開始執行do something,此時A線程恰好執行到DEL(key),會將B的key刪除掉,此時相當於B線程在訪問沒有加鎖的臨界資源,而其餘進程都有機會同時去操作這個臨界資源,會造成一些錯誤的結果。對於該問題的解決辦法是進程在刪除key之前可以做一個判斷,驗證當前的鎖是不是本進程加的鎖。
String threadId = Thread.currentThread().getId() try{ if(SET(key, threadId, 30, timeout, NX) == 1){ //do something ...... } }finally{ if(threadId.equals(redisClient.get(key))){ DEL(key); } }
String threadId = Thread.currentThread().getId() try{ if(SET(key, threadId, 30, timeout, NX) == 1){ new Thread(){ @Override public void run() { //start Daemon } } //do something ...... } }finally{ if(threadId.equals(redisClient.get(key))){ DEL(key); } }
基於以上的分析,基本上可以通過Redis實現一個分佈式鎖,如果我們想提升該分佈式的性能,我們可以對連接資源進行分段處理,將請求均勻的分佈到這些臨界資源段中,比如一個買票系統,我們可以將100張票分為10 部分,每部分包含10張票放在其他的服務節點上,這些請求可以通過Nginx被均勻的分散到這些處理節點上,可以加快對臨界資源的處理。
-
B站視頻上一部分講解