Redis快取設計與效能優化
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.快取預熱
比如說上線一個搶購活動,肯定到點開始就會有很多人來請求了,這個時候就可以提前做資料的預熱,既可以把連線池初始化好,也可以把資料放