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


前面講解到實戰問題】-- 設計禮品領取的架構設計以及多次領取現象解決?,如果出現網絡延遲的情況下,多個請求阻塞,那么惡意攻擊就可以全部請求領取接口成功,而針對這種做法,我們使用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 "領取失敗,活動不存在";
    }

下面,我們就專門講解一下setnxsetnx可以用作分布式鎖,但是這個場景並不是分布式鎖的一個較好的實踐,因為每個用戶的key都是不一樣的,我們主要是防止同一個用戶惡意領取setnx本身是一個原子操作,可以保證多個線程只有一個能拿到鎖,能返回true,其他的都會返回false

但是上面的做法,沒有設置過期時間,在生產上一般是不可以這么使用。不設置過期時間的key多了之后,redis服務器很容易內存打滿,這時候不知道哪些是強制依賴的,只能擴容,從代碼層面去清理,如果直接清理不常用的,也很難保證不出事。(基本不允許這么干,除非是基礎數據,跟着服務器啟動,寫入redis的,不會變更的,比如城市數據,國家數據等等,當然,這些也可以考慮在本地內存中實現)

如果在上面的代碼中,加入超時時間,假設是一個月或者半年,流程變成這樣:

設置key的超時時間使用expire,但是這樣還有缺陷么?

redis 2.6.12之前,setnxexpire都不是原子操作,也就是很有可能在setnx成功之后,redis當季,expire設置失敗,也就不會有超時時間了。雖然這個影響在當前業務不是很大,但是還是一個小缺陷。

Redis2.6.12以上版本,可以用set獲取鎖,set包含setnxexpire,實現了原子操作。也就是兩步要么一起成功,要么一起失敗。

除此之外,上面的流程可能還存在的一個問題,是請求C服務的時候出現超時,然后刪除key,恰好這個時候redis有問題,刪除失敗了,這個key就永遠存在了。表現在業務上,就是A用戶點擊了領取,領取失敗了,但是后面再怎么點,都是已經領取的狀態了。

那這種現象怎么優化呢?

這種情況,其實已經是很少見的情況,按照我們當前的業務場景也看,就是當前的用戶,redis記錄了它已經領取過了,但是由於接口的失敗,成功之后還沒將mysql/其他數據庫更新,兩個數據庫不一致了。

我能想到的一個方法,就是再刪除失敗的時候,告警,並且將業務相關的數據記錄下來,比如keyuid等等,針對這部分數據,做一次補發,或者手動刪除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等,認真寫好每一篇文章,不喜歡標題黨,不喜歡花里胡哨,大多寫系列文章,不能保證我寫的都完全正確,但是我保證所寫的均經過實踐或者查找資料。遺漏或者錯誤之處,還望指正。

2020年我寫了什么?

開源刷題筆記

平日時間寶貴,只能使用晚上以及周末時間學習寫作,關注我,我們一起成長吧~


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM