鍍金池/ 教程/ 大數(shù)據(jù)/ 內(nèi)存數(shù)據(jù)管理
Redis 數(shù)據(jù)淘汰機制
積分排行榜
小剖 Memcache
Redis 數(shù)據(jù)結(jié)構(gòu) intset
分布式鎖
從哪里開始讀起,怎么讀
Redis 數(shù)據(jù)結(jié)構(gòu) dict
不在浮沙筑高臺
Redis 集群(上)
Redis 監(jiān)視器
源碼閱讀工具
Redis 日志和斷言
內(nèi)存數(shù)據(jù)管理
Redis 數(shù)據(jù)結(jié)構(gòu)綜述
源碼日志
Web 服務(wù)器存儲 session
消息中間件
Redis 與 Lua 腳本
什么樣的源代碼適合閱讀
Redis 數(shù)據(jù)結(jié)構(gòu) sds
Memcached slab 分配策略
訂閱發(fā)布機制
Redis 是如何提供服務(wù)的
Redis 事務(wù)機制
Redis 集群(下)
主從復(fù)制
Redis 應(yīng)用
RDB 持久化策略
Redis 數(shù)據(jù)遷移
Redis 事件驅(qū)動詳解
初探 Redis
Redis 與 Memcache
AOF 持久化策略
Redis 數(shù)據(jù)結(jié)構(gòu) redisOb
作者簡介
Redis 數(shù)據(jù)結(jié)構(gòu) ziplist
Redis 數(shù)據(jù)結(jié)構(gòu) skiplist
Redis 哨兵機制

內(nèi)存數(shù)據(jù)管理

共享對象

在 Redis 服務(wù)器初始化的時候,便將一些常用的字符串變量創(chuàng)建好了,免去 Redis 在線服務(wù)時不必要的字符串創(chuàng)建。共享對象的結(jié)構(gòu)體為 struct sharedObjectsStruct,摘抄它的內(nèi)容如下:

struct sharedObjectsStruct {
robj *crlf, *ok, *err, *emptybulk, *czero, *cone, *cnegone, *pong, *space,
......
};

譬如在 Redis 通信協(xié)議里面,會較多使用的”\r\n”這些字符串都在 initServer() 函數(shù)被初始化。

兩種內(nèi)存分配策略

在 zmalloc.c 中 Redis 對內(nèi)存分配策略做了包裝。Redis 允許使用四種內(nèi)存管理策略,分別是jemalloc,tcmalloc, 蘋果系統(tǒng)自帶的malloc 和其他系統(tǒng)自帶的malloc。當(dāng)有前面三種分配策略的時候,就使用前面三種,最后一個種分配策略是不選之選。

jemalloc 是 freebsd 操作系統(tǒng)自帶的內(nèi)存分配策略,它具有速度快,多線程優(yōu)化的特點 TODO,firefox 以及facebook 都在使用 jemalloc。而 tcmalloc 是google 開發(fā)的,內(nèi)部集成了很多內(nèi)存分配的測試工具,chrome 瀏覽器和 protobuf TODO 用的都是 tcmalloc。兩者在業(yè)界都很出名,性能也不分伯仲。Redis 是一個內(nèi)存數(shù)據(jù)庫,對存取的速度要求非常高,因此一個好的內(nèi)存分配策略能幫助提升 Redis 的性能。

本篇不對這兩種內(nèi)存分配策略做深入的講解。

memory aware 支持

Redis 所說的 memory aware 即為能感知所使用內(nèi)存總量的特性,能夠?qū)崟r獲取 Redis 所使用內(nèi)存的大小,從而監(jiān)控內(nèi)存。所使用的思路較為簡單,每次分配/釋放內(nèi)存的時候都更新一個全局的內(nèi)存使用值。我們先來看malloc_size(void *ptr) 函數(shù),這種類似的函數(shù)的存在只是為了方便開發(fā)人員監(jiān)控內(nèi)存。

上述的內(nèi)存分配策略 jemalloc,tcmalloc 和蘋果系統(tǒng)自帶的內(nèi)存分配策略可以實時獲取指針?biāo)竷?nèi)存的大小,如果上述三種內(nèi)存分配策略都不支持,Redis 有一個種近似的方法來記錄指針?biāo)竷?nèi)存的大小,這個 trick 和 sds 字符串的做法是類似的。

zmalloc() 函數(shù)會在所需分配內(nèi)存大小的基礎(chǔ)上,預(yù)留一個整型的空間,來存儲指針?biāo)竷?nèi)存的大小。這種辦法是備選的,其所統(tǒng)計的所謂“指針?biāo)竷?nèi)存大小”不夠準(zhǔn)確。因為,平時我們所使用的 malloc() 申請內(nèi)存空間的時候,可能實際申請的內(nèi)存大小會比所需大,也就是說有一部分內(nèi)存被浪費了,所以 Redis 提供的這種方法不能統(tǒng)計浪費的內(nèi)存空間。

http://wiki.jikexueyuan.com/project/redis/images/m.png" alt="" />

摘抄 zmalloc() 函數(shù)的實現(xiàn):

void *zmalloc(size_t size) {
    // 預(yù)留了一小段空間
    void *ptr = malloc(size+PREFIX_SIZE);
    // 內(nèi)存溢出
    // error. out of memory.
    if (!ptr) zmalloc_oom_handler(size);
        // 更新已用內(nèi)存大小。
        // jemalloc,tcmalloc 或者蘋果系統(tǒng)支持實時獲取指針?biāo)竷?nèi)存大小
#ifdef HAVE_MALLOC_SIZE
    update_zmalloc_stat_alloc(zmalloc_size(ptr));
    return ptr;
    // 其他情況使用 Redis 自己的策略獲知指針?biāo)竷?nèi)存大小
#else
    *((size_t*)ptr) = size;
    update_zmalloc_stat_alloc(size+PREFIX_SIZE);
    return (char*)ptr+PREFIX_SIZE;
#endif
}

update_zmalloc_stat_alloc() 宏所要做的即為更新內(nèi)存占用數(shù)值大小,因為這個數(shù)值是全局的,所以 Redis 做了互斥的保護(hù)。有同學(xué)可能會有疑問,Redis 服務(wù)器的工作模式不是單進(jìn)程單線程的么,這里不需要做互斥的保護(hù)。在 Redis 關(guān)閉一些客戶端連接的時候,有時 TODO 交給后臺線程來做。因此,嚴(yán)格意義上來講,互斥是要做的。

update_zmalloc_stat_alloc() 宏首先會檢測 zmalloc_threadsafe 值是否為 1,zmalloc-thread_safe 默認(rèn)為 0,也就是說 Redis 默認(rèn)不考慮互斥的情況;倘若 zmalloc_thread_safe 為 1,會使用原子操作函數(shù)或加鎖的方式更新內(nèi)存占用數(shù)值。

// 更新已使用內(nèi)存大小
#define update_zmalloc_stat_alloc(__n) do { \
    size_t _n = (__n); \
    // 按4 字節(jié)向上取整
    if (_n&(sizeof(long)-1)) _n += sizeof(long)-(_n&(sizeof(long)-1)); \
        // 如果設(shè)置了線程安全,調(diào)用專門線程安全函數(shù)
    if (zmalloc_thread_safe) { \
        // 使用院子操作或者互斥鎖,更新內(nèi)存占用數(shù)值used_memory
        update_zmalloc_stat_add(_n); \
    } else { \
        used_memory += _n; \
    } \
} while(0)

上述是分配內(nèi)存的情況,釋放內(nèi)存的情況則反過來。

zmalloc_get_private_dirty() 函數(shù)

在 RDB 持久化的篇章中,曾經(jīng)提到這函數(shù),我打算在這一節(jié)中稍微詳細(xì)展開講。操作系統(tǒng)為每一個進(jìn)程維護(hù)了一個虛擬地址空間,虛擬地址空間對應(yīng)著物理地址空間,在虛擬地址空間上的連續(xù)并不代表物理地址空間上的連續(xù)。

http://wiki.jikexueyuan.com/project/redis/images/m1.png" alt="" />

在 linux 編程中,進(jìn)程調(diào)用 fork() 函數(shù)后會產(chǎn)生子進(jìn)程。之前的做法是,將父進(jìn)程的物理空間為子進(jìn)程拷貝一份。出于效率的考慮,可以只在父子進(jìn)程出現(xiàn)寫內(nèi)存操作的時候,才為子進(jìn)程拷貝一份。如此不僅節(jié)省了內(nèi)存空間,且提高了 fork() 的效率。在 RDB 持久化過程中,父進(jìn)程繼續(xù)提供服務(wù),子進(jìn)程進(jìn)行 RDB 持久化。持久化完畢后,會調(diào)用 zmalloc_get_private_dirty() 獲取寫時拷貝的內(nèi)存大小,此值實際為子進(jìn)程在 RDB 持久化操作過程中所消耗的內(nèi)存。

總結(jié)

Redis 是內(nèi)存數(shù)據(jù)庫,對內(nèi)存的使用較為謹(jǐn)慎。

有一點建議。我們前面講過,Redis 服務(wù)器中有多個數(shù)據(jù)集,在平時的數(shù)據(jù)集的選擇上,可以按業(yè)務(wù)來講不同來將數(shù)據(jù)存儲在不同的數(shù)據(jù)集中。將數(shù)據(jù)集中在一兩個數(shù)據(jù)集,查詢的效率會降低。