redis续集

Redis 中的 SDS(Simple Dynamic String)是一种动态字符串实现,它比 C 语言的字符串更高效且支持二进制安全。文章详细介绍了 SDS 的数据结构,包括 header 的不同类型,以及 SDS 的创建、销毁和操作。接着,文章转向了 Redis 中的 Skiplist,这是一种概率性数据结构,具有近似 O(log n) 的查找、插入和删除性能。Skiplist 的工作原理、随机层数生成策略以及与平衡树和哈希表的性能比较都进行了探讨。此外,Redis 中的 Skiplist 实现还支持根据排名查询,这需要对原始 Skiplist 进行扩展。最后,文章提到了 Redis 中 Sorted Set 的实现,它是通过 dict 和 Skiplist 结合使用来支持高效查询的。
摘要由CSDN通过智能技术生成

SDS (Simple Dynamic String) 最基础的数据结构。直译过来就是”简单的动态字符串“。Redis 自己实现了一个动态的字符串,而不是直接使用了 C 语言中的字符串。

sds 的数据结构:

struct sdshdr { 
// buf 中已占用空间的长度 int len; 
// buf 中剩余可用空间的长度 int free; 
// 数据空间 
char buf[];
 
}

所以一个 SDS 的就如下图:

所以我们看到,sds 包含3个参数。buf 的长度 len,buf 的剩余长度,以及buf。

为什么这么设计呢?

    1. 预分配
      如果对 SDS 修改后,如果 len 小于 1MB 那 len = 2 * len + 1byte。 这个 1 是用于保存空字节。
      如果 SDS 修改后 len 大于 1MB 那么 len = 1MB + len + 1byte。
    2. 惰性释放
      如果缩短 SDS 的字符串长度,redis并不是马上减少 SDS 所占内存。只是增加 free 的长度。同时向外提供 API 。真正需要释放的时候,才去重新缩小 SDS 所占的内存

链表

C语言中并没有链表这个数据结构所以 Redis 自己实现了一个。Redis 中的链表是:

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;

链表的结构比较简单,数据结构如下:

总结一下性质:

  • 双向链表,某个节点寻找上一个或者下一个节点时间复杂度 O(1)。
  • list 记录了 head 和 tail,寻找 head 和 tail 的时间复杂度为 O(1)。
  • 获取链表的长度 len 时间复杂度 O(1)。

字典

字典数据结构极其类似 java 中的 Hashmap。

Redis的字典由三个基础的数据结构组成。最底层的单位是哈希表节点。结构如下:

typedef struct dictEntry {
 
 // 键
 void *key;

 // 值
 union {
  void *val;
  uint64_t u64;
  int64_t s64;
 } v;

 // 指向下个哈希表节点,形成链表
 struct dictEntry *next;

} dictEntry;

实际上哈希表节点就是一个单项列表的节点。保存了一下下一个节点的指针。 key 就是节点的键,v是这个节点的值。这个 v 既可以是一个指针,也可以是一个 uint64_t或者 int64_t 整数。*next 指向下一个节点。

通过一个哈希表的数组把各个节点链接起来:
typedef struct dictht {

 // 哈希表数组
 dictEntry **table;

 // 哈希表大小
 unsigned long size;
 
 // 哈希表大小掩码,用于计算索引值
 // 总是等于 size - 1
 unsigned long sizemask;

 // 该哈希表已有节点的数量
 unsigned long used;

} dictht;

dictht

通过图示我们观察:

实际上,如果对java 的基本数据结构了解的同学就会发现,这个数据结构和 java 中的 HashMap 是很类似的,就是数组加链表的结构。

字典的数据结构:

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;

其中的dictType 是一组方法,代码如下:

/*
 * 字典类型特定函数
 */
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;

这里我们可以看到一个dict 拥有两个 dictht。一般来说只使用 ht[0],当扩容的时候发生了rehash的时候,ht[1]才会被使用。

当我们观察或者研究一个hash结构的时候偶我们首先要考虑的这个 dict 如何插入一个数据?

我们梳理一下插入数据的逻辑。

    1. 根据ht[0] 的数据和操作的类型(扩大或缩小),分配 ht[1] 的大小。
    2. 将 ht[0] 的数据 rehash 到 ht[1] 上。
    3. rehash 完成以后,将ht[1] 设置为 ht[0],生成一个新的ht[1]备用。
    1. 分配 ht[1] 的空间,让字典同时持有 ht[1] 和 ht[0]。
    2. 在字典中维护一个 rehashidx,设置为 0 ,表示字典正在 rehash。
    3. 在rehash期间,每次对字典的操作除了进行指定的操作以外,都会根据 ht[0] 在 rehashidx 上对应的键值对 rehash 到 ht[1]上。
    4. 随着操作进行, ht[0] 的数据就会全部 rehash 到 ht[1] 。设置ht[0] 的 rehashidx 为 -1,渐进的 rehash 结束。

这样保证数据能够平滑的进行 rehash。防止 rehash 时间过久阻塞线程

  • 在进行 rehash 的过程中,如果进行了 delete 和 update 等操作,会在两个哈希表上进行。如果是 find 的话优先在ht[0] 上进行,如果没有找到,再去 ht[1] 中查找。如果是 insert 的话那就只会在 ht[1]中插入数据。这样就会保证了 ht[1] 的数据只增不减,ht[0]的数据只减不增。

dict的数据结构定义

为了实现增量式重哈希(incremental rehashing),dict的数据结构里包含两个哈希表。在重哈希期间,数据从第一个哈希表向第二个哈希表迁移。

dict的C代码定义如下(出自Redis源码dict.h):

typedef struct dictEntry {
 void *key;
 union {
  void *val;
  uint64_t u64;
  int64_t s64;
  double d;
 } v;
 struct dictEntry *next;
} dictEntry;

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;

/* This is our hash table structure. Every dictionary has two of this as we
 * implement incremental rehashing, for the old to the new table. */
typedef struct dictht {
 dictEntry **table;
 unsigned long size;
 unsigned long sizemask;
 unsigned long used;
} dictht;

typedef struct dict {
 dictType *type;
 void *privdata;
 dictht ht[2];
 long rehashidx; /* rehashing not in progress if rehashidx == -1 */
 int iterators; /* number of iterators currently running */
} dict;

为了能更清楚地展示dict的数据结构定义,我们用一张结构图来表示它。如下。

结合上面的代码和结构图,可以很清楚地看出dict的结构。一个dict由如下若干项组成:

  • 一个指向dictType结构的指针(type)。它通过自定义的方式使得dict的key和value能够存储任何类型的数据。
  • 一个私有数据指针(privdata)。由调用者在创建dict的时候传进来。
  • 两个哈希表(ht[2])。只有在重哈希的过程中,ht[0]和ht[1]才都有效。而在平常情况下,只有ht[0]有效,ht[1]里面没有任何数据。上图表示的就是重哈希进行到中间某一步时的情况。
  • 当前重哈希索引(rehashidx)。如果rehashidx = -1,表示当前没有在重哈希过程中;否则,表示当前正在进行重哈希,且它的值记录了当前重哈希进行到哪一步了。
  • 当前正在进行遍历的iterator的个数。这不是我们现在讨论的重点,暂时忽略。

dictType结构包含若干函数指针,用于dict的调用者对涉及key和value的各种操作进行自定义。这些操作包含:

  • hashFunction,对key进行哈希值计算的哈希算法。
  • keyDup和valDup,分别定义key和value的拷贝函数,用于在需要的时候对key和value进行深拷贝,而不仅仅是传递对象指针。
  • keyCompare,定义两个key的比较操作,在根据key进行查找时会用到。
  • keyDestructor和valDestructor,分别定义对key和value的析构函数。

私有数据指针(privdata)就是在dictType的某些操作被调用时会传回给调用者。

需要详细察看的是dictht结构。它定义一个哈希表的结构,由如下若干项组成:

  • 一个dictEntry指针数组(table)。key的哈希值最终映射到这个数组的某个位置上(对应一个bucket)。如果多个key映射到同一个位置,就发生了冲突,那么就拉出一个dictEntry链表。
  • size:标识dictEntry指针数组的长度。它总是2的指数。
  • sizemask:用于将哈希值映射到table的位置索引。它的值等于(size-1),比如7, 15, 31, 63,等等,也就是用二进制表示的各个bit全1的数字。每个key先经过hashFunction计算得到一个哈希值,然后计算(哈希值 & sizemask)得到在table上的位置。相当于计算取余(哈希值 % size)。
  • used:记录dict中现有的数据个数。它与size的比值就是装载因子(load factor)。这个比值越大,哈希值冲突概率越高。

dictEntry结构中包含k, v和指向链表下一项的next指针。k是void指针,这意味着它可以指向任何类型。v是个union,当它的值是uint64_t、int64_t或double类型时,就不再需要额外的存储,这有利于减少内存碎片。当然,v也可以是void指针,以便能存储任何类型的数据。

dict的创建(dictCreate)

dict *dictCreate(dictType *type,
  void *privDataPtr)
{
 dict *d = zmalloc(sizeof(*d));

 _dictInit(d,type,privDataPtr);
 return d;
}

int _dictInit(dict *d, dictType *type,
  void *privDataPtr)
{
 _dictReset(&d->ht[0]);
 _dictReset(&d->ht[1]);
 d->type = type;
 d->privdata = privDataPtr;
 d->rehashidx = -1;
 d->iterators = 0;
 return DICT_OK;
}

static void _dictReset(dictht *ht)
{
 ht->table = NULL;
 ht->size = 0;
 ht->sizemask = 0;
 ht->used = 0;
}

dictCreate为dict的数据结构分配空间并为各个变量赋初值。其中两个哈希表ht[0]和ht[1]起始都没有分配空间,table指针都赋为NULL。这意味着要等第一个数据插入时才会真正分配空间。

dict的查找(dictFind)

#define dictIsRehashing(d) ((d)->rehashidx != -1)

dictEntry *dictFind(dict *d, const void *key)
{
 dictEntry *he;
 unsigned int h, idx, table;

 if (d->ht[0].used + d->ht[1].used == 0) return NULL; /* dict is empty */
 if (dictIsRehashing(d)) _dictRehashStep(d);
 h = dictHashKey(d, key);
 for (table = 0; table <= 1; table++) {
  idx = h & d->ht[table].sizemask;
  he = d->ht[table].table[idx];
  while(he) {
   if (key==he->key || dictCompareKeys(d, key, he->key))
    return he;
   he = he->next;
  }
  if (!dictIsRehashing(d)) return NULL;
 }
 return NULL;
}

上述dictFind的源码,根据dict当前是否正在重哈希,依次做了这么几件事:

  • 如果当前正在进行重哈希,那么将重哈希过程向前推进一步(即调用_dictRehashStep)。实际上,除了查找,插入和删除也都会触发这一动作。这就将重哈希过程分散到各个查找、插入和删除操作中去了,而不是集中在某一个操作中一次性做完。
  • 计算key的哈希值(调用dictHashKey,里面的实现会调用前面提到的hashFunction)。
  • 先在第一个哈希表ht[0]上进行查找。在table数组上定位到哈希值对应的位置(如前所述,通过哈希值与sizemask进行按位与),然后在对应的dictEntry链表上进行查找。查找的时候需要对key进行比较,这时候调用dictCompareKeys,它里面的实现会调用到前面提到的keyCompare。如果找到就返回该项。否则,进行下一步。
  • 判断当前是否在重哈希,如果没有,那么在ht[0]上的查找结果就是最终结果(没找到,返回NULL)。否则,在ht[1]上进行查找(过程与上一步相同)。

下面我们有必要看一下增量式重哈希的_dictRehashStep的实现。

static void _dictRehashStep(dict *d) {
 if (d->iterators == 0) dictRehash(d,1);
}

int dictRehash(dict *d, int n) {
 int empty_visits = n*10; /* Max number of empty buckets to visit. */
 if (!dictIsRehashing(d)) return 0;

 while(n-- && d->ht[0].used != 0) {
  dictEntry *de, *nextde;

  /* Note that rehashidx can't overflow as we are sure there are more
   * elements because ht[0].used != 0 */
  assert(d->ht[0].size > (unsigned long)d->rehashidx);
  while(d->ht[0].table[d->rehashidx] == NULL) {
   d->rehashidx++;
   if (--empty_visits == 0) return 1;
  }
  de = d->ht[0].table[d->rehashidx];
  /* Move all the keys in this bucket from the old to the new hash HT */
  while(de) {
   unsigned int h;

   nextde = de->next;
   /* Get the index in the new hash table */
   h = dictHashKey(d, de->key) & d->ht[1].sizemask;
   de->next = d->ht[1].table[h];
   d->ht[1].table[h] = de;
   d->ht[0].used--;
   d->ht[1].used++;
   de = nextde;
  }
  d->ht[0].table[d->rehashidx] = NULL;
  d->rehashidx++;
 }

 /* Check if we already rehashed the whole table... */
 if (d->ht[0].used == 0) {
  zfree(d->ht[0].table);
  d->ht[0] = d->ht[1];
  _dictReset(&d->ht[1]);
  d->rehashidx = -1;
  return 0;
 }

 /* More to rehash... */
 return 1;
}

dictRehash每次将重哈希至少向前推进n步(除非不到n步整个重哈希就结束了),每一步都将ht[0]上某一个bucket(即一个dictEntry链表)上的每一个dictEntry移动到ht[1]上,它在ht[1]上的新位置根据ht[1]的sizemask进行重新计算。rehashidx记录了当前尚未迁移(有待迁移)的ht[0]的bucket位置。

如果dictRehash被调用的时候,rehashidx指向的bucket里一个dictEntry也没有,那么它就没有可迁移的数据。这时它尝试在ht[0].table数组中不断向后遍历,直到找到下一个存有数据的bucket位置。如果一直找不到,则最多走n*10步,本次重哈希暂告结束。

最后,如果ht[0]上的数据都迁移到ht[1]上了(即d->ht[0].used == 0),那么整个重哈希结束,ht[0]变成ht[1]的内容,而ht[1]重置为空。

根据以上对于重哈希过程的分析,我们容易看出,本文前面的dict结构图中所展示的正是rehashidx=2时的情况,前面两个bucket(ht[0].table[0]和ht[0].table[1])都已经迁移到ht[1]上去了。

dict的插入(dictAdd和dictReplace)

dictAdd插入新的一对key和value,如果key已经存在,则插入失败。

dictReplace也是插入一对key和value,不过在key存在的时候,它会更新value。

int dictAdd(dict *d, void *key, void *val)
{
 dictEntry *entry = dictAddRaw(d,key);

 if (!entry) return DICT_ERR;
 dictSetVal(d, entry, val);
 return DICT_OK;
}

dictEntry *dictAddRaw(dict *d, void *key)
{
 int index;
 dictEntry *entry;
 dictht *ht;

 if (dictIsRehashing(d)) _dictRehashStep(d);

 /* Get the index of the new element, or -1 if
  * the element already exists. */
 if ((index = _dictKeyIndex(d, key)) == -1)
  return NULL;

 /* Allocate the memory and store the new entry.
  * Insert the element in top, with the assumption that in a database
  * system it is more likely that recently added entries are accessed
  * more frequently. */
 ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0];
 entry = zmalloc(sizeof(*entry));
 entry->next = ht->table[index];
 ht->table[index] = entry;
 ht->used++;

 /* Set the hash entry fields. */
 dictSetKey(d, entry, key);
 return entry;
}

static int _dictKeyIndex(dict *d, const void *key)
{
 unsigned int h, idx, table;
 dictEntry *he;

 /* Expand the hash table if needed */
 if (_dictExpandIfNeeded(d) == DICT_ERR)
  return -1;
 /* Compute the key hash value */
 h = dictHashKey(d, key);
 for (table = 0; table <= 1; table++) {
  idx = h & d->ht[table].sizemask;
  /* Search if this slot does not already contain the given key */
  he = d->ht[table].table[idx];
  while(he) {
   if (key==he->key || dictCompareKeys(d, key, he->key))
    return -1;
   he = he->next;
  }
  if (!dictIsRehashing(d)) break;
 }
 return idx;
}

以上是dictAdd的关键实现代码。我们主要需要注意以下几点:

  • 它也会触发推进一步重哈希(_dictRehashStep)。
  • 如果正在重哈希中,它会把数据插入到ht[1];否则插入到ht[0]。
  • 在对应的bucket中插入数据的时候,总是插入到dictEntry的头部。因为新数据接下来被访问的概率可能比较高,这样再次查找它时就比较次数较少。
  • _dictKeyIndex在dict中寻找插入位置。如果不在重哈希过程中,它只查找ht[0];否则查找ht[0]和ht[1]。
  • _dictKeyIndex可能触发dict内存扩展(_dictExpandIfNeeded,它将哈希表长度扩展为原来两倍,具体请参考dict.c中源码)。

dictReplace在dictAdd基础上实现,如下:

int dictReplace(dict *d, void *key, void
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值