在一些網絡服務的系統中,Redis 的性能,多是比 MySQL 等硬盤數據庫的性能更重要的課題。好比微博,把熱點微博[1],最新的用戶關係,都存儲在 Redis 中,大量的查詢擊中 Redis,而不走 MySQL。node
那麼,針對 Redis 服務,咱們能作哪些性能優化呢?或者說,應該避免哪些性能浪費呢?linux
在討論優化以前,咱們須要知道,Redis 服務自己就有一些特性,好比單線程運行。除非修改 Redis 的源代碼,否則這些特性,就是咱們思考性能優化的基本面。程序員
那麼,有哪些 Redis 基本特性須要咱們考慮呢?Redis 的項目介紹中歸納了它特性:面試
Redis is an in-memory database that persists on disk. The data model is key-value, but many different kind of values are supported.redis
首先,Redis 使用操做系統提供的虛擬內存來存儲數據。並且,這個操做系統通常就是指 Unix。Windows 上也能運行 Redis,可是須要特殊處理。若是你的操做系統使用交換空間,那麼 Redis 的數據可能會被實際保存在硬盤上。關注公衆號:程序員白楠楠,獲取2020最新面試題算法
其次,Redis 支持持久化,能夠把數據保存在硬盤上。不少時候,咱們也確實有必要進行持久化來實現備份,數據恢復等需求。但持久化不會憑空發生,它也會佔用一部分資源。mongodb
第三,Redis 是用 key-value 的方式來讀寫的,而 value 中又能夠是不少不一樣種類的數據;更進一步,一個數據類型的底層還有被存儲爲不一樣的結構。不一樣的存儲結構決定了數據增刪改查的複雜度以及性能開銷。數據庫
最後,在上面的介紹中沒有提到的是,Redis 大多數時候是單線程運行[2]的(single-threaded),即同一時間只佔用一個 CPU,只能有一個指令在運行,並行讀寫是不存在的。不少操做帶來的延遲問題,均可以在這裏找到答案。編程
關於最後這個特性,爲何 Redis 是單線程的,卻能有很好的性能(根據 Amdahl’s Law,優化耗時佔比大的過程,才更有意義),兩句話歸納是:Redis 利用了多路 I/O 複用機制[3],處理客戶端請求時,不會阻塞主線程;Redis 單純執行(大多數指令)一個指令不到 1 微秒[4],如此,單核 CPU 一秒就能處理 1 百萬個指令(大概對應着幾十萬個請求吧),用不着實現多線程(網絡纔是瓶頸[5])。設計模式
Redis 的官方博客在幾個地方都說,性能瓶頸更多是網絡[6],那麼咱們如何優化網絡上的延時呢?
首先,若是大家使用單機部署(應用服務和 Redis 在同一臺機器上)的話,使用 Unix 進程間通信來請求 Redis 服務,速度比 localhost 局域網(學名 loopback)更快。官方文檔[7]是這麼說的,想想,理論上也應該是這樣的。
但不少公司的業務規模不是單機部署能支撐的,因此仍是得用 TCP。
Redis 客戶端和服務器的通信通常使用 TCP 長連接。若是客戶端發送請求後須要等待 Redis 返回結果再發送下一個指令,客戶端和 Redis 的多個請求就構成下面的關係:
(備註:若是不是你要發送的 key 特別長,一個 TCP 包徹底能放下 Redis 指令,因此只畫了一個 push 包)
這樣這兩次請求中,客戶端都須要經歷一段網絡傳輸時間。
但若是有可能,徹底可使用 multi-key 類的指令來合併請求,好比兩個 GET key 能夠用 MGET key1 key2 合併。這樣在實際通信中,請求數也減小了,延時天然獲得好轉。
若是不能用 multi-key 指令來合併,好比一個 SET,一個 GET 沒法合併。怎麼辦?
Redis 中有至少這樣兩個方法能合併多個指令到一個 request 中,一個是MULTI/EXEC,一個是 script。前者原本是構建 Redis 事務的方法,但確實能夠合併多個指令爲一個 request,它到通信過程以下。至於 script,最好利用緩存腳本的 sha1 hash key 來調起腳本,這樣通信量更小。
這樣確實更能減小網絡傳輸時間,不是麼?但如此以來,就必需要求這個 transaction / script 中涉及的 key 在同一個 node 上,因此要酌情考慮。
若是上面的方法咱們都考慮過了,仍是沒有辦法合併多個請求,咱們還能夠考慮合併多個 responses。好比把 2 個回覆信息合併:
這樣,理論上能夠省去 1 次回覆所用的網絡傳輸時間。這就是 pipeline 作的事情。舉個 ruby 客戶端使用 pipeline 的例子:
require 'redis' @redis = Redis.new() @redis.pipelined do @redis.get 'key1' @redis.set 'key2' 'some value' end # => [1, 2]
聽說,有些語言的客戶端,甚至默認就使用 pipeline 來優化延時問題,好比 node_redis。
另外,不是任意多個回覆信息均可以放進一個 TCP 包中,若是請求數太多,回覆的數據很長(好比 get 一個長字符串),TCP 仍是會分包傳輸,但使用 pipeline,依然能夠減小傳輸次數。
pipeline 和上面的其餘方法都不同的是,它不具備原子性。因此在 cluster 狀態下的集羣上,實現 pipeline 比那些原子性的方法更有可能。
小結一下:
使用 unix 進程間通訊,若是單機部署
使用 multi-key 指令合併多個指令,減小請求數,若是有可能的話
使用 transaction、script 合併 requests 以及 responses
在大數據量的狀況下,有些操做的執行時間會相對長,好比 KEYS *,LRANGE mylist 0 -1,以及其餘算法複雜度爲 O(n) 的指令。由於 Redis 只用一個線程來作數據查詢,若是這些指令耗時很長,就會阻塞 Redis,形成大量延時。
儘管官方文檔中說 KEYS * 的查詢挺快的,(在普通筆記本上)掃描 1 百萬個 key,只需 40 毫秒(參見:https://redis.io/commands/keys),但幾十 ms 對於一個性能要求很高的系統來講,已經不短了,更況且若是有幾億個 key(一臺機器徹底可能存幾億個 key,好比一個 key 100字節,1 億個 key 只有 10GB),時間更長。
因此,儘可能不要在生產環境的代碼使用這些執行很慢的指令,這一點 Redis 的做者在博客[8]中也提到了。另外,運維同窗查詢 Redis 的時候也儘可能不要用。甚至,Redis Essential 這本書建議利用 rename-command KEYS '' 來禁止使用這個耗時的指令。
除了這些耗時的指令,Redis 中 transaction,script,由於能夠合併多個 commands 爲一個具備原子性的執行過程,因此也可能佔用 Redis 很長時間,須要注意。
若是你想找出生產環境使用的「慢指令」,那麼能夠利用 SLOWLOG GET count 來查看最近的 count 個執行時間很長的指令。至於多長算長,能夠經過在 redis.conf 中設置 slowlog-log-slower-than 來定義。
除此以外,在不少地方都沒有提到的一個可能的慢指令是 DEL,但 redis.conf 文件的註釋[9]中卻是說了。長話短說就是 DEL 一個大的 object 時候,回收相應的內存可能會須要很長時間(甚至幾秒),因此,建議用 DEL 的異步版本:UNLINK。後者會啓動一個新的 thread 來刪除目標 key,而不阻塞原來的線程。
更進一步,當一個 key 過時以後,Redis 通常也須要同步的把它刪除。其中一種刪除 keys 的方式是,每秒 10 次的檢查一次有設置過時時間的 keys,這些 keys 存儲在一個全局的 struct 中,能夠用 server.db->expires 訪問。檢查的方式是:
從中隨機取出 20 個 keys
把過時的刪掉。
這裏對於性能的影響是,若是真的有不少的 keys 在同一時間過時,那麼 Redis 真的會一直循環執行刪除,佔用主線程。
對此,Redis 做者的建議[10]是警戒 EXPIREAT 這個指令,由於它更容易產生 keys 同時過時的現象。我還見到過一些建議是給 keys 的過時時間設置一個隨機波動量。最後,redis.conf 中也給出了一個方法,把 keys 的過時刪除操做變爲異步的,即,在 redis.conf 中設置 lazyfree-lazy-expire yes。
一種數據類型(好比 string,list)進行增刪改查的效率是由其底層的存儲結構決定的。
咱們在使用一種數據類型時,能夠適當關注一下它底層的存儲結構及其算法,避免使用複雜度過高的方法。舉兩個例子:
ZADD 的時間複雜度是 O(log(N)),這比其餘數據類型增長一個新元素的操做更復雜,因此要當心使用。
除了時間性能上的考慮,有時候咱們還須要節省存儲空間。好比上面提到的 ziplist 結構,就比 hashtable 結構節省存儲空間(Redis Essentials 的做者分別在 hashtable 和 ziplist 結構的 Hash 中插入 500 個 fields,每一個 field 和 value 都是一個 15 位左右的字符串,結果是 hashtable 結構使用的空間是 ziplist 的 4 倍。)。但節省空間的數據結構,其算法的複雜度可能很高。因此,這裏就須要在具體問題面前作出權衡。歡迎關注公衆號:朱小廝的博客,回覆:1024,能夠領取redis專屬資料。
如何作出更好的權衡?我以爲得深挖 Redis 的存儲結構才能讓本身安心。這方面的內容咱們下次再說。
以上這三點都是編程層面的考慮,寫程序時應該注意啊。下面這幾點,也會影響 Redis 的性能,但解決起來,就不僅是靠代碼層面的調整了,還須要架構和運維上的考慮。
Redis 運行的外部環境,也就是操做系統和硬件顯然也會影響 Redis 的性能。在官方文檔中,就給出了一些例子:
CPU:Intel 多種 CPU 都比 AMD 皓龍系列好
虛擬化:實體機比虛擬機好,主要是由於部分虛擬機上,硬盤不是本地硬盤,監控軟件致使 fork 指令的速度慢(持久化時會用到 fork),尤爲是用 Xen 來作虛擬化時。
內存管理:在 linux 操做系統中,爲了讓 translation lookaside buffer,即 TLB,可以管理更多內存空間(TLB 只能緩存有限個 page),操做系統把一些 memory page 變得更大,好比 2MB 或者 1GB,而不是一般的 4096 字節,這些大的內存頁叫作 huge pages。同時,爲了方便程序員使用這些大的內存 page,操做系統中實現了一個 transparent huge pages(THP)機制,使得大內存頁對他們來講是透明的,能夠像使用正常的內存 page 同樣使用他們。但這種機制並非數據庫所須要的,多是由於 THP 會把內存空間變得緊湊而連續吧,就像mongodb 的文檔[11]中明確說的,數據庫須要的是稀疏的內存空間,因此請禁掉 THP 功能。Redis 也不例外,但 Redis 官方博客上給出的理由是:使用大內存 page 會使 bgsave 時,fork 的速度變慢;若是 fork 以後,這些內存 page 在原進程中被修改了,他們就須要被複制(即 copy on write),這樣的複製會消耗大量的內存(畢竟,人家是 huge pages,複製一份消耗成本很大)。因此,請禁止掉操做系統中的 transparent huge pages 功能。
Redis 的一項重要功能就是持久化,也就是把數據複製到硬盤上。基於持久化,纔有了 Redis 的數據恢復等功能。
但維護這個持久化的功能,也是有性能開銷的。
首先說,RDB 全量持久化。
這種持久化方式把 Redis 中的全量數據打包成 rdb 文件放在硬盤上。可是執行 RDB 持久化過程的是原進程 fork 出來一個子進程,而 fork 這個系統調用是須要時間的,根據Redis Lab 6 年前作的實驗[12],在一臺新型的 AWS EC2 m1.small^13 上,fork 一個內存佔用 1GB 的 Redis 進程,須要 700+ 毫秒,而這段時間,redis 是沒法處理請求的。
雖然如今的機器應該都會比那個時候好,可是 fork 的開銷也應該考慮吧。爲此,要使用合理的 RDB 持久化的時間間隔,不要太頻繁。
接下來,咱們看另一種持久化方式:AOF 增量持久化。
這種持久化方式會把你發到 redis server 的指令以文本的形式保存下來(格式遵循 redis protocol),這個過程當中,會調用兩個系統調用,一個是 write(2),同步完成,一個是 fsync(2),異步完成。
這兩部均可能是延時問題的緣由:
write 可能會由於輸出的 buffer 滿了,或者 kernal 正在把 buffer 中的數據同步到硬盤,就被阻塞了。
其中,write 的阻塞貌似只能接受,由於沒有更好的方法把數據寫到一個文件中了。但對於 fsync,Redis 容許三種配置,選用哪一種取決於你對備份及時性和性能的平衡:
always:當把 appendfsync 設置爲 always,fsync 會和客戶端的指令同步執行,所以最可能形成延時問題,但備份及時性最好。
everysec:每秒鐘異步執行一次 fsync,此時 redis 的性能表現會更好,可是 fsync 依然可能阻塞 write,算是一個折中選擇。
以上,咱們都是基於單臺,或者單個 Redis 服務進行優化。下面,咱們考慮當網站的規模變大時,利用分佈式架構來保障 Redis 性能的問題。
首先說,哪些狀況下不得不(或者最好)使用分佈式架構:
數據量很大,單臺服務器內存不可能裝得下,好比 1 個 T 這種量級
須要服務高可用
解決這些問題能夠採用數據分片或者主從分離,或者二者都用(即,在分片用的 cluster 節點上,也設置主從結構)。
這樣的架構,能夠爲性能提高加入新的切入點:
把慢速的指令發到某些從庫中執行
把持久化功能放在一個不多使用的從庫上
其中前兩條都是根據 Redis 單線程的特性,用其餘進程(甚至機器)作性能補充的方法。
固然,使用分佈式架構,也可能對性能有影響,好比請求須要被轉發,數據須要被不斷複製分發。(待查)
其實還有不少東西也影響 Redis 的性能,好比 active rehashing(keys 主表的再哈希,每秒 10 次,關掉它能夠提高一點點性能),可是這篇博客已經寫的很長了。並且,更重要不是收集已經被別人提出的問題,而後記憶解決方案;而是掌握 Redis 的基本原理,以不變應萬變的方式決絕新出現的問題。
小編總結了2020面試題,這份面試題的包含的模塊分爲19個模塊,分別是: Java 基礎、容器、多線程、反射、對象拷貝、Java Web 、異常、網絡、設計模式、Spring/Spring MVC、Spring Boot/Spring Cloud、Hibernate、MyBatis、RabbitMQ、Kafka、Zookeeper、MySQL、Redis、JVM 。
關注公衆號:程序員白楠楠,獲取上述資料。