Redis 中 String 類型的記憶體開銷比較大
使用 String 類型記憶體開銷大
如果我們有大量的數據需要來保存,在選型數據類型我們就需要知道 String 的記憶體開銷是很大的
這裡我們來分析下使用一個 String 類型需要用到的記憶體
1、簡單動態字元串
Redis 中的 String,使用的是簡單動態字元串(Simple Dynamic Strings,SDS)。
來看下數據結構
struct sdshdr {
// 記錄 buf 數組中已使用位元組的數量
// 等於 SDS 保存字元串的長度,不包含'\0'
long len;
// 記錄buf數組中未使用位元組的數量
long free;
// 位元組數組,用於保存字元串
char buf[];
};
如果,使用 SDS 存儲了一個字元串 hello,對應的 len 就是5,同時也申請了5個為未使用的空間,所以 free 就是5。對於 buf 來說,len 和 free 的記憶體佔用都是額外開銷。
2、RedisObject
因為 Redis 中有很多數據類型,對於這些不同的數據結構,Redis 為了能夠統一處理,所以引入了 RedisObject。
typedef struct redisObject {
unsigned type:4; // 類型
unsigned encoding:4; // 編碼
unsigned lru:LRU_BITS; // 最近被訪問的時間
int refcount; // 引用次數
void *ptr; // 指向具體底層數據的指針
} robj;
一個 RedisObject 包含了8位元組的元數據和一個8位元組指針,指針指向實際的數據記憶體地址。
不過需要注意的是這裡 Redis 做了優化
1、當保存的數據是 Long 類型整數時,RedisObjec t中的指針就直接賦值為整數數據了,就不用使用額外的指針了。
2、如果保存的是字元串數據,並且字元串大小小於等於44位元組時,RedisObject中的元數據、指針和SDS是一塊連續的記憶體區域,這樣就可以避免記憶體碎片。這種布局方式也被稱為 embstr 編碼方式。
3、如果保存的是字元串數據,並且字元串大小大於44位元組時,Redis 就不再把 SDS 和 RedisObject 布局在一起了,而是會給 SDS 分配獨立的空間,並用指針指向 SDS 結構。這種布局方式被稱為 raw 編碼模式。
這個引用一張Redis核心技術與實戰中的圖片
3、全局哈希表
Redis 中會有一個全局的哈希表來保存所有的鍵值對,哈希表中每一項存儲的是 dictEntry 結構體
typedef struct dictEntry {
void *key;
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
struct dictEntry *next;
} dictEntry;
dictEntry 結構體中有三個指針,在64位機器下佔24個位元組,jemalloc 會為它分配32位元組大小的記憶體單元。
jemalloc 作為 Redis 的默認記憶體分配器,在減小記憶體碎片方面做的相對比較好。jemalloc 在64位系統中,將記憶體空間劃分為小、大、巨大三個範圍;每個範圍內又劃分了許多小的記憶體塊單位;當 Redis 存儲數據時,會選擇大小最合適的記憶體塊進行存儲。
所以選用 String 類型來存儲字元串,上面的 RedisObject 結構、SDS 結構、dictEntry 結構的都會存在一定的記憶體開銷
Redis 中的底層數據結構,提供了壓縮列表,這種是很節省記憶體空間的。
我們可以使用 Hash 這種數據結構,因為在一定情況下這種結構底層的用的是壓縮列表,這是一種很節省記憶體的數據結構。
使用 Hash 來存儲
關於壓縮列表的細節可參見Redis中的壓縮列表
這些entry會挨個兒放置在記憶體中,不需要再用額外的指針進行連接,這樣就可以節省指針所佔用的空間。
Redis基於壓縮列表實現了 Hash 這樣的集合類型,因為一個集合可以保存多個鍵值對,使用一個鍵值對就能對應到這個集合中了。使用 String 類型時,一個鍵值對就對應一個 dictEntry,這點對於使用集合類型來講也是節省記憶體的一個點。
使用集合我們還需要注意一下幾點:
1、我們要去保證存放到集合中的元素不要太多,使用 ziplist 作為內部數據結構的限制元素數默認不超過 512 個。可以通過修改配置來調整zset_max_ziplist_entries
閥值的大小。如果超過了限制就不使用 ziplist 而是使用 Hash 類型來實現這個映射關係了。
2、同時元素也不能太少,如果一個 Hash 集合中只存入了一對filed/value
,就相當於每個鍵值對也使用了一個全局的哈希表的 dictEntry。
3、同時鍵值對的 value 也不要太長,超過了hash-max-ziplist-value
的限制也是會使用 Hash 類型而不是 ziplist。
原來使用 String 類型存儲,是一個k/v
結構,使用 Hash 類型,就需要兩個 key 了,可以將原來的k/v
中的 k 進行拆分,分成兩部分即可。
127.0.0.1:6379> set 202220222111 xiaoming
OK
127.0.0.1:6379> hset 20222 0222111 xiaoming
(integer) 1
總結
String 類型的元數據是會佔用一部分的記憶體空間,如果我們的數據,單個數據不大,但是數量很多,選用 String 這種類型的時候,需要考慮一下記憶體的佔用。
參考
【Redis核心技術與實戰】//time.geekbang.org/column/intro/100056701
【Redis設計與實現】//book.douban.com/subject/25900156/
【redis 一組kv實際記憶體佔用計算】//kernelmaker.github.io/Redis-StringMem
【Redis學習筆記】//github.com/boilingfrog/Go-POINT/tree/master/redis
【Redis 中 String 類型的記憶體開銷比較大】//boilingfrog.github.io/2022/02/22/redis中的string類型記憶體開銷比較大/