Redis使用指南(避免踩坑)

能堅持別人不能堅持的,才能擁有別人未曾擁有的。
關注編程大道公衆號,讓我們一同堅持心中所想,一起成長!!

設置過期時間、釋放資源

使用Redis做K-V存儲,一定要注意過期時間的把控,任何K-V的存儲都要設置過期時間,不管多長時間。一般在封裝Redis操作工具類時提供默認使用系統公共超時時間的操作API,避免新手在使用時不設置過期時間,導致內存的浪費。另外,通過連接池 Jedis jedis = JedisPool.getResource(); 這樣獲取Redis連接最好使用try/finally塊,並且在finally塊中調用 jedis.close(); 將連接歸還給連接池,否則將會一直持有連接,很有可能導致在將來的某一時刻報拿不到連接的錯。這也是之前某一個同事犯過的錯導致生產bug!

緩存穿透

你以爲Redis做緩存就萬無一失嗎?就單純的遵循那種經典操作嗎?(即:請求來了,先看緩存有沒有,有直接返回,沒有就查數據庫,數據庫有的話先存緩存,然後返回,數據庫沒有就返回空)這樣就是Redis緩存的正確姿勢嗎?如果你這樣做,很可能疏忽一點,那就是緩存穿透。如之前在項目中做的一個需求-頁面廣告可配置化自動上下線(我在之前專門寫過一篇文章介紹這個需求的一步步演進過程,對Redis新手很有幫助,感興趣的可以去看看),簡單的提一下吧,就是比如在支付完成的頁面大家都應該見過吧,比如支付完成後的結果頁,可能會彈出來一個紅包什麼的,頁面下方的廣告位等,就是類似的這樣一個需求。因爲這個頁面訪問量很大,進這個頁面就查這個廣告位的數據,當運營最近不想配置廣告了,這邊查到的是不是就是是空啊?數據庫也是空的,緩存也沒有數據,那很多請求都來,這樣就平白無故的造成了數據庫的壓力呀,多麼的浪費!如果是別的其他業務,黑客鑽了空子,專門請求你係統根本不存在的數據,請求多了,都打到數據庫,是很有可能把你數據庫打死的。如果你在做需求的時候沒想到這一點,那後續出了問題,你就等着背鍋了。

怎麼避免呢?

好辦,可以將數據庫也不存在的數據存個null值或一個空json(總之你自己約定好就行),也給放到Redis裏,設置個較短的過期時間,下次再來取的時候看到是空就直接返回。另外,可以使用布隆過濾器做一層系統級的防護,專門去攔截系統中根本不存在的key。

緩存雪崩

剛說完緩存穿透,再聊聊緩存雪崩。比如你將用戶數據放到緩存裏,當某一時刻這些數據全部都過期了,大量請求都過來,發現緩存無法命中,不就都去數據庫了嗎,數據庫一下子來這麼多請求不就搞掛了嗎?解決辦法就是儘量是key的過期時間分散開,不要集中。在一個固定的過期時間上+一個隨機值,比如你設置的過期時間是5小時,你可以加一個0-600秒的隨機值。

緩存併發

緩存失效時多個請求同時請求同一個key,都發現緩存中空了,都去查數據庫,這不是浪費嗎,正常一個去查就行了,查完放緩存別的請求直接從緩存拿就行了。這就是緩存併發問題。當請求非常的多的時候,會對數據庫造成很大的衝擊,也是有可能把數據庫搞掛的吧?怎麼解決,可以對更新緩存的操作加鎖,使用synchronized嗎?不行,因爲生產上是分佈式部署的,需要使用redis分佈式鎖。

例如,當緩存數據失效的時候,某一線程使用資源ID作爲key嘗試加分佈式鎖,加鎖成功的線程執行更新緩存的操作將查到的數據放入緩存緩存中,其他線程就可以直接使用緩存數據了。

分佈式鎖

正如上面所說,在集羣部署的情況下synchronized就失效了,所以分佈式鎖就派上用場了。常見的分佈式鎖的實現方式有三種:基於數據庫,基於Redis,基於Zookeeper。

Redis分佈式鎖需要特別注意的點就是鎖的過期時間,如,使用redis的setnx命令,設置成功即表示拿到鎖,然後設置過期時間,命令執行失敗的線程表示獲取鎖失敗。一定要注意鎖的過期時間的設置,有加鎖的操作,也要有解鎖的操作。如之前我們項目的一個臨時性的一個組團競走的活動,10人成團競走PK的活動,在組團階段,用戶可以邀請朋友加入自己的團。我們的團數據是存放在Redis中的,包括每個團的人數。當用戶發起入團操作時,後臺邏輯會從redis取該團的現有成員數,如果小於10才能繼續走下面的邏輯。當併發場景下,如團長分享給很多人入團邀請,這些人的入團請求併發執行的情況下很有可能能造成組團人數超過10人的情況。因爲在併發場景下,執行獲取當前團成員數的這行代碼會被多個請求獲取到,比如臨界的時候,團成員已經有了9個,同時來了倆入團請求,如果不加控制,同時執行讀取現有團成員個數時都讀到的是9,然後都執行入團操作,就會造成團成員超過10人的bug。

所以在入團請求的邏輯上,要加分佈式鎖,獲取到鎖才能執行後續邏輯。因爲獲取鎖的操作是使用setnx命令,並沒有等待鎖的機制,我們需要在獲取鎖的邏輯加一個自旋,每隔一定時間嘗試一次獲取,超過一定時間後返回加鎖失敗。

public boolean tryLock(String lockKey,long expireTime){
    long waitTime = 0;
    //setIfAbsent使用的是redis的setnx方法
    boolean success = redisTemplate.opsForValue().setIfAbsent(lockKey,"jingzouLock",
            expireTime,TimeUnit.MILLISECONDS);
    if(success==true){
        return success;
    }else{
        while(success==false && waitTime <50000L){
            success = redisTemplate.opsForValue().setIfAbsent(lockKey,"jingzouLock",
                    expireTime, TimeUnit.MILLISECONDS);
            try{
                Thread.sleep(100);
            }catch(Exception e){}
            waitTime+=100L;
        }
    }
    return success;
}

另外,還需要遵循“解鈴還須繫鈴人”的原則,誰加的鎖誰解,不然自己加的鎖,被別人解了也是會造成問題的。例如,用戶A,請求入團,拿到分佈式鎖,如果A因爲某些原因在鎖超時時間內沒有執行完代碼,鎖就過期自動釋放了,如果此時B請求加入同一個團,拿到了分佈式鎖,如果此時A請求執行完了,釋放鎖了,但是釋放的是B的鎖,這樣也有可能造成團人數超過10的bug。所以,設置分佈式鎖時的value可以設置成不同的值,如A請求是用戶ID爲12的用戶,設置分佈式鎖的時候就value就可以用這個唯一的元素,當解鎖的時候再驗證value是12時才能執行解鎖操作。

如上加鎖代碼,我們增加一個參數String value傳入動態值,在上述場景中可以用用戶ID,代替我們寫死的"jingzouLock"。然後在釋放鎖的方法裏,我們先判斷value值,相同再執行刪除。

public void releaseLock(String lockKey,String value){
    String valueInRedis = redisTemplate.get(lockKey);
    if(value.equals(valueInRedis)){
        redisTemplate.delete(lockKey);
    }
}

還有一種場景需要考慮。當Redis master發生故障,主備切換時往往會造成數據丟失,包括分佈式鎖的Key-Value。這樣就會導致鎖間接的被釋放了,假如操作還沒執行完,鎖被其他請求拿到了,分佈式鎖就起不到作用了。

考慮到這方面的問題,Redis官方提供了Redlock算法,以及相應的開源實現Redisson。用到分佈式鎖的場景,大家可以直接使用 Redisson,非常方便,後期可能會寫一寫Redisson的技術乾貨。

另外,如果系統對可靠性要求很高,如需用到分佈式鎖,建議使用分佈式鎖的另外實現方式,如:Zookeeper,etcd等。

好了,今天就分享到這。如果感覺本文對您有幫助,有勞點下在看,把知識分享給更多的人哦

更多幹貨文章,歡迎關注公衆號

你可能感興趣的文章:

《[需求設計]從一個小需求感受Redis的獨特魅力》 

《【面試突擊】— Redis篇》--Redis數據類型?適用於哪些場景?

《【面試突擊】— Redis篇》--Redis的線程模型瞭解嗎?爲啥單線程效率還這麼高?

《【面試突擊】— Redis篇》-- Redis的主從複製?哨兵機制?

《【面試突擊】— Redis篇》-- Redis哨兵原理及持久化機制

《【面試突擊】— Redis篇》--Redis Cluster及緩存使用和架構設計的常見問題

《你真的瞭解Redis的發佈訂閱?》 

《Redis緩存穿透,緩存擊穿,緩存雪崩,熱點Key》

《高併發場景下緩存+數據庫雙寫不一致問題分析與解決方案設計》

《什麼?我往Redis裏寫的數據怎麼沒了?》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章