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中文網其他相關文章!

熱AI工具

Undresser.AI Undress
人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover
用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool
免費脫衣圖片

Clothoff.io
AI脫衣器

AI Hentai Generator
免費產生 AI 無盡。

熱門文章

熱工具

記事本++7.3.1
好用且免費的程式碼編輯器

SublimeText3漢化版
中文版,非常好用

禪工作室 13.0.1
強大的PHP整合開發環境

Dreamweaver CS6
視覺化網頁開發工具

SublimeText3 Mac版
神級程式碼編輯軟體(SublimeText3)

熱門話題

Redis集群模式通過分片將Redis實例部署到多個服務器,提高可擴展性和可用性。搭建步驟如下:創建奇數個Redis實例,端口不同;創建3個sentinel實例,監控Redis實例並進行故障轉移;配置sentinel配置文件,添加監控Redis實例信息和故障轉移設置;配置Redis實例配置文件,啟用集群模式並指定集群信息文件路徑;創建nodes.conf文件,包含各Redis實例的信息;啟動集群,執行create命令創建集群並指定副本數量;登錄集群執行CLUSTER INFO命令驗證集群狀態;使

使用 Redis 指令需要以下步驟:打開 Redis 客戶端。輸入指令(動詞 鍵 值)。提供所需參數(因指令而異)。按 Enter 執行指令。 Redis 返迴響應,指示操作結果(通常為 OK 或 -ERR)。

啟動 Redis 服務器的步驟包括:根據操作系統安裝 Redis。通過 redis-server(Linux/macOS)或 redis-server.exe(Windows)啟動 Redis 服務。使用 redis-cli ping(Linux/macOS)或 redis-cli.exe ping(Windows)命令檢查服務狀態。使用 Redis 客戶端,如 redis-cli、Python 或 Node.js,訪問服務器。

Redis 使用哈希表存儲數據,支持字符串、列表、哈希表、集合和有序集合等數據結構。 Redis 通過快照 (RDB) 和追加只寫 (AOF) 機制持久化數據。 Redis 使用主從復制來提高數據可用性。 Redis 使用單線程事件循環處理連接和命令,保證數據原子性和一致性。 Redis 為鍵設置過期時間,並使用 lazy 刪除機制刪除過期鍵。

使用Redis進行鎖操作需要通過SETNX命令獲取鎖,然後使用EXPIRE命令設置過期時間。具體步驟為:(1) 使用SETNX命令嘗試設置一個鍵值對;(2) 使用EXPIRE命令為鎖設置過期時間;(3) 當不再需要鎖時,使用DEL命令刪除該鎖。

要從 Redis 讀取隊列,需要獲取隊列名稱、使用 LPOP 命令讀取元素,並處理空隊列。具體步驟如下:獲取隊列名稱:以 "queue:" 前綴命名,如 "queue:my-queue"。使用 LPOP 命令:從隊列頭部彈出元素並返回其值,如 LPOP queue:my-queue。處理空隊列:如果隊列為空,LPOP 返回 nil,可先檢查隊列是否存在再讀取元素。

解決redis-server找不到問題的步驟:檢查安裝,確保已正確安裝Redis;設置環境變量REDIS_HOST和REDIS_PORT;啟動Redis服務器redis-server;檢查服務器是否運行redis-cli ping。

要查看 Redis 中的所有鍵,共有三種方法:使用 KEYS 命令返回所有匹配指定模式的鍵;使用 SCAN 命令迭代鍵並返回一組鍵;使用 INFO 命令獲取鍵的總數。
