本文目錄一覽:
Redis 分布式鎖詳細分析
鎖的作用,我想大家都理解,就是讓不同的線程或者進程可以安全地操作共享資源,而不會產生衝突。
比較熟悉的就是 Synchronized 和 ReentrantLock 等,這些可以保證同一個 jvm 程序中,不同線程安全操作共享資源。
但是在分布式系統中,這種方式就失效了;由於分布式系統多線程、多進程並且分布在不同機器上,這將使單機並發控制鎖策略失效,為了解決這個問題就需要一種跨 JVM 的互斥機制來控制共享資源的訪問。
比較常用的分布式鎖有三種實現方式:
本篇文章主要講解基於 Redis 分布式鎖的實現。
分布式鎖最主要的作用就是保證任意一個時刻,只有一個客戶端能訪問共享資源。
我們知道 redis 有 SET key value NX 命令,僅在不存在 key 的時候才能被執行成功,保證多個客戶端只有一個能執行成功,相當於獲取鎖。
釋放鎖的時候,只需要刪除 del key 這個 key 就行了。
上面的實現看似已經滿足要求了,但是忘了考慮在分布式環境下,有以下問題:
最大的問題就是因為客戶端或者網絡問題,導致 redis 中的 key 沒有刪除,鎖無法釋放,因此其他客戶端無法獲取到鎖。
針對上面的情況,使用了下面命令:
使用 PX 的命令,給 key 添加一個自動過期時間(30秒),保證即使因為意外情況,沒有調用釋放鎖的方法,鎖也會自動釋放,其他客戶端仍然可以獲取到鎖。
注意給這個 key 設置的值 my_random_value 是一個隨機值,而且必須保證這個值在客戶端必須是唯一的。這個值的作用是為了更加安全地釋放鎖。
這是為了避免刪除其他客戶端成功獲取的鎖。考慮下面情況:
因此這裡使用一個 my_random_value 隨機值,保證客戶端只會釋放自己獲取的鎖,即只刪除自己設置的 key 。
這種實現方式,存在下面問題:
上面章節介紹了,簡單實現存在的問題,下面來介紹一下 Redisson 實現又是怎麼解決的這些問題的。
主要關注 tryAcquireOnceAsync 方法,有三個參數:
方法主要流程:
這個方法的流程與 tryLock(long waitTime, long leaseTime, TimeUnit unit) 方法基本相同。
這個方法與 tryAcquireOnceAsync 方法的區別,就是一個獲取鎖過期時間,一個是能否獲取鎖。即 獲取鎖過期時間 為 null 表示獲取到鎖,其他表示沒有獲取到鎖。
獲取鎖最終都會調用這個方法,通過 lua 腳本與 redis 進行交互,來實現分布式鎖。
首先分析,傳給 lua 腳本的參數:
lua 腳本的流程:
為了實現無限制持有鎖,那麼就需要定時刷新鎖的過期時間。
這個類最重要的是兩個成員屬性:
使用一個靜態並發集合 EXPIRATION_RENEWAL_MAP 來存儲所有鎖對應的 ExpirationEntry ,當有新的 ExpirationEntry 並存入到 EXPIRATION_RENEWAL_MAP 集合中時,需要調用 renewExpiration 方法,來刷新過期時間。
創建一個超時任務 Timeout task ,超時時間是 internalLockLeaseTime / 3 , 過了這個時間,即調用 renewExpirationAsync(threadId) 方法,來刷新鎖的過期時間。
判斷如果是當前線程持有的鎖,那麼就重新設置過期時間,並返回 1 即 true 。否則返回 0 即 false 。
通過調用 unlockInnerAsync(threadId) 來刪除 redis 中的 key 來釋放鎖。特別注意一點,當不是持有鎖的線程釋放鎖時引起的失敗,不需要調用 cancelExpirationRenewal 方法,取消定時,因為鎖還是被其他線程持有。
傳給這個 lua 腳本的值:
這個 lua 腳本的流程:
調用了 LockPubSub 的 subscribe 進行訂閱。
這個方法的作用就是向 redis 發起訂閱,但是對於同一個鎖的同一個客戶端(即 一個 jvm 系統) 只會發起一次訂閱,同一個客戶端的其他等待同一個鎖的線程會記錄在 RedissonLockEntry 中。
方法流程:
只有當 counter = permits 的時候,回調 listener 才會運行,起到控制 listener 運行的效果。
釋放一個控制量,讓其中一個回調 listener 能夠運行。
主要屬性:
這個過程對應的 redis 中監控的命令日誌:
因為看門狗的默認時間是 30 秒,而定時刷新程序的時間是看門狗時間的 1/3 即 10 秒鐘,示例程序休眠了 15 秒,導致觸發了刷新鎖的過期時間操作。
注意 rLock.tryLock(10, TimeUnit.SECONDS); 時間要設置大一點,如果等待時間太短,小於獲取鎖 redis 命令的時間,那麼就直接返回獲取鎖失敗了。
分析源碼我們了解 Redisson 模式的分布式,解決了鎖過期時間和可重入的問題。但是針對 redis 本身可能存在的單點失敗問題,其實是沒有解決的。
基於這個問題, redis 作者提出了一種叫做 Redlock 算法, 但是這種算法本身也是有點問題的,想了解更多,請看 基於Redis的分布式鎖到底安全嗎?
Redis分布式鎖的原理是什麼?如何續期?
在傳統單體應用單機部署的情況下,並發問題可以通過使用Java並發相關的鎖如synchronized,但是當規模上升到分布式集群的情況下,要控制共享資源訪問,就需要通過分布式鎖來實現。常見的分布式鎖方案如數據庫樂觀鎖,Redis鎖,zk鎖等。
Redis分布式鎖的原理
Redis分布式鎖可以有多種方式實現但是其核心就是通過以下三個Redis命令組合實現。
SETNX SETNX key val 當且僅當key不存在時,set一個key為val的字符串,返回1;若key存在,則什麼都不做,返回0。
Expire expire key timeout 為key設置一個超時時間,單位為second,超過這個時間鎖會自動釋放,避免死鎖。
Delete delete key 刪除key
核心思想
使用setnx獲取鎖。如果成功取到鎖,則使用expire命令為鎖添加一個超時時間,超過該時間則自動釋放鎖。
獲取鎖的時候還設置一個獲取的超時時間,若超過這個時間則放棄獲取鎖。
注意
上面為Redis的一個最簡單的鎖實現原理,實際中還需要考慮更多具體的情況作出相應的調整。如
上面的demo中,當集群系統時間不一致時會有問題
當服務器異常關閉或是重啟,加鎖後沒來得急設置鎖超時時間,如何避免死鎖
實際開發環境中不確定的因素有很多,需要慢慢地去調整實踐達到理想狀態,可以考慮使用redisson框架來實現。
如何續期?
這個情況比較獨特,出現這個問題的根本原因在於鎖失效的時間小於業務處理的時間導致業務還沒處理完畢鎖就釋放了。那麼解決方案是合理地結合業務去設置鎖失效的時間。
但是也有更好的方案就如前文提到的redisson,其中的可重入鎖概念。
默認情況下,加鎖的時間是30秒.如果加鎖的業務沒有執行完,那麼到 30-10 = 20秒的時候,就會進行一次續期,把鎖重置成30秒。
以上就是redis鎖的原理及續期的方式,希望我的回答能對你有所幫助。
redis分布式鎖常見問題及解決方案
1.1 鎖需要具備唯一性
1.2 鎖需要有超時時間,防止死鎖
1.3 鎖的創建和設置鎖超時時間需要具備原子性
1.4 鎖的超時的續期問題
1.5 B的鎖被A給釋放了的問題
1.6 鎖的可重入問題
1.7 集群下分布式鎖的問題
問題講解:
首先分布式鎖要解決的問題就是分布式環境下同一資源被多個進程進行訪問和操作的問題,既然是同一資源,那麼肯定要考慮數據安全問題.其實和單進程下加鎖解鎖的原理是一樣的,單進程下需要考慮多線程對同一變量進行訪問和修改問題,為了保證同一變量不被多個線程同時訪問,按照順序對變量進行修改,就要在訪問變量時進行加鎖,這個加鎖可以是重量級鎖,也可以是基於cas的樂觀鎖.
解決方案:
使用redis命令setnx(set if not exist),即只能被一個客戶端占坑,如果redis實例存在唯一鍵(key),如果再想在該鍵(key)上設置值,就會被拒絕.
問題講解:
redis釋放鎖需要客戶端的操作,如果此時客戶端突然掛了,就沒有釋放鎖的操作了,也意味着其他客戶端想要重新加鎖,卻加不了的問題.
解決方案:
所以,為了避免客戶端掛掉或者說是客戶端不能正常釋放鎖的問題,需要在加鎖的同時,給鎖加上超時時間.
即,加鎖和給鎖加上超時時間的操作如下操作:
setnx lockkey true #加鎖操作
ok
expire lockkey 5 #給鎖加上超時時間
… do something critical …
del lockkey #釋放鎖
(integer) 1
問題講解:
通過2.3加鎖和超時時間的設置可以看到,setnx和expire需要兩個命令來完成操作,也就是需要兩次RTT操作,如果在setnx和expire兩次命令之間,客戶端突然掛掉,這時又無法釋放鎖,且又回到了死鎖的問題.
解決方案:
使用set擴展命令
如下:
set lockkey true ex 5 nx #加鎖,過期時間5s
ok
… do something critical …
del lockkey
以上的set lockkey true ex 5 nx命令可以一次性完成setnx和expire兩個操作,也就是解決了原子性問題.
問題講解:
redis分布式鎖過期,而業務邏輯沒執行完的場景,不過,這裡換一種思路想問題,把redis鎖的過期時間再弄長點不就解決了嗎?那還是有問題,我們可以在加鎖的時候,手動調長redis鎖的過期時間,可這個時間多長合適?業務邏輯的執行時間是不可控的,調的過長又會影響操作性能。
解決方案:
使用redis客戶端redisson,redisson很好的解決了redis在分布式環境下的一些棘手問題,它的宗旨就是讓使用者減少對Redis的關注,將更多精力用在處理業務邏輯上。redisson對分布式鎖做了很好封裝,只需調用API即可。RLock lock = redissonClient.getLock(“stockLock”);
redisson在加鎖成功後,會註冊一個定時任務監聽這個鎖,每隔10秒就去查看這個鎖,如果還持有鎖,就對過期時間進行續期。默認過期時間30秒。這個機制也被叫做:“看門狗”
問題講解:
A、B兩個線程來嘗試給key myLock加鎖,A線程先拿到鎖(假如鎖3秒後過期),B線程就在等待嘗試獲取鎖,到這一點毛病沒有。那如果此時業務邏輯比較耗時,執行時間已經超過redis鎖過期時間,這時A線程的鎖自動釋放(刪除key),B線程檢測到myLock這個key不存在,執行 SETNX命令也拿到了鎖。但是,此時A線程執行完業務邏輯之後,還是會去釋放鎖(刪除key),這就導致B線程的鎖被A線程給釋放了。
解決方案:
一般我們在每個線程加鎖時要帶上自己獨有的value值來標識,只釋放指定value的key,否則就會出現釋放鎖混亂的場景一般我們可以設置value為業務前綴_當前線程ID或者uuid,只有當前value相同的才可以釋放鎖
問題講解:
上面我們講了,為了保證鎖具有唯一性,需要使用setnx,後來為了與超時時間一起設置,我們選用了set命令。 在我們想要在加鎖期間,擁有鎖的客戶端想要再次獲得鎖,也就是鎖重入
解決方案:
給鎖設置hash結構的加鎖次數,每次加鎖就+1
問題講解:
這一問題是在redis集群方案時會出現的.事實上,現在為了保證redis的高可用和訪問性能,都會設置redis的主節點和從節點,主節點負責寫操作,從節點負責讀操作,也就意味着,我們所有的鎖都要寫在主redis服務器實例中,如果主redis服務器宕機,資源釋放(在沒有加持久化時候,如果加了持久化,這一問題會更加複雜),此時redis主節點的數據並沒有複製到從服務器,此時,其他客戶端就會趁機獲取鎖,而之前擁有鎖的客戶端可能還在對資源進行操作,此時又會出現多客戶端對同一資源進行訪問和操作的問題.
解決方案:
使用redlock,原理與zookeeper分布式鎖原理相同.多台主機超過半數設置成功則獲取鎖成功,要注意下主機個數必須是奇數,不過這有效率問題
php 怎麼給redis加查詢鎖
能不能加鎖這個不知道,但是可以用監控watch 和事務結合起來用。因為watch的功能就是當它監控一個鍵的時候,如果這個鍵被修改了,那麼它後面的事務就不會執行。
比如:
set key 1;
watch key
set key 2
mulit
set key 3
exec
get key =’2′ //key在watch後被修改了,所以後面的事務沒有執行
原創文章,作者:小藍,如若轉載,請註明出處:https://www.506064.com/zh-hant/n/194229.html