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

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

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

image.png

我們在進入請求之後首選嘗試獲取鎖對象,那麼這個鎖對象的鍵其實就是用戶的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); 
}



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章