redis 分散式鎖的 5個坑 Redission的Rlock trylock方法

oktokeep發表於2024-08-09

RLock tryLock leaseTime
在 Redission 透過續約機制,每隔一段時間去檢測鎖是否還在進行,如果還在執行就將對應的 key 增加一定的時間,保證在鎖執行的情況下不會發生 key 到了過期時間自動刪除的情況


RLock tryLock WRONGTYPE Operation against a key holding the wrong kind of value
原因:用的方法與redis伺服器中儲存資料的型別存在衝突。
比如:有一個key的資料儲存的是list型別的,但使用redis執行資料操作的時候卻使用了非list的操作方法。


RLock和Lock獲取鎖的方法:關鍵是:long leaseTime引數,自動超時時間的設定,解決finally異常導致鎖未正常釋放的情況。
該RLock介面主要繼承了Lock介面還有其他Redisson, 並擴充套件了部分方法, 比如:boolean tryLock(long waitTime, long leaseTime, TimeUnit unit)新加入的leaseTime主要是用來設定鎖的過期時間, 如果超過leaseTime還沒有解鎖的話, redis就強制解鎖. leaseTime的預設時間是30s

RLock.java
Returns true as soon as the lock is acquired. If the lock is currently held by another thread in this or any other process in the distributed system this method keeps trying to acquire the lock for up to waitTime before giving up and returning false. If the lock is acquired, it is held until unlock is invoked, or until leaseTime have passed since the lock was granted - whichever comes first.
Params:
waitTime – the maximum time to aquire the lock
leaseTime – lease time
unit – time unit
Returns:
true if lock has been successfully acquired
Throws:
InterruptedException – - if the thread is interrupted before or during this method.

##中文翻譯
獲取鎖後立即返回true。如果鎖當前由分散式系統中此程序或任何其他程序中的另一個執行緒持有,則此方法在放棄並返回false之前,會嘗試獲取鎖長達waitTime。
如果獲得了鎖,它將一直被持有,直到呼叫解鎖,或者直到自授予鎖以來已經過了租賃時間——以先到者為準。
引數:
waitTime–獲取鎖的最長時間
租賃時間-租賃時間
單位-時間單位
退貨:
如果已成功獲取鎖,則為true
投擲:
InterruptedException–如果執行緒在此方法之前或期間中斷。

##方法
boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException;




Lock.java
Acquires the lock if it is free within the given waiting time and the current thread has not been interrupted.
If the lock is available this method returns immediately with the value true. If the lock is not available then the current thread becomes disabled for thread scheduling purposes and lies dormant until one of three things happens:
The lock is acquired by the current thread; or
Some other thread interrupts the current thread, and interruption of lock acquisition is supported; or
The specified waiting time elapses
If the lock is acquired then the value true is returned.
##中文翻譯
如果鎖在給定的等待時間內空閒並且當前執行緒未被中斷,則獲取鎖。
如果鎖可用,則此方法立即返回值true。如果鎖不可用,則出於執行緒排程目的,當前執行緒將被禁用,並處於休眠狀態,直到發生以下三種情況之一:
鎖由當前執行緒獲取;或
其他執行緒中斷當前執行緒,支援中斷鎖獲取;或
指定的等待時間已過
如果獲取了鎖,則返回值true。

If the current thread:
has its interrupted status set on entry to this method; or
is interrupted while acquiring the lock, and interruption of lock acquisition is supported,
then InterruptedException is thrown and the current thread's interrupted status is cleared.
If the specified waiting time elapses then the value false is returned. If the time is less than or equal to zero, the method will not wait at all.
Implementation Considerations
##中文翻譯
如果當前執行緒:
在進入此方法時設定其中斷狀態;或
在獲取鎖的同時被中斷並且支援鎖獲取的中斷,
則丟擲InterruptedException,並清除當前執行緒的中斷狀態。
如果經過了指定的等待時間,則返回值false。如果時間小於或等於零,則該方法根本不會等待。
實施注意事項


The ability to interrupt a lock acquisition in some implementations may not be possible, and if possible may be an expensive operation. The programmer should be aware that this may be the case. An implementation should document when this is the case.
An implementation can favor responding to an interrupt over normal method return, or reporting a timeout.
A Lock implementation may be able to detect erroneous use of the lock, such as an invocation that would cause deadlock, and may throw an (unchecked) exception in such circumstances. The circumstances and the exception type must be documented by that Lock implementation.
Params:
time – the maximum time to wait for the lock
unit – the time unit of the time argument
##中文翻譯
在某些實現中,中斷鎖獲取的能力可能是不可能的,如果可能的話,這可能是一項昂貴的操作。程式設計師應該意識到這可能是事實。在這種情況下,實施應該記錄下來。
一個實現可能更傾向於響應中斷而不是正常的方法返回,或者報告超時。
Lock實現可能能夠檢測鎖的錯誤使用,例如會導致死鎖的呼叫,並在這種情況下丟擲(未檢查的)異常。該Lock實現必須記錄情況和異常型別。
引數:
time–等待鎖的最長時間
unit–時間引數的時間單位

Returns:
true if the lock was acquired and false if the waiting time elapsed before the lock was acquired
Throws:
InterruptedException – if the current thread is interrupted while acquiring the lock (and interruption of lock acquisition is supported)
##中文翻譯
退貨:
如果獲取了鎖,則為true,如果在獲取鎖之前經過了等待時間,則為false
投擲:
InterruptedException-如果當前執行緒在獲取鎖時中斷(並且支援中斷鎖獲取)

##方法
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

補充:
redis 分散式鎖的 5個坑
1.鎖未被釋放
拿到鎖的執行緒處理完業務及時釋放鎖,如果是重入鎖未拿到鎖後,執行緒可以釋放當前連線並且sleep一段時間。
RLock lock = redissonClient.getLock("stockLock");
finally {
lock.unlock();
}

// 釋放當前redis連線
redis.close();
// 休眠1000毫秒
sleep(1000);

2.B的鎖被A給釋放了
Redis實現鎖的原理在於 SETNX命令。當 key不存在時將 key的值設為 value ,返回值為 1;若給定的 key 已經存在,則 SETNX不做任何動作,返回值為 0
SETNX key value
A、B兩個執行緒來嘗試給key myLock加鎖,A執行緒先拿到鎖(假如鎖3秒後過期),B執行緒就在等待嘗試獲取鎖,到這一點毛病沒有。
那如果此時業務邏輯比較耗時,執行時間已經超過redis鎖過期時間,這時A執行緒的鎖自動釋放(刪除key),B執行緒檢測到myLock這個key不存在,執行 SETNX命令也拿到了鎖。
但是,此時A執行緒執行完業務邏輯之後,還是會去釋放鎖(刪除key),這就導致B執行緒的鎖被A執行緒給釋放了。
為避免上邊的情況,一般我們在每個執行緒加鎖時要帶上自己獨有的value值來標識,只釋放指定value的key,否則就會出現釋放鎖混亂的場景。

3.資料庫事務超時

@Transaction
   public void lock() {
   
        while (true) {
            boolean flag = this.getLock(key);
            if (flag) {
                insert();
            }
        }
    }

比如:我們解析一個大檔案,再將資料存入到資料庫,如果執行時間太長,就會導致事務超時自動回滾。
一旦你的key長時間獲取不到鎖,獲取鎖等待的時間遠超過資料庫事務超時時間,程式就會報異常。
一般為解決這種問題,我們就需要將資料庫事務改為手動提交、回滾事務。

@Autowired
    DataSourceTransactionManager dataSourceTransactionManager;
	
    @Transaction
    public void lock() {
        //手動開啟事務
        TransactionStatus transactionStatus = dataSourceTransactionManager.getTransaction(transactionDefinition);
        try {
            while (true) {
                boolean flag = this.getLock(key);
                if (flag) {
                    insert();
                    //手動提交事務
                    dataSourceTransactionManager.commit(transactionStatus);
                }
            }
        } catch (Exception e) {
            //手動回滾事務
            dataSourceTransactionManager.rollback(transactionStatus);
        }
	}

4.鎖過期了,業務還沒執行完
我們可以在加鎖的時候,手動調長redis鎖的過期時間,可這個時間多長合適?業務邏輯的執行時間是不可控的,調的過長又會影響操作效能。
要是redis鎖的過期時間能夠自動續期就好了
為了解決這個問題我們使用redis客戶端redisson,redisson很好的解決了redis在分散式環境下的一些棘手問題,它的宗旨就是讓使用者減少對Redis的關注,將更多精力用在處理業務邏輯上。
redisson在加鎖成功後,會註冊一個定時任務監聽這個鎖,每隔10秒就去檢視這個鎖,如果還持有鎖,就對過期時間進行續期。預設過期時間30秒。這個機制也被叫做:“看門狗”
舉例子:假如加鎖的時間是30秒,過10秒檢查一次,一旦加鎖的業務沒有執行完,就會進行一次續期,把鎖的過期時間再次重置成30秒。

5.redis主從複製
redis cluster叢集環境下,假如現在A客戶端想要加鎖,它會根據路由規則選擇一臺master節點寫入key mylock,在加鎖成功後,master節點會把key非同步複製給對應的slave節點。
如果此時redis master節點當機,為保證叢集可用性,會進行主備切換,slave變為了redis master。B客戶端在新的master節點上加鎖成功,而A客戶端也以為自己還是成功加了鎖的。
此時就會導致同一時間內多個客戶端對一個分散式鎖完成了加鎖,導致各種髒資料的產生。
至於解決辦法嘛,目前看還沒有什麼根治的方法,只能儘量保證機器的穩定性,減少發生此事件的機率

如果在某個時間進行主備切換,很有可能在預備slave 上還沒有master節點的鎖。具體流程如下
1.Redis的master節點上拿到了鎖;
2.但是這個加鎖的key還沒有同步到slave節點;
3.master故障,發生故障轉移,slave節點升級為master節點;
最終導致 導致鎖丟失。
在這個背景下,Redis作者antirez基於分散式環境下提出了一種更高階的分散式鎖的實現方式:Redlock。

我理解的演算法大致如下
假設有N個Redis 節點。這些節點完全互相獨立,不存在主從複製或者其他叢集協調機制。確保將在N個例項上使用與在Redis單例項下相同方法獲取和釋放鎖。
這裡重點就是 完全互相獨立!

演示一下簡單操作:

RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
// 這裡的lock1  lock2   lock3 就是從各個redis節點獲取的 鎖
boolean isLock;
try {
    isLock = redLock.tryLock(500, 30000, TimeUnit.MILLISECONDS);
    System.out.println("isLock = "+isLock);
    if (isLock) {
        //TODO if get lock success, do something;
        Thread.sleep(30000);
    }
} catch (Exception e) {
} finally {
    // 無論如何, 最後都要解鎖
    System.out.println("");
    redLock.unlock();
}


最大的變化就是RedLock 的初始化RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3); 這裡選擇的是三個節點, 可以選擇多個。

相關文章