一篇關於Redis的好文章

OldBoy~發表於2017-05-17

Redis作為快取使用,在值大小為1k的情況下,可以支援到每秒近十萬次的set操作,可見redis的執行效率是非常的高的。但是為什麼我們還會有的時候會遇到redis的瓶頸呢?一般來說,都是因為我們沒有對redis的所有的操作有一個全面直觀的瞭解。
Redis執行模式
Redis是執行在單執行緒下的,也就是說,一個redis例項最多也就只能佔用一個cpu,當redis例項執行緒執行所使用的cpu到達100%後,就無法進行更多的操作了,甚至從作業系統層面就開始拒絕連線了。然後我們就遇到了最迷茫的事情,為什麼我設定了很多的連線,但是都無法連線到redis服務呢,原因大抵如此。

Redis在執行一個操作的時候,其他所有的操作,包括系統的,後臺的等等,都會出現阻塞,一旦有阻塞,就會出現返回變慢的情況。

雖然說redis的執行時非常快的,但是也會受到很多其他因素的影響,在分析過程中,要注意都有什麼耗時的操作在霸佔cpu(依舊是單執行緒的問題),這些操作能不能分解,這些操作能不能再做快取,這些操作是否是必要的,在優化處理時都應該考慮這樣的問題。

總而言之,redis是以單執行緒來應對上層多執行緒的呼叫,在編碼過程中,使用到redis就要考慮到這一層。
Redis操作中需要規避的一些問題
Redis提供了比較多的資料結構,讓單純的kv結構更加豐富,但隨之而來的問題就是,資料結構越是複雜,操作時間越長,記住redis只能單執行緒!

當然,redis就是為了大量快速地讀寫一些資料而生的,放心大膽地去用,不然搭建一個redis幹嘛呢?

為了放心地使用redis,下面列舉一些需要特別注意,必要時要規避這些操作。這些操作有時會很慢,但是具體有多慢呢?我們不能用通常的模式去理解,如果說這個操作只需要10ms呢?是否算慢呢?當一個操作需要10ms時,這個redis的例項,在1s中只能執行100次同樣的操作,這樣算快還是慢呢?當redis的呼叫量上來以後,這個操作就成了系統的瓶頸了。

SISMEMBER
這個操作是檢視集合中的成員。在集合較大的情況下,會變得比較慢。所以,一般不要直接返回整個集合中的成員,而是採用標準的呼叫方式SISMEMBER key member,檢視集合是否具有該成員,從而避免大量的無用的資料處理。

SINTER
返回一個集合的全部成員,該集合是所有給定集合的交集。當兩個巨大的集合進行交集的運算,其效果也可以是毀滅性的。

LPUSH & RPUSH
在佇列頭部/尾部插入一串key。這個操作本身並無什麼大問題,但是我們又考慮到redis是一個單執行緒,那麼我們一次性插入了大量的值,就會造成阻塞,那麼就有必要把這一次操作分散為多次的操作,迴圈去做了,雖然覺得迴圈會耗時,但是在沒有讀寫分離的情況下,這種處理方式總歸是比直接阻塞redis要來得好的。建議一次性增加200個左右為佳。但是這也受限於key值的大小。具體的問題具體分析,在除錯和測試中要注意這部分的耗時,可以控制在10ms以內即可(redis預設超過10ms的操作對於redis是慢的)。

MSET & MGET
批量設定和獲取一串key的值。此操作與上面的也是一樣的,要考慮到整體的耗時,在需要的時候要做到迴圈設定/獲取,標準也是儘量控制一次操作在10ms以內。

KEYS
KEYS操作,查詢並列舉redis中與給定規則相符的key。此物為大殺器,看似每個操作只需要40-70ms,在編碼和功能性的測試中,根本無法感知到其帶來的惡果,因為70ms實在是很難感知到到底有多長,但是用另一個指標來衡量,就很明顯看出來了。取平均值,假設KEYS操作耗時50ms,那麼一個redis例項的qps只有可憐的20,還是在不做其他動作的情況下。
此處要著重說明之前的一個線上問題的解決,就是因為KEYS操作引起,最後呼叫者臨時做了讀寫分離,並且將redis的例項拉到了恐怖的21個才勉強支援起線上的呼叫量,堪稱本司有史以來最豪華redis,並且這個還是在使用者數沒有暴增的情況下。
所以切記,KEYS是禁止使用的!(後續再新機房中的redis會直接改掉這個操作,具體怎麼再來呼叫KEYS操作,我就不告訴你!)

CONFIG GET & CONFIG SET
非管理人員,禁止使用!

Redis需要注意的操作其實並不多,並且很多情況下,根本不會造成壓力,但是這些情況都是需要在編碼時注意的,去衡量並正確高效地使用redis。記住一句,我們是單執行緒!我們是執行在毫秒級別的服務!
Redis的設計中需要注意的
Redis本身沒有強的模式,在使用中,所有人都可以隨意地設定redis的儲存,那麼我們要如何才能高效地使用redis呢?有一些問題就需要在儲存的設計時要注意了。

Key的命名要簡短,且有意義。Key在redis中也是要佔用空間的,並且記憶體是很寶貴的,所以要節約使用。當key名有意義,那麼在程式的邏輯處理過程中,就有可能直接獲得key名,從而直接去獲取到相應的value了。

Value值不宜過大。當value在1k的時候,redis可以支援到10w/s左右,那當value的大小達到10k,那麼redis就只能支援到1w/s左右了。可見value的大小對於redis的效率還是很有影響的。所以,每個key的value值要儘量精簡,哪怕是冗餘一些多餘的key也不要製造一個超級大的key因為那樣的話,還不如使用其他的儲存來得實惠了。

返回的資料要儘量緊湊,小型。雖然redis沒有對客戶端做什麼限制,但是他是預留了功能的,可以在一次請求超過了多久,或者是總資料量超過多少,還有就是規定時間內傳送資料超過多少的情況下,就可以直接殺死連結,沒有任何返回,也沒有任何道理。所以要注意,只返回有用的東西。

對於list要活學活用。我們不能做KEYS,那麼當我們又需要將某種型別的KEY進行計數的時候,我們就應該去使用list了。將一些相關的key都存放於一個list中,我們就可以輕鬆地使用LLEN來獲取到list的長度,其效果可以抵消一部分KEYS的應用場景了。對於list也並不是萬靈藥,雖然在很多情況下,list可以消耗更少的資源來做到和key相同的效果,但是也要注意,list不適宜過長,要做到可控。

Set(集合)的運用,與list類似,就不多描述了,參見一下redis的命令列表吧。這也是一種非常有用的資料結構。

對於list和set還是要說兩句,正因為有了這樣的資料結構,我們得以用更小的代價管理更加龐大的key。

要用SETEX去設定key的過期時間。Redis本身就是一個快取,只是一個快取,在需要持久化的場景下,不要妄想能在redis中做的儲存大量的值。所以redis中儘量儲存一些不需要持久化的東西,並且如果關聯到持久化的資料的話,最好能有快取載入的指令碼,能手工載入一些快取資料,在出現整個redis叢集完全崩潰的情況下可以減少crash到資料庫的時間。

我們是單執行緒!又說到這個話題了,redis就是個單執行緒,我覺得也不太可能改造了。那麼我們怎麼來應對大量併發呢?
其實並不是沒招,招有的是。
方法一,我們可以使用佇列,將請求放入佇列中,再取出處理,將大量的併發序列化,持續地,高效地在redis中處理。
方法二,讀寫分離。在有可能的情況下一定要具備讀寫分離功能。當序列化後依然無法響應大量的併發,那麼我們就需要做讀寫分離了。這樣只要在量上來時,多拉例項,並新增到負載均衡即可了。

要為分散式做好準備。現在本司在redis的分散式上選型是twemproxy,在分散式的情景下,很多操作會受到限制,所以,當可以預見到redis在將來會增長到需要做分散式的場景下,就需要注意了,在編碼中就要有意去規避這些操作,以減少將來修改的成本。

如何去界定一個redis是否需要做成分散式,建議當redis的資料量超過15G時就需要更換為分散式,當redis的資料量達到8G,並且有增長的趨勢時,就需要考慮分散式的方案了。

就本司當前情況來看,只有少量的一些應用會達到分散式的要求,其他的,因為歷史原因,不適合馬上切換為分散式。大量的應用還是處於讀寫分離即可支援的情況下,所以不一定上線,但一定要有讀寫分離的方案,或者是預案。
後續的一些展望
在後續的規劃中,redis更加傾向於小型化,輕量化的部署。這裡有兩條路,一是集中化的分散式redis,雖然將資料打散了,這樣解決了容量的問題,但是,但是!我們特麼還是單執行緒!同時,叢集化,也限制了一些特殊的操作,但是這些小技巧有的時候又是非常nice的。那麼我們就提出了第二個方案,二級快取。在持久化層上有幾個叢集,這些叢集又可做讀寫分離,在這個大容量的快取上每個服務,甚至是每個程式都對應一個自己的超級輕量化redis,不需要預載入,不怕資料丟失,隨時可重啟的這種,當然,可以做到讀寫分離也是極好的。每個關於快取的請求,都會先去查詢二級快取中的redis(輕量化的那個),當不命中時,會去一級快取(大容量的那個)中查詢,如果命中,則將資料載入至二級快取,返回結果。當一級快取也不能命中時,就要去到持久化層獲取實際的資料,並同時將資料載入到一級和二級快取了。

二級快取的模式下,又涉及到讀寫分離等問題,但那時,將是千萬級別併發訪問了,希望將來有一天能夠看見。

相關文章