Redis實(shí)現(xiàn)分布式鎖的2種方式

Redis實(shí)現(xiàn)分布式鎖

1旦委、普通實(shí)現(xiàn)

??關(guān)于Redis分布式鎖大部分人都會想到:setnx+lua沥阱,或者知道set key value px milliseconds nx扬舒。后一種方式的核心實(shí)現(xiàn)命令如下:

- 獲取鎖(unique_value可以是UUID等)
SET resource_name unique_value NX PX 30000

\- 釋放鎖(lua腳本中狂秦,一定要比較value养涮,防止誤解鎖)
if redis.call("get",KEYS[1]) == ARGV[1] then
  return redis.call("del",KEYS[1])
else
  return 0
end

1.1后频、這種實(shí)現(xiàn)方式有3大要點(diǎn)

  1. set命令要用set key value px milliseconds nx
  2. value要具有唯一性唐责;
  3. 釋放鎖時(shí)要驗(yàn)證value值鳞溉,不能誤解鎖;

1.2鼠哥、setnx實(shí)現(xiàn)分布式鎖的缺點(diǎn)

??事實(shí)上這類瑣最大的缺點(diǎn)就是它加鎖時(shí)只作用在一個(gè)Redis節(jié)點(diǎn)上熟菲,即使Redis通過sentinel保證高可用,如果這個(gè)master節(jié)點(diǎn)由于某些原因發(fā)生了主從切換朴恳,那么就會出現(xiàn)鎖丟失的情況:

  1. 客戶端A在master節(jié)點(diǎn)拿到了鎖抄罕。
  2. master節(jié)點(diǎn)在把A創(chuàng)建的key寫入slave之前宕機(jī)了。(加鎖的key還沒有同步到slave節(jié)點(diǎn))
  3. slave變成了master節(jié)點(diǎn)
  4. B也得到了和A還持有的相同的鎖(因?yàn)樵瓉淼膕lave里還沒有A持有鎖的信息)

??正因?yàn)槿绱擞谟保琑edis作者antirez基于分布式環(huán)境下提出了一種更高級的分布式鎖的實(shí)現(xiàn)方式:Redlock呆贿。筆者認(rèn)為,Redlock也是Redis所有分布式鎖實(shí)現(xiàn)方式中唯一能讓面試官高潮的方式森渐。

2做入、Redlock實(shí)現(xiàn)

多加幾個(gè)master,一般5個(gè)就夠了章母。

  1. 保證 客戶端請求獲得鎖的個(gè)數(shù) > N/2 + 1 這里是3
  2. 保證 設(shè)置的超時(shí)時(shí)間 > 請求N個(gè)master獲取鎖時(shí)間的總和母蛛。

2.1、紅鎖實(shí)現(xiàn)思路

antirez提出的(紅鎖)redlock算法大概是這樣的:

在Redis的分布式環(huán)境中乳怎,我們假設(shè)有N個(gè)Redis master彩郊。

??這些節(jié)點(diǎn)完全互相獨(dú)立,不存在主從復(fù)制或者其他集群協(xié)調(diào)機(jī)制蚪缀。我們確保將在N個(gè)實(shí)例上使用與在Redis單實(shí)例下相同方法獲取和釋放鎖★牛現(xiàn)在我們假設(shè)有5個(gè)Redis master節(jié)點(diǎn),同時(shí)我們需要在5臺服務(wù)器上面運(yùn)行這些Redis實(shí)例询枚,這樣保證他們不會同時(shí)都宕掉违帆。

為了取到鎖,客戶端應(yīng)該執(zhí)行以下操作:

  1. 獲取當(dāng)前Unix時(shí)間金蜀,以毫秒為單位刷后。

  2. 依次嘗試從5個(gè)實(shí)例的畴,使用相同的key和具有唯一性的value(例如UUID)獲取鎖。當(dāng)向Redis請求獲取鎖時(shí)尝胆,客戶端應(yīng)該設(shè)置一個(gè)網(wǎng)絡(luò)連接和響應(yīng)超時(shí)時(shí)間丧裁,這個(gè)超時(shí)時(shí)間應(yīng)該小于鎖的失效時(shí)間。

   例如含衔,你的鎖自動(dòng)失效時(shí)間為10秒煎娇,則超時(shí)時(shí)間應(yīng)該在5-50毫秒之間。這樣可以避免服務(wù)器端Redis已經(jīng)掛掉的情況下贪染,客戶端還在死死地等待響應(yīng)結(jié)果缓呛。
   
   如果服務(wù)器端沒有在規(guī)定時(shí)間內(nèi)響應(yīng),客戶端應(yīng)該盡快嘗試去另外一個(gè)Redis實(shí)例請求獲取鎖杭隙。
  1. 客戶端使用當(dāng)前時(shí)間減去開始獲取鎖時(shí)間(步驟1記錄的時(shí)間)就得到獲取鎖使用的時(shí)間哟绊。當(dāng)且僅當(dāng)從大多數(shù)(N/2+1,這里是3個(gè)節(jié)點(diǎn))的Redis節(jié)點(diǎn)都取到鎖寺渗,并且使用的時(shí)間小于鎖失效時(shí)間時(shí)匿情,鎖才算獲取成功兰迫。

  2. 如果取到了鎖信殊,key的真正有效時(shí)間等于有效時(shí)間減去獲取鎖所使用的時(shí)間(步驟3計(jì)算的結(jié)果)。

  3. 如果因?yàn)槟承┰蛑@取鎖失斘芯小(沒有在至少N/2+1個(gè)Redis實(shí)例取到鎖或者取鎖時(shí)間已經(jīng)超過了有效時(shí)間),客戶端應(yīng)該在所有的Redis實(shí)例上進(jìn)行解鎖(即便某些Redis實(shí)例根本就沒有加鎖成功据德,防止某些節(jié)點(diǎn)獲取到鎖但是客戶端沒有得到響應(yīng)而導(dǎo)致接下來的一段時(shí)間不能被重新獲取鎖)鳄乏。

2.3、RedLock存在的問題

根據(jù)上述提出的算法棘利,當(dāng)N個(gè)節(jié)點(diǎn)中有一個(gè)節(jié)點(diǎn)宕機(jī)橱野,仍然存在鎖的安全性問題。

如果有節(jié)點(diǎn)發(fā)生崩潰重啟善玫,還是會對鎖的安全性有影響的水援,具體的影響跟redis的持久化程度有關(guān)

  • 假設(shè)一共有5個(gè)Redis節(jié)點(diǎn):A, B, C, D, E。設(shè)想發(fā)生了如下的事件序列:
  1. 客戶端1成功鎖住了A, B, C茅郎,獲取鎖成功(但D和E沒有鎖孜显)。
  2. 節(jié)點(diǎn)C崩潰重啟了系冗,但客戶端1在C上加的鎖沒有持久化下來奕扣,丟失了。
  3. 節(jié)點(diǎn)C重啟后掌敬,客戶端2鎖住了C, D, E惯豆,獲取鎖成功池磁。
  • 默認(rèn)情況下

Redis的AOF持久化方式是每秒寫一次磁盤(即執(zhí)行fsync),因此最壞情況下可能丟失1秒的數(shù)據(jù)楷兽。

  • 延遲重試

為了應(yīng)對這一問題框仔,antirez又提出了延遲重啟(delayed restarts)的概念。

??也就是說拄养,一個(gè)節(jié)點(diǎn)崩潰后离斩,先不立即重啟它,而是等待一段時(shí)間再重啟瘪匿,這段時(shí)間應(yīng)該大于鎖的有效時(shí)間(lock validity time)跛梗。這樣的話,這個(gè)節(jié)點(diǎn)在重啟前所參與的鎖都會過期棋弥,它在重啟后就不會對現(xiàn)有的鎖造成影響核偿。

在最后釋放鎖的時(shí)候,antirez在算法描述中特別強(qiáng)調(diào)顽染,客戶端應(yīng)該向所有Redis節(jié)點(diǎn)發(fā)起釋放鎖的操作漾岳。也就是說,即使當(dāng)時(shí)向某個(gè)節(jié)點(diǎn)獲取鎖沒有成功粉寞,在釋放鎖的時(shí)候也不應(yīng)該漏掉這個(gè)節(jié)點(diǎn)尼荆。

  • 最后釋放鎖,要向所有節(jié)點(diǎn)發(fā)起釋放的原因

??設(shè)想這樣一種情況唧垦,客戶端發(fā)給某個(gè)Redis節(jié)點(diǎn)的獲取鎖的請求成功到達(dá)了該Redis節(jié)點(diǎn)捅儒,這個(gè)節(jié)點(diǎn)也成功執(zhí)行了SET操作,但是它返回給客戶端的響應(yīng)包卻丟失了振亮。

??這在客戶端看來巧还,獲取鎖的請求由于超時(shí)而失敗了,但在Redis這邊看來坊秸,加鎖已經(jīng)成功了麸祷。因此,釋放鎖的時(shí)候褒搔,客戶端也應(yīng)該對當(dāng)時(shí)獲取鎖失敗的那些Redis節(jié)點(diǎn)同樣發(fā)起請求阶牍。實(shí)際上,這種情況在異步通信模型中是有可能發(fā)生的:客戶端向服務(wù)器通信是正常的站超,但反方向卻是有問題的荸恕。

2.3、其它問題

  1. 客戶端長時(shí)間阻塞死相,導(dǎo)致獲得的鎖釋放浴栽,訪問的共享資源不受保護(hù)的問題窃这。
  • 有兩種情況會引發(fā)阻塞
1吮旅、客戶端跟redis通信的問題,

2县昂、客戶端跟共享資源服務(wù)器交互延時(shí)
  1. 在Redlock的算法中,我們可以看到第3步陷舅,當(dāng)獲取鎖耗時(shí)太多倒彰,留給客戶端的訪問共享資源的時(shí)間很短,這種情況若來不及操作莱睁,是不是要釋放鎖呢待讳?且到底剩下多少時(shí)間才算短?這又是一個(gè)選擇難題仰剿。
  2. Redlock算法對時(shí)鐘依賴性太強(qiáng)创淡,若N個(gè)節(jié)點(diǎn)中的某個(gè)節(jié)點(diǎn)發(fā)生時(shí)間跳躍,也可能會引此而引發(fā)鎖安全性問題南吮。

3琳彩、Redlock源碼

??redisson已經(jīng)有對redlock算法封裝,接下來對其用法進(jìn)行簡單介紹部凑,并對核心源碼進(jìn)行分析(假設(shè)5個(gè)redis實(shí)例)露乏。

  • POM依賴
<!-- https://mvnrepository.com/artifact/org.redisson/redisson -->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.3.2</version>
</dependency>

3.1、用法

??首先涂邀,我們來看一下redission封裝的redlock算法實(shí)現(xiàn)的分布式鎖用法瘟仿,非常簡單,跟重入鎖(ReentrantLock)有點(diǎn)類似:

Config config1 = new Config();
config1.useSingleServer().setAddress("redis://192.168.0.1:5378")
        .setPassword("a123456").setDatabase(0);
RedissonClient redissonClient1 = Redisson.create(config1);

Config config2 = new Config();
config2.useSingleServer().setAddress("redis://192.168.0.1:5379")
        .setPassword("a123456").setDatabase(0);
RedissonClient redissonClient2 = Redisson.create(config2);

Config config3 = new Config();
config3.useSingleServer().setAddress("redis://192.168.0.1:5380")
        .setPassword("a123456").setDatabase(0);
RedissonClient redissonClient3 = Redisson.create(config3);

String resourceName = "REDLOCK_KEY";

RLock lock1 = redissonClient1.getLock(resourceName);
RLock lock2 = redissonClient2.getLock(resourceName);
RLock lock3 = redissonClient3.getLock(resourceName);
// 向3個(gè)redis實(shí)例嘗試加鎖
RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
boolean isLock;
try {
    // isLock = redLock.tryLock();
    // 500ms拿不到鎖, 就認(rèn)為獲取鎖失敗必孤。10000ms即10s是鎖失效時(shí)間猾骡。
    isLock = redLock.tryLock(500, 10000, TimeUnit.MILLISECONDS);
    System.out.println("isLock = "+isLock);
    if (isLock) {
        //TODO if get lock success, do something;
    }
} catch (Exception e) {
} finally {
    // 無論如何, 最后都要解鎖
    redLock.unlock();
}

3.2、唯一ID

??實(shí)現(xiàn)分布式鎖的一個(gè)非常重要的點(diǎn)就是set的value要具有唯一性敷搪,redisson的value是怎樣保證value的唯一性呢?答案是UUID+threadId幢哨。入口在redissonClient.getLock("REDLOCK_KEY")赡勘,源碼在Redisson.java和RedissonLock.java中:



protected final UUID id = UUID.randomUUID();
String getLockName(long threadId) {
    return id + ":" + threadId;
}

3.3、獲取鎖

??獲取鎖的代碼為redLock.tryLock()或者redLock.tryLock(500, 10000, TimeUnit.MILLISECONDS)捞镰,兩者的最終核心源碼都是下面這段代碼闸与,只不過前者獲取鎖的默認(rèn)續(xù)約時(shí)間(leaseTime)是LOCK_EXPIRATION_INTERVAL_SECONDS,即30s:

<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
    internalLockLeaseTime = unit.toMillis(leaseTime);
    // 獲取鎖時(shí)需要在redis實(shí)例上執(zhí)行的lua命令
    return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
              // 首先分布式鎖的KEY不能存在岸售,如果確實(shí)不存在践樱,那么執(zhí)行hset命令(hset REDLOCK_KEY uuid+threadId 1),并通過pexpire設(shè)置失效時(shí)間(也是鎖的租約時(shí)間)
              "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已經(jīng)存在凸丸,并且value也匹配拷邢,表示是當(dāng)前線程持有的鎖,那么重入次數(shù)加1屎慢,并且設(shè)置失效時(shí)間
              "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的失效時(shí)間毫秒數(shù)
              "return redis.call('pttl', KEYS[1]);",
              // 這三個(gè)參數(shù)分別對應(yīng)KEYS[1]瞭稼,ARGV[1]和ARGV[2]
                Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
}

??獲取鎖的命令中忽洛,

  • KEYS[1]就是Collections.singletonList(getName()),表示分布式鎖的key环肘,即REDLOCK_KEY欲虚;
  • ARGV[1]就是internalLockLeaseTime,即鎖的租約時(shí)間悔雹,默認(rèn)30s复哆;
  • ARGV[2]就是getLockName(threadId),是獲取鎖時(shí)set的唯一值腌零,即UUID+threadId:

3.4寂恬、釋放鎖

??釋放鎖的代碼為redLock.unlock(),核心源碼如下:

protected RFuture<Boolean> unlockInnerAsync(long threadId) {
    // 釋放鎖時(shí)需要在redis實(shí)例上執(zhí)行的lua命令
    return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
            // 如果分布式鎖KEY不存在莱没,那么向channel發(fā)布一條消息
            "if (redis.call('exists', KEYS[1]) == 0) then " +
                "redis.call('publish', KEYS[2], ARGV[1]); " +
                "return 1; " +
            "end;" +
            // 如果分布式鎖存在初肉,但是value不匹配,表示鎖已經(jīng)被占用饰躲,那么直接返回
            "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
                "return nil;" +
            "end; " +
            // 如果就是當(dāng)前線程占有分布式鎖牙咏,那么將重入次數(shù)減1
            "local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
            // 重入次數(shù)減1后的值如果大于0,表示分布式鎖有重入過嘹裂,那么只設(shè)置失效時(shí)間妄壶,還不能刪除
            "if (counter > 0) then " +
                "redis.call('pexpire', KEYS[1], ARGV[2]); " +
                "return 0; " +
            "else " +
                // 重入次數(shù)減1后的值如果為0,表示分布式鎖只獲取過1次寄狼,那么刪除這個(gè)KEY丁寄,并發(fā)布解鎖消息
                "redis.call('del', KEYS[1]); " +
                "redis.call('publish', KEYS[2], ARGV[1]); " +
                "return 1; "+
            "end; " +
            "return nil;",
            // 這5個(gè)參數(shù)分別對應(yīng)KEYS[1],KEYS[2]泊愧,ARGV[1]伊磺,ARGV[2]和ARGV[3]
            Arrays.<Object>asList(getName(), getChannelName()), LockPubSub.unlockMessage, internalLockLeaseTime, getLockName(threadId));

}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市删咱,隨后出現(xiàn)的幾起案子屑埋,更是在濱河造成了極大的恐慌,老刑警劉巖痰滋,帶你破解...
    沈念sama閱讀 218,284評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件摘能,死亡現(xiàn)場離奇詭異,居然都是意外死亡敲街,警方通過查閱死者的電腦和手機(jī)团搞,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,115評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來多艇,“玉大人逻恐,你說我怎么就攤上這事。” “怎么了梢莽?”我有些...
    開封第一講書人閱讀 164,614評論 0 354
  • 文/不壞的土叔 我叫張陵萧豆,是天一觀的道長。 經(jīng)常有香客問我昏名,道長涮雷,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,671評論 1 293
  • 正文 為了忘掉前任轻局,我火速辦了婚禮洪鸭,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘仑扑。我一直安慰自己览爵,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,699評論 6 392
  • 文/花漫 我一把揭開白布镇饮。 她就那樣靜靜地躺著蜓竹,像睡著了一般。 火紅的嫁衣襯著肌膚如雪储藐。 梳的紋絲不亂的頭發(fā)上俱济,一...
    開封第一講書人閱讀 51,562評論 1 305
  • 那天,我揣著相機(jī)與錄音钙勃,去河邊找鬼蛛碌。 笑死,一個(gè)胖子當(dāng)著我的面吹牛辖源,可吹牛的內(nèi)容都是我干的蔚携。 我是一名探鬼主播,決...
    沈念sama閱讀 40,309評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼克饶,長吁一口氣:“原來是場噩夢啊……” “哼酝蜒!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起彤路,我...
    開封第一講書人閱讀 39,223評論 0 276
  • 序言:老撾萬榮一對情侶失蹤秕硝,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后洲尊,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,668評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡奈偏,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,859評論 3 336
  • 正文 我和宋清朗相戀三年坞嘀,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片惊来。...
    茶點(diǎn)故事閱讀 39,981評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡丽涩,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情矢渊,我是刑警寧澤继准,帶...
    沈念sama閱讀 35,705評論 5 347
  • 正文 年R本政府宣布,位于F島的核電站矮男,受9級特大地震影響移必,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜毡鉴,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,310評論 3 330
  • 文/蒙蒙 一崔泵、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧猪瞬,春花似錦憎瘸、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,904評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至痊项,卻和暖如春锅风,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背线婚。 一陣腳步聲響...
    開封第一講書人閱讀 33,023評論 1 270
  • 我被黑心中介騙來泰國打工遏弱, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人塞弊。 一個(gè)月前我還...
    沈念sama閱讀 48,146評論 3 370
  • 正文 我出身青樓漱逸,卻偏偏與公主長得像,于是被迫代替她去往敵國和親游沿。 傳聞我的和親對象是個(gè)殘疾皇子饰抒,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,933評論 2 355