1. 程式人生 > >Redis如何保證介面的冪等性?

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,再執行我們後面的業務邏輯,如果獲取鎖失敗,會獲取當前時間再加上設定的過期時間,跟當前時間比較,如果還在等待時間內,那麼就再次嘗試獲取鎖,直到過了等待時間。

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

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

@Test
public 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);
}