使用Redis實現分布式鎖


一.介紹

  分布式鎖,或者稱為“全局鎖”,在分布式環境中,保證鎖只能被一個對象(或者成為“事務”)獲取,經常出現在“避免數據重復處理”、“接口冪等”的場景。

  下面介紹了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]命令刪除鎖。

  

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM