前面講解到實戰問題】-- 設計禮品領取的架構設計以及多次領取現象解決?,如果出現網絡延遲的情況下,多個請求阻塞,那么惡意攻擊就可以全部請求領取接口成功,而針對這種做法,我們使用setnx
來解決,確保只有一個請求可以進入接口請求。
public String receiveGitf(int activityId,int giftId,String uid){
// isExist判斷活動是否存在,內部包括redis和數據庫請求,省略
if(isActivityExist(activityId,giftId)){
// 活動和禮品有效,判斷是否領取過
if(!userReceived(uid,activityId,giftId)){
// 沒有領取過,調用C系統
try {
// setnx
if(redis.setnx("uid_activityId_giftId")){
boolean receivedResult = Http.getMethod(C_Client.class, "distributeGift");
if(receivedResult){
// 領取成功更新mysql
updateMysql(uid,activityId,giftId);
}else{
// 領取成功更新redis
deleteRedis(uid,activityId,giftId);
return "已經領過/領取失敗";
}
}else{
return "已經領過/領取失敗";
}
}catch (Exception e){
// 記錄日志
logHelper.log(e);
return "調用領券系統失敗,請重試";
}
}
}
return "領取失敗,活動不存在";
}
下面,我們就專門講解一下setnx
,setnx
可以用作分布式鎖,但是這個場景並不是分布式鎖的一個較好的實踐,因為每個用戶的key都是不一樣的,我們主要是防止同一個用戶惡意領取,setnx
本身是一個原子操作,可以保證多個線程只有一個能拿到鎖,能返回true
,其他的都會返回false
。
但是上面的做法,沒有設置過期時間,在生產上一般是不可以這么使用。不設置過期時間的key多了之后,redis服務器很容易內存打滿,這時候不知道哪些是強制依賴的,只能擴容,從代碼層面去清理,如果直接清理不常用的,也很難保證不出事。(基本不允許這么干,除非是基礎數據,跟着服務器啟動,寫入redis
的,不會變更的,比如城市數據,國家數據等等,當然,這些也可以考慮在本地內存中實現)
如果在上面的代碼中,加入超時時間,假設是一個月或者半年,流程變成這樣:
設置key的超時時間使用expire
,但是這樣還有缺陷么?
在redis 2.6.12
之前,setnx
和expire
都不是原子操作,也就是很有可能在setnx
成功之后,redis當季,expire設置失敗,也就不會有超時時間了。雖然這個影響在當前業務不是很大,但是還是一個小缺陷。
Redis2.6.12
以上版本,可以用set
獲取鎖,set包含setnx
和expire
,實現了原子操作。也就是兩步要么一起成功,要么一起失敗。
除此之外,上面的流程可能還存在的一個問題,是請求C
服務的時候出現超時,然后刪除key,恰好這個時候redis
有問題,刪除失敗了,這個key
就永遠存在了。表現在業務上,就是A
用戶點擊了領取,領取失敗了,但是后面再怎么點,都是已經領取的狀態了。
那這種現象怎么優化呢?
這種情況,其實已經是很少見的情況,按照我們當前的業務場景也看,就是當前的用戶,redis
記錄了它已經領取過了,但是由於接口的失敗,成功之后還沒將mysql/其他數據庫
更新,兩個數據庫不一致了。
我能想到的一個方法,就是再刪除失敗的時候,告警,並且將業務相關的數據記錄下來,比如key
,uid
等等,針對這部分數據,做一次補發,或者手動刪除key。
或者,啟動一個定時任務或者lua
腳本,去判定redis
和數據庫不一致的情況,但是切記不要全部查詢,應該是隔一段時間,查詢最后增加的部分,做一個校驗以及相應的處理。枚舉key
是十分耗時的操作!!!
setnx
除了解決上面的問題,還可以應用在解決緩存擊穿的問題上。
譬如現在有熱點數據,不僅在mysql
數據庫存儲了,還在redis
中存了一份緩存,那么如果有一個時間點,緩存失效了,這時候,大量的請求打過來,同時到達,緩存拿不到數據,都去數據庫取數據,假設數據庫操作比較耗時,那么壓力全都在數據庫服務器上了。
這個時候所有的請求都去更新數據,明顯是不合適的,應該是使用分布式鎖,讓一個線程去請求mysql
一次即可。但是為了避免死鎖的情況,如果超時,得及時額外釋放鎖,要不可能請求mysql
都失敗了,其他線程又拿不到鎖,那么數據就會一直為null
了。
可以使用以下的命令:
SETNX lock.foo <current Unix time + lock timeout + 1>
關於這個場景下的setnx
先講到這里,后面再講講分布式鎖相關的知識。
【刷題筆記】
Github倉庫地址:https://github.com/Damaer/codeSolution
筆記地址:https://damaer.github.io/codeSolution/
【作者簡介】:
秦懷,公眾號【秦懷雜貨店】作者,技術之路不在一時,山高水長,縱使緩慢,馳而不息。個人寫作方向:Java源碼解析,JDBC,Mybatis,Spring,redis,分布式,劍指Offer,LeetCode等,認真寫好每一篇文章,不喜歡標題黨,不喜歡花里胡哨,大多寫系列文章,不能保證我寫的都完全正確,但是我保證所寫的均經過實踐或者查找資料。遺漏或者錯誤之處,還望指正。
平日時間寶貴,只能使用晚上以及周末時間學習寫作,關注我,我們一起成長吧~