一.介紹
分布式鎖,或者稱為“全局鎖”,在分布式環境中,保證鎖只能被一個對象(或者成為“事務”)獲取,經常出現在“避免數據重復處理”、“接口冪等”的場景。
下面介紹了Redis中兩種分布式鎖的實現方式。
二.setnx + expire組合
2.1命令介紹
使用setnx和expire命令組合實現,這兩個命令用法如下:
setnx key value expire key seconds
對於setnx來說,只有key不存在,或者已經過期,setnx才會成功(返回1),否則失敗(返回0)。
expire可以用來對key的有效期進行設置,若不設置key的有效期,則默認為-1,表示一直有效;
2.2操作步驟
實現分布式鎖的時候,方式很簡單:
2.現有A、B兩個線程嘗試獲取同一個全局鎖,假設先接收到A的請求;
3.直接執行setnx命令,key為foo,value可以根據業務制定,比如A的名稱或者某個特殊的值;
4.如果setnx加鎖成功,那么使用expire去設置key的過期時間,防止一直不釋放鎖的情況出現;
5.加鎖成功后,且設置過期時間成功后,執行自己的業務邏輯(獲取到全局鎖的邏輯),等待鎖自動過期釋放或者手動刪除(del命令);
6.如果setnx加鎖失敗,則根據自己業務邏輯進行其他操作(未獲取到全局鎖的邏輯)。
2.3釋放鎖存在的問題
對於鎖的釋放,有兩種方式選擇:鎖自動過期釋放鎖,手動刪除鎖。其中讓鎖過期自動釋放比較簡單,手動刪除鎖存在一些問題。
如果選擇鎖自動過期的方式來釋放鎖,那么需要注意過期時間不要設置太長,不然大量的請求會阻塞,導致系統效率降低;
如果選擇手動刪除,可以通過key對應的value來判斷加鎖和釋放鎖(del)的是否為同一個事務,如果是的話,則進行刪除操作,但是這個不是原子操作,原因如下:
1.查詢鎖是否本次加的鎖,如果是,則進行第2步,否則就是已經過期了,被其他線程獲取了鎖;
2.進行del操作;
在第一步的查,到第二步的刪,是存在時間間隔的,這段時間內,鎖可能會過期並被其他線程獲取到,此時再del,則會讓其他線程獲取到的鎖失效。
可以考慮使用watch命令來對key進行監聽。
2.4設置過期時間存在的問題
如果在setnx之后,還沒有來得及expire設置過期時間,那么鎖就一直不會釋放,后續請求無法再加鎖。
此時可以使用帶有多個參數的set命令:
set key value [EX seconds] [PX milliseconds] [NX|XX]
http://www.redis.cn/commands/set.html
2.5鎖可重入的問題
可以通過value判斷是否為本事務獲取了事務,如果是,則直接進入。
三.Redis + Lua實現分布式鎖
使用Redisson框架,下面簡單說一下原理
3.1加鎖的Lua偽代碼
可以使用Lua編程語言,編寫一段代碼,然后在redis中執行,可以認為是原子操作,下面是偽代碼:
if (redis.call('exists', keys[1]) == 0) then redis.call('hset', keys[1], argv[2], 1); redis.call('pexpire', keys[1], argv[1]); return nil; end if (redis.call('hexists', keys[1], argv[2]) == 1) then redis.call('hincrby', keys[1], argv[2], 1); redis.call('pexpire', keys[1], argv[1]); return nil; end return redis.call('pttl', keys[1]);
上面涉及一個redis.call()方法,他的功能根據傳入的參數執行redis命令,
以及出現了幾個參數,解釋如下:
keys[1]:表示的就是鎖的名稱,比如要對foo這個字符串加鎖,那么keys[1]就是foo;
argv就是argument value的簡寫,表示的是傳入的參數列表,是一個數組;
argv[1]:表示鎖的生存時間,默認在30秒后釋放鎖(失效);
argv[2]:表示加鎖的客戶端ID(可以理解為事務ID)
3.2加鎖的代碼介紹
分別介紹上面三段代碼:
if (redis.call('exists', keys[1]) == 0) then redis.call('hset', keys[1], argv[2], 1); redis.call('pexpire', keys[1], argv[1]); return nil; end
這段代碼,判斷key是否存在,如果不存在,則創建一個hash,key為鎖的名稱,值為客戶端id,后面的1表示加鎖的次數,后面在判斷可重入的時候使用;然后設置key的有效時間;之后就返回鎖獲取成功。
if (redis.call('hexists', keys[1], argv[2]) == 1) then redis.call('hincrby', keys[1], argv[2], 1); redis.call('pexpire', keys[1], argv[1]); return nil; end
這段代碼主要實現“鎖的可重入”,前提是key(鎖)已經存在了,那么就判斷hash中key對應的客戶端是否為當前客戶端id,如果是的話,那么就執行hincrby命令,將加鎖的次數進行加1,然后重新設置key(鎖)的過期時間,之后再返回。
return redis.call('pttl', keys[1]);
執行到上面這段代碼,表示鎖已經存在,且不是當前客戶端獲取到鎖,那么就會執行pttl查看鎖的有效期。
3.3watch dog自動延時
如果客戶端獲取到鎖后,超過設置的過期時間,還希望持有鎖,那么有watch dog機制,也就是該客戶端獲取到鎖后,就會啟動后台線程去判斷客戶端是否仍舊持有鎖,如果是,則會延長過期時間。
3.4釋放鎖
釋放鎖,需要注意有鎖的重入問題,當鎖重入后,有計數器來保存重入的次數(獲取鎖的次數),每次unlock的時候,將計數減1,當計數為0的時候,表示不再持有該鎖,則執行del key[1]命令刪除鎖。