Redis如何保證接口的冪等性?

在最近的一次業務升級中,遇到這樣一個問題,咱們設計了新的帳戶體系,須要在用戶將應用升級以後將原來帳戶的數據手動的同步過來,就是須要用戶本身去觸發同步按鈕進行同步,由於有些數據是用戶存在本身本地的。那麼在這個過程當中就存在一個問題,要是由於網絡的問題,用戶重複點擊了這個按鈕怎麼辦?就算咱們在客戶端作了一些處理,在同步的過程當中,不能再次點擊,可是通過我最近的爬蟲實踐,要是別人抓到了咱們的接口那麼仍是不安全的。redis

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




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


華麗的分割線網絡


那麼有了這個需求以後,咱們就來用Redis實現如下這個代碼。首先咱們要知道咱們要介紹一下Redis的一個方法。dom

那麼咱們想要用Redis作用戶惟一的鎖對象,那麼它在Redis中應該是惟一的,並且還不該該被覆蓋,這個方法就是存儲成功以後會返回true,若是該元素已經存在於Redis實例中,那麼直接返回false測試

setIfAbsent(key,value)複製代碼

可是這中間又存在一個問題,若是在獲取了鎖對象以後,咱們的服務掛了,那麼這個時候其餘請求確定是拿不到鎖的,基於這種狀況的考慮咱們還應該給這個元素添加一個過時時間,防止咱們的服務掛掉以後,出現死鎖的問題。ui

/**
 * 添加元素
 *
 * @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就是用戶若是獲取不到鎖,能夠等待多久,過了這個時間就再也不等待,最後一個參數就是該鎖的多久後過時,防止服務掛了以後,發生死鎖。
spa

當進入lock()以後,先進行加鎖操做,若是加鎖成功,那麼返回true,再執行咱們後面的業務邏輯,若是獲取鎖失敗,會獲取當前時間再加上設置的過時時間,跟當前時間比較,若是還在等待時間內,那麼就再次嘗試獲取鎖,直到過了等待時間。設計


注意:在設置值的時候,咱們爲了防止死鎖設置了一個過時時間,你們必定要注意,不要等設置成功以後再去給元素設置過時時間,由於這個過程不是一個原子操做,等你剛設置成功以後,還沒等設置過時時間成功,服務直接掛了,那麼這個時候就會發生死鎖問題,因此你們要保證存儲元素和設置過時時間必定要是原子操做。code

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

@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);
}複製代碼
相關文章
相關標籤/搜索