一、設計優化
1、估算Redis內存使用量
要估算redis中的數據占據的內存大小,需要對redis的內存模型有比較全面的了解,下面以最簡單的字符串類型來舉例說明:
假設有90000個鍵值對,每個key的長度是12個字節,每個value的長度也是12個字節,且鍵值都不是整數類型。
然后我們可以預估一下這90000個鍵值對占用的空間,首先,我們可以判定字符串類型使用的是embstr;90000個鍵值對占用的空間只要可以分為兩部分,一部分是90000個dictEntry占據的字符串,一部分是鍵值對所需要的bucket空間。

每個dictEntry占據的空間包括:一個dictEntry結構、一個key、一個redisObject、一個value
dictEntry結構:24個字節,jemalloc會分配32個字節的內存塊(64位操作系統下,一個指針8字節,一個dictEntry由三個指針組成)
key:12個字節,所以SDS需要 12 + 4 = 16 個字節(SDS的長度=4 + 字符串長度),jemalloc會分配16個字節的內存塊
redisObject:16字節,jemalloc會分配16個字節的內存塊(固定長度:4bit + 4bit + 2bit + 4byte + 8byte = 16byte)
value:12個字節,所以SDS占16個字節,原理同key
綜上,一個dictEntry所占的空間為: 32+16+16+16 = 80 個字節。
bucket空間:
bucket數組大小為大於90000的最小的2的n次方,是131072,每個bucket元素(bucket中存儲的是指針元素)為8字節(因為64位操作系統中指針大小位8個字節)。
因此可以推算出,90000個鍵值對占據的內存大小為 90000*80 + 131072*8 = 82488576
2、優化內存占用
了解了redis的內存模型,對優化redis內存有很大的幫助,下面從4個角度來進行優化:
(1)利用jemalloc特性進行優化
以上面講述的90000個鍵值對的例子,由於jemalloc分配的空間是不連續的,因此KV字符串變化一個字節,可能會引起占用內存的很大變化,在設計時可以利用這一點。例如,如果key的長度是13,則SDS為17個字節,那么jemalloc就會分配32個字節;如果將key的長度縮減為12個字節,則SDS為16個字節,jemalloc也會分配16個字節,這樣每個key所占用的空間就可以縮小一半。
(2)使用整型/長整型
如果使用整型或長整型,Redis會使用int類型(8個字節)存儲來替代字符串類型,可以節省更多空間,因此在可以使用整型或長整型代替字符串的場景下,盡量使用整型或長整型。
(3)共享對象
利用共享對象,可以減少對象的創建,同時也減少了redisObject的創建,從而節省空間。
目前Redis中的共享對象只包括10000個整數(0-9999),可以通過調整REDIS_SHARED_INTEGERS參數提高共享對象的個數;例如將REDIS_SHARED_INTEGERS提高到20000,那么0-19999就都是共享對象。例如文章的瀏覽次數,基本上都是20000次以內,因此就可以將REDIS_SHARED_INTEGERS設置為20000,以便節省空間。
(4)縮短鍵值對的存儲長度
鍵值對的長度是和性能成反比的,比如我們做一組寫入數據的性能測試,執行結果如下:

從數據可以看出,在key不變的情況下,value值越大操作效率就越慢,因為Redis對於同一種數據類型會使用不同的內部編碼進行存儲,比如字符串的內部編碼就有三種:int、embstr、raw,這是因為Redis是想通過不同的編碼實現效率和空間的平衡,然而數據量越大使用的內部編碼就越復雜,而越復雜的內部編碼存儲的性能就越低。
這還只是寫入時的速度,當鍵值對內容較大時,還會帶來以下幾個問題:
內容越大,需要的持久化時間就越長,需要掛起的時間越長,Redis的性能就越低。
內容越大,在網絡上傳輸的內容就越多,需要的時間就越長,整體的運行速度就越低
內容越大,占用的內存就越多,就會更頻繁的觸發內存淘汰機制,從而給Redis帶來更多的運行負擔。
綜上所示,在盡量保證完整語義的同時,我們要盡量的縮短鍵值對的存儲長度,必要時要對數據進行序列化和壓縮再存儲,以Java為例,序列化我們可以使用protostuff或kryo,壓縮我們可以使用snappy。
二、設置鍵值的過期時間
我們應該根據實際的業務情況,對鍵值對設置合理的過期時間,這樣Redis會自動的清楚過期的鍵值對,從而達到節省內存占用的效果,可以避免鍵值對過多的堆積,頻繁觸發內存淘汰策略。
Redis有四個命令可以設置鍵的存活時間或過期時間:
expire:用於設置key還剩余多少秒過期
pexpire:用於設置key還有多少毫秒過期
expireat:用於設置key的過期時間(到期的具體秒數時間戳)
pexpireat:用於設置key的過期時間(到期的具體毫秒時間戳)
127.0.0.1:6388> set d1 va OK 127.0.0.1:6388> set d2 va OK 127.0.0.1:6388> set d3 va OK 127.0.0.1:6388> set d4 va OK 127.0.0.1:6388> expire d1 100 (integer) 1 127.0.0.1:6388> pexpire d2 100000 (integer) 1 127.0.0.1:6388> ttl d1 (integer) 83 127.0.0.1:6388> ttl d2 (integer) 91 127.0.0.1:6388> expireat d3 1612363856 (integer) 1 127.0.0.1:6388> ttl d3 (integer) 87127.0.0.1:6388> pexpireat d4 1612364096000 (integer) 1 127.0.0.1:6388> ttl d4 (integer) 112
三、限制Redis內存大小
需要使用maxmemory來設置Redis的最大內存,例如 maxmemory 1GB
在64位操作系統中,Redis的內存大小是沒有限制的,因為maxmemory配置項是被注釋掉的,這樣就會導致在Redis內存不足時,Redis會使用磁盤作為其虛擬內存,而當操作系統將Redis所用的內存分配至磁盤時,將會阻塞Redis進程,到處Redis出現延遲,從而影響Redis的整體性能,因此我們要限制Redis的內存大小為一個固定的值,並且該值不能大於服務器的內存。當Redis的運行達到此值時會觸發內部的淘汰策略,從而將內存回收。
首先說一下淘汰策略LRU和LFU:
LRU(最近最少被使用):新數據插入鏈表頭部;當命中緩存時,將數據移動到鏈表頭部;當鏈表滿時,將鏈表尾部數據丟棄
LFU(最少使用):新數據插入鏈表頭部,並設置訪問次數,並按照訪問次數排序;沒插入或者命中一次就記一次數,然后重新排序;如果鏈表滿時,從鏈表尾部刪除;
二者對比:LFU和LRU的側重點不同,LRU側重的是最近被訪問的數據不刪除,而LFU是保證訪問頻率最高的數據不被刪除,但是LFU有一個缺點,就是如果在某一個時段,某個key的訪問頻率非常高,那么該KEY就會變為熱點數據,但是實際上該KEY在其他時間節點不會被用到,那么會造成這些數據不會被刪除。
Redis4.0之后有8種淘汰策略:
| 淘汰策略 | 說明 |
| noeviction | 不淘汰任何數據,當內存不足時,新增操作會報錯,Redis默認淘汰策略 |
| allkeys-lru | 淘汰整個鍵值對中最久未使用的數據 |
| allkeys-random | 隨即淘汰任意值 |
| volatile-lru | 淘汰所有設置了過期時間的key中最久的key |
| volatile-random | 隨機淘汰所有設置了過期時間的key |
| volatile-ttl | 優先淘汰最早過期的key |
| volatile-lfu | 淘汰所有設置了過期時間的key中使用最少的key(4.0之后新增) |
| allkeys-lfu | 淘汰所有key中的最少被使用的key(4.0之后新增) |
四、使用 lazy free 特性
lazy free特性是Redis4.0新增的一個非常實用的功能,他可以理解為惰性刪除或延遲刪除,意思就是在刪除的時候提供異步刪除的功能,他把刪除key的操作放在BIO單獨的子線程處理中,以減少刪除操作對Redis主線程的阻塞,同時可以有效的避免刪除大的key時帶來的性能和可用性問題。
lazy free對應四種場景,默認都是關閉的:
lazyfree-lazy-eviction no lazyfree-lazy-expire no lazyfree-lazy-server-del no replica-lazy-flush no
lazyfree-lazy-eviction:表示當Redis運行內存超過maxmemory時,是否開啟 lazy free 機制刪除。
lazyfree-lazy-expire:表示設置了過期時間的key,是否開啟 lazy free 機制刪除。
lazyfree-lazy-server-del:有些指令在操作已存在的key時,是否開啟 lazy free機制刪除。這個需要特別說明一下,有些指令,會自帶一個隱式的del命令,例如rename指令,當目標key存在時,會先刪除目標key,再創建一個新的key,如果目標key是一個 big key 時,就會造成阻塞刪除的情況,因此使用該配置來設置是否要在該種情況下開啟 lazy free 機制刪除數據。
slave-lazy-flush:針對slave進行全量同步數據時,slave在加載master上的RDB前,會使用flushall來清理自己的數據,該配置用來設置該種場景下是否要開啟 lazy free 機制刪除數據。
這里比較建議開啟前三種 lazy free 配置,這樣就可以有效的提高主線程的執行效率。
五、禁用長耗時的查詢命令
Redis絕大多數的讀寫命令的時間復雜度都是在O(1)到O(N)之間,其中O(1)就可以放心使用,但是O(N)就要當心了,因為N表示不確定性,數據越大,查詢的速度就會越慢,因為Redis只用一個線程來做數據查詢,如果這些指令非常耗時,就會造成Redis的阻塞,那么就會造成大量的延時。
要避免O(N)類的命令對Redis性能造成的影響,就要從以下幾個方面進行改造:
1、禁止使用key * 命令;
2、避免一次查詢所有成員,要使用 scan 命令進行分批的、游標式的遍歷
3、通過機制嚴格空指Hset、Set、Scorted Set 等數據結構的大小
4、將排序、並集、交集等操作放在客戶端執行,以減少Redis服務器的運行壓力
5、刪除一個大的數據時,可能會需要很長的時間,所以建議用異步刪除的方式unlink,他會啟動一個新的線程來刪除目標數據,而不是阻塞Redis主線程。
六、使用 slowlog 優化耗時命令
我們可以使用slowlog功能,找出最耗時的Redis命令進行優化,以提升Redis的運行速度,慢查詢有兩個重要的配置項:
slowlog-log-slower-than:用於設置慢查詢的評定時間,也就是說執行時間超過改時間的命令,將會被當成慢查詢記錄在日志中,它的執行時間是微妙
slowlog-max-len:用來配置慢查詢日志的最大記錄數
slowlog-log-slower-than 10000 # There is no limit to this length. Just be aware that it will consume memory. # You can reclaim memory used by the slow log with SLOWLOG RESET. slowlog-max-len 128
我們可以根據實際的業務情況進行響應的配置,其中慢日志是按照插入順序倒敘存入慢查詢日志中的。
我們可以使用 slowlog get n 來獲取相關的慢查詢日志,再找到這些慢查詢對應的業務進行相關優化。
127.0.0.1:6388> slowlog get 10 (empty list or set)
七、避免大量數據同時失效
Redis過期Key的刪除使用的是貪心策略,其會在一秒內進行10次掃描(改配置可以在redis.conf文件中配置,默認配置是 hz 10),redis會隨機抽取20個key,刪除這20個key中過期的key,如果過期的key比重超過25%,則重復執行該流程,直到比重低於25%為止。
那么如果在同一時期如果存在大量的key一起過期,就會導致Redis多次循環持續掃描刪除過期key,直到被刪除的數據足夠多(redis的過期數據不再很密集,隨機抽取低於25%),才會停止,在此過程中,由於循環和刪除的原因,會導致Redis的獨寫出現明顯的卡頓,卡頓的另一種原因是內存管理器需要頻繁回收內存,因此也會消耗一定的CPU。
為了避免因為這種情況導致的卡頓現象,我們要預防大量的key在同一時刻一起過期,簡單的解決方案就是在過期時間的基礎上加一個指定范圍的隨機數。
八、檢查數據持久化策略
Redis的持久化策略有RDB、AOF和混合持久化方式(4.0之后新增),由於RDB和AOF各有利弊,RDB可能會造成一定的數據丟失,AOF由於文件較大,會影響Redis的啟動速度,因此如果是Redis4.0及以上版本,可以直接使用混合持久化方式。
同樣,如果在業務中,Redis不需要持久化數據庫時,可以關閉持久化,這樣可以有效地提升Redis的運行速度,不會出現間歇性卡頓的問題。
九、使用 Pipeline 批量操作數據
Pipeline(管道技術)是客戶端提供的一種批處理技術,用於一次處理多個Redis命令,從而提高整個交互的性能。
十、客戶端優化
在客戶端的使用上,我們盡量使用Pipeline技術外,還需要盡量使用Redis連接池,而不是頻繁的創建、銷毀Redis連接,這樣就可以減少網絡傳輸次數和減少非必要的調用命令。
十一、使用分布式架構來增加讀寫速度
Redis的分布式架構有主從同步、哨兵模式、Redis Cluster集群,主從同步可以將寫操作放在master上處理,可以將讀操作放在slave上處理,因此極大的提高了redis的處理性能;而Sentinel是對主從同步的一個升級,解決了單點故障以及故障遷移的問題;而RedisCluster集群,在Redis3.0推出,解決了redis的擴展問題,並且將獨寫操作使用hash分布到了不同的redis服務器上,更大的提高了redis命令的操作性能。
從選擇上,首選Redis Clluster集群的實現方案。
十二、使用物理機而非虛擬機
在虛擬機中運行Redis服務器,因為和物理機共享一個物理端口,並且一台物理機可以有多個虛擬機在運行,因此在內存占用上和網絡延遲方面會有很糟糕的表現,我們可以通過 ./redis-cli -- intrinsic-latency 100 命令來超看延遲時間,如果對Redis性能要求很高的話,應盡可能在物理機上直接部署Redis服務器。
[root@liconglong-aliyun redis-5.0.4]# ./src/redis-cli -p 6388 --intrinsic-latency 100 Max latency so far: 1 microseconds. Max latency so far: 11 microseconds. Max latency so far: 13 microseconds. Max latency so far: 21 microseconds. Max latency so far: 57 microseconds. Max latency so far: 136 microseconds. Max latency so far: 416 microseconds. Max latency so far: 498 microseconds. Max latency so far: 536 microseconds. Max latency so far: 577 microseconds. Max latency so far: 692 microseconds.
十三、禁用 THP 特性
Linux kernel在2.6.38內核增加了Transparent Huge Pages(THP)特性,支持大內存頁2M分配,默認開啟。
當開啟了THP時,fork的速度會變慢,fork之后每個內存頁由原來的4KB變為2MB,會大幅增加重寫期間父進程的內存消耗,同時每次寫命令引起的復制內存頁放大了512倍,會拖慢寫操作的執行時間,導致大量寫操作慢查詢,例如簡單的incr命令也會出現在慢查詢中,因此Redis建議將此特性禁用,禁用方法如下:
echo never > /sys/kernel/mm/transparent_hugepage/enabled
