国内最全IT社区平台 联系我们 | 收藏本站
华晨云阿里云优惠2
您当前位置:首页 > 数据库 > 数据库应用 > 《Redis设计与实现》[第一部分]数据结构与对象-C源码阅读(一)

《Redis设计与实现》[第一部分]数据结构与对象-C源码阅读(一)

来源:程序员人生   发布时间:2016-06-03 19:25:39 阅读次数:3699次

1、简单动态字符串SDS

关键字:空间预分配,惰性空间释放,2进制安全

C字符串不容易更改,所以Redis中把C字符串用在1些不必对字符串值进行修改的地方,作为字符串字面量(String literal),比如打印日志:
redisLog(REDIS_WARING, “Redis is now ready to exit, bye bye…”);

在Redis数据库中,包括字符串的键值对在底层都是由SDS实现的。

SDS还被用作缓冲区(buffer):AOF模块中的AOF缓冲区,和客户端状态中的输入缓冲区,都是SDS实现的。

源码

SDS结构的定义在sds.h中:

/* * 保存字符串对象的结构 */ struct sdshdr { // buf 中已占用空间的长度 int len; // buf 中剩余可用空间的长度,即未使用空间 int free; // 数据空间 char buf[]; };

获得1个SDS长度的复杂度为O(1),由SDS的API在履行时自动设置和更新SDS长度,使用SDS不必进行任何手动修改长度的工作。

空间分配

SDS的空间分配策略是:当SDS API需要对SDS进行修改时,API会先检查SDS的空间是不是满足修改所需的要求,若不满足,API会自动将SDS的空间扩大至履行修改所需的大小,然后才履行实际的修改操作,杜绝了产生缓冲区溢出的可能性。

通过未使用空间,SDS实现了空间预分配和惰性空间释放两种优化策略:

  • 空间预分配

空间预分配用于减少连续履行字符串增长操作所需的内存分配次数。
通过这类预分配策略,SDS将连续增长N次字符串所需的内存重分配次数从一定N次下降为最多N次。
其中额外分配的未使用空间数量由以下公式决定:

1. 如果对SDS进行修改后,SDS的长度(即len属性的值)小于1MB,就分配和len属性一样大小的未使用空间,即len属性的值和free属性的值相同   
2. 如果对SDS进行修改以后,SDS的长度大于等于1MB,就分配1MB的未使用空间。  
  • 惰性空间释放

惰性空间释放用于优化SDS字符串缩短操作的内存重分配操作:当SDS的API需要缩短SDS保存的字符串时,程序其实不立即便用内存重分配来回收缩短后多出来的字节,而是使用free属性将这些字节的数量记录起来,并等待将来使用。

SDS的API都是2进制安全的(binary-safe),所有SDS API都会以2进制的方式处理SDS寄存在buf数组里的数据,程序不会对其中的数据做任何限制、过滤、或假定,数据在写入时是甚么样的,被读取时就是甚么样。

Redis用SDS的buf数组保存2进制数据而不是字符。

SDS可以兼容部份C字符串函数。

2、链表

关键字:多态

当1个列表键包括了数量比较多的元素,或是列表中包括的元素都是比较长的字符串时,Redis就会使用链表作为列表键的底层实现。

integers列表键的底层实现就是1个链表,链表中的每一个结点都保存了1个整数值。

除链表以外,发布与定阅、慢查询、监视器等功能也用到了链表,Redis服务器本身还使用链表保存多个客户真个状态信息,和使用链表来构建客户端输出缓冲区(output buffer)。

源码

链表结构的定义在adlist.h中:

/* - 双端链表节点 */ 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;

list结构为链表提供了表头指针head、表尾指针tail,和链表长度计数器len,dup、free和match成员则是用于实现多态链表所需的类型特定函数:

  • dup函数用于复制链表结点所保存的值
  • free函数用于释放链表结点所保存的值;
  • match函数则用于对照链表结点所保存的值和另外一个输入值是不是相等。

Redis的链表实现的特性以下:

  • 双端、无环、带表头指针和表尾指针、带链表长度计数器、多态

3、字典

关键字:多态,渐进式rehash,murmurhash2

Redis的数据库就是使用字典来作为底层实现的,对数据库的增、删、改、查也是构建在对字典的操作之上的。

字典还是哈希键的底层实现之1,当1个哈希键包括的键值对照较多,或是键值对中的元素都是比较长的字符串时,Redis就使用字典作为哈希键的底层实现。

Redis的字典使用哈希表作为底层实现,1个哈希表里可以有多个哈希表结点,每一个哈希表结点就保存了字典中的1个键值对。

源码

字典所使用的哈希表在dict.h中定义:

/* * 哈希表 * 每一个字典都使用两个哈希表,从而实现渐进式 rehash 。 */ typedef struct dictht { // 哈希表数组,数组中的每一个元素都是1个指向dictEntry结构的指针 dictEntry **table; // 哈希表大小 unsigned long size; // 哈希表大小掩码,用于计算索引值 // 总是等于 size - 1 unsigned long sizemask; // 该哈希表已有节点的数量 unsigned long used; } dictht;
  • table属性是1个数组,数组中的每一个元素都是1个指向dictEntry结构的指针,每一个dictEntry结构保存着1个键值对。
  • size属性记录了哈希表的大小,即是table数组的大小。
  • used属性则记录了哈希表目前已有结点(键值对的数量)
  • sizemask属性和哈希值1起决定1个键应当被放到table数组的哪一个索引上面
/* * 哈希表节点 */ typedef struct dictEntry { // 键 void *key; // 值 union { void *val; uint64_t u64; int64_t s64; } v; // 指向下个哈希表节点,构成链表 struct dictEntry *next; } dictEntry;
  • key属性保存着键值对中的键
  • v属性保存键值对中的值,其中键值对中的值可以是1个指针,或是1个uint64_t整数,或是1个int64_t整数
  • next属性指向另外一个哈希表结点的指针,使用链地址法解决键冲突问题。
/* * 字典 */ typedef struct dict { // 类型特定函数 dictType *type; // 私有数据 void *privdata; // 哈希表 dictht ht[2]; // rehash 索引 // 当 rehash 不在进行时,值为 ⑴ int rehashidx; /* rehashing not in progress if rehashidx == ⑴ */ // 目前正在运行的安全迭代器的数量 int iterators; /* number of iterators currently running */ } dict;

type属性和privdata属性是针对不同类型的键值对,为创建多态字典而设置的:

  • type属性是1个指向dictType结构的指针,每一个dictType结构保存了1簇用于操作特定类型键值对的函数,Redis会为用处不同的字典设置不同的类型特定函数。
  • privdata属性保存了需要传给那些类型特定函数的可选参数。
/* * 字典类型特定函数 */ 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;
  • ht属性是1个包括两个项的数组,数组中的每一个项都是1个dictht哈希表,1般,字典只使用ht[0]哈希表,ht[1]哈希表只会在对ht[0]哈希表进行rehash时使用。
  • rehashidx属性记录了rehash目前的进度,如果目前没有在进行rehash,那末它的值为⑴.
/* * 字典迭代器 * - 如果 safe 属性的值为 1 ,那末在迭代进行的进程中, - 程序依然可以履行 dictAdd 、 dictFind 和其他函数,对字典进行修改。 * - 如果 safe 不为 1 ,那末程序只会调用 dictNext 对字典进行迭代, - 而不对字典进行修改。 */ typedef struct dictIterator { // 被迭代的字典 dict *d; // table :正在被迭代的哈希表号码,值可以是 0 或 1 。 // index :迭代器当前所指向的哈希表索引位置。 // safe :标识这个迭代器是不是安全 int table, index, safe; // entry :当前迭代到的节点的指针 // nextEntry :当前迭代节点的下1个节点 // 由于在安全迭代器运作时, entry 所指向的节点可能会被修改, // 所以需要1个额外的指针来保存下1节点的位置, // 从而避免指针丢失 dictEntry *entry, *nextEntry; long long fingerprint; /* unsafe iterator fingerprint for misuse detection */ } dictIterator;

哈希

Redis计算哈希值和索引值的方法以下:

// 使用字典设置的哈希函数,计算键key的哈希值 hash = dict->type->hashFunction(key); // 使用哈希表的sizemask属性和哈希值,计算出索引值 // 根据情况不同,ht[x]可以是ht[0]或ht[1] index = hash & dict->ht[x].sizemask;
/* ------------------------- hash functions ------------------------------ */ /* Thomas Wang's 32 bit Mix Function */ unsigned int dictIntHashFunction(unsigned int key) { key += ~(key << 15); key ^= (key >> 10); key += (key << 3); key ^= (key >> 6); key += ~(key << 11); key ^= (key >> 16); return key; } /* Identity hash function for integer keys */ unsigned int dictIdentityHashFunction(unsigned int key) { return key; } static uint32_t dict_hash_function_seed = 5381; void dictSetHashFunctionSeed(uint32_t seed) { dict_hash_function_seed = seed; } uint32_t dictGetHashFunctionSeed(void) { return dict_hash_function_seed; } /* MurmurHash2, by Austin Appleby * Note - This code makes a few assumptions about how your machine behaves - * 1. We can read a 4-byte value from any address without crashing * 2. sizeof(int) == 4 * * And it has a few limitations - * * 1. It will not work incrementally. * 2. It will not produce the same results on little-endian and big-endian * machines. */ unsigned int dictGenHashFunction(const void *key, int len) { /* 'm' and 'r' are mixing constants generated offline. They're not really 'magic', they just happen to work well. */ uint32_t seed = dict_hash_function_seed; const uint32_t m = 0x5bd1e995; const int r = 24; /* Initialize the hash to a 'random' value */ uint32_t h = seed ^ len; /* Mix 4 bytes at a time into the hash */ const unsigned char *data = (const unsigned char *)key; while(len >= 4) { uint32_t k = *(uint32_t*)data; k *= m; k ^= k >> r; k *= m; h *= m; h ^= k; data += 4; len -= 4; } /* Handle the last few bytes of the input array */ switch(len) { case 3: h ^= data[2] << 16; case 2: h ^= data[1] << 8; case 1: h ^= data[0]; h *= m; }; /* Do a few final mixes of the hash to ensure the last few * bytes are well-incorporated. */ h ^= h >> 13; h *= m; h ^= h >> 15; return (unsigned int)h; } /* And a case insensitive hash function (based on djb hash) */ unsigned int dictGenCaseHashFunction(const unsigned char *buf, int len) { unsigned int hash = (unsigned int)dict_hash_function_seed; while (len--) hash = ((hash << 5) + hash) + (tolower(*buf++)); /* hash * 33 + c */ return hash; }

当字典被用作数据库的底层实现,或是哈希键的底层实现时,Redis使用MurmurHash2算法计算键的哈希值:

  • 该算法的优点在于,即便输入的键是有规律的,算法仍能给出1个很好的随机散布性,并且算法的计算速度也非常快。

为了让哈希表的负载因子(load factor)保持在1个公道的范围以内,当哈希表保存的键值对数量太多或太少时,程序需要对哈希表的大小进行相应的扩大或收缩。

  • 哈希表的负载因子计算公式:load_factor = ht[0].used/ht[0].size

rehash

扩大和收缩哈希表的工作可以通过履行rehash(重新散列)操作来完成,Redis对字典的哈希表履行rehash的步骤以下:

  • 为字典的ht[1]哈希表分配空间,这个哈希表的空间大小取决于要履行的操作,和ht[0]当前包括的键值对数量(即ht[0].used属性的值)

    1. 如果履行的是扩大操作,那末ht[1]的大小为第1个大于等于ht[0].used*2的2^n(2的n次方幂);
    2. 如果履行的是收缩操作,那末ht[1]的大小为第1个大于等于ht[0].used的2^n。
  • 将保存在ht[0]中的所有键值对rehash到ht[1]上面:rehash指的是重新计算键的哈希值和索引值,然后将键值对放置到ht[1]哈希表的指定位置上。

  • 当ht[0]包括的所有键值对都迁移到ht[1]以后(ht[0]变成空表),释放ht[0],将ht[1]设置为ht[0],并在ht[1]新创建1个空白哈希表,为下1次rehash做准备。

当以下条件中的任意1个被满足时,程序会自动开始对哈希表履行扩大操作:

  • 服务器目前没有在履行BGSAVE命令或BGREWRITEAOF命令,并且哈希表的负载因子大于等于1
  • 服务器目前正在履行BGSAVE命令或BGREWRITEAOF命令,并且哈希表的负载因子大于等于5

在履行BGSAVE命令或BGREWRITEAOF命令的进程中,Redis需要创建当前服务器进程的子进程,而大多数操作系统都采取写时复制(copy-on-write)技术来优化子进程的使用效力,所以在子进程存在期间,服务器会提高履行扩大操作所需的负载因子,从而尽量地避免在子进程存在期间进行哈希表扩大操作,这避免了没必要要的内存写入操作,最大限度地节俭内存。

当哈希表的负载因子小于0.1时,程序自动开始对哈希表履行收缩操作。

渐进式rehash

为了不rehash对服务器性能造成影响,服务器不是1次性将ht[0]里面的所有键值对全部rehash到ht[1],而是分屡次、渐进式地将ht[0]里面的键值对渐渐rehash到ht[1]。

以下是哈希表渐进式rehash的详细步骤:

  1. 为ht[1]分配空间,让字典同时持有ht[0]和ht[1]两个哈希表。

  2. 在字典中保持1个索引计数器变量rehashidx,值设置为0,表示rehash工作正式开始

  3. 在rehash进行期间,每次对字典履行添加、删除、查找或更新操作时,程序除履行指定的操作以为,还会顺带将ht[0]哈希表在rehashidx索引上的所有键值对rehash到ht[1],当rehash工作完成以后,程序将rehashidx属性的值增1。

  4. 随着字典操作的不断履行,终究在某个时间点上,ht[0]的所有键值对都会被rehash到ht[1]上,这是程序将rehashidx属性的值设为⑴,表示rehash操作已完成

渐进式rehash采取分而治之的方式,将rehash键值对所需的计算工作均摊到对字典的每一个添加、删除、查找和更新操作上,从而避免了集中式rehash而带来的庞大计算量。

在进行渐进式rehash的进程中,字典会同时使用ht[0]和ht[1]两个哈希表,所以在渐进式rehash进行期间,字典的删除、查找、更新会在两个哈希表上进行,比如现在ht[0]中查找,没找到再去ht[1]查找

在渐进式rehash履行期间,新添加到字典的键值对1律会被保存到ht[1]里面,而ht[0]则不再进行任何添加操作,这样保证了ht[0]包括的键值对数量只减不增,随着rehash操作的履行终究变成空表。

生活不易,码农辛苦
如果您觉得本网站对您的学习有所帮助,可以手机扫描二维码进行捐赠
程序员人生
------分隔线----------------------------
分享到:
------分隔线----------------------------
关闭
程序员人生