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