以下為個人學習Redis的備忘錄--記憶體優化,基於Redis4.0.2 1.隨時查看info memory,瞭解記憶體使用狀況:127.0.0.1:6379> info memory# Memoryused_memory:2314624 //(位元組單位形式)used_memory_human:2.21 ...
以下為個人學習Redis的備忘錄--記憶體優化,基於Redis4.0.2
1.隨時查看info memory,瞭解記憶體使用狀況:127.0.0.1:6379> info memory
# Memory
used_memory:2314624 //(位元組單位形式)
used_memory_human:2.21M //Redis已分配的記憶體總量(易讀單位形式)
used_memory_rss:1282048
used_memory_rss_human:1.22M //操作系統為Redis進程分配的記憶體總量
used_memory_peak:18010560
used_memory_peak_human:17.18M //最大使用記憶體總量(峰值)
used_memory_peak_perc:12.85%
used_memory_overhead:2078792
used_memory_startup:963088
used_memory_dataset:235832
used_memory_dataset_perc:17.45%
total_system_memory:4294967296
total_system_memory_human:4.00G
used_memory_lua:37888
used_memory_lua_human:37.00K //緩存Lua腳本占用的記憶體
maxmemory:0
maxmemory_human:0B //最大記憶體限制,0表示無限制
maxmemory_policy:noeviction //超過記憶體限制後的處理策略
mem_fragmentation_ratio:0.55 //碎片率(used_memory_rss/used_memory的比值),>1表示有碎片,<1表示部分Redis的記憶體被系統交換到硬碟(此時Redis性能變差)
mem_allocator:libc
active_defrag_running:0
lazyfree_pending_objects:0
2.Redis主進程的記憶體消耗:
- Redis自身使用的記憶體:消耗很少,3MB多點
- 對象記憶體
- 緩衝記憶體
- 記憶體碎片
- 每次創建鍵值對時,至少創建兩個類型對象:key對象、value對象,應該使用短鍵名
- 每個客戶端的輸入、輸出緩衝記憶體:
- 輸入緩衝最大1G,超出則關閉該客戶端連接;
- 輸出緩衝:16KB的固定緩衝區、動態緩衝區,動態緩衝區可通過client-output-buffer-limit配置參數限制(根據客戶端類型normal、slave、pubsub,分開設置)
- client-output-buffer-limit normal 0 0 0
- client-output-buffer-limit slave 256mb 64mb 60 //超過256MB時,或者持續超過64MB達60秒,關閉連接
- client-output-buffer-limit pubsub 32mb 8mb 60
- 複製積壓緩衝記憶體:用於主從複製的部分複製,所有客戶端共用該緩衝區,預設1MB,可通過repl-backlog-size調整,適當調大,可有效避免全量複製;
- AOF緩衝記憶體:用於保存在AOF重寫期間的寫命令,便於重寫完畢後把緩衝的命令追加到AOF文件中;
- 當存儲的數據長短差異較大時,就容易出現大量記憶體碎片,應該儘可能地保持數據對齊或使用固定長度的字元串;
- 記憶體碎片只能通過完全重啟Redis來清除;
- 在執行AOF重寫和RDB快照持久化時,會fork一個子進程,父子進程將共用此刻的記憶體快照,期間,在Linux下使用寫時複製技術:父進程會為新進的寫命令請求需要修改的記憶體頁複製出一份副本來完成寫操作,子進程結束後,父進程再把該副本覆蓋回原來的記憶體頁。
- Linux預設開啟的THP把寫時複製期間的記憶體頁複製單位從4KB變為2MB,加大了持久化時的記憶體消耗,應該關閉該功能:sudo echo never > /sys/kernel/mm/transparent_hugepage/enabled
- 設置記憶體上限,並指定記憶體回收策略;
- maxmemory配置參數可限制當前Redis實例可使用的最大記憶體;
- 通過config set maxmemory可根據業務需求,動態調整記憶體限制;
- 通過設置記憶體上限,可方便地在一臺伺服器上部署多個Redis實例
- 為鍵設置過期屬性,Redis採用惰性刪除和定時任務刪除機制實現過期鍵的記憶體回收;
- 惰性刪除:在讀取鍵時才檢查是否過期
- 定時任務刪除:通過hz配置參數設置頻率,預設每秒10次;
- 記憶體溢出控制策略:共6中策略,通過maxmemory-policy配置參數控制,預設noeviction(不刪除,拒絕寫入,返回錯誤)
- LRU演算法表示最近最少使用的,LFU演算法表示最不常用的:
- #volatile-lru - >在設置了過期的key中,刪除最近最少使用的key,直到空間足夠為止
- #allkeys-lru - >從所有key里刪除最近最少使用的key,不管有沒設置過期,直到空間足夠為止
- #volatile-lfu - >在設置了過期的key中,刪除最少使用的key,直到空間足夠為止
- #allkeys-lfu - >從所有key里刪除最少使用的key,不管有沒設置過期,直到空間足夠為止
- #volatile-random - >刪除一個過期集合中的隨機key。
- #allkeys-random - >刪除一個隨機key,不管有沒設置過期。
- #volatile-ttl - >刪除即將過期的key(次TTL)
- #noviction - >不刪除,拒絕寫入,寫入操作時返回錯誤。
- maxmemory-samples 5 是說每次進行淘汰的時候,會隨機抽取5個key 從裡面淘汰最少使用的(預設選項)
- 應避免記憶體溢出,因為在記憶體溢出且非noeviction策略時,會頻繁觸發回收記憶體的操作,影響Redis性能,若有從節點,還會把刪除命令同步給從節點;
- 對於只做緩存的場景下,可通過調小maxmemory,並執行一次命令,如果使用非noeviction策略,則會一次性回收到maxmemory指定的記憶體使用量,實現記憶體的快速回收,但會導致數據丟失和短暫阻塞;
- Redis存儲的所有數據都使用redisObject來封裝,包括string、hash、list、set、zset
- redisObject的欄位:
- type欄位:保存對象使用的數據類型,命令type {key}返回值對象的數據類型
- encoding欄位:保存對象使用的內部編碼類型,命令object encoding {key}返回值對象的內部編碼類型
- lru欄位:記錄對象最後一次被訪問的時間(用於記憶體回收),命令object idletime {key}查看鍵的空閑時間(可配合scan命令批量查找長期空閑的鍵進行清理)
- refcount欄位:記錄對象的引用計數(用於回收),命令object refcount {key}查看鍵的引用數
- *ptr欄位:存儲值對象的數據或指針,如果是整數,則直接存儲數據,否則表示指向數據的指針
- 字元串長度在39位元組以內對象,在創建redisObject封裝對象時只需分配記憶體1次,可提高性能;
- 縮減鍵、值對象的長度:簡化鍵名,使用高效的序列化工具來序列化值對象,還可使用壓縮工具(Google Snappy)壓縮序列化後的數據;
- 共用對象池:Redis內部維護[0-9999]的整數對象池,對於0-9999的內部整數類型的元素、整數值對象都會直接引用整數對象池中的對象,因此儘量使用整數對象可節省記憶體;
- 註意:
- 啟用LRU相關的溢出策略時,無法使用共用對象池;
- 對於ziplist編碼的值對象,也無法使用共用對象池(成本過高);
- Redis對字元串的優化:
- Redis所有key都是string類型,且value對象的數據除了整數之外,最終也都使用string來存儲;
- Redis字元串結構採用SDS(內部簡單動態字元串):
- int len欄位:已用位元組長度
- int free欄位:未用位元組長度
- char buf[]欄位:位元組數組
- SDS字元串特點:
- 獲取字元串長度、未用長度速度快,時間複雜度為O(1)
- 用位元組數組保存數據,支持安全的二進位數據存儲
- 內部實現了預分配記憶體機制,降低記憶體分配次數
- 惰性刪除機制,字元串縮減後的空間不釋放,作為預分配空間保留
- SDS字元串記憶體預分配機制:
- 首次創建時,不做預分配,數據剛好填滿位元組數組,len欄位為位元組數組長度,free欄位為0
- 在修改字元後,如果原本的free空間不足,且當前總數據大小<1MB,則每次預分配1倍容量,而如果總數據大小>1MB,則每次預分配1MB容量。
- 如:(忽略len、free欄位所占記憶體,只考慮buf所占記憶體)
- 對於首次創建的30位元組字元串,對它執行append追加10位元組,將使用(30+10)+40+1=81位元組的記憶體
- 而直接set這40位元組的字元串,只使用41位元組的記憶體(1位元組為結尾標識'\0')
- 應該儘量避免頻繁執行增長字元串的命令,如append、setrange,改為直接用set一次性創建字元串,減少預分配帶來的記憶體浪費和降低記憶體碎片率;
- 字元串重構:(編碼為ziplist的hash數據結構的妙用1)
- 對於非簡單字元串數據,可用hash數據結構代替
- 因為小hash使用ziplist編碼,可節省記憶體(字元串數據必須小於hash-max-ziplit-value配置的值)
- 且hash可用使用hmget、hmset命令,支持field-value的部分讀取修改,而不必每次都整體存取
Redis的每種數據結構都有至少兩種內部數據編碼類型:object encoding {key} 獲取key對應的value對象的編碼類型
string | int | 8個位元組的長整型 |
embstr | <=39位元組的字元串 | |
raw | >39位元組的字元串(最大不能超過512MB) | |
hash | ziplist | 壓縮列表(模擬雙向鏈表),記憶體占用少,但讀寫時間複雜度為O(n²) |
hashtable | 哈希表,記憶體占用較大,但讀寫時間複雜度為O(1) | |
list | quicklist (ziplist) | 快速雙向鏈表(每個節點都是ziplist) |
set | intset | 整數集合 |
hashtable | 哈希表 | |
zset | ziplist | 壓縮列表 |
skiplist | 跳躍表 |
- Redis在寫入數據時自動完成編碼轉換,且在超過配置的限制值時將轉換為新的內部編碼,動態修改限制參數不會回退為舊編碼,只有在重啟Redis重新載入數據後才會回退;
- ziplist編碼:
- ziplist內部結構:
- zlbytes欄位:int-32類型,記錄整個ziplist總位元組數,便於重新調整ziplist空間;
- zltail欄位:記錄距離尾節點的偏移量,便於尾節點的彈出操作;
- zllen欄位:記錄節點數量;
- entry1...entryN節點:記錄具體的節點,長度根據具體的數據
- prev_entry_bytes_length:記錄前一節點所占空間,用於快速定位前一節點實現列表的反向迭代;
- encoding:當前節點編碼和長度,前兩位表示編碼類型(字元串、整數),其餘位表示數據長度;
- contents:保存節點的值,針對實際數據長度做記憶體占用優化;
- zlend欄位:記錄列表結尾,占1個位元組
- ziplist是一塊連續的記憶體,它模擬了雙向鏈表的功能,兩端的push和pop速度快,但是對中間元素的修改不方便,每次在中間插入、刪除都會引發記憶體重新分配和數據拷貝,ziplist越長性能越低,所以ziplist僅適合存儲小對象和長度有限的數據。
- 因此,ziplist的長度不宜過長(建議1000個以內),且元素大小不宜過大(建議512位元組以內),且最好每個元素的大小差別不宜過大(否則碎片多)。
- 對於較小的hash、zset 數據結構,Redis會自動使用ziplist編碼,雖然list的編碼為quicklist,但list的節點也是ziplist編碼。
- hash同時滿足以下條件則使用ziplist編碼,超過則使用hashtable編碼
- hash-max-ziplist-entries 64
- hash-max-ziplist-value 512
- list使用的是quicklist編碼,quicklist的每個節點都是ziplist,以下指定節點的設置
- list-max-ziplist-size -2 //>0時表示每個節點最多包含幾個數據項,即ziplist的長度。<0時,只能取-5~-1,指每個節點ziplist的最大位元組大小≤64KB~4KB位元組(超過該限制時,則新建一個節點)
- list-compress-depth n //n表示兩端不被壓縮的節點個數(壓縮所有中間節點),預設0不壓縮
- zset同時滿足以下條件則使用ziplist編碼,超過則使用skiplist編碼
- zset-max-ziplist-entries 128
- zset-max-ziplist-value 64
- set所有元素都為整數,且個數小於以下參數時,使用intset編碼,否則使用hashtable編碼
- set-max-intset-entries 512
- intset編碼:
- intset編碼是無序集合(set)類型編碼的一種,內部表現為存儲有序、不重覆的整數集合
- intset結構:
- encoding:根據集合內最長整數值確定所有元素的類型(int-16、int-32、int-64),當插入一個更長的整數類型時,會觸發類型升級操作(會導致重新申請記憶體空間,並複製數據到新數組)
- length:集合元素的個數;
- contents:整數數組,按從小到大順序保存;
- 所以,在使用set集合,且為整數時,應該保持整數長度類型的一致性,避免記憶體浪費;
- set小集合重構(編碼為ziplist的hash數據結構的妙用2):因為當set集合中有一個是非整數時,將使用hashtable編碼,無法使用intset實現記憶體優化,如果集合元素個數和大小滿足hash的ziplist編碼條件,則此時可用hash類型來模擬集合,把hash的field設為set的元素,而hash的value設為1位元組占位符即可;
- 假如緩存數據小於4GB,就使用32位Redis實例,因為對於每一個key,將使用更少的記憶體,指針占用的位元組數更少。
- 使用make 32bit命令編譯生成32位的redis。但記憶體受限在4G內,不過他們的RDB和AOF文件是相容在32位和64位的。
- 因為Redis在儲存小於100個欄位的Hash結構上,其存儲效率是非常高的。所以在不需要集合(set)操作或list的push/pop操作的時候,儘可能的使用hash結構
- 使用單命令多參數的命令取代多命令單參數的命令:
- set -> mset
- get -> mget
- lset -> lpush, rpush
- lindex -> lrange
- hset -> hmset
- hget -> hmget
- 把大量value為string的普通key-value抽象為分組的小hash的field-value,建議field總個數<1000,value的長度<512位元組,value越小,越省空間(最好50位元組以內)
key = username0000 value =strs ... key = username9999 value =strs
- 以上可重構為10組hash key,每組1000個field
key = username0 field = 000 value = str ... field =999 value =str ... key = username9 field = 000 value = str ... field =999 value =str
- 對於只含可計算的field的Hash:
- 也可使用分組hash:如下,每100個用戶ID共用一個hash key
- key=userId/100, field1=userId%100, field1Value=str, field2=userId%100, field2Value=str, ...
- 即:userId為1~100的所有用戶的userId-value鍵值對都存儲在key=0的field-value中,而101~200則存在key=1中,......