作者:冰峰編者說:比較實用的redis加鎖的方式,代碼段可以收藏。
在最近的一次業(yè)務(wù)升級中,遇到這樣一個問題,我們設(shè)計了新的賬戶體系,需要在用戶將應(yīng)用升級之后將原來賬戶的數(shù)據(jù)手動的同步過來,就是需要用戶自己去觸發(fā)同步按鈕進(jìn)行同步,因為有些數(shù)據(jù)是用戶存在自己本地的。那么在這個過程中就存在一個問題,要是因為網(wǎng)絡(luò)的問題,用戶重復(fù)點擊了這個按鈕怎么辦?就算我們在客戶端做了一些處理,在同步的過程中,不能再次點擊,但是經(jīng)過我最近的爬蟲實踐,要是別人抓到了我們的接口那么還是不安全的。
基于這樣的業(yè)務(wù)場景,我就使用Redis加鎖的方式,限制了用戶在請求的時候,不能發(fā)起二次請求。
我們在進(jìn)入請求之后首選嘗試獲取鎖對象,那么這個鎖對象的鍵其實就是用戶的id,如果獲取成功,我們判斷用戶時候已經(jīng)同步數(shù)據(jù),如果已同步,那么可以直接返回,提示用戶已經(jīng)同步,如果沒有那么直接執(zhí)行同步數(shù)據(jù)的業(yè)務(wù)邏輯,最后將鎖釋放,如果在進(jìn)入方法之后獲取鎖失敗,那么有可能就是在第一次請求還沒有結(jié)束的時候,接著又發(fā)起了請求,那么這個時候是獲取不到鎖的,也就不會發(fā)生數(shù)據(jù)同步出現(xiàn)同步好幾次的情況。
華麗的分割線
那么有了這個需求之后,我們就來用Redis實現(xiàn)以下這個代碼。首先我們要知道我們要介紹一下Redis的一個方法。
那么我們想要用Redis做用戶唯一的鎖對象,那么它在Redis中應(yīng)該是唯一的,而且還不應(yīng)該被覆蓋,這個方法就是存儲成功之后會返回true,如果該元素已經(jīng)存在于Redis實例中,那么直接返回false
setIfAbsent(key,value)
但是這中間又存在一個問題,如果在獲取了鎖對象之后,我們的服務(wù)掛了,那么這個時候其他請求肯定是拿不到鎖的,基于這種情況的考慮我們還應(yīng)該給這個元素添加一個過期時間,防止我們的服務(wù)掛掉之后,出現(xiàn)死鎖的問題。
/** * 添加元素 * * @param key * @param value */ public void set(Object key, Object value) { if (key == null || value == null) { return; } redisTemplate.opsForValue().set(key, value.toString()); } /** * 如果已經(jīng)存在返回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); } /** * 獲取數(shù)據(jù) * * @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 { // 獲取失敗 // 現(xiàn)在時間 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); }
我們整個加鎖的代碼邏輯已經(jīng)寫完了,我們來分析一下,用戶在進(jìn)來之后,首先調(diào)用lock嘗試獲取鎖,并進(jìn)行加鎖,lock()方法有三個參數(shù)分別是:key,waitTime就是用戶如果獲取不到鎖,可以等待多久,過了這個時間就不再等待,最后一個參數(shù)就是該鎖的多久后過期,防止服務(wù)掛了之后,發(fā)生死鎖。
當(dāng)進(jìn)入lock()之后,先進(jìn)行加鎖操作,如果加鎖成功,那么返回true,再執(zhí)行我們后面的業(yè)務(wù)邏輯,如果獲取鎖失敗,會獲取當(dāng)前時間再加上設(shè)置的過期時間,跟當(dāng)前時間比較,如果還在等待時間內(nèi),那么就再次嘗試獲取鎖,直到過了等待時間。
注意:在設(shè)置值的時候,我們?yōu)榱朔乐顾梨i設(shè)置了一個過期時間,大家一定要注意,不要等設(shè)置成功之后再去給元素設(shè)置過期時間,因為這個過程不是一個原子操作,等你剛設(shè)置成功之后,還沒等設(shè)置過期時間成功,服務(wù)直接掛了,那么這個時候就會發(fā)生死鎖問題,所以大家要保證存儲元素和設(shè)置過期時間一定要是原子操作。
最后我們來寫個測試類測試一下
@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);
}