數據持久化
Redis提供了將數據定期自動持久化至硬盤的能力,包括RDB和AOF兩種方案,兩種方案分別有其長處和短板,可以配合起來同時運行,確保數據的穩定性。
必須使用數據持久化嗎?
Redis的數據持久化機制是可以關閉的。如果你只把Redis作為緩存服務使用,Redis中存儲的所有數據都不是該數據的主體而僅僅是同步過來的備份,那么可以關閉Redis的數據持久化機制。
但通常來說,仍然建議至少開啟RDB方式的數據持久化,因為:
- RDB方式的持久化幾乎不損耗Redis本身的性能,在進行RDB持久化時,Redis主進程唯一需要做的事情就是fork出一個子進程,所有持久化工作都由子進程完成
- Redis無論因為什么原因crash掉之后,重啟時能夠自動恢復到上一次RDB快照中記錄的數據。這省去了手工從其他數據源(如DB)同步數據的過程,而且要比其他任何的數據恢復方式都要快
- 現在硬盤那么大,真的不缺那一點地方
RDB
采用RDB持久方式,Redis會定期保存數據快照至一個rbd文件中,並在啟動時自動加載rdb文件,恢復之前保存的數據。可以在配置文件中配置Redis進行快照保存的時機:
save [seconds] [changes]
意為在[seconds]秒內如果發生了[changes]次數據修改,則進行一次RDB快照保存,例如
save 60 100
會讓Redis每60秒檢查一次數據變更情況,如果發生了100次或以上的數據變更,則進行RDB快照保存。
可以配置多條save指令,讓Redis執行多級的快照保存策略。
Redis默認開啟RDB快照,默認的RDB策略如下:
save 900 1 save 300 10 save 60 10000
也可以通過BGSAVE命令手工觸發RDB快照保存。
RDB的優點:
- 對性能影響最小。如前文所述,Redis在保存RDB快照時會fork出子進程進行,幾乎不影響Redis處理客戶端請求的效率。
- 每次快照會生成一個完整的數據快照文件,所以可以輔以其他手段保存多個時間點的快照(例如把每天0點的快照備份至其他存儲媒介中),作為非常可靠的災難恢復手段。
- 使用RDB文件進行數據恢復比使用AOF要快很多。
RDB的缺點:
- 快照是定期生成的,所以在Redis crash時或多或少會丟失一部分數據。
- 如果數據集非常大且CPU不夠強(比如單核CPU),Redis在fork子進程時可能會消耗相對較長的時間(長至1秒),影響這期間的客戶端請求。
AOF
采用AOF持久方式時,Redis會把每一個寫請求都記錄在一個日志文件里。在Redis重啟時,會把AOF文件中記錄的所有寫操作順序執行一遍,確保數據恢復到最新。
AOF默認是關閉的,如要開啟,進行如下配置:
appendonly yes
AOF提供了三種fsync配置,always/everysec/no,通過配置項[appendfsync]指定:
- appendfsync no:不進行fsync,將flush文件的時機交給OS決定,速度最快
- appendfsync always:每寫入一條日志就進行一次fsync操作,數據安全性最高,但速度最慢
- appendfsync everysec:折中的做法,交由后台線程每秒fsync一次
隨着AOF不斷地記錄寫操作日志,必定會出現一些無用的日志,例如某個時間點執行了命令SET key1 "abc",在之后某個時間點又執行了SET key1 "bcd",那么第一條命令很顯然是沒有用的。大量的無用日志會讓AOF文件過大,也會讓數據恢復的時間過長。
所以Redis提供了AOF rewrite功能,可以重寫AOF文件,只保留能夠把數據恢復到最新狀態的最小寫操作集。
AOF rewrite可以通過BGREWRITEAOF命令觸發,也可以配置Redis定期自動進行:
auto-aof-rewrite-percentage 100 auto-aof-rewrite-min-size 64mb
上面兩行配置的含義是,Redis在每次AOF rewrite時,會記錄完成rewrite后的AOF日志大小,當AOF日志大小在該基礎上增長了100%后,自動進行AOF rewrite。同時如果增長的大小沒有達到64mb,則不會進行rewrite。
AOF的優點:
- 最安全,在啟用appendfsync always時,任何已寫入的數據都不會丟失,使用在啟用appendfsync everysec也至多只會丟失1秒的數據。
- AOF文件在發生斷電等問題時也不會損壞,即使出現了某條日志只寫入了一半的情況,也可以使用redis-check-aof工具輕松修復。
- AOF文件易讀,可修改,在進行了某些錯誤的數據清除操作后,只要AOF文件沒有rewrite,就可以把AOF文件備份出來,把錯誤的命令刪除,然后恢復數據。
AOF的缺點:
- AOF文件通常比RDB文件更大
- 性能消耗比RDB高
- 數據恢復速度比RDB慢
內存管理與數據淘汰機制
最大內存設置
默認情況下,在32位OS中,Redis最大使用3GB的內存,在64位OS中則沒有限制。
在使用Redis時,應該對數據占用的最大空間有一個基本准確的預估,並為Redis設定最大使用的內存。否則在64位OS中Redis會無限制地占用內存(當物理內存被占滿后會使用swap空間),容易引發各種各樣的問題。
通過如下配置控制Redis使用的最大內存:
maxmemory 100mb
在內存占用達到了maxmemory后,再向Redis寫入數據時,Redis會:
- 根據配置的數據淘汰策略嘗試淘汰數據,釋放空間
- 如果沒有數據可以淘汰,或者沒有配置數據淘汰策略,那么Redis會對所有寫請求返回錯誤,但讀請求仍然可以正常執行
在為Redis設置maxmemory時,需要注意:
- 如果采用了Redis的主從同步,主節點向從節點同步數據時,會占用掉一部分內存空間,如果maxmemory過於接近主機的可用內存,導致數據同步時內存不足。所以設置的maxmemory不要過於接近主機可用的內存,留出一部分預留用作主從同步。
數據淘汰機制
Redis提供了5種數據淘汰策略:
- volatile-lru:使用LRU算法進行數據淘汰(淘汰上次使用時間最早的,且使用次數最少的key),只淘汰設定了有效期的key
- allkeys-lru:使用LRU算法進行數據淘汰,所有的key都可以被淘汰
- volatile-random:隨機淘汰數據,只淘汰設定了有效期的key
- allkeys-random:隨機淘汰數據,所有的key都可以被淘汰
- volatile-ttl:淘汰剩余有效期最短的key
最好為Redis指定一種有效的數據淘汰策略以配合maxmemory設置,避免在內存使用滿后發生寫入失敗的情況。
一般來說,推薦使用的策略是volatile-lru,並辨識Redis中保存的數據的重要性。對於那些重要的,絕對不能丟棄的數據(如配置類數據等),應不設置有效期,這樣Redis就永遠不會淘汰這些數據。對於那些相對不是那么重要的,並且能夠熱加載的數據(比如緩存最近登錄的用戶信息,當在Redis中找不到時,程序會去DB中讀取),可以設置上有效期,這樣在內存不夠時Redis就會淘汰這部分數據。
配置方法:
maxmemory-policy volatile-lru #默認是noeviction,即不進行數據淘汰
Pipelining
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
網絡引發的延遲
- 盡可能使用長連接或連接池,避免頻繁創建銷毀連接
- 客戶端進行的批量數據操作,應使用Pipeline特性在一次交互中完成。具體請參照本文的Pipelining章節
數據持久化引發的延遲
Redis的數據持久化工作本身就會帶來延遲,需要根據數據的安全級別和性能要求制定合理的持久化策略:
- AOF + fsync always的設置雖然能夠絕對確保數據安全,但每個操作都會觸發一次fsync,會對Redis的性能有比較明顯的影響
- AOF + fsync every second是比較好的折中方案,每秒fsync一次
- AOF + fsync never會提供AOF持久化方案下的最優性能
- 使用RDB持久化通常會提供比使用AOF更高的性能,但需要注意RDB的策略配置
- 每一次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/<pid>/smaps文件中會保存進程的swap記錄,通過查看這個文件,能夠判斷Redis的延遲是否由Swap產生。如果這個文件中記錄了較大的Swap size,則說明延遲很有可能是Swap造成的。
數據淘汰引發的延遲
當同一秒內有大量key過期時,也會引發Redis的延遲。在使用時應盡量將key的失效時間錯開。
引入讀寫分離機制
Redis的主從復制能力可以實現一主多從的多節點架構,在這一架構下,主節點接收所有寫請求,並將數據同步給多個從節點。
在這一基礎上,我們可以讓從節點提供對實時性要求不高的讀請求服務,以減小主節點的壓力。
尤其是針對一些使用了長耗時命令的統計類任務,完全可以指定在一個或多個從節點上執行,避免這些長耗時命令影響其他請求的響應。
關於讀寫分離的具體說明,請參見后續章節