㈠ 大家所推崇的 Redis 分布式鎖,真的可以萬無一失嗎
使用Redis實現分布式鎖最簡單的方案是使用命令SETNX。SETNX(SET if Not eXist)的使用方式為:SETNX key value,只在鍵key不存在的情況下,將鍵key的值設置為value,若鍵key存在,則SETNX不做任何動作。SETNX在設置成功時返回,設置失敗時返回0。當要獲取鎖時,直接使用SETNX獲取鎖,當要釋放鎖時,使用DEL命令刪除掉對應的鍵key即可。
上面這種方案有一個致命問題,就是某個線程在獲取鎖之後由於某些異常因素(比如宕機)而不能正常的執行解鎖操作,那麼這個鎖就永遠釋放不掉了。為此,我們可以為這個鎖加上一個超時時間。第一時間我們會聯想到Redis的EXPIRE命令(EXPIRE key seconds)。但是這里我們不能使用EXPIRE來實現分布式鎖,因為它與SETNX一起是兩個操作,在這兩個操作之間可能會發生異常,從而還是達不到預期的結果
這里我們一眼就可以看出問題來:GET和DEL是兩個分開的操作,在GET執行之後且在DEL執行之前的間隙是可能會發生異常的。如果我們只要保證解鎖的代碼是原子性的就能解決問題了。這里我們引入了一種新的方式,就是Lua腳本,解鎖的時候還是使用DEL命令來解鎖。
修改之後的方案看上去很完美,但實際上還是會有問題。試想一下,某線程A獲取了鎖並且設置了過期時間為10s,然後在執行業務邏輯的時候耗費了15s,此時線程A獲取的鎖早已被Redis的過期機制自動釋放了。在線程A獲取鎖並經過10s之後,改鎖可能已經被其它線程獲取到了。當線程A執行完業務邏輯准備解鎖(DEL key)的時候,有可能刪除掉的是其它線程已經獲取到的鎖,總的來說Redis 分布式鎖不是那麼萬無一失的。
㈡ Redis的Setnx命令實現分布式鎖
首先,分布式鎖和我們平常講到的鎖原理基本一樣,目的就是確保在多個線程並發時,只有一個線程在同一刻操作這個業務或者說方法、變數。
在一個進程中,也就是一個jvm或者說應用中,我們很容易去處理控制,在 java.util 並發包中已經為我們提供了這些方法去加鎖,比如 synchronized 關鍵字或者 Lock 鎖,都可以處理。
但是如果在分布式環境下,要保證多個線程同時只有1個能訪問某個資源,就需要用到分布式鎖。這里我們將介紹用Redis的 setnx 命令來實現分布式鎖。
其實目前通常所說的 setnx 命令,並非單指redis的 setnx key value 這條命令,這條命令可能會在後期redis版本中刪除。
一般代指redis中對 set 命令加上 nx 參數進行使用, set 這個命令,目前已經支持這么多參數可選:
從 Redis 2.6.12 版本開始, SET 命令的行為可以通過一系列參數來修改:
注入bean
這里同時啟動5個線程並發往redis中存儲 lock 這個key(key可以自定義,但需要一致),同時設置10秒的過期時間。
setIfAbsent 這個函數實現的功能與 setnx 命令一樣,代表如果沒有這個key則set成功獲取到鎖,否則set失敗沒有獲取到鎖。
獲得鎖後進行資源的操作,最後釋放鎖。
執行效果 :
可以看到同時只有1個線程能夠獲取到鎖。
使用 setnx 命令方式雖然操作比較簡單方便,但是會有如下問題:
可以在叢肢再次獲取鎖時,如果鎖被佔用就get值,判斷值是否是當前線程存的隨機值,如果是則再次執行 set 命令重新上鎖;當然為了保證原子性這些操作都要用 lua 腳本來執行。
可以使用 while 循環重復執行 setnx 命令,並設置一個超時時間退出循環。
可以盡量把鎖自動過期的時間設的冗餘一些。但也不能絕橡徹底解決。
可以在刪除鎖的時候先get值,判斷值是否是當前線程存的隨機值,只有相同才執行滲宏世刪鎖的操作;當然也要使用 lua 腳本執行來保證原子性。
分布式鎖需要滿足的特性
綜上:使用 setnx 命令來實現分布式鎖並不是一個很嚴謹的方案,如果是Java技術棧,我們可以使用 Redisson 庫來解決以上問題,接下來的文章會介紹如何使用。
Redisson實現分布式鎖
Redlock實現分布式鎖