1. 程式人生 > >【實戰問題】-- 併發的時候分散式鎖setnx細節

【實戰問題】-- 併發的時候分散式鎖setnx細節

前面講解到[實戰問題】-- 設計禮品領取的架構設計以及多次領取現象解決?](https://mp.weixin.qq.com/s?__biz=MzA3NTUwNzk0Mw==&mid=2729166936&idx=1&sn=c47c3cb443c10c08c63993c57ed5e953&chksm=b83019d08f4790c63888064e6c1576a7d575ed9a3b9037cf32427326c388a5abf39527aa4ea6&token=1888775354&lang=zh_CN#rd),如果出現網路延遲的情況下,多個請求阻塞,那麼惡意攻擊就可以全部請求領取介面成功,而針對這種做法,我們使用`setnx`來解決,確保只有一個請求可以進入介面請求。 ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/20210226230957.png) ```java 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`的,不會變更的,比如城市資料,國家資料等等,當然,這些也可以考慮在本地記憶體中實現) 如果在上面的程式碼中,加入超時時間,假設是一個月或者半年,流程變成這樣: ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/20210228165201.png) 設定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`了。 可以使用以下的命令: ```shell SETNX lock.foo ``` 關於這個場景下的`setnx`先講到這裡,後面再講講分散式鎖相關的知識。 > **【刷題筆記】** > Github倉庫地址:https://github.com/Damaer/codeSolution > 筆記地址:https://damaer.github.io/codeSolution/ **【作者簡介】**: 秦懷,公眾號【**秦懷雜貨店**】作者,技術之路不在一時,山高水長,縱使緩慢,馳而不息。個人寫作方向:Java原始碼解析,JDBC,Mybatis,Spring,redis,分散式,劍指Offer,LeetCode等,認真寫好每一篇文章,不喜歡標題黨,不喜歡花裡胡哨,大多寫系列文章,不能保證我寫的都完全正確,但是我保證所寫的均經過實踐或者查詢資料。遺漏或者錯誤之處,還望指正。 [2020年我寫了什麼?](http://aphysia.cn/archives/2020) [開源刷題筆記](https://damaer.github.io/CodeSolution/#/) 平日時間寶貴,只能使用晚上以及週末時間學習寫作,關注我,我們一起成