本文共 8774 字,大约阅读时间需要 29 分钟。
由于C字符串存在大量问题,所以在Redis中,并没有使用C风格字符串,而是自己构建了一个简单动态字符串即SDS(simple dynamic string)
struct sdshdr { // buf 中已占用空间的长度 int len; // buf 中剩余可用空间的长度 int free; // 数据空间 char buf[];};
为解决C字符串缓冲区溢出问题以及长度计算问题,SDS中引入了len来统计当前已使用空间长度,free来计算剩余的空间长度
C字符串的主要缺陷就是因为它没有记录自己的长度,而如果在需要了解长度时,就只能通过O(N)的效率进行一次遍历
由于Redis作为一个高效的内存数据库,用于速度要求严苛,插入删除频繁
的场景,为了提高内存分配的效率,防止大量使用内存重分配而调用系统函数导致的性能损失问题(用户态和内核态的切换),Redis主要依靠空间预分配和惰性空间释放来解决这个问题为减少空间分配的次数,当需要进行空间拓展时,不仅仅会为SDS分配修改所必须要的空间,并且会为SDS预分配额外的未使用空间。
预分配未使用空间的策略如下
当我们对SDS进行删除操作时,并不会立即回收删除后空余的空间,而是将空余空间以free字段记录下来,以备后面使用。
这样做的目的在于防止因为空间缩短后因为再度插入导致的空间拓展问题。 并且如果有需求需要真正释放空间,Redis也提供了对应的API,所以不必担心会因为惰性的空间释放而导致的内存浪费问题。比起 C 字符串, SDS 具有以下优点:
typedef struct listNode { // 前置节点 struct listNode *prev; // 后置节点 struct listNode *next; // 节点的值 void *value;} listNode;/* * 双端链表迭代器 */typedef struct listIter { // 当前迭代到的节点 listNode *next; // 迭代的方向 int direction;} listIter;/* * 双端链表结构 */typedef struct list { // 表头节点 listNode *head; // 表尾节点 listNode *tail; // 节点值复制函数 void *(*dup)(void *ptr); // 节点值释放函数 void (*free)(void *ptr); // 节点值对比函数 int (*match)(void *ptr, void *key); // 链表所包含的节点数量 unsigned long len;} list;
同时为了实现多态与泛型,链表中还提供了dup,free,match属性来设置相关的函数,使得链表支持不同类型的值的存储
Redis的字典底层采用了哈希表来进行实现。
首先看看字典底层哈希表的结构
typedef struct dictht { // 哈希表数组 dictEntry **table; // 哈希表大小 unsigned long size; // 哈希表大小掩码,用于计算索引值 // 总是等于 size - 1 unsigned long sizemask; // 该哈希表已有节点的数量 unsigned long used;} dictht;
为解决哈希冲突,Redis字典采用了链地址法来构造了哈希桶的结构,也就是哈希数组中的每个元素都是一个链表。
下面来看看哈希节点的结构
typedef struct dictEntry { // 键 void *key; // 值 union { void *val; uint64_t u64; int64_t s64; } v; // 指向下个哈希表节点,形成链表 struct dictEntry *next;} dictEntry;
可以看到,为保证键值对适用于多重类型,key值使用的时void的形式,而value使用了64位有符号整型和64位无符号整型,void指针的一个联合体,每个节点使用next来链接成一个链表
typedef struct dictType { // 计算哈希值的函数 unsigned int (*hashFunction)(const void *key); // 复制键的函数 void *(*keyDup)(void *privdata, const void *key); // 复制值的函数 void *(*valDup)(void *privdata, const void *obj); // 对比键的函数 int (*keyCompare)(void *privdata, const void *key1, const void *key2); // 销毁键的函数 void (*keyDestructor)(void *privdata, void *key); // 销毁值的函数 void (*valDestructor)(void *privdata, void *obj);} dictType;
为保证字典具有多态及泛型,dictType中提供了如哈希函数以及K-V的各种操作函数,使得字典适用于多重情景
/* * 字典 */typedef struct dict { // 类型特定函数 dictType *type; // 私有数据 void *privdata; // 哈希表 dictht ht[2]; // rehash 索引 // 当 rehash 不在进行时,值为 -1 int rehashidx; /* rehashing not in progress if rehashidx == -1 */ // 目前正在运行的安全迭代器的数量 int iterators; /* number of iterators currently running */} dict;
从字典的结构中,我们可以看到里面同时存放了两个哈希表,以及一个rehashidx属性。
这就牵扯到了字典的核心之一,rehash。Redis作为一个插入频繁且对效率要求高的数据库,当插入的数据过多时,就会因为哈希表中的负载因子过高而导致查询或者插入的效率降低,此时就需要通过rehash来进行重新扩容并重新映射。
但是如果只是用一个哈希表,映射时就会导致数据库暂时不可用,作为一个使用频繁的数据库,短期的停机几乎是不可容许的问题,所以Redis设计时采用了双哈希的结构,并采用了渐进式rehash的方法来解决这个问题。rehash的步骤如下
由于数据库中可能存在大量的数据,而rehash的时候又过长,为了避免因为rehash造成的服务器停机,rehash的过程并不是一次完成的,而是一个多次的,渐进式的过程。