redis分布式鎖實現(xiàn)方法介紹

一、使用分布式鎖要滿足的幾個條件:

1、系統(tǒng)是一個分布式系統(tǒng)(關鍵是分布式例隆,單機的可以使用ReentrantLock或者synchronized代碼塊來實現(xiàn))

2论巍、共享資源(各個系統(tǒng)訪問同一個資源,資源的載體可能是傳統(tǒng)關系型數(shù)據(jù)庫或者NoSQL)

3地沮、同步訪問(即有很多個進程同事訪問同一個共享資源嗜浮。沒有同步訪問,誰管你資源競爭不競爭)

二摩疑、應用的場景例子

管理后臺的部署架構(多臺tomcat服務器+redis【多臺tomcat服務器訪問一臺redis】+mysql【多臺tomcat服務器訪問一臺服務器上的mysql】)就滿足使用分布式鎖的條件危融。多臺服務器要訪問redis全局緩存的資源,如果不使用分布式鎖就會出現(xiàn)問題雷袋。 看如下偽代碼:

long N=0L;

//N從redis獲取值

if(N<5){

N++吉殃;

//N寫回redis

}

上面的代碼主要實現(xiàn)的功能:

從redis獲取值N,對數(shù)值N進行邊界檢查楷怒,自加1蛋勺,然后N寫回redis中。 這種應用場景很常見率寡,像秒殺迫卢,全局遞增ID、IP訪問限制等冶共。

以IP訪問限制來說乾蛤,惡意攻擊者可能發(fā)起無限次訪問每界,并發(fā)量比較大,分布式環(huán)境下對N的邊界檢查就不可靠家卖,因為從redis讀的N可能已經是臟數(shù)據(jù)眨层。

傳統(tǒng)的加鎖的做法(如java的synchronized和Lock)也沒用,因為這是分布式環(huán)境上荡,這個同步問題的救火隊員也束手無策趴樱。在這危急存亡之秋,分布式鎖終于有用武之地了酪捡。

分布式鎖可以基于很多種方式實現(xiàn)叁征,比如zookeeper、redis...逛薇。不管哪種方式捺疼,他的基本原理是不變的:用一個狀態(tài)值表示鎖,對鎖的占用和釋放通過狀態(tài)值來標識永罚。

這里主要講如何用redis實現(xiàn)分布式鎖啤呼。

三、使用redis的setNX命令實現(xiàn)分布式鎖  

1呢袱、實現(xiàn)的原理

Redis為單進程單線程模式官扣,采用隊列模式將并發(fā)訪問變成串行訪問,且多客戶端對Redis的連接并不存在競爭關系羞福。redis的SETNX命令可以方便的實現(xiàn)分布式鎖惕蹄。

2、基本命令解析

1)setNX(SET if Not eXists)

語法:

SETNX key value

將 key 的值設為 value 坯临,當且僅當 key 不存在焊唬。

若給定的 key 已經存在,則 SETNX 不做任何動作看靠。

SETNX 是『SET if Not eXists』(如果不存在赶促,則 SET)的簡寫

返回值:

設置成功,返回 1 挟炬。

設置失敗鸥滨,返回 0 。

例子:

redis> EXISTS job# job 不存在

(integer) 0


redis> SETNX job "programmer"# job 設置成功

(integer) 1


redis> SETNX job "code-farmer"# 嘗試覆蓋 job 谤祖,失敗

(integer) 0


redis> GET job# 沒有被覆蓋

"programmer"

所以我們使用執(zhí)行下面的命令

SETNX lock.foo <current Unix time + lock timeout + 1>

如返回1婿滓,則該客戶端獲得鎖,把lock.foo的鍵值設置為時間值表示該鍵已被鎖定粥喜,該客戶端最后可以通過DEL lock.foo來釋放該鎖凸主。

如返回0,表明該鎖已被其他客戶端取得额湘,這時我們可以先返回或進行重試等對方完成或等待鎖超時卿吐。

2)getSET

語法:

GETSET key value

將給定 key 的值設為 value 旁舰,并返回 key 的舊值(old value)。

當 key 存在但不是字符串類型時嗡官,返回一個錯誤箭窜。

返回值:

返回給定 key 的舊值。

當 key 沒有舊值時衍腥,也即是磺樱, key 不存在時,返回 nil 婆咸。

3)get

語法:

GET key

返回值:

當 key 不存在時竹捉,返回 nil ,否則擅耽,返回 key 的值活孩。

如果 key 不是字符串類型物遇,那么返回一個錯誤乖仇。

四、解決死鎖

上面的鎖定邏輯有一個問題:如果一個持有鎖的客戶端失敗或崩潰了不能釋放鎖询兴,該怎么解決乃沙?

我們可以通過鎖的鍵對應的時間戳來判斷這種情況是否發(fā)生了,如果當前的時間已經大于lock.foo的值诗舰,說明該鎖已失效警儒,可以被重新使用。

發(fā)生這種情況時眶根,可不能簡單的通過DEL來刪除鎖蜀铲,然后再SETNX一次(講道理,刪除鎖的操作應該是鎖擁有這執(zhí)行的属百,這里只需要等它超時即可)记劝,當多個客戶端檢測到鎖超時后都會嘗試去釋放它,這里就可能出現(xiàn)一個競態(tài)條件,讓我們模擬一下這個場景:

C0操作超時了族扰,但它還持有著鎖厌丑,C1和C2讀取lock.foo檢查時間戳,先后發(fā)現(xiàn)超時了渔呵。

C1 發(fā)送DEL lock.foo

C1 發(fā)送SETNX lock.foo 并且成功了怒竿。

C2 發(fā)送DEL lock.foo

C2 發(fā)送SETNX lock.foo 并且成功了。

這樣一來扩氢,C1耕驰,C2都拿到了鎖!問題大了录豺!

幸好這種問題是可以避免的朦肘,讓我們來看看C3這個客戶端是怎樣做的:

C3發(fā)送SETNX lock.foo 想要獲得鎖托嚣,由于C0還持有鎖,所以Redis返回給C3一個0

C3發(fā)送GET lock.foo 以檢查鎖是否超時了厚骗,如果沒超時示启,則等待或重試。

反之领舰,如果已超時夫嗓,C3通過下面的操作來嘗試獲得鎖:

GETSET lock.foo <current Unix time + lock timeout + 1>

通過GETSET,C3拿到的時間戳如果仍然是超時的冲秽,那就說明舍咖,C3如愿以償拿到鎖了。

如果在C3之前锉桑,有個叫C4的客戶端比C3快一步執(zhí)行了上面的操作排霉,那么C3拿到的時間戳是個未超時的值,這時民轴,C3沒有如期獲得鎖攻柠,需要再次等待或重試。留意一下后裸,盡管C3沒拿到鎖瑰钮,但它改寫了C4設置的鎖的超時值,不過這一點非常微小的誤差帶來的影響可以忽略不計微驶。

注意:為了讓分布式鎖的算法更穩(wěn)鍵些浪谴,持有鎖的客戶端在解鎖之前應該再檢查一次自己的鎖是否已經超時,再去做DEL操作因苹,因為可能客戶端因為某個耗時的操作而掛起苟耻,操作完的時候鎖因為超時已經被別人獲得,這時就不必解鎖了扶檐。

五凶杖、代碼實現(xiàn)

expireMsecs 鎖持有超時,防止線程在入鎖以后蘸秘,無限的執(zhí)行下去官卡,讓鎖無法釋放

timeoutMsecs 鎖等待超時,防止線程饑餓醋虏,永遠沒有入鎖執(zhí)行代碼的機會

注意:項目里面需要先搭建好redis的相關配置

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.dao.DataAccessException;

import org.springframework.data.redis.connection.RedisConnection;

import org.springframework.data.redis.core.RedisCallback;

import org.springframework.data.redis.core.RedisTemplate;

import org.springframework.data.redis.serializer.StringRedisSerializer;


/**

?* Redis distributed lock implementation.

?*

?* @author zhengcanrui

?*/

public class RedisLock {


????private static Logger logger = LoggerFactory.getLogger(RedisLock.class);


????private RedisTemplate redisTemplate;


????private static final int DEFAULT_ACQUIRY_RESOLUTION_MILLIS = 100;


????/**

?????* Lock key path.

?????*/

????private String lockKey;


????/**

?????* 鎖超時時間寻咒,防止線程在入鎖以后,無限的執(zhí)行等待

?????*/

????private int expireMsecs = 60 * 1000;


????/**

?????* 鎖等待時間颈嚼,防止線程饑餓

?????*/

????private int timeoutMsecs = 10 * 1000;


????private volatile boolean locked = false;


????/**

?????* Detailed constructor with default acquire timeout 10000 msecs and lock expiration of 60000 msecs.

?????*

?????* @param lockKey lock key (ex. account:1, ...)

?????*/

????public RedisLock(RedisTemplate redisTemplate, String lockKey) {

????????this.redisTemplate = redisTemplate;

????????this.lockKey = lockKey + "_lock";

????}


????/**

?????* Detailed constructor with default lock expiration of 60000 msecs.

?????*

?????*/

????public RedisLock(RedisTemplate redisTemplate, String lockKey, int timeoutMsecs) {

????????this(redisTemplate, lockKey);

????????this.timeoutMsecs = timeoutMsecs;

????}


????/**

?????* Detailed constructor.

?????*

?????*/

????public RedisLock(RedisTemplate redisTemplate, String lockKey, int timeoutMsecs, int expireMsecs) {

????????this(redisTemplate, lockKey, timeoutMsecs);

????????this.expireMsecs = expireMsecs;

????}


????/**

?????* @return lock key

?????*/

????public String getLockKey() {

????????returnlockKey;

????}


????private String get(final String key) {

????????Object obj = null;

????????try{

????????????obj = redisTemplate.execute(newRedisCallback<Object>() {

????????????????@Override

????????????????public Object doInRedis(RedisConnection connection) throws DataAccessException {

????????????????????StringRedisSerializer serializer = newStringRedisSerializer();

????????????????????byte[] data = connection.get(serializer.serialize(key));

????????????????????connection.close();

????????????????????if(data == null) {

????????????????????????returnnull;

????????????????????}

????????????????????returnserializer.deserialize(data);

????????????????}

????????????});

????????} catch(Exception e) {

????????????logger.error("get redis error, key : {}", key);

????????}

????????returnobj != null? obj.toString() : null;

????}


????private boolean setNX(final String key, final String value) {

????????Object obj = null;

????????try{

????????????obj = redisTemplate.execute(newRedisCallback<Object>() {

????????????????@Override

????????????????public Object doInRedis(RedisConnection connection) throws DataAccessException {

????????????????????StringRedisSerializer serializer = newStringRedisSerializer();

????????????????????Boolean success = connection.setNX(serializer.serialize(key), serializer.serialize(value));

????????????????????connection.close();

????????????????????returnsuccess;

????????????????}

????????????});

????????} catch(Exception e) {

????????????logger.error("setNX redis error, key : {}", key);

????????}

????????returnobj != null? (Boolean) obj : false;

????}


????private String getSet(final String key, final String value) {

????????Object obj = null;

????????try{

????????????obj = redisTemplate.execute(newRedisCallback<Object>() {

????????????????@Override

????????????????public Object doInRedis(RedisConnection connection) throws DataAccessException {

????????????????????StringRedisSerializer serializer = newStringRedisSerializer();

????????????????????byte[] ret = connection.getSet(serializer.serialize(key), serializer.serialize(value));

????????????????????connection.close();

????????????????????returnserializer.deserialize(ret);

????????????????}

????????????});

????????} catch(Exception e) {

????????????logger.error("setNX redis error, key : {}", key);

????????}

????????returnobj != null? (String) obj : null;

????}


????/**

?????* 獲得 lock.

?????* 實現(xiàn)思路: 主要是使用了redis 的setnx命令,緩存了鎖.

?????* reids緩存的key是鎖的key,所有的共享, value是鎖的到期時間(注意:這里把過期時間放在value了,沒有時間上設置其超時時間)

?????* 執(zhí)行過程:

?????* 1.通過setnx嘗試設置某個key的值,成功(當前沒有這個鎖)則返回,成功獲得鎖

?????* 2.鎖已經存在則獲取鎖的到期時間,和當前時間比較,超時的話,則設置新的值

?????*

?????* @return true if lock is acquired, false acquire timeouted

?????* @throws InterruptedException in case of thread interruption

?????*/

????public synchronized boolean lock() throws InterruptedException {

????????int timeout = timeoutMsecs;

????????while(timeout >= 0) {

????????????long expires = System.currentTimeMillis() + expireMsecs + 1;

????????????String expiresStr = String.valueOf(expires); //鎖到期時間

????????????if(this.setNX(lockKey, expiresStr)) {

????????????????// lock acquired

????????????????locked = true;

????????????????returntrue;

????????????}


????????????String currentValueStr = this.get(lockKey); //redis里的時間

????????????if(currentValueStr != null&& Long.parseLong(currentValueStr) < System.currentTimeMillis()) {

????????????????//判斷是否為空毛秘,不為空的情況下,如果被其他線程設置了值,則第二個條件判斷是過不去的

????????????????// lock is expired


????????????????String oldValueStr = this.getSet(lockKey, expiresStr);

????????????????//獲取上一個鎖到期時間叫挟,并設置現(xiàn)在的鎖到期時間艰匙,

????????????????//只有一個線程才能獲取上一個線上的設置時間,因為jedis.getSet是同步的

????????????????if(oldValueStr != null&& oldValueStr.equals(currentValueStr)) {

????????????????????//防止誤刪(覆蓋抹恳,因為key是相同的)了他人的鎖——這里達不到效果员凝,這里值會被覆蓋,但是因為什么相差了很少的時間奋献,所以可以接受


????????????????????//[分布式的情況下]:如過這個時候健霹,多個線程恰好都到了這里,但是只有一個線程的設置值和當前值相同瓶蚂,他才有權利獲取鎖

????????????????????// lock acquired

????????????????????locked = true;

????????????????????returntrue;

????????????????}

????????????}

????????????timeout -= DEFAULT_ACQUIRY_RESOLUTION_MILLIS;


????????????/*

????????????????延遲100 毫秒,? 這里使用隨機時間可能會好一點,可以防止饑餓進程的出現(xiàn),即,當同時到達多個進程,

????????????????只會有一個進程獲得鎖,其他的都用同樣的頻率進行嘗試,后面有來了一些進行,也以同樣的頻率申請鎖,這將可能導致前面來的鎖得不到滿足.

????????????????使用隨機的等待時間可以一定程度上保證公平性

?????????????*/

????????????Thread.sleep(DEFAULT_ACQUIRY_RESOLUTION_MILLIS);


????????}

????????returnfalse;

????}



????/**

?????* Acqurired lock release.

?????*/

????public synchronized void unlock() {

????????if(locked) {

????????????redisTemplate.delete(lockKey);

????????????locked = false;

????????}

????}


}

調用:

RedisLock lock = newRedisLock(redisTemplate, key, 10000, 20000);

?try{

????????????if(lock.lock()) {

???????????????????//需要加鎖的代碼

????????????????}

????????????}

????????} catch(InterruptedException e) {

????????????e.printStackTrace();

????????}finally {

????????????//為了讓分布式鎖的算法更穩(wěn)鍵些糖埋,持有鎖的客戶端在解鎖之前應該再檢查一次自己的鎖是否已經超時,再去做DEL操作窃这,因為可能客戶端因為某個耗時的操作而掛起瞳别,

????????????//操作完的時候鎖因為超時已經被別人獲得,這時就不必解鎖了杭攻。 ————這里沒有做

????????????lock.unlock();

????????}

六祟敛、一些問題

1、為什么不直接使用expire設置超時時間朴上,而將時間的毫秒數(shù)其作為value放在redis中垒棋?

如下面的方式,把超時的交給redis處理:

lock(key, expireSec){

isSuccess = setnx key

if(isSuccess)

expire key expireSec

}

這種方式貌似沒什么問題痪宰,但是假如在setnx后,redis崩潰了畔裕,expire就沒有執(zhí)行衣撬,結果就是死鎖了。鎖永遠不會超時扮饶。

2具练、為什么前面的鎖已經超時了,還要用getSet去設置新的時間戳的時間獲取舊的值甜无,然后和外面的判斷超時時間的時間戳比較呢扛点?

因為是分布式的環(huán)境下,可以在前一個鎖失效的時候岂丘,有兩個進程進入到鎖超時的判斷陵究。如:

C0超時了,還持有鎖,C1/C2同時請求進入了方法里面

C1/C2獲取到了C0的超時時間

C1使用getSet方法

C2也執(zhí)行了getSet方法

假如我們不加 oldValueStr.equals(currentValueStr) 的判斷奥帘,將會C1/C2都將獲得鎖铜邮,加了之后,能保證C1和C2只能一個能獲得鎖,一個只能繼續(xù)等待松蒜。

注意:這里可能導致超時時間不是其原本的超時時間扔茅,C1的超時時間可能被C2覆蓋了,但是他們相差的毫秒及其小秸苗,這里忽略了召娜。

本文轉自:https://www.php.cn/redis/437668.html

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市惊楼,隨后出現(xiàn)的幾起案子萤晴,更是在濱河造成了極大的恐慌,老刑警劉巖胁后,帶你破解...
    沈念sama閱讀 206,602評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件店读,死亡現(xiàn)場離奇詭異,居然都是意外死亡攀芯,警方通過查閱死者的電腦和手機屯断,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,442評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來侣诺,“玉大人殖演,你說我怎么就攤上這事∧暝В” “怎么了趴久?”我有些...
    開封第一講書人閱讀 152,878評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長搔确。 經常有香客問我彼棍,道長,這世上最難降的妖魔是什么膳算? 我笑而不...
    開封第一講書人閱讀 55,306評論 1 279
  • 正文 為了忘掉前任座硕,我火速辦了婚禮,結果婚禮上涕蜂,老公的妹妹穿的比我還像新娘华匾。我一直安慰自己,他們只是感情好机隙,可當我...
    茶點故事閱讀 64,330評論 5 373
  • 文/花漫 我一把揭開白布蜘拉。 她就那樣靜靜地躺著,像睡著了一般有鹿。 火紅的嫁衣襯著肌膚如雪旭旭。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,071評論 1 285
  • 那天印颤,我揣著相機與錄音您机,去河邊找鬼。 笑死,一個胖子當著我的面吹牛际看,可吹牛的內容都是我干的咸产。 我是一名探鬼主播,決...
    沈念sama閱讀 38,382評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼仲闽,長吁一口氣:“原來是場噩夢啊……” “哼脑溢!你這毒婦竟也來了?” 一聲冷哼從身側響起赖欣,我...
    開封第一講書人閱讀 37,006評論 0 259
  • 序言:老撾萬榮一對情侶失蹤屑彻,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后顶吮,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體社牲,經...
    沈念sama閱讀 43,512評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 35,965評論 2 325
  • 正文 我和宋清朗相戀三年悴了,在試婚紗的時候發(fā)現(xiàn)自己被綠了搏恤。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,094評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡湃交,死狀恐怖熟空,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情搞莺,我是刑警寧澤息罗,帶...
    沈念sama閱讀 33,732評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站才沧,受9級特大地震影響迈喉,放射性物質發(fā)生泄漏。R本人自食惡果不足惜糜工,卻給世界環(huán)境...
    茶點故事閱讀 39,283評論 3 307
  • 文/蒙蒙 一弊添、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧捌木,春花似錦、人聲如沸嫉戚。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,286評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽彬檀。三九已至帆啃,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間窍帝,已是汗流浹背努潘。 一陣腳步聲響...
    開封第一講書人閱讀 31,512評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人疯坤。 一個月前我還...
    沈念sama閱讀 45,536評論 2 354
  • 正文 我出身青樓报慕,卻偏偏與公主長得像,于是被迫代替她去往敵國和親压怠。 傳聞我的和親對象是個殘疾皇子眠冈,可洞房花燭夜當晚...
    茶點故事閱讀 42,828評論 2 345

推薦閱讀更多精彩內容