內存優化
我們都知道 Redis 的數據都存儲在內存中,而內存又是非常寶貴的資源,本文將講解如何進行內存優化。
redisObject 對象
首先需要了解什麼是 redisObject,在 Redis 中存儲的所有值對象在內部都被定義為redisObject,結構如下。
-
type:表示當前對象使用的數據類型,主要就是 string、hash、list、set、zset 五種。4 表示佔 4 個 bit 位。📢:使用 type [key] 命令可以查看對象的所屬類型,返回的是值對象的類型,鍵都為 string 類型。
encoding:表示內部編碼的類型,代表當前對象使用哪種數據結構實現。理解內部編碼類型對於內存優化非常重要。-
lru:記錄對象最後一次被訪問的時間,當配置了 maxmemory 和 maxmemory-policy=volatile-lru 或者 allkeys-lru 時,用於輔助 LRU 算法刪除鍵數據。📢:使用 object idletime [key] 在不更新 lru 字段時間的情況下查看當前鍵的空閒時間。
使用 scan + object idletime 命令可以批量查詢出那些鍵長時間未被訪問並進行清理,降低內存佔用。
-
refcount:記錄當前對象被引用的次數,當 refcount=0 的時候可以安全的進行對象空間回收。📢:使用 object recount [key] 獲取當前對象引用。當對象值在[0-9999]的時候,redis 會使用共享對象池來節省內存。
* ptr:如果存的是整數,則直接存儲數據,否則存儲指向數據的指針。當值對象為字符串並且<=44 字節時候,內部編碼為 embstr 類型,當>44 字節時候,使用 raw 類型。
縮減鍵值對象
縮減 key 和 value 的長度可以有效減少 Redis 內存使用情況,比如將 key 進行縮寫等。
value 比較複雜,如果是將業務對象進行序列化為二進制數組,可以去掉不必要的屬性,其次在序列化工具上可以選擇更高效的如:protostuff、kryo 等。除了二進制數組外,我們也會存入 json、xml 等字符串,在內存緊張情況下,我們可以使用壓縮算法來壓縮 json、xml 再存入 redis。
共享對象池
共享對象池指的是在 Redis 中維護了[0-9999]的整數對象池。因為創建大量的整數類型的 redisObject 存在內存開銷,一個 redisObject 內部至少佔用 16 字節,所以 Redis 維護了整數對象池來節約內存。另外,list、hash、set、zset 中也是可以使用共享對象池的。
如上圖,可以看到當值為 100 時,refcount 是 2147483647,其實就是 INT_MAX, 這是一個共享對象。而 12000 的引用計數是 1,是一個新創建的對象。
此時的 redisObject 如下:
📢:需要注意的是,在設置了 maxmemory 和 LRU 相關淘汰策略入:volatile-lru,allkeys-lru 時,Redis 此時會禁用共享對象池。
LRU 算法需要獲取對象最近一次訪問的時間,但共享對象池可能存在多個引用同時指向同一個 redisObject,這時 lru 字段也會被共享,導致無法獲取每個對象的最後一次訪問時間。但如果沒有設置 maxmemory 的話,直到內存用完之前都不會觸發回收機制,所以共享對象池可以一直使用。
📢:另外需要注意的是,如果內部編碼使用的是 ziplist 的值對象,即使所有數據為整數也無法使用共享對象池。因為 ziplist 使用壓縮且內存連續的結構,對象判斷成本過高。
字符串優化
在 Redis 中最常見的就是字符串,所有的鍵都是字符串類型,值對象數據類型除了整數就是字符串類型。所以如何進行字符串優化也是重點之一。
首先我們先來了解字符串結構。
字符串結構
redis 並沒有使用 C 語言中的字符串,而是自己實現了字符串結構。
簡單動態字符串(simple dynamic string)SDS
struct sdshdr{
//字節數組
char buf[];
//buf數組中已使用字節數量
int len;
//buf數組中未使用字節數量
int free;
}
SDS 的優點:
- O(1)的時間複雜度獲取字符串長度,已使用長度,未使用長度。
- 可以保存字節數組,支持安全的二進制數據存儲。
- 內部實現空間預分配機制,降低內存再分配次數。
- 惰性刪除機制,在字符串縮減後空間不立即釋放,作為預分配空間保留。
PS:有關字符串 SDS 的相關內容可以看之前的文章。
📢:需要注意的是,字符串使用預分配機制是為了防止頻繁修改字符串內容導致頻繁地進行重分配內存和字符串拷貝。所以需要儘可能減少修改,比如 append,setrange。可以改為直接使用 set 修改字符串,降低分配帶來的內存浪費和內存碎片化。
字符串重構
如果保存的是 json 數據,可以使用 hash 結構來進行存儲,使用 hmget,hmset 進行批量獲取和修改。
如果存在長字符串的情況下進行測試性能:
需要注意的是如果值對象字符串長度大於65則Redis會使用hashtable編碼方式,反而會消耗更多內存。
通過調整hash-max-ziplist-value=xx設置一個合適的值,則會使用 ziplist 編碼方式,會更節省內存。
編碼優化
瞭解編碼
Redis 提供了 string、list、hash、set、zet 等類型。但對每種類型存在不同編碼的概念,其實就是具體底層使用了哪種數據結構。不同的編碼會直接影響內存佔用和讀寫效率。
使用 object encoding [key]命令來獲取編碼類型。
> object encoding jack
int
> hset hello student jack
1
> object encoding hello
ziplist
...
| 類型 | 編碼方式 | 數據結構 |
|---|---|---|
| string | raw | 動態字符串 |
| string | embstr | 優化內存分配的字符串 |
| string | int | 整數 |
| hash | hashtable | 散列表 |
| hash | ziplist | 壓縮列表 |
| list | linkedlist | 雙向鏈表 |
| list | ziplist | 壓縮列表 |
| list | quicklist | 快速列表 |
| set | hashtable | 散列表 |
| set | intset | 整數集合 |
| zset | skiplist | 跳躍表 |
| zset | ziplist | 壓縮列表 |
📢:編碼類型在 Redis 寫入數據時自動完成,只能從小內存編碼轉換為大內存編碼,過程是不可逆的。
接下來我們看一下轉換條件是什麼樣的。
| 類型 | 編碼 | 轉換條件 |
|---|---|---|
| string | embstr | value 字節長度 <= 44 |
| string | raw | value 字節長度 > 44 |
| string | int | 整數 |
| hash | ziplist | value 字節長度<=hash-max-ziplist-value 並且 field 個數<=hash-max-ziplist-entries |
| hash | hashtable | value 字節長度>hash-max-ziplist-value 或者 field 個數>hash-max-ziplist-entries |
| list | ziplist | value 字節長度<=list-max-ziplist-value 並且鏈表長度<=list-max-ziplist-entries |
| list | linkedlist | value 字節長度>list-max-ziplist-value 或者鏈表長度>list-max-ziplist-entries |
| list | quicklist | 廢棄上述 list-max-ziplist-value、list-max-ziplist-entries 配置。使用:list-max-ziplist-size 表示最大壓縮空間或長度。最大空間使用[-5~1]範圍配置,默認-2 表示 8KB。正整數表示最大壓縮長度。list-compress-depth:表示最大壓縮深度,默認 0 不壓縮 |
| set | intset | 元素均為整數並且集合長度<=hash-max-ziplist-entries |
| set | hashtable | 元素非整數或者集合長度>hash-max-ziplist-entries |
| zset | ziplist | value 字節長度<=zset-max-ziplist-value 並且集合長度<=zset-max-ziplist-entries |
| zset | skiplist | value 字節長度>zset-max-ziplist-value 或者集合長度>zset-max-ziplist-entries |
ziplist 編碼
本文着重介紹一下 ziplist,ziplist 中所有數據都是採用線性連續存儲的內存結構,可以作為 list、hash、zset 底層數據結構實現。
zlbytes:記錄整個壓縮列表所佔用的字節長度。類型是 int-32,長度為 4 字節。zltail:記錄距離尾結點的偏移量,方便尾節點彈出操作。類型是 int-32,長度為 4 字節。zllen:記錄壓縮鏈表節點數量。entry:記錄具體的節點。prev_entry_bytes_length:記錄前一個節點所佔空間,用於快速定位上一個節點,也可以實現列表反向迭代。encoding:標示當前節點編碼和長度,前兩位標示編碼類型:字符串/整數,其餘位表示數據長度。contents:保存節點的值。zlend:記錄列表結尾,佔用一個字符。
特點:
- 內部為數據緊湊排列的一塊連續內存數組。
- 可以模擬雙向鏈表結構,O(1)時間複雜度入隊和出隊。
- ziplist 在空間利用率上極高,每個 entry 最多隻有 6 字節的浪費。
- ziplist 底層結構無鏈表,通過內存偏移量獲取 next 或 last 節點位置
- ziplist 在插入和刪除的時候有很大的概率出現連鎖更新,因此在使用時儘量保證所存儲的 value 位數相同,否則最壞會出現 O(n^2)的時間複雜度。
總結
- Redis 內存消耗主要在於:鍵值對象、緩衝區內存。
- 通過
maxmemory來控制 Redis 最大可用內存,當超出設置的內存大小後,根據maxmemory-policy控制內存回收策略。 - 使用共享對象池優化小整數對象。
- 優先使用整數,比字符串更節省空間。
- 優化字符串使用,避免預分配造成的內存浪費。
- 使用 ziplist 壓縮編碼優化 hash、list、zset 結構。
- 使用 intset 編碼優化整數集合。