Redis優化 - 邱乘屹的個人技術博客

Pipelining

Redis提供許多批量操作的命令,如MSET/MGET/HMSET/HMGET等等,這些命令存在的意義是減少維護網絡連接和傳輸數據所消耗的資源和時間。
例如連續使用5次SET命令設置5個不同的key,比起使用一次MSET命令設置5個不同的key,效果是一樣的,但前者會消耗更多的RTT(Round Trip Time)時長,永遠應優先使用後者。

然而,如果客戶端要連續執行的多次操作無法通過Redis命令組合在一起,例如:

SET a “abc”
INCR b
HSET c name “hi”
此時便可以使用Redis提供的pipelining功能來實現在一次交互中執行多條命令。
使用pipelining時,只需要從客戶端一次向Redis發送多條命令(以\r\n)分隔,Redis就會依次執行這些命令,並且把每個命令的返回按順序組裝在一起一次返回,比如:

$ (printf “PING\r\nPING\r\nPING\r\n”; sleep 1) | nc localhost 6379
+PONG
+PONG
+PONG
大部分的Redis客戶端都對Pipelining提供支持,所以開發者通常並不需要自己手工拼裝命令列表。

Pipelining的侷限性

Pipelining只能用於執行連續且無相關性的命令,當某個命令的生成需要依賴於前一個命令的返回時,就無法使用Pipelining了。

通過Scripting功能,可以規避這一侷限性

事務與Scripting

Pipelining能夠讓Redis在一次交互中處理多條命令,然而在一些場景下,我們可能需要在此基礎上確保這一組命令是連續執行的。

比如獲取當前累計的PV數並將其清0

> GET vCount
 12384
> SET vCount 0
 OK

如果在GET和SET命令之間插進來一個INCR vCount,就會使客戶端拿到的vCount不準確。

Redis的事務可以確保複數命令執行時的原子性。也就是說Redis能夠保證:一個事務中的一組命令是絕對連續執行的,在這些命令執行完成之前,絕對不會有來自於其他連接的其他命令插進去執行。

通過MULTI和EXEC命令來把這兩個命令加入一個事務中:

> MULTI
 OK
> GET vCount
 QUEUED
> SET vCount 0
 QUEUED
> EXEC
 1)12384
 2)OK

Redis在接收到MULTI命令後便會開啓一個事務,這之後的所有讀寫命令都會保存在隊列中但並不執行,直到接收到EXEC命令後,Redis會把隊列中的所有命令連續順序執行,並以數組形式返回每個命令的返回結果。

可以使用DISCARD命令放棄當前的事務,將保存的命令隊列清空。

需要注意的是,Redis事務不支持回滾
如果一個事務中的命令出現了語法錯誤,大部分客戶端驅動會返回錯誤,2.6.5版本以上的Redis也會在執行EXEC時檢查隊列中的命令是否存在語法錯誤,如果存在,則會自動放棄事務並返回錯誤。
但如果一個事務中的命令有非語法類的錯誤(比如對String執行HSET操作),無論客戶端驅動還是Redis都無法在真正執行這條命令之前發現,所以事務中的所有命令仍然會被依次執行。在這種情況下,會出現一個事務中部分命令成功部分命令失敗的情況,然而與RDBMS不同,Redis不提供事務回滾的功能,所以只能通過其他方法進行數據的回滾。

通過事務實現CAS

Redis提供了WATCH命令與事務搭配使用,實現CAS樂觀鎖的機制。

假設要實現將某個商品的狀態改爲已售:

if(exec(HGET stock:1001 state) == "in stock")
    exec(HSET stock:1001 state "sold");

這一僞代碼執行時,無法確保併發安全性,有可能多個客戶端都獲取到了"in stock"的狀態,導致一個庫存被售賣多次。

使用WATCH命令和事務可以解決這一問題:

exec(WATCH stock:1001);
if(exec(HGET stock:1001 state) == "in stock") {
    exec(MULTI);
    exec(HSET stock:1001 state "sold");
    exec(EXEC);
}

WATCH的機制是:在事務EXEC命令執行時,Redis會檢查被WATCH的key,只有被WATCH的key從WATCH起始時至今沒有發生過變更,EXEC纔會被執行。如果WATCH的key在WATCH命令到EXEC命令之間發生過變化,則EXEC命令會返回失敗。

Scripting

通過EVAL與EVALSHA命令,可以讓Redis執行LUA腳本。這就類似於RDBMS的存儲過程一樣,可以把客戶端與Redis之間密集的讀/寫交互放在服務端進行,避免過多的數據交互,提升性能。

Scripting功能是作爲事務功能的替代者誕生的,事務提供的所有能力Scripting都可以做到。Redis官方推薦使用LUA Script來代替事務,前者的效率和便利性都超過了事務。

關於Scripting的具體使用,本文不做詳細介紹,請參考官方文檔 https://redis.io/commands/eval

Redis性能調優

儘管Redis是一個非常快速的內存數據存儲媒介,也並不代表Redis不會產生性能問題。
前文中提到過,Redis採用單線程模型,所有的命令都是由一個線程串行執行的,所以當某個命令執行耗時較長時,會拖慢其後的所有命令,這使得Redis對每個任務的執行效率更加敏感。

針對Redis的性能優化,主要從下面幾個層面入手:

最初的也是最重要的,確保沒有讓Redis執行耗時長的命令
使用pipelining將連續執行的命令組合執行
操作系統的Transparent huge pages功能必須關閉: echo never >
/sys/kernel/mm/transparent_hugepage/enabled
如果在虛擬機中運行Redis,可能天然就有虛擬機環境帶來的固有延遲。可以通過./redis-cli --intrinsic-latency
100命令查看固有延遲。同時如果對Redis的性能有較高要求的話,應儘可能在物理機上直接部署Redis。
檢查數據持久化策略
考慮引入讀寫分離機制
長耗時命令
Redis絕大多數讀寫命令的時間複雜度都在O(1)到O(N)之間,在文本和官方文檔中均對每個命令的時間複雜度有說明。

通常來說,O(1)的命令是安全的,O(N)命令在使用時需要注意,如果N的數量級不可預知,則應避免使用。例如對一個field數未知的Hash數據執行HGETALL/HKEYS/HVALS命令,通常來說這些命令執行的很快,但如果這個Hash中的field數量極多,耗時就會成倍增長。
又如使用SUNION對兩個Set執行Union操作,或使用SORT對List/Set執行排序操作等時,都應該嚴加註意。

避免在使用這些O(N)命令時發生問題主要有幾個辦法:

不要把List當做列表使用,僅當做隊列來使用
通過機制嚴格控制Hash、Set、Sorted Set的大小
可能的話,將排序、並集、交集等操作放在客戶端執行
絕對禁止使用KEYS命令
避免一次性遍歷集合類型的所有成員,而應使用SCAN類的命令進行分批的,遊標式的遍歷
Redis提供了SCAN命令,可以對Redis中存儲的所有key進行遊標式的遍歷,避免使用KEYS命令帶來的性能問題。同時還有SSCAN/HSCAN/ZSCAN等命令,分別用於對Set/Hash/Sorted Set中的元素進行遊標式遍歷。SCAN類命令的使用請參考官方文檔:https://redis.io/commands/scan

Redis提供了Slow Log功能,可以自動記錄耗時較長的命令。相關的配置參數有兩個:

slowlog-log-slower-than xxxms  #執行時間慢於xxx毫秒的命令計入Slow Log
slowlog-max-len xxx  #Slow Log的長度,即最大紀錄多少條Slow Log

使用SLOWLOG GET [number]命令,可以輸出最近進入Slow Log的number條命令。
使用SLOWLOG RESET命令,可以重置Slow Log

網絡引發的延遲

  1. 儘可能使用長連接或連接池,避免頻繁創建銷燬連接
  2. 客戶端進行的批量數據操作,應使用Pipeline特性在一次交互中完成。具體請參照本文的Pipelining章節

數據持久化引發的延遲

Redis的數據持久化工作本身就會帶來延遲,需要根據數據的安全級別和性能要求制定合理的持久化策略:

  1. AOF + fsync always的設置雖然能夠絕對確保數據安全,但每個操作都會觸發一次fsync,會對Redis的性能有比較明顯的影響
  2. AOF + fsync every second是比較好的折中方案,每秒fsync一次
  3. AOF + fsync never會提供AOF持久化方案下的最優性能
  4. 使用RDB持久化通常會提供比使用AOF更高的性能,但需要注意RDB的策略配置
  5. 每一次RDB快照和AOF
    Rewrite都需要Redis主進程進行fork操作。fork操作本身可能會產生較高的耗時,與CPU和Redis佔用的內存大小有關。根據具體的情況合理配置RDB快照和AOF
    Rewrite時機,避免過於頻繁的fork帶來的延遲
    Redis在fork子進程時需要將內存分頁表拷貝至子進程,以佔用了24GB內存的Redis實例爲例,共需要拷貝24GB / 4kB * 8 = 48MB的數據。在使用單Xeon 2.27Ghz的物理機上,這一fork操作耗時216ms。

可以通過INFO命令返回的latest_fork_usec字段查看上一次fork操作的耗時(微秒)

Swap引發的延遲

當Linux將Redis所用的內存分頁移至swap空間時,將會阻塞Redis進程,導致Redis出現不正常的延遲。Swap通常在物理內存不足或一些進程在進行大量I/O操作時發生,應儘可能避免上述兩種情況的出現。

/proc//smaps文件中會保存進程的swap記錄,通過查看這個文件,能夠判斷Redis的延遲是否由Swap產生。如果這個文件中記錄了較大的Swap size,則說明延遲很有可能是Swap造成的。

數據淘汰引發的延遲

當同一秒內有大量key過期時,也會引發Redis的延遲。在使用時應儘量將key的失效時間錯開。

引入讀寫分離機制

Redis的主從複製能力可以實現一主多從的多節點架構,在這一架構下,主節點接收所有寫請求,並將數據同步給多個從節點。
在這一基礎上,我們可以讓從節點提供對實時性要求不高的讀請求服務,以減小主節點的壓力。
尤其是針對一些使用了長耗時命令的統計類任務,完全可以指定在一個或多個從節點上執行,避免這些長耗時命令影響其他請求的響應

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