當我們開始開發項目部署運行時,項目規模不大,只是在一個JVM實例中運行,對同一資源的併發訪問用JDK自帶的鎖機制就可以解決資源同時訪問的問題。而隨著項目的不斷發展,單體應用已經無法滿足日益增長的訪問需求,我們開始考慮多台部署,提高接收客戶端的連接請求,提高項目的吞吐量。一臺變多台,其中不可避免的問題 ...
當我們開始開發項目部署運行時,項目規模不大,只是在一個JVM實例中運行,對同一資源的併發訪問用JDK自帶的鎖機制就可以解決資源同時訪問的問題。而隨著項目的不斷發展,單體應用已經無法滿足日益增長的訪問需求,我們開始考慮多台部署,提高接收客戶端的連接請求,提高項目的吞吐量。一臺變多台,其中不可避免的問題就是如何控制解決不同線程對同一資源的併發訪問。其中一種手段就是使用redis進行分散式鎖的控制。
我們可以在獲取訪問資源鎖之前判斷redis中是否存在對應代表該資源鎖key的value,如果存在,則說明已經被獲取,反之還沒有客戶端獲取該資源對應的鎖,可以進行獲取鎖。
1 boolean lock = false; 2 try { 3 lcok = getLock(taskId); //獲取鎖 4 if (lock) { 5 doSomething(); //業務邏輯 6 } 7 } finally { 8 if (lock) { 9 releaseLock(taskId); //釋放鎖 10 } 11 }
1 public static boolean getLock(String taskId) { 2 if (existsKey(taskId)) { 3 return false; 4 } else { 5 setKey(taskId); 6 return true; 7 } 8 }
上面的部分實現代碼給了一個大概的解決思路,看起來沒有問題的,但是仔細看看還是存在問題滴,存在什麼問題呢?
當正在執行doSomething()方法時,突然系統宕機掛掉了,無法執行釋放鎖的操作,redis中對應的資源key的鎖一直存在,之後運行代碼就會出現問題。另一個問題就是執行getLock(taskId)方法時,該方法不是原子性的,有可能同時兩個線程都判斷為不存在該資源鎖,都執行了setKey方法,導致同時獲得鎖資源的情況。
如何解決上面的兩個問題呢?從Redis官方API中有SET my_key my_value NX PX milliseconds的方法,得到瞭解決方案。它提供了一個只有在某個key不存在的情況下才會設置key的值的原子命令,該命令也能設置key值過期時間。其中,NX表示只有當鍵key不存在的時候才會設置key的值,PX表示設置鍵key的過期時間,單位是毫秒。
到現在是否完全解決了併發獲取鎖的問題了呢?系統可能存在這種情況,當客戶端A獲取鎖之後,執行業務代碼的時間超過了之前設置的過期時間,導致鎖的自動釋放,而客戶端B剛好獲得新的資源鎖,但客戶端A恰好執行完業務操作,釋放鎖的時候,該鎖是客戶端B重新獲得的鎖,導致出現問題。這時,我們想到可以在設置key值時給定一個隨機數,在釋放資源鎖的同時,判斷是否和之前設置的value值相同,相同則釋放,反之不釋放。
1 if(getKey(taskId)==random_value){ 2 deleteKey(taskId); 3 }
很可惜,上面的整個if操作也不是原子性的,getKey方法和deleteKey方法之間由於某種原因而延遲1秒鐘操作了,而這1秒內剛好設置的的超時時間而鎖釋放,被新的客戶端獲得鎖,1秒之後執行deleteKey方法又會誤刪除新客戶端的鎖,問題依舊存在。接下來我們只要想辦法解決上面判斷的原子性就能解決誤刪除鎖的問題。Redis可以使用Lua腳本保證操作的原子性。
1 if redis.call("get",KEYS[1]) == ARGV[1] then 2 return redis.call("del",KEYS[1]) 3 else 4 return 0 5 end
其中ARGV[1]表示設置key時指定的隨機值。由於Lua腳本的原子性,在Redis執行該腳本的過程中,其他客戶端的命令都需要等待該Lua腳本執行完才能執行,所以不會出現上面所說的誤刪除鎖問題。至此,使用Redis實現分散式鎖的方案就相對完善了。上述分散式鎖的實現方案中,都是針對單節點Redis而言的。