首頁  >  文章  >  資料庫  >  Redis實作分散式鎖需要注意什麼? 【注意事項總結】

Redis實作分散式鎖需要注意什麼? 【注意事項總結】

青灯夜游
青灯夜游轉載
2022-03-04 16:21:583747瀏覽

Redis實作分散式鎖定需要注意什麼?以下這篇文章就來給大家總結分享一些使用Redis作為分散式鎖的注意點,希望對大家有幫助!

Redis實作分散式鎖需要注意什麼? 【注意事項總結】

Redis實作分散式鎖定

在最近看分散式鎖定的過程中看到一篇不錯的文章,特地的加工一番自己的理解:

Redis分散式鎖定實現的三個核心要素:

1.加鎖

#最簡單的方法是使用setnx指令。 key是鎖的唯一標識,依業務決定命名,value為目前執行緒的執行緒ID。 【相關推薦:Redis影片教學

例如想要給一種商品的秒殺活動加鎖,可以為key命名為 “lock_sale_ID” 。而value設定成什麼呢?我們可以姑且設置成1。加鎖的偽代碼如下:    

setnx(key,1)當一個執行緒執行setnx回傳1,表示key原本不存在,該執行緒成功得到了鎖,當其他執行緒執行setnx回傳0,表示key已經存在,該執行緒搶鎖失敗。

2.解鎖

有加鎖就得有解鎖。當得到鎖的執行緒執行完任務,需要釋放鎖,以便其他執行緒可以進入。釋放鎖定最簡單的方式是執行del指令,偽代碼如下:

del(key)釋放鎖定之後,其他執行緒就可以繼續執行setnx指令來獲得鎖。

3.鎖定逾時

鎖定逾時是什麼意思呢?如果一個被鎖的執行緒在執行任務的過程中掛掉,來不及明確地釋放鎖,這塊資源將會永遠被鎖住,別的執行緒再也別想進來。

所以,setnx的key必須設定一個超時時間,以確保即使沒有被明確釋放,這把鎖也要在一定時間後自動釋放。 setnx不支援超時參數,所以需要額外的指令,偽代碼如下:

expire(key, 30)綜合起來,我們分散式鎖實現的第一版偽代碼如下:

if(setnx(key,1) == 1){
    expire(key,30)
    try {
        do something ......
    }catch()  {  }  finally {
       del(key)
    }

}

因為上面的偽代碼中,存在著三個致命問題:

1. setnx和expire的非原子性

設想一個極端場景,當某線程執行setnx,成功得到了鎖:

setnx剛執行成功,還未來得及執行expire指令,節點1 Duang的一聲掛掉了。

if(setnx(key,1) == 1){  //此处挂掉了.....
    expire(key,30)
    try {
        do something ......
    }catch()
  {
  }
  finally {
       del(key)
    }
 
}

這樣一來,這把鎖就沒有設定過期時間,變得“長生不老”,別的線程再也無法獲得鎖了。

怎麼解決呢? setnx指令本身是不支援傳入超時時間的,Redis 2.6.12以上版本為set指令增加了可選參數,偽代碼如下:set(key,1,30,NX),這樣就可以取代setnx指令

2. 逾時後使用del 導致誤刪其他執行緒的鎖定

又是一個極端場景,假如某線程成功得到了鎖,並且設定的超時時間是30秒。

如果某些原因導致線程A執行的很慢很慢,過了30秒都沒執行完,這時候鎖過期自動釋放,線程B得到了鎖。

隨後,執行緒A執行完了任務,執行緒A接著執行del指令來釋放鎖定。但這時候執行緒B還沒執行完,執行緒A實際上刪除的是執行緒B加的鎖定

怎麼避免這種情況呢?可以在del釋放鎖之前做一個判斷,驗證目前的鎖是不是自己加的鎖。

至於具體的實現,可以在加鎖的時候把目前的執行緒ID當作value,並在刪除之前驗證key對應的value是不是自己執行緒的ID。

加锁:
String threadId = Thread.currentThread().getId()
set(key,threadId ,30,NX)
doSomething.....
 
解锁:
if(threadId .equals(redisClient.get(key))){
    del(key)
}

但是,這樣做又隱含了一個新的問題,if判斷和釋放鎖定是兩個獨立操作,不是原子性

我們都是追求極致的程式設計師,所以這一塊要用Lua腳本來實現:

String luaScript = 'if redis .call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end' ;

redisClient.eval(luaScript , Collections.singletonList(key) , Collections.singletonList(threadId));

這樣一來,驗證和刪除過程就是原子操作了。

3. 出現並發的可能性##

还是刚才第二点所描述的场景,虽然我们避免了线程A误删掉key的情况,但是同一时间有A,B两个线程在访问代码块,仍然是不完美的。

怎么办呢?我们可以让获得锁的线程开启一个守护线程,用来给快要过期的锁“续航”

当过去了29秒,线程A还没执行完,这时候守护线程会执行expire指令,为这把锁“续命”20秒。守护线程从第29秒开始执行,每20秒执行一次。

当线程A执行完任务,会显式关掉守护线程。

另一种情况,如果节点1 忽然断电,由于线程A和守护线程在同一个进程,守护线程也会停下。这把锁到了超时的时候,没人给它续命,也就自动释放了。

 memcache实现分布式锁

首页top 10, 由数据库加载到memcache缓存n分钟
微博中名人的content cache, 一旦不存在会大量请求不能命中并加载数据库
需要执行多个IO操作生成的数据存在cache中, 比如查询db多次
问题
在大并发的场合,当cache失效时,大量并发同时取不到cache,会同一瞬间去访问db并回设cache,可能会给系统带来潜在的超负荷风险。我们曾经在线上系统出现过类似故障。

解决方法

if (memcache.get(key) == null) {
// 3 min timeout to avoid mutex holder crash
if (memcache.add(key_mutex, 3 * 60 * 1000) == true) {
value = db.get(key);
memcache.set(key, value);
memcache.delete(key_mutex);
} else {
 
sleep(50);
retry();
}
}

在load db之前先add一个mutex key, mutex key add成功之后再去做加载db, 如果add失败则sleep之后重试读取原cache数据。为了防止死锁,mutex key也需要设置过期时间。伪代码如下

Zookeeper实现分布式缓存

Zookeeper的数据存储结构就像一棵树,这棵树由节点组成,这种节点叫做Znode

Znode分为四种类型:

  • 1.持久节点 (PERSISTENT)

默认的节点类型。创建节点的客户端与zookeeper断开连接后,该节点依旧存在 。

  • 2.持久节点顺序节点(PERSISTENT_SEQUENTIAL)

所谓顺序节点,就是在创建节点时,Zookeeper根据创建的时间顺序给该节点名称进行编号:

  • 3.临时节点(EPHEMERAL)

和持久节点相反,当创建节点的客户端与zookeeper断开连接后,临时节点会被删除:

  • 4.临时顺序节点(EPHEMERAL_SEQUENTIAL)

顾名思义,临时顺序节点结合和临时节点和顺序节点的特点:在创建节点时,Zookeeper根据创建的时间顺序给该节点名称进行编号;当创建节点的客户端与zookeeper断开连接后,临时节点会被删除。

Zookeeper分布式锁恰恰应用了临时顺序节点。具体如何实现呢?让我们来看一看详细步骤:

  • 获取锁

首先,在Zookeeper当中创建一个持久节点ParentLock。当第一个客户端想要获得锁时,需要在ParentLock这个节点下面创建一个临时顺序节点 Lock1

之后,Client1查找ParentLock下面所有的临时顺序节点并排序,判断自己所创建的节点Lock1是不是顺序最靠前的一个。如果是第一个节点,则成功获得锁。

这时候,如果再有一个客户端 Client2 前来获取锁,则在ParentLock下载再创建一个临时顺序节点Lock2

Client2查找ParentLock下面所有的临时顺序节点并排序,判断自己所创建的节点Lock2是不是顺序最靠前的一个,结果发现节点Lock2并不是最小的。

於是,Client2向排序只比它靠前的節點Lock1註冊Watcher,用於監聽Lock1 節點是否存在。 這表示Client2搶鎖失敗,進入了等待狀態。

這時候,如果又有一個客戶端Client3來取得鎖定,則在ParentLock下載再建立一個臨時順序節點Lock3

Client3找出ParentLock下面所有的暫時順序節點並排序,判斷自己所建立的節點Lock3是不是順序最前面的一個,結果同樣發現節點Lock3並不是最小的。

於是,Client3向排序僅比它靠前的節點Lock2註冊Watcher,用於監聽Lock2節點是否存在。這意味著Client3同樣搶鎖失敗,進入了等待狀態。

這樣一來,Client1得到了鎖,Client2監聽了Lock1 Client3監聽了Lock2。這正好形成了一個等待佇列,很像是Java當中ReentrantLock所依賴的AQS(AbstractQueuedSynchronizer)

  • 釋放鎖定

#釋放鎖定分為兩種情況:

1.任務完成,客戶端顯示釋放

當任務完成時,Client1會顯示呼叫刪除節點Lock1的指令。

2.任務執行過程中,客戶端崩潰

獲得鎖定的Client1在任務執行過程中,如果Duang的一聲崩潰,則會中斷與Zookeeper服務端的連結。根據臨時節點的特性,相關聯的節點​​Lock1會隨之自動刪除。

由於Client2一直監聽著Lock1的存在狀態,當Lock1節點被刪除,Client2會立刻收到通知。這時候Client2會再查詢ParentLock下面的所有節點,確認自己建立的節點Lock2是不是目前最小的節點。如果是最小,則Client2順理成章獲得了鎖。

同理,如果Client2也因為任務完成或節點崩潰而刪除了節點Lock2,那麼Cient3 就會接到通知。

最終,Client3成功得到了鎖定。

#Zookeeper和Redis分散式鎖定的比較

下面的表格總結了Zookeeper和Redis分散式鎖定的優缺點:

#更多程式相關知識,請造訪:程式設計入門! !

以上是Redis實作分散式鎖需要注意什麼? 【注意事項總結】的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:cnblogs.com。如有侵權,請聯絡admin@php.cn刪除