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和守护线程在同一个进程,守护线程也会停下。这把锁到了超时的时候,没人给它续命,也就自动释放了。
首页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的数据存储结构就像一棵树,这棵树由节点组成,这种节点叫做Znode
。
Znode
分为四种类型:
持久节点
(PERSISTENT)默认的节点类型。创建节点的客户端与zookeeper断开连接后,该节点依旧存在 。
持久节点顺序节点
(PERSISTENT_SEQUENTIAL)所谓顺序节点,就是在创建节点时,Zookeeper根据创建的时间顺序给该节点名称进行编号:
临时节点
(EPHEMERAL)和持久节点相反,当创建节点的客户端与zookeeper断开连接后,临时节点会被删除:
临时顺序节点
(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分散式鎖定的優缺點:
#更多程式相關知識,請造訪:程式設計入門! !
以上是Redis實作分散式鎖需要注意什麼? 【注意事項總結】的詳細內容。更多資訊請關注PHP中文網其他相關文章!