返回首页 Redis 源码日志

源码日志

Redis 服务框架

Redis 基础数据结构

Redis 内功心法

Redis 应用

其他

内存数据管理

共享对象

在 Redis 服务器初始化的时候,便将一些常用的字符串变量创建好了,免去 Redis 在线服务时不必要的字符串创建。共享对象的结构体为 struct sharedObjectsStruct,摘抄它的内容如下:

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

譬如在 Redis 通信协议里面,会较多使用的”\r\n”这些字符串都在 initServer() 函数被初始化。

两种内存分配策略

在 zmalloc.c 中 Redis 对内存分配策略做了包装。Redis 允许使用四种内存管理策略,分别是jemalloc,tcmalloc, 苹果系统自带的malloc 和其他系统自带的malloc。当有前面三种分配策略的时候,就使用前面三种,最后一个种分配策略是不选之选。

jemalloc 是 freebsd 操作系统自带的内存分配策略,它具有速度快,多线程优化的特点 TODO,firefox 以及facebook 都在使用 jemalloc。而 tcmalloc 是google 开发的,内部集成了很多内存分配的测试工具,chrome 浏览器和 protobuf TODO 用的都是 tcmalloc。两者在业界都很出名,性能也不分伯仲。Redis 是一个内存数据库,对存取的速度要求非常高,因此一个好的内存分配策略能帮助提升 Redis 的性能。

本篇不对这两种内存分配策略做深入的讲解。

memory aware 支持

Redis 所说的 memory aware 即为能感知所使用内存总量的特性,能够实时获取 Redis 所使用内存的大小,从而监控内存。所使用的思路较为简单,每次分配/释放内存的时候都更新一个全局的内存使用值。我们先来看malloc_size(void *ptr) 函数,这种类似的函数的存在只是为了方便开发人员监控内存。

上述的内存分配策略 jemalloc,tcmalloc 和苹果系统自带的内存分配策略可以实时获取指针所指内存的大小,如果上述三种内存分配策略都不支持,Redis 有一个种近似的方法来记录指针所指内存的大小,这个 trick 和 sds 字符串的做法是类似的。

zmalloc() 函数会在所需分配内存大小的基础上,预留一个整型的空间,来存储指针所指内存的大小。这种办法是备选的,其所统计的所谓“指针所指内存大小”不够准确。因为,平时我们所使用的 malloc() 申请内存空间的时候,可能实际申请的内存大小会比所需大,也就是说有一部分内存被浪费了,所以 Redis 提供的这种方法不能统计浪费的内存空间。

摘抄 zmalloc() 函数的实现:

void *zmalloc(size_t size) {
    // 预留了一小段空间
    void *ptr = malloc(size+PREFIX_SIZE);
    // 内存溢出
    // error. out of memory.
    if (!ptr) zmalloc_oom_handler(size);
        // 更新已用内存大小。
        // jemalloc,tcmalloc 或者苹果系统支持实时获取指针所指内存大小
#ifdef HAVE_MALLOC_SIZE
    update_zmalloc_stat_alloc(zmalloc_size(ptr));
    return ptr;
    // 其他情况使用 Redis 自己的策略获知指针所指内存大小
#else
    *((size_t*)ptr) = size;
    update_zmalloc_stat_alloc(size+PREFIX_SIZE);
    return (char*)ptr+PREFIX_SIZE;
#endif
}

update_zmalloc_stat_alloc() 宏所要做的即为更新内存占用数值大小,因为这个数值是全局的,所以 Redis 做了互斥的保护。有同学可能会有疑问,Redis 服务器的工作模式不是单进程单线程的么,这里不需要做互斥的保护。在 Redis 关闭一些客户端连接的时候,有时 TODO 交给后台线程来做。因此,严格意义上来讲,互斥是要做的。

update_zmalloc_stat_alloc() 宏首先会检测 zmalloc_threadsafe 值是否为 1,zmalloc-thread_safe 默认为 0,也就是说 Redis 默认不考虑互斥的情况;倘若 zmalloc_thread_safe 为 1,会使用原子操作函数或加锁的方式更新内存占用数值。

// 更新已使用内存大小
#define update_zmalloc_stat_alloc(__n) do { \
    size_t _n = (__n); \
    // 按4 字节向上取整
    if (_n&(sizeof(long)-1)) _n += sizeof(long)-(_n&(sizeof(long)-1)); \
        // 如果设置了线程安全,调用专门线程安全函数
    if (zmalloc_thread_safe) { \
        // 使用院子操作或者互斥锁,更新内存占用数值used_memory
        update_zmalloc_stat_add(_n); \
    } else { \
        used_memory += _n; \
    } \
} while(0)

上述是分配内存的情况,释放内存的情况则反过来。

zmalloc_get_private_dirty() 函数

在 RDB 持久化的篇章中,曾经提到这函数,我打算在这一节中稍微详细展开讲。操作系统为每一个进程维护了一个虚拟地址空间,虚拟地址空间对应着物理地址空间,在虚拟地址空间上的连续并不代表物理地址空间上的连续。

在 linux 编程中,进程调用 fork() 函数后会产生子进程。之前的做法是,将父进程的物理空间为子进程拷贝一份。出于效率的考虑,可以只在父子进程出现写内存操作的时候,才为子进程拷贝一份。如此不仅节省了内存空间,且提高了 fork() 的效率。在 RDB 持久化过程中,父进程继续提供服务,子进程进行 RDB 持久化。持久化完毕后,会调用 zmalloc_get_private_dirty() 获取写时拷贝的内存大小,此值实际为子进程在 RDB 持久化操作过程中所消耗的内存。

总结

Redis 是内存数据库,对内存的使用较为谨慎。

有一点建议。我们前面讲过,Redis 服务器中有多个数据集,在平时的数据集的选择上,可以按业务来讲不同来将数据存储在不同的数据集中。将数据集中在一两个数据集,查询的效率会降低。