Redis我們一般是用作緩存,扛並發;或者用於某些特定的業務場景,比如前面說到redis各種數據類型的使用場景以及redis的哨兵和集群模式。
這里主要整理了下redis用作緩存,存在的一些問題,以及改善方案。
簡單的流程就像這個樣子,一般請先到緩存區獲取,如果緩存沒有再到后端的數據庫去查詢。
1.緩存穿透
緩存穿透是指,是指查詢一個根本不存在數據,這樣緩存層里面沒有,就會去訪問后面的存儲層了。如果有大量的這種惡意請求過來,都打向后面的存儲層。顯然我們的存儲層是扛不住這樣的壓力。這樣緩存就失去了保護后面存儲的意義了。
解決方案:
1.緩存空對象
對於緩存穿透,可以采用緩存空對象,第一次進來緩存和DB都沒有,就存個空對象到緩存里面。但是如果大批量的惡意請求過來,這樣做就會導致緩存的key暴增,顯然不是一個很好的方案。
2.布隆過濾器
對於不存在的數據布隆過濾器一般都能夠過濾掉,不讓請求再往后端發送。當布隆過濾器說某個值存在時,這個值可能不存在;但是它說不存在時,那就肯定不存在。布隆過濾器是一個大型的位數組和幾個不一樣的無偏 hash 函數。所謂無偏就是能夠把元素的hash值算得比較均勻。向布隆過濾器中添加 key 時,會使用多個hash 函數對key進行hash分別算得一個整數索引值然后對位數組長度進行取模運算得到一個位置,每個hash函數都會算得一個不同的位置。再把位數組的這幾個位置都置為 1 就 完成了 add 操作。
向布隆過濾器詢問 key 是否存在時,跟 add 一樣,也會把 hash 的幾個位置都算出來,看看位數組中這幾個位置是否都為1,只要有一個位為0,那么說明布隆過濾器中這個key肯定不存在。但是都是 1,這並不能說明這個key就一定存在,只是極有可能存在,因為這些位被置為1可能是因為其它的key存在所致。
guvua包布隆過濾器的使用,導包
<dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> </dependency>
偽代碼:
public void bloomFilterTest() { BloomFilter<CharSequence> bloomFilter = BloomFilter.create( Funnels.stringFunnel(Charset.forName("UTF-8")), 1000, //期望存入的數據個數 0.001);//誤差率 //添加到布隆過濾器 String[] keys = new String[1000]; for (String key: keys) { bloomFilter.put(key); } String key = "key"; boolean exist = bloomFilter.mightContain(key); if (!exist) { return; } //todo 存在才去緩存獲取 }
可以看到這個類里面有很多的hash算法:com.google.common.hash.Hashing
redisson也有布隆過濾器的實現。
2.緩存失效
由於大批量的key同時失效,導致,大量的請求同時打向數據庫,造成數據庫壓力過大,甚至直接掛掉。我們在批量寫入緩存的時候,設置超時時間,可以是一個固定時間+隨機時間方式來生成,這樣就可以錯開失效時間。更加保險的方案可以加分布式鎖,拿到鎖的才去訪問數據庫。
3.緩存雪崩
緩存雪崩是指緩存層掛掉之后,所有請求都打向數據庫,數據庫扛不住,也可能掛掉,就導致對應的服務也掛掉,也會影響上游的調用服務。這樣的級聯問題。就像雪崩最開始一小片,然后越來越大,導致整個服務崩潰。
解決方案:
1.保證緩存層的高可用性,比如redis哨兵或者redis集群。
2.各依賴服務之間做限流,熔斷,降級等,比如Hystri,阿里的sentinel
4.緩存一致性
引入緩存之后,隨之而來的問題就是當DB數據更新時,緩存中的數據就會與db數據不一致。所以數據修改時是先更新緩存還是先更新DB?
如果先更新緩存,然后更新DB失敗,那么下一個請求過來讀取的緩存數據不是最新的。而我們實際上最終數據肯定都是以DB為准的。
先更新db 在更新緩存,這是在更新DB的時候來的請求讀取的數據也是不是最新的
淘汰緩存——更新DB——重新刷進緩存,在更新db是來的請求在緩存沒有數據,就會去請求DB,如果並發 可能操作多各請求去寫DB,那么就需要加鎖了
加鎖——淘汰緩存——更新DB——重新刷進緩存,這樣相對而言就比較保險了
5.bigkey問題
Bigkey是什么?在redis中,一個字符串最大512MB;hash,list,set,zset可以存儲2^31 - 1 個元素。
一般來說字符串超過10kb,其他的幾種元素個數不要超過5000個。
可以使用src/redis-cli --bigkeys 來查看bigkey,我這里設置了一個30多K的字符串,看下掃描結果,掃除了一個字符串類型的bigkey,4084字節。
Bigkey有哪些危害。一是刪除時阻塞其他請求,比如一個bigkey,平時都沒什么,但是設置了過期時間,到期了刪除時,可能就會阻塞其他請求,4.0之后可以開啟lazyfree-lazy- expire yes來異步刪除;二是造成網絡擁堵,比如一個key數據量達到1MB,假設並發量1000,這個時候獲取它就會產生1000MB的流量,千兆網卡,峰值的速率也才128MB/S,並不是扛不住並發,而是會占用大量網絡帶寬。
對於很大list,set這些,我們可以將數據拆分,生成一個系列的的key去存放數據。如果是redis集群這些key自然就可以分到不同的小主從上面去,如果是單機,那么可以自己實現一個路由算法,來如何獲取這一系列key中的某一個。
6. 客戶端使用
1.避免多個服務使用一個redis實例,如果實在有,可以看下將業務拆分,把這些公共數據服務化。
2.使用連接池,控制有效連接,同時也提高效率。連接池重要參數設置:
1 maxActive 資源池中最大連接數 默認值8
2 maxIdle 資源池允許最大空閑 的連接數 默認值8
3 minIdle 資源池確保最少空閑 的連接數 默認值0
4 blockWhenExhausted 當資源池用盡后,調用者是否要等待。只有當為true時,下面的maxWaitMillis才會生效,默認值true 建議使用默認值
5 maxWaitMillis 當資源池連接用盡后,調用者的最大等待時間(單位為毫秒) -1:表示永不超時 不建議使用默認值
6 testOnBorrow 向資源池借用連接時是否做連接有效性檢測(ping),無效連接會被移除 默認值false 業務量很大時候建議 設置為false(多一次 ping的開銷)。
7 testOnReturn 向資源池歸還連接時是否做連接有效性檢測(ping),無效連接會被移除 默認值false 業務量很大時候建議 設置為false(多一次 ping的開銷)。
8 jmxEnabled 是否開啟jmx監控,可用於監控 默認值true 建議開啟,但應用本身也要開啟
前面三個參數相對而言更重要,單獨拎出來再說下:
最大連接數maxActive:
可以從業務希望的並發量,客戶端執行時間,redis資源設置(應用個數(集群部署多少個實例) * maxActive <= maxclients(redis最大連接數,redis配置中設置的)),等因素考慮。
比如一次客戶端執行時間2ms,那么一個連接的QPS就是500,業務期望的QPS是3000,那么理論上連接池大小3000/500=60個,實際上考慮其他影響,一般設置比理論值稍微大點。但這個值不是越大越好,一方面連接太多占用客戶端和服務端資源,另一方面對 於Redis這種高 QPS的服務器,一個大命令的阻塞即使設置再大資源池仍然會無濟於事。
最大空閑連接數maxIdle:
maxIdle實際上才是業務需要的最大連接數,空閑的連接造好放在那兒,進來一個請求就可以直接拿來用了。maxActive是為了給出總量,所以maxIdle不要設置過小,否則會有當空閑連接不夠,就會創建新的連接,又會有新的開銷,最佳就是maxActive = maxIdle。這樣就避免連接池伸縮帶來的性能干擾。但是如果並發量不大或者maxActive設置過高,會導致不必要的連接資源浪費。一般推薦maxIdle可以設置為按上面的業務期望QPS計算出來的理論連接數,maxActive可以再放大一些。
最小空閑連接數minIdle:
至少保持多少空閑連接,在使用連接的過程中,如果連接數超過了minIdle,那么繼續建立連接,如果超過了 maxIdle,當超過的連接執行完業務后會慢慢被移出連接池釋放掉。
3.緩存預熱
比如說上線一個搶購活動,肯定到點開始就會有很多人來請求了,這個時候就可以提前做數據的預熱,既可以把連接池初始化好,也可以把數據放好。