使用Redis實現一個分布式鎖---怎么保證冪等性?


在最近的一次業務升級中,遇到這樣一個問題,我們設計了新的賬戶體系,需要在用戶將應用升級之后將原來賬戶的數據手動的同步過來,就是需要用戶自己去觸發同步按鈕進行同步,因為有些數據是用戶存在自己本地的。那么在這個過程中就存在一個問題,要是因為網絡的問題,用戶重復點擊了這個按鈕怎么辦?就算我們在客戶端做了一些處理,在同步的過程中,不能再次點擊,但是經過我最近的爬蟲實踐,要是別人抓到了我們的接口那么還是不安全的。

基於這樣的業務場景,我就使用Redis加鎖的方式,限制了用戶在請求的時候,不能發起二次請求。

 

 

 

我們在進入請求之后首選嘗試獲取鎖對象,那么這個鎖對象的鍵其實就是用戶的id,如果獲取成功,我們判斷用戶時候已經同步數據,如果已同步,那么可以直接返回,提示用戶已經同步,如果沒有那么直接執行同步數據的業務邏輯,最后將鎖釋放,如果在進入方法之后獲取鎖失敗,那么有可能就是在第一次請求還沒有結束的時候,接着又發起了請求,那么這個時候是獲取不到鎖的,也就不會發生數據同步出現同步好幾次的情況。

 

華麗的分割線

 

那么有了這個需求之后,我們就來用Redis實現以下這個代碼。首先我們要知道我們要介紹一下Redis的一個方法。

那么我們想要用Redis做用戶唯一的鎖對象,那么它在Redis中應該是唯一的,而且還不應該被覆蓋,這個方法就是存儲成功之后會返回true,如果該元素已經存在於Redis實例中,那么直接返回false

setIfAbsent(key,value)

但是這中間又存在一個問題,如果在獲取了鎖對象之后,我們的服務掛了,那么這個時候其他請求肯定是拿不到鎖的,基於這種情況的考慮我們還應該給這個元素添加一個過期時間,防止我們的服務掛掉之后,出現死鎖的問題。

    /**
* 添加元素 * * @param key * @param value
*/
public void set(Object key, Object value) {
if (key == null || value == null) {
return;
}
redisTemplate.opsForValue().set(key, value.toString());
}

/**
* 如果已經存在返回false,否則返回true * * @param key * @param value * @return
*/
public Boolean setNx(Object key, Object value, Long expireTime, TimeUnit mimeUnit) {
if (key == null || value == null) {
return false;
}
return redisTemplate.opsForValue().setIfAbsent(key, value, expireTime, mimeUnit);
}

/**
* 獲取數據 * * @param key * @return
*/
public Object get(Object key) {
if (key == null) {
return null;
}
return redisTemplate.opsForValue().get(key);
}

/**
* 刪除 * * @param key * @return
*/
public Boolean remove(Object key) {
if (key == null) {
return false;
}
return redisTemplate.delete(key);
}

/**
* 加鎖 * * @param key * @param waitTime 等待時間 * @param expireTime 過期時間
*/
public Boolean lock(String key, Long waitTime, Long expireTime) {
String value = UUID.randomUUID().toString().replaceAll("-", "").toLowerCase();
Boolean flag = setNx(key, value, expireTime, TimeUnit.MILLISECONDS);
// 嘗試獲取鎖 成功返回 if (flag) { return flag; } else { // 獲取失敗
// 現在時間 long newTime = System.currentTimeMillis();
// 等待過期時間 long loseTime = newTime + waitTime;
// 不斷嘗試獲取鎖成功返回 while (System.currentTimeMillis() < loseTime) {
Boolean testFlag = setNx(key, value, expireTime, TimeUnit.MILLISECONDS);
if (testFlag) {
return testFlag;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} return false;}
/**
* 釋放鎖 * * @param key * @return
*/public Boolean unLock(Object key){return remove(key);}

 

 

我們整個加鎖的代碼邏輯已經寫完了,我們來分析一下,用戶在進來之后,首先調用lock嘗試獲取鎖,並進行加鎖,lock()方法有三個參數分別是:key,waitTime就是用戶如果獲取不到鎖,可以等待多久,過了這個時間就不再等待,最后一個參數就是該鎖的多久后過期,防止服務掛了之后,發生死鎖。

當進入lock()之后,先進行加鎖操作,如果加鎖成功,那么返回true,再執行我們后面的業務邏輯,如果獲取鎖失敗,會獲取當前時間再加上設置的過期時間,跟當前時間比較,如果還在等待時間內,那么就再次嘗試獲取鎖,直到過了等待時間。

 

注意:在設置值的時候,我們為了防止死鎖設置了一個過期時間,大家一定要注意,不要等設置成功之后再去給元素設置過期時間,因為這個過程不是一個原子操作,等你剛設置成功之后,還沒等設置過期時間成功,服務直接掛了,那么這個時候就會發生死鎖問題,所以大家要保證存儲元素和設置過期時間一定要是原子操作。

 

最后我們來寫個測試類測試一下

    @Testpublic
    void test01() {
        String key = "uid:12011";
        Boolean flag = redisUtil.lock(key, 10L, 1000L * 60);
        if (!flag) {
            // 獲取鎖失敗        System.err.println("獲取鎖失敗");    } else {
            // 獲取鎖成功        System.out.println("獲取鎖成功");    }
            // 釋放鎖    redisUtil.unLock(key);}

 

 

轉載自:一個程序員的成長公眾號


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM