以下為個人學習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
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來清除;
3.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位的。
8.盡可能的使用hash數據結構:
- 因為Redis在儲存小於100個字段的Hash結構上,其存儲效率是非常高的。所以在不需要集合(set)操作或list的push/pop操作的時候,盡可能的使用hash結構
- 使用單命令多參數的命令取代多命令單參數的命令:
- set -> mset
- get -> mget
- lset -> lpush, rpush
- lindex -> lrange
- hset -> hmset
- hget -> hmget
9.減少key的數量(編碼為ziplist的hash數據結構的妙用3)
- 把大量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中,......
