redis scan 效率太慢_Redis 基础、高级特性与性能调优(下)
數(shù)據(jù)淘汰機(jī)制
Redis提供了5種數(shù)據(jù)淘汰策略:
- volatile-lru:使用LRU算法進(jìn)行數(shù)據(jù)淘汰(淘汰上次使用時間最早的,且使用次數(shù)最少的key),只淘汰設(shè)定了有效期的key
- allkeys-lru:使用LRU算法進(jìn)行數(shù)據(jù)淘汰,所有的key都可以被淘汰
- volatile-random:隨機(jī)淘汰數(shù)據(jù),只淘汰設(shè)定了有效期的key
- allkeys-random:隨機(jī)淘汰數(shù)據(jù),所有的key都可以被淘汰
- volatile-ttl:淘汰剩余有效期最短的key
最好為Redis指定一種有效的數(shù)據(jù)淘汰策略以配合maxmemory設(shè)置,避免在內(nèi)存使用滿后發(fā)生寫入失敗的情況。
一般來說,推薦使用的策略是volatile-lru,并辨識Redis中保存的數(shù)據(jù)的重要性。對于那些重要的,絕對不能丟棄的數(shù)據(jù)(如配置類數(shù)據(jù)等),應(yīng)不設(shè)置有效期,這樣Redis就永遠(yuǎn)不會淘汰這些數(shù)據(jù)。對于那些相對不是那么重要的,并且能夠熱加載的數(shù)據(jù)(比如緩存最近登錄的用戶信息,當(dāng)在Redis中找不到時,程序會去DB中讀取),可以設(shè)置上有效期,這樣在內(nèi)存不夠時Redis就會淘汰這部分?jǐn)?shù)據(jù)。
配置方法:
maxmemory-policy volatile-lru #默認(rèn)是noeviction,即不進(jìn)行數(shù)據(jù)淘汰Pipelining
Pipelining
Redis提供許多批量操作的命令,如MSET/MGET/HMSET/HMGET等等,這些命令存在的意義是減少維護(hù)網(wǎng)絡(luò)連接和傳輸數(shù)據(jù)所消耗的資源和時間。
例如連續(xù)使用5次SET命令設(shè)置5個不同的key,比起使用一次MSET命令設(shè)置5個不同的key,效果是一樣的,但前者會消耗更多的RTT(Round Trip Time)時長,永遠(yuǎn)應(yīng)優(yōu)先使用后者。
然而,如果客戶端要連續(xù)執(zhí)行的多次操作無法通過Redis命令組合在一起,例如:
SET a "abc"INCR bHSET c name "hi"此時便可以使用Redis提供的pipelining功能來實現(xiàn)在一次交互中執(zhí)行多條命令。
使用pipelining時,只需要從客戶端一次向Redis發(fā)送多條命令(以rn)分隔,Redis就會依次執(zhí)行這些命令,并且把每個命令的返回按順序組裝在一起一次返回,比如:
$ (printf "PINGrnPINGrnPINGrn"; sleep 1) | nc localhost 6379+PONG+PONG+PONG大部分的Redis客戶端都對Pipelining提供支持,所以開發(fā)者通常并不需要自己手工拼裝命令列表。
Pipelining的局限性
Pipelining只能用于執(zhí)行連續(xù)且無相關(guān)性的命令,當(dāng)某個命令的生成需要依賴于前一個命令的返回時,就無法使用Pipelining了。
通過Scripting功能,可以規(guī)避這一局限性
事務(wù)與Scripting
Pipelining能夠讓Redis在一次交互中處理多條命令,然而在一些場景下,我們可能需要在此基礎(chǔ)上確保這一組命令是連續(xù)執(zhí)行的。
比如獲取當(dāng)前累計的PV數(shù)并將其清0
> GET vCount12384> SET vCount 0OK如果在GET和SET命令之間插進(jìn)來一個INCR vCount,就會使客戶端拿到的vCount不準(zhǔn)確。
Redis的事務(wù)可以確保復(fù)數(shù)命令執(zhí)行時的原子性。也就是說Redis能夠保證:一個事務(wù)中的一組命令是絕對連續(xù)執(zhí)行的,在這些命令執(zhí)行完成之前,絕對不會有來自于其他連接的其他命令插進(jìn)去執(zhí)行。
通過MULTI和EXEC命令來把這兩個命令加入一個事務(wù)中:
> MULTIOK> GET vCountQUEUED> SET vCount 0QUEUED> EXEC1) 123842) OKRedis在接收到MULTI命令后便會開啟一個事務(wù),這之后的所有讀寫命令都會保存在隊列中但并不執(zhí)行,直到接收到EXEC命令后,Redis會把隊列中的所有命令連續(xù)順序執(zhí)行,并以數(shù)組形式返回每個命令的返回結(jié)果。
可以使用DISCARD命令放棄當(dāng)前的事務(wù),將保存的命令隊列清空。
需要注意的是,Redis事務(wù)不支持回滾:
如果一個事務(wù)中的命令出現(xiàn)了語法錯誤,大部分客戶端驅(qū)動會返回錯誤,2.6.5版本以上的Redis也會在執(zhí)行EXEC時檢查隊列中的命令是否存在語法錯誤,如果存在,則會自動放棄事務(wù)并返回錯誤。
但如果一個事務(wù)中的命令有非語法類的錯誤(比如對String執(zhí)行HSET操作),無論客戶端驅(qū)動還是Redis都無法在真正執(zhí)行這條命令之前發(fā)現(xiàn),所以事務(wù)中的所有命令仍然會被依次執(zhí)行。在這種情況下,會出現(xiàn)一個事務(wù)中部分命令成功部分命令失敗的情況,然而與RDBMS不同,Redis不提供事務(wù)回滾的功能,所以只能通過其他方法進(jìn)行數(shù)據(jù)的回滾。
通過事務(wù)實現(xiàn)CAS
Redis提供了WATCH命令與事務(wù)搭配使用,實現(xiàn)CAS樂觀鎖的機(jī)制。
假設(shè)要實現(xiàn)將某個商品的狀態(tài)改為已售:
if(exec(HGET stock:1001 state) == "in stock") exec(HSET stock:1001 state "sold");這一偽代碼執(zhí)行時,無法確保并發(fā)安全性,有可能多個客戶端都獲取到了”in stock”的狀態(tài),導(dǎo)致一個庫存被售賣多次。
使用WATCH命令和事務(wù)可以解決這一問題:
exec(WATCH stock:1001);if(exec(HGET stock:1001 state) == "in stock") { exec(MULTI); exec(HSET stock:1001 state "sold"); exec(EXEC);}WATCH的機(jī)制是:在事務(wù)EXEC命令執(zhí)行時,Redis會檢查被WATCH的key,只有被WATCH的key從WATCH起始時至今沒有發(fā)生過變更,EXEC才會被執(zhí)行。如果WATCH的key在WATCH命令到EXEC命令之間發(fā)生過變化,則EXEC命令會返回失敗。
Scripting
通過EVAL與EVALSHA命令,可以讓Redis執(zhí)行LUA腳本。這就類似于RDBMS的存儲過程一樣,可以把客戶端與Redis之間密集的讀/寫交互放在服務(wù)端進(jìn)行,避免過多的數(shù)據(jù)交互,提升性能。
Scripting功能是作為事務(wù)功能的替代者誕生的,事務(wù)提供的所有能力Scripting都可以做到。Redis官方推薦使用LUA Script來代替事務(wù),前者的效率和便利性都超過了事務(wù)。
關(guān)于Scripting的具體使用,本文不做詳細(xì)介紹,請參考官方文檔
https://redis.io/commands/eval
Redis性能調(diào)優(yōu)
盡管Redis是一個非常快速的內(nèi)存數(shù)據(jù)存儲媒介,也并不代表Redis不會產(chǎn)生性能問題。
前文中提到過,Redis采用單線程模型,所有的命令都是由一個線程串行執(zhí)行的,所以當(dāng)某個命令執(zhí)行耗時較長時,會拖慢其后的所有命令,這使得Redis對每個任務(wù)的執(zhí)行效率更加敏感。
針對Redis的性能優(yōu)化,主要從下面幾個層面入手:
- 最初的也是最重要的,確保沒有讓Redis執(zhí)行耗時長的命令
- 使用pipelining將連續(xù)執(zhí)行的命令組合執(zhí)行
- 操作系統(tǒng)的Transparent huge pages功能必須關(guān)閉:
- 如果在虛擬機(jī)中運行Redis,可能天然就有虛擬機(jī)環(huán)境帶來的固有延遲??梢酝ㄟ^./redis-cli –intrinsic-latency 100命令查看固有延遲。同時如果對Redis的性能有較高要求的話,應(yīng)盡可能在物理機(jī)上直接部署Redis。
- 檢查數(shù)據(jù)持久化策略
- 考慮引入讀寫分離機(jī)制
長耗時命令
Redis絕大多數(shù)讀寫命令的時間復(fù)雜度都在O(1)到O(N)之間,在文本和官方文檔中均對每個命令的時間復(fù)雜度有說明。
通常來說,O(1)的命令是安全的,O(N)命令在使用時需要注意,如果N的數(shù)量級不可預(yù)知,則應(yīng)避免使用。例如對一個field數(shù)未知的Hash數(shù)據(jù)執(zhí)行HGETALL/HKEYS/HVALS命令,通常來說這些命令執(zhí)行的很快,但如果這個Hash中的field數(shù)量極多,耗時就會成倍增長。
又如使用SUNION對兩個Set執(zhí)行Union操作,或使用SORT對List/Set執(zhí)行排序操作等時,都應(yīng)該嚴(yán)加注意。
避免在使用這些O(N)命令時發(fā)生問題主要有幾個辦法:
- 不要把List當(dāng)做列表使用,僅當(dāng)做隊列來使用
- 通過機(jī)制嚴(yán)格控制Hash、Set、Sorted Set的大小
- 可能的話,將排序、并集、交集等操作放在客戶端執(zhí)行
- 絕對禁止使用KEYS命令
- 避免一次性遍歷集合類型的所有成員,而應(yīng)使用SCAN類的命令進(jìn)行分批的,游標(biāo)式的遍歷
Redis提供了SCAN命令,可以對Redis中存儲的所有key進(jìn)行游標(biāo)式的遍歷,避免使用KEYS命令帶來的性能問題。同時還有SSCAN/HSCAN/ZSCAN等命令,分別用于對Set/Hash/Sorted Set中的元素進(jìn)行游標(biāo)式遍歷。SCAN類命令的使用請參考官方文檔:
https://redis.io/commands/scan
Redis提供了Slow Log功能,可以自動記錄耗時較長的命令。相關(guān)的配置參數(shù)有兩個:
slowlog-log-slower-than xxxms #執(zhí)行時間慢于xxx毫秒的命令計入Slow Logslowlog-max-len xxx #Slow Log的長度,即最大紀(jì)錄多少條Slow Log使用SLOWLOG GET [number]命令,可以輸出最近進(jìn)入Slow Log的number條命令。
使用SLOWLOG RESET命令,可以重置Slow Log
網(wǎng)絡(luò)引發(fā)的延遲
- 盡可能使用長連接或連接池,避免頻繁創(chuàng)建銷毀連接
- 客戶端進(jìn)行的批量數(shù)據(jù)操作,應(yīng)使用Pipeline特性在一次交互中完成。具體請參照本文的Pipelining章節(jié)
數(shù)據(jù)持久化引發(fā)的延遲
Redis的數(shù)據(jù)持久化工作本身就會帶來延遲,需要根據(jù)數(shù)據(jù)的安全級別和性能要求制定合理的持久化策略:
- AOF + fsync always的設(shè)置雖然能夠絕對確保數(shù)據(jù)安全,但每個操作都會觸發(fā)一次fsync,會對Redis的性能有比較明顯的影響
- AOF + fsync every second是比較好的折中方案,每秒fsync一次
- AOF + fsync never會提供AOF持久化方案下的最優(yōu)性能
- 使用RDB持久化通常會提供比使用AOF更高的性能,但需要注意RDB的策略配置
- 每一次RDB快照和AOF Rewrite都需要Redis主進(jìn)程進(jìn)行fork操作。fork操作本身可能會產(chǎn)生較高的耗時,與CPU和Redis占用的內(nèi)存大小有關(guān)。根據(jù)具體的情況合理配置RDB快照和AOF Rewrite時機(jī),避免過于頻繁的fork帶來的延遲
Swap引發(fā)的延遲
當(dāng)Linux將Redis所用的內(nèi)存分頁移至swap空間時,將會阻塞Redis進(jìn)程,導(dǎo)致Redis出現(xiàn)不正常的延遲。Swap通常在物理內(nèi)存不足或一些進(jìn)程在進(jìn)行大量I/O操作時發(fā)生,應(yīng)盡可能避免上述兩種情況的出現(xiàn)。
/proc//smaps文件中會保存進(jìn)程的swap記錄,通過查看這個文件,能夠判斷Redis的延遲是否由Swap產(chǎn)生。如果這個文件中記錄了較大的Swap size,則說明延遲很有可能是Swap造成的。
數(shù)據(jù)淘汰引發(fā)的延遲
當(dāng)同一秒內(nèi)有大量key過期時,也會引發(fā)Redis的延遲。在使用時應(yīng)盡量將key的失效時間錯開。
引入讀寫分離機(jī)制
Redis的主從復(fù)制能力可以實現(xiàn)一主多從的多節(jié)點架構(gòu),在這一架構(gòu)下,主節(jié)點接收所有寫請求,并將數(shù)據(jù)同步給多個從節(jié)點。
在這一基礎(chǔ)上,我們可以讓從節(jié)點提供對實時性要求不高的讀請求服務(wù),以減小主節(jié)點的壓力。
尤其是針對一些使用了長耗時命令的統(tǒng)計類任務(wù),完全可以指定在一個或多個從節(jié)點上執(zhí)行,避免這些長耗時命令影響其他請求的響應(yīng)。
關(guān)于讀寫分離的具體說明,請參見后續(xù)章節(jié)
主從復(fù)制與集群分片
主從復(fù)制
Redis支持一主多從的主從復(fù)制架構(gòu)。一個Master實例負(fù)責(zé)處理所有的寫請求,Master將寫操作同步至所有Slave。
借助Redis的主從復(fù)制,可以實現(xiàn)讀寫分離和高可用:
- 實時性要求不是特別高的讀請求,可以在Slave上完成,提升效率。特別是一些周期性執(zhí)行的統(tǒng)計任務(wù),這些任務(wù)可能需要執(zhí)行一些長耗時的Redis命令,可以專門規(guī)劃出1個或幾個Slave用于服務(wù)這些統(tǒng)計任務(wù)
- 借助Redis Sentinel可以實現(xiàn)高可用,當(dāng)Master crash后,Redis Sentinel能夠自動將一個Slave晉升為Master,繼續(xù)提供服務(wù)
啟用主從復(fù)制非常簡單,只需要配置多個Redis實例,在作為Slave的Redis實例中配置:
slaveof 192.168.1.1 6379 #指定Master的IP和端口當(dāng)Slave啟動后,會從Master進(jìn)行一次冷啟動數(shù)據(jù)同步,由Master觸發(fā)BGSAVE生成RDB文件推送給Slave進(jìn)行導(dǎo)入,導(dǎo)入完成后Master再將增量數(shù)據(jù)通過Redis Protocol同步給Slave。之后主從之間的數(shù)據(jù)便一直以Redis Protocol進(jìn)行同步
使用Sentinel做自動failover
Redis的主從復(fù)制功能本身只是做數(shù)據(jù)同步,并不提供監(jiān)控和自動failover能力,要通過主從復(fù)制功能來實現(xiàn)Redis的高可用,還需要引入一個組件:Redis Sentinel
Redis Sentinel是Redis官方開發(fā)的監(jiān)控組件,可以監(jiān)控Redis實例的狀態(tài),通過Master節(jié)點自動發(fā)現(xiàn)Slave節(jié)點,并在監(jiān)測到Master節(jié)點失效時選舉出一個新的Master,并向所有Redis實例推送新的主從配置。
Redis Sentinel需要至少部署3個實例才能形成選舉關(guān)系。
關(guān)鍵配置:
另外需要注意的是,Redis Sentinel實現(xiàn)的自動failover不是在同一個IP和端口上完成的,也就是說自動failover產(chǎn)生的新Master提供服務(wù)的IP和端口與之前的Master是不一樣的,所以要實現(xiàn)HA,還要求客戶端必須支持Sentinel,能夠與Sentinel交互獲得新Master的信息才行。
集群分片
為何要做集群分片:
- Redis中存儲的數(shù)據(jù)量大,一臺主機(jī)的物理內(nèi)存已經(jīng)無法容納
- Redis的寫請求并發(fā)量大,一個Redis實例以無法承載
當(dāng)上述兩個問題出現(xiàn)時,就必須要對Redis進(jìn)行分片了。
Redis的分片方案有很多種,例如很多Redis的客戶端都自行實現(xiàn)了分片功能,也有向Twemproxy這樣的以代理方式實現(xiàn)的Redis分片方案。然而首選的方案還應(yīng)該是Redis官方在3.0版本中推出的Redis Cluster分片方案。
本文不會對Redis Cluster的具體安裝和部署細(xì)節(jié)進(jìn)行介紹,重點介紹Redis Cluster帶來的好處與弊端。
Redis Cluster的能力
- 能夠自動將數(shù)據(jù)分散在多個節(jié)點上
- 當(dāng)訪問的key不在當(dāng)前分片上時,能夠自動將請求轉(zhuǎn)發(fā)至正確的分片
- 當(dāng)集群中部分節(jié)點失效時仍能提供服務(wù)
其中第三點是基于主從復(fù)制來實現(xiàn)的,Redis Cluster的每個數(shù)據(jù)分片都采用了主從復(fù)制的結(jié)構(gòu),原理和前文所述的主從復(fù)制完全一致,唯一的區(qū)別是省去了Redis Sentinel這一額外的組件,由Redis Cluster負(fù)責(zé)進(jìn)行一個分片內(nèi)部的節(jié)點監(jiān)控和自動failover。
Redis Cluster分片原理
Redis Cluster中共有16384個hash slot,Redis會計算每個key的CRC16,將結(jié)果與16384取模,來決定該key存儲在哪一個hash slot中,同時需要指定Redis Cluster中每個數(shù)據(jù)分片負(fù)責(zé)的Slot數(shù)。Slot的分配在任何時間點都可以進(jìn)行重新分配。
客戶端在對key進(jìn)行讀寫操作時,可以連接Cluster中的任意一個分片,如果操作的key不在此分片負(fù)責(zé)的Slot范圍內(nèi),Redis Cluster會自動將請求重定向到正確的分片上。
hash tags
在基礎(chǔ)的分片原則上,Redis還支持hash tags功能,以hash tags要求的格式明明的key,將會確保進(jìn)入同一個Slot中。例如:{uiv}user:1000和{uiv}user:1001擁有同樣的hash tag {uiv},會保存在同一個Slot中。
使用Redis Cluster時,pipelining、事務(wù)和LUA Script功能涉及的key必須在同一個數(shù)據(jù)分片上,否則將會返回錯誤。如要在Redis Cluster中使用上述功能,就必須通過hash tags來確保一個pipeline或一個事務(wù)中操作的所有key都位于同一個Slot中。
有一些客戶端(如Redisson)實現(xiàn)了集群化的pipelining操作,可以自動將一個pipeline里的命令按key所在的分片進(jìn)行分組,分別發(fā)到不同的分片上執(zhí)行。但是Redis不支持跨分片的事務(wù),事務(wù)和LUA Script還是必須遵循所有key在一個分片上的規(guī)則要求。主從復(fù)制 vs 集群分片
在設(shè)計軟件架構(gòu)時,要如何在主從復(fù)制和集群分片兩種部署方案中取舍呢?
從各個方面看,Redis Cluster都是優(yōu)于主從復(fù)制的方案
- Redis Cluster能夠解決單節(jié)點上數(shù)據(jù)量過大的問題
- Redis Cluster能夠解決單節(jié)點訪問壓力過大的問題
- Redis Cluster包含了主從復(fù)制的能力
那是不是代表Redis Cluster永遠(yuǎn)是優(yōu)于主從復(fù)制的選擇呢?
并不是。
軟件架構(gòu)永遠(yuǎn)不是越復(fù)雜越好,復(fù)雜的架構(gòu)在帶來顯著好處的同時,一定也會帶來相應(yīng)的弊端。采用Redis Cluster的弊端包括:
- 維護(hù)難度增加。在使用Redis Cluster時,需要維護(hù)的Redis實例數(shù)倍增,需要監(jiān)控的主機(jī)數(shù)量也相應(yīng)增加,數(shù)據(jù)備份/持久化的復(fù)雜度也會增加。同時在進(jìn)行分片的增減操作時,還需要進(jìn)行reshard操作,遠(yuǎn)比主從模式下增加一個Slave的復(fù)雜度要高。
- 客戶端資源消耗增加。當(dāng)客戶端使用連接池時,需要為每一個數(shù)據(jù)分片維護(hù)一個連接池,客戶端同時需要保持的連接數(shù)成倍增多,加大了客戶端本身和操作系統(tǒng)資源的消耗。
- 性能優(yōu)化難度增加。你可能需要在多個分片上查看Slow Log和Swap日志才能定位性能問題。
- 事務(wù)和LUA Script的使用成本增加。在Redis Cluster中使用事務(wù)和LUA Script特性有嚴(yán)格的限制條件,事務(wù)和Script中操作的key必須位于同一個分片上,這就使得在開發(fā)時必須對相應(yīng)場景下涉及的key進(jìn)行額外的規(guī)劃和規(guī)范要求。如果應(yīng)用的場景中大量涉及事務(wù)和Script的使用,如何在保證這兩個功能的正常運作前提下把數(shù)據(jù)平均分到多個數(shù)據(jù)分片中就會成為難點。
所以說,在主從復(fù)制和集群分片兩個方案中做出選擇時,應(yīng)該從應(yīng)用軟件的功能特性、數(shù)據(jù)和訪問量級、未來發(fā)展規(guī)劃等方面綜合考慮,只在確實有必要引入數(shù)據(jù)分片時再使用Redis Cluster。
下面是一些建議:
綜合上面幾點考慮,如果單臺主機(jī)的可用物理內(nèi)存完全足以支撐對Redis的容量需求,且Redis面臨的并發(fā)寫壓力距離Benchmark值還尚有距離,建議采用主從復(fù)制的架構(gòu),可以省去很多不必要的麻煩。同時,如果應(yīng)用中大量使用pipelining和事務(wù),也建議盡可能選擇主從復(fù)制架構(gòu),可以減少設(shè)計和開發(fā)時的復(fù)雜度。
Redis Java客戶端的選擇
Redis的Java客戶端很多,官方推薦的有三種:Jedis、Redisson和lettuce。
在這里對Jedis和Redisson進(jìn)行對比介紹
Jedis:
- 輕量,簡潔,便于集成和改造
- 支持連接池
- 支持pipelining、事務(wù)、LUA Scripting、Redis Sentinel、Redis Cluster
- 不支持讀寫分離,需要自己實現(xiàn)
- 文檔差(真的很差,幾乎沒有……)
Redisson:
- 基于Netty實現(xiàn),采用非阻塞IO,性能高
- 支持異步請求
- 支持連接池
- 支持pipelining、LUA Scripting、Redis Sentinel、Redis Cluster
- 不支持事務(wù),官方建議以LUA Scripting代替事務(wù)
- 支持在Redis Cluster架構(gòu)下使用pipelining
- 支持讀寫分離,支持讀負(fù)載均衡,在主從復(fù)制和Redis Cluster架構(gòu)下都可以使用
- 內(nèi)建Tomcat Session Manager,為Tomcat 6/7/8提供了會話共享功能
- 可以與Spring Session集成,實現(xiàn)基于Redis的會話共享
- 文檔較豐富,有中文文檔
對于Jedis和Redisson的選擇,同樣應(yīng)遵循前述的原理,盡管Jedis比起Redisson有各種各樣的不足,但也應(yīng)該在需要使用Redisson的高級特性時再選用Redisson,避免造成不必要的程序復(fù)雜度提升。
本文轉(zhuǎn)自k8s中文社區(qū)
總結(jié)
以上是生活随笔為你收集整理的redis scan 效率太慢_Redis 基础、高级特性与性能调优(下)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python自动点击脚本_[Python
- 下一篇: select隐藏_数仓|几种SQL隐藏的