redis源码分析(一)-字典(dict)的实现

一 字典的简介以及在Redis中的应用

    字典(dictionary), 又名映射(map)或关联数组(associative array), 是一种抽象数据结构, 由一集键值对(key-value pairs)组成, 各个键值对的键各不相同, 程序可以添加新的键值对到字典中, 或者基于键进行查找、更新或删除等操作。字典在Redis应用很广泛,和SDS、双向链表一样使用频率很高。

其中, 字典的主要用途有以下两个:

  1)实现数据库键空间(key space);
  2)用作 Hash 类型键的底层实现之一;
以下两个小节分别介绍这两种用途。


二 实现数据库键空间

  Redis 是一个键值对数据库, 数据库中的键值对由字典保存: 每个数据库都有一个对应的字典, 这个字典被称之为键空间(key space)。

  当用户添加一个键值对到数据库时(不论键值对是什么类型), 程序就将该键值对添加到键空间; 当用户从数据库中删除键值对时, 程序就会将这个键值对从键空间中删除; 等等。

三 用作 Hash 类型键的底层实现之一

Redis 的 Hash 类型键使用以下两种数据结构作为底层实现:

  1. 字典;
  2. 压缩列表

因为压缩列表比字典更节省内存, 所以程序在创建新 Hash 键时, 默认使用压缩列表作为底层实现, 当有需要时, 程序才会将底层实现从压缩列表转换到字典。

当用户操作一个 Hash 键时, 键值在底层就可能是一个哈希表:

redis> HSET book name "The design and implementation of Redis"
(integer) 1

redis> HSET book type "source code analysis"
(integer) 1

redis> HSET book release-date "2013.3.8"
(integer) 1

redis> HGETALL book
1) "name"
2) "The design and implementation of Redis"
3) "type"
4) "source code analysis"
5) "release-date"
6) "2013.3.8"
四 字典的实现

  Redis的底层使用哈希表来实现的。其一系列操作复杂度如下:

操作 函数 算法复杂度
创建一个新字典 dictCreate O(1) O(1)
添加新键值对到字典 dictAdd O(1) O(1)
添加或更新给定键的值 dictReplace O(1) O(1)
在字典中查找给定键所在的节点 dictFind O(1) O(1)
在字典中查找给定键的值 dictFetchValue O(1) O(1)
从字典中随机返回一个节点 dictGetRandomKey O(1) O(1)
根据给定键,删除字典中的键值对 dictDelete O(1) O(1)
清空并释放字典 dictRelease O(N) O(N)
清空并重置(但不释放)字典 dictEmpty O(N) O(N)
缩小字典 dictResize O(N) O(N)
扩大字典 dictExpand O(N) O(N)
对字典进行给定步数的 rehash dictRehash O(N) O(N)
在给定毫秒内,对字典进行rehash dictRehashMilliseconds O(N)

其中, 0 号哈希表(ht[0])是字典主要使用的哈希表, 而 1 号哈希表(ht[1])则只有在程序对 0 号哈希表进行 rehash 时才使用。

哈希I定义参考源码:dict.h

/*
 * 哈希表节点
 */
typedef struct dictEntry {
    
    // 键
    void *key;

    // 值
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
    } 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. */
/*
 * 哈希表
 *
 * 每个字典都使用两个哈希表,从而实现渐进式 rehash 。
 */
typedef struct dictht {
    
    // 哈希表数组
    dictEntry **table;

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

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

} dictht;

/*
 * 字典
 */
typedef struct dict {

    // 类型特定函数
    dictType *type;

    // 私有数据
    void *privdata;

    // 哈希表,用两个哈希表实现渐进式rehash
    dictht ht[2];

    // rehash 索引
    // 当 rehash 不在进行时,值为 -1
    int rehashidx; /* rehashing not in progress if rehashidx == -1 */

    // 目前正在运行的安全迭代器的数量
    int iterators; /* number of iterators currently running */

} dict;

next 属性指向另一个 dictEntry 结构, 多个 dictEntry 可以通过 next 指针串连成链表, 从这里可以看出, dictht 使用链地址法来处理键碰撞: 当多个不同的键拥有相同的哈希值时,哈希表用一个链表将这些键连接起来。

下图展示了一个由 dictht 和数个 dictEntry 组成的哈希表例子:

digraph hash_table_example {    // setting    rankdir = LR;    node[shape = record, style = filled];    edge [style = bold];    // nodes    ht1 [label="<dictht>dictht |<table> table | size: 4 | sizemask: 3 | used: 3", fillcolor = "#95BBE3"];    bucket [label="<head>dictEntry**\n(bucket) |<table0> 0 |<table1> 1 |<table2> 2 |<table3> 3 ", fillcolor = "#F2F2F2"];    pair_1 [label="<head>dictEntry |{key1 | value1 |<next>next}", fillcolor = "#FADCAD"];    pair_2 [label="<head>dictEntry |{key2 | value2 |<next>next}", fillcolor = "#FADCAD"];    pair_3 [label="<head>dictEntry |{key3 | value3 |<next>next}", fillcolor = "#FADCAD"];    null0 [label="NULL", shape=plaintext];    null1 [label="NULL", shape=plaintext];    null2 [label="NULL", shape=plaintext];    null3 [label="NULL", shape=plaintext];    // lines    ht1:table -> bucket:head;    bucket:table0 -> pair_1:head; pair_1:next -> null0;    bucket:table1 -> null1;    bucket:table2 -> pair_2:head; pair_2:next -> null2;    bucket:table3 -> pair_3:head; pair_3:next -> null3;}

如果再加上之前列出的 dict 类型,那么整个字典结构可以表示如下:

digraph hash_table_example {    // setting    rankdir = LR;    node[shape=record, style = filled];    edge [style = bold];    // nodes    dict [label="dict | type | privdata |<ht> ht[2] | rehashidx: -1 | iterators: 0", fillcolor = "#A8E270"];    ht0 [label="<dictht>dictht |<table> table | size: 4 | sizemask: 3 | used: 3", fillcolor = "#95BBE3"];    ht1 [label="<dictht>dictht |<table> table | size: 0 | sizemask: 0 | used: 0", fillcolor = "#95BBE3"];    bucket [label="<head>dictEntry**\n(bucket) |<table0> 0 |<table1> 1 |<table2> 2 |<table3> 3 ", fillcolor = "#F2F2F2"];    pair_1 [label="<head>dictEntry |{key1 | value1 |<next>next}", fillcolor = "#FADCAD"];    pair_2 [label="<head>dictEntry |{key2 | value2 |<next>next}", fillcolor = "#FADCAD"];    pair_3 [label="<head>dictEntry |{key3 | value3 |<next>next}", fillcolor = "#FADCAD"];    null0 [label="NULL", shape=plaintext];    null1 [label="NULL", shape=plaintext];    null2 [label="NULL", shape=plaintext];    null3 [label="NULL", shape=plaintext];    tnull1 [label="NULL", shape=plaintext];    // lines    dict:ht -> ht0:dictht [label="ht[0]"];    dict:ht -> ht1:dictht [label="ht[1]"];    ht0:table -> bucket:head;    ht1:table -> tnull1;    bucket:table0 -> pair_1:head; pair_1:next -> null0;    bucket:table1 -> null1;    bucket:table2 -> pair_2:head; pair_2:next -> null2;    bucket:table3 -> pair_3:head; pair_3:next -> null3;}

在上图的字典示例中, 字典虽然创建了两个哈希表, 但正在使用的只有 0 号哈希表, 这说明字典未进行 rehash 状态。

哈希算法

Redis 目前使用两种不同的哈希算法:

  1. MurmurHash2 32 bit 算法:这种算法的分布率和速度都非常好, 具体信息请参考 MurmurHash 的主页: http://code.google.com/p/smhasher/ 。
  2. 基于 djb 算法实现的一个大小写无关散列算法:具体信息请参考 http://www.cse.yorku.ca/~oz/hash.html 。

使用哪种算法取决于具体应用所处理的数据:

  • 命令表以及 Lua 脚本缓存都用到了算法 2 。
  • 算法 1 的应用则更加广泛:数据库、集群、哈希键、阻塞操作等功能都用到了这个算法

六 添加键值对到字典 

根据字典所处的状态, 将给定的键值对添加到字典可能会引起一系列复杂的操作:

  • 如果字典为未初始化(即字典的 0 号哈希表的 table 属性为空),则程序需要对 0 号哈希表进行初始化;
  • 如果在插入时发生了键碰撞,则程序需要处理碰撞;
  • 如果插入新元素,使得字典满足了 rehash 条件,则需要启动相应的 rehash 程序;

当程序处理完以上三种情况之后,新的键值对才会被真正地添加到字典上。

整个添加流程可以用下图表示:

digraph dictAdd {    node[shape=plaintext, style = filled];    edge [style = bold];    //    start [label="dictAdd", fillcolor = "#A8E270"];    key_exists_or_not [label="键已经存在?", shape=diamond, fillcolor = "#95BBE3"];    start -> key_exists_or_not;    return_null_if_key_exists [label="返回 NULL ,\n表示添加失败"];    key_exists_or_not -> return_null_if_key_exists [label="是"];    dict_empty_or_not [label="ht[0]\n 未分配任何空间?", shape=diamond, fillcolor = "#95BBE3"];    key_exists_or_not -> dict_empty_or_not [label="否"];    init_hash_table_one [label="初始化 ht[0]"];    dict_empty_or_not -> init_hash_table_one [label="是"];    init_hash_table_one -> need_rehash_or_not;    need_rehash_or_not [label="需要 rehash ?", shape=diamond, fillcolor = "#95BBE3"];    dict_empty_or_not -> need_rehash_or_not [label="否"];    begin_incremental_rehash [label="开始渐进式 rehash "];    need_rehash_or_not -> begin_incremental_rehash [label="需要,\n并且 rehash 未进行"];    begin_incremental_rehash -> rehashing_or_not;    rehashing_or_not [label="rehash\n 正在进行中?", shape=diamond, fillcolor = "#95BBE3"];    need_rehash_or_not -> rehashing_or_not [label="不需要,\n或者 rehash 正在进行"];    is_rehashing [label="选择 ht[1] 作为新键值对的添加目标"];    not_rehashing [label="选择 ht[0] 作为新键值对的添加目标"];    rehashing_or_not -> is_rehashing [label="是"];    rehashing_or_not -> not_rehashing [label="否"];    calc_hash_code_and_index_by_key [label="根据给定键,计算出哈希值,以及索引值"];    is_rehashing -> calc_hash_code_and_index_by_key;    not_rehashing -> calc_hash_code_and_index_by_key;    create_entry_and_assoc_key_and_value [label="创建新 dictEntry ,并保存给定键值对"];    calc_hash_code_and_index_by_key -> create_entry_and_assoc_key_and_value;    add_entry_to_hashtable [label="根据索引值,将新节点添加到目标哈希表"];        create_entry_and_assoc_key_and_value -> add_entry_to_hashtable;}

在接下来的三节中, 我们将分别看到,添加操作如何在以下三种情况中执行:

  1. 字典为空;
  2. 添加新键值对时发生碰撞处理;
  3. 添加新键值对时触发了 rehash 操作

添加新元素到空白字典

 当第一次往空字典里添加键值对时, 程序会根据 dict.h/DICT_HT_INITIAL_SIZE 里指定的大小为 d->ht[0]->table 分配空间 (在目前的版本中, DICT_HT_INITIAL_SIZE 的值为 4 )。

以下是字典空白时的样子:

digraph empty_dict {    // setting    rankdir = LR;    node[shape = record, style = filled];    edge [style = bold];    // nodes    dict [label="dict | type | privdata |<ht> ht[2] | rehashidx | iterators", fillcolor = "#A8E270"];    ht0 [label="<dictht>dictht |<table> table | size: 0 | sizemask: 0 | used: 0", fillcolor = "#95BBE3"];    ht1 [label="<dictht>dictht |<table> table | size: 0 | sizemask: 0 | used: 0", fillcolor = "#95BBE3"];    null0 [label="NULL", shape=plaintext];    null1 [label="NULL", shape=plaintext];    // lines    dict:ht -> ht0:dictht [label="ht[0]"];    dict:ht -> ht1:dictht [label="ht[1]"];    ht0:table -> null0;    ht1:table -> null1;}

以下是往空白字典添加了第一个键值对之后的样子:

digraph add_first_entry_to_empty_dict {    // setting    rankdir = LR;    node[shape=record, style = filled];    edge [style = bold];    // nodes    dict [label="dict | type | privdata |<ht> ht[2] | rehashidx | iterators", fillcolor = "#A8E270"];    ht0 [label="<dictht>dictht |<table> table | size: 4 | sizemask: 3 | used: 1", fillcolor = "#95BBE3"];    ht1 [label="<dictht>dictht |<table> table | size: 0 | sizemask: 0 | used: 0", fillcolor = "#95BBE3"];    null0 [label="NULL", shape=plaintext];    null1 [label="NULL", shape=plaintext];    bucket [label="<head>dictEntry**\n(bucket) |<table0> 0 |<table1> 1 |<table2> 2 |<table3> 3 "fillcolor = "#F2F2F2"];    entry [label="<head>dictEntry |{<start>key1 | value1 |<next>next}", fillcolor = "#FADCAD"];    // lines    dict:ht -> ht0:dictht [label="ht[0]"];    dict:ht -> ht1:dictht [label="ht[1]"];    ht0:table -> bucket:head; bucket:table1 -> entry:head; entry:next -> null0;    ht1:table -> null1;    // table nulls    tnull0 [label="NULL", shape=plaintext];    tnull2 [label="NULL", shape=plaintext];    tnull3 [label="NULL", shape=plaintext];    bucket:table0 -> tnull0;    bucket:table2 -> tnull2;    bucket:table3 -> tnull3;}

八 添加新键值对时发生碰撞处理

 在哈希表实现中, 当两个不同的键拥有相同的哈希值时, 称这两个键发生碰撞(collision), 而哈希表实现必须想办法对碰撞进行处理。

字典哈希表所使用的碰撞解决方法被称之为链地址法: 这种方法使用链表将多个哈希值相同的节点串连在一起, 从而解决冲突问题。

假设现在有一个带有三个节点的哈希表,如下图:

digraph before_key_collision {    // setting    rankdir = LR;    node[shape=record, style = filled];    edge [style = bold];    // nodes    bucket [label="dictEntry**\n(bucket) |<table0> 0 |<table1> 1 |<table2> 2 |<table3> 3 ", fillcolor = "#F2F2F2"];    pair_1 [label="<head>dictEntry |{key1 | value1 |<next>next}", fillcolor = "#FADCAD"];    pair_2 [label="<head>dictEntry |{key2 | value2 |<next>next}", fillcolor = "#FADCAD"];    pair_3 [label="<head>dictEntry |{key3 | value3 |<next>next}", fillcolor = "#FADCAD"];    null0 [label="NULL", shape=plaintext];    null1 [label="NULL", shape=plaintext];    null2 [label="NULL", shape=plaintext];    null3 [label="NULL", shape=plaintext];    // lines    bucket:table0 -> pair_1:head; pair_1:next -> null0;    bucket:table1 -> null1;    bucket:table2 -> pair_2:head; pair_2:next -> null2;    bucket:table3 -> pair_3:head; pair_3:next -> null3;    // label    label = "添加碰撞节点之前";}

对于一个新的键值对 key4 和 value4 , 如果 key4 的哈希值和 key1 的哈希值相同, 那么它们将在哈希表的 0 号索引上发生碰撞。

通过将 key4-value4 和 key1-value1 两个键值对用链表连接起来, 就可以解决碰撞的问题:

digraph after_key_collision {    // setting    rankdir = LR;    node[shape=record, style = filled];    edge [style = bold];    // nodes    bucket [label="dictEntry**\n(bucket) |<table0> 0 |<table1> 1 |<table2> 2 |<table3> 3 ", fillcolor = "#F2F2F2"];    pair_1 [label="<head>dictEntry |{key1 | value1 |<next>next}", fillcolor = "#FADCAD"];    pair_2 [label="<head>dictEntry |{key2 | value2 |<next>next}", fillcolor = "#FADCAD"];    pair_3 [label="<head>dictEntry |{key3 | value3 |<next>next}", fillcolor = "#FADCAD"];    pair_4 [label="<head>dictEntry |{key4 | value4 |<next>next}", fillcolor = "#FFC1C1"];    null0 [label="NULL", shape=plaintext];    null1 [label="NULL", shape=plaintext];    null2 [label="NULL", shape=plaintext];    null3 [label="NULL", shape=plaintext];    // lines    bucket:table0 -> pair_4:head; pair_4:next -> pair_1:head; pair_1:next -> null0;    bucket:table1 -> null1;    bucket:table2 -> pair_2:head; pair_2:next -> null2;    bucket:table3 -> pair_3:head; pair_3:next -> null3;    // label    label = "添加碰撞节点之后";}

九 添加新键值对时触发了 rehash 操作

对于使用链地址法来解决碰撞问题的哈希表 dictht 来说, 哈希表的性能取决于大小(size属性)与保存节点数量(used属性)之间的比率:

  • 哈希表的大小与节点数量,比率在 1:1 时,哈希表的性能最好;
  • 如果节点数量比哈希表的大小要大很多的话,那么哈希表就会退化成多个链表,哈希表本身的性能优势便不复存在;

举个例子, 下面这个哈希表, 平均每次失败查找只需要访问 1 个节点(非空节点访问 2 次,空节点访问 1 次):

digraph good_performance_hash {    rankdir = LR;    node[shape=record, style = filled];    edge [style = bold];    // bucket    bucket [label="bucket |<0> 0 |<1> 1 |<2> 2 |<3> 3 |<4> 4 |<5> 5 |<6> 6 |<7> 7", fillcolor = "#F2F2F2"];    // nodes    node [height=.1];    node0 [label="Entry", fillcolor = "#FADCAD"];    node0_null [label="NULL", shape=plaintext];    node1 [label="Entry", fillcolor = "#FADCAD"];    node1_null [label="NULL", shape=plaintext];    node2 [label="NULL", shape=plaintext];    node3 [label="Entry", fillcolor = "#FADCAD"];    node3_null [label="NULL", shape=plaintext];    node4 [label="NULL", shape=plaintext];    node5 [label="Entry", fillcolor = "#FADCAD"];    node5_null [label="NULL", shape=plaintext];    node6 [label="NULL", shape=plaintext];    node7 [label="NULL", shape=plaintext];    bucket:0 -> node0; node0 -> node0_null;    bucket:1 -> node1; node1 -> node1_null;    bucket:2 -> node2;    bucket:3 -> node3; node3 -> node3_null;    bucket:4 -> node4;    bucket:5 -> node5; node5 -> node5_null;    bucket:6 -> node6;    bucket:7 -> node7;}

而下面这个哈希表, 平均每次失败查找需要访问 5 个节点:

digraph bad_performance_hash {    rankdir = LR;    node[shape=record, style = filled];    edge [style = bold];    // bucket    bucket [label="bucket |<0> 0 |<1> 1 |<2> 2 |<3> 3 |<4> 4 |<5> 5 |<6> 6 |<7> 7", fillcolor = "#F2F2F2"];    // nodes    node [height=.1];    // node 0    node0 [label="Entry", fillcolor = "#FADCAD"];    node01 [label="Entry", fillcolor = "#FADCAD"];    node02 [label="Entry", fillcolor = "#FADCAD"];    node03 [label="Entry", fillcolor = "#FADCAD"];    node04 [label="Entry", fillcolor = "#FADCAD"];    node05 [label="NULL", shape=plaintext];    bucket:0 -> node0;    node0 -> node01;    node01 -> node02;    node02 -> node03;    node03 -> node04;    node04 -> node05;    // node 1    node1 [label="Entry", fillcolor = "#FADCAD"];    node11 [label="Entry", fillcolor = "#FADCAD"];    node12 [label="Entry", fillcolor = "#FADCAD"];    node13 [label="NULL", shape=plaintext];    bucket:1 -> node1;    node1 -> node11;    node11 -> node12;    node12 -> node13;    // node 2    node2 [label="Entry", fillcolor = "#FADCAD"];    node21 [label="Entry", fillcolor = "#FADCAD"];    node22 [label="Entry", fillcolor = "#FADCAD"];    node23 [label="Entry", fillcolor = "#FADCAD"];    node24 [label="Entry", fillcolor = "#FADCAD"];    node25 [label="NULL", shape=plaintext];    bucket:2 -> node2;    node2 -> node21;    node21 -> node22;    node22 -> node23;    node23 -> node24;    node24 -> node25;    // node 3    node3 [label="Entry", fillcolor = "#FADCAD"];    node31 [label="Entry", fillcolor = "#FADCAD"];    node32 [label="Entry", fillcolor = "#FADCAD"];    node33 [label="Entry", fillcolor = "#FADCAD"];    node34 [label="Entry", fillcolor = "#FADCAD"];    node35 [label="NULL", shape=plaintext];    bucket:3 -> node3;    node3 -> node31;    node31 -> node32;    node32 -> node33;    node33 -> node34;    node34 -> node35;    // node 4    node4 [label="Entry", fillcolor = "#FADCAD"];    node41 [label="Entry", fillcolor = "#FADCAD"];    node42 [label="NULL", shape=plaintext];    bucket:4 -> node4;    node4 -> node41;    node41 -> node42;    // node 5    node5 [label="Entry", fillcolor = "#FADCAD"];    node51 [label="Entry", fillcolor = "#FADCAD"];    node52 [label="Entry", fillcolor = "#FADCAD"];    node53 [label="Entry", fillcolor = "#FADCAD"];    node54 [label="Entry", fillcolor = "#FADCAD"];    node55 [label="NULL", shape=plaintext];    bucket:5 -> node5;    node5 -> node51;    node51 -> node52;    node52 -> node53;    node53 -> node54;    node54 -> node55;    // node 6    node6 [label="Entry", fillcolor = "#FADCAD"];    node61 [label="Entry", fillcolor = "#FADCAD"];    node62 [label="Entry", fillcolor = "#FADCAD"];    node63 [label="Entry", fillcolor = "#FADCAD"];    node64 [label="NULL", shape=plaintext];    bucket:6 -> node6;    node6 -> node61;    node61 -> node62;    node62 -> node63;    node63 -> node64;    // node 7    node7 [label="Entry", fillcolor = "#FADCAD"];    node71 [label="Entry", fillcolor = "#FADCAD"];    node72 [label="Entry", fillcolor = "#FADCAD"];    node73 [label="Entry", fillcolor = "#FADCAD"];    node74 [label="Entry", fillcolor = "#FADCAD"];    node75 [label="NULL", shape=plaintext];    bucket:7 -> node7;    node7 -> node71;    node71 -> node72;    node72 -> node73;    node73 -> node74;    node74 -> node75;}

为了在字典的键值对不断增多的情况下保持良好的性能, 字典需要对所使用的哈希表(ht[0])进行 rehash 操作: 在不修改任何键值对的情况下,对哈希表进行扩容, 尽量将比率维持在 1:1 左右。

dictAdd 在每次向字典添加新键值对之前, 都会对哈希表 ht[0] 进行检查, 对于 ht[0] 的 size 和 used 属性, 如果它们之间的比率 ratio= used / size 满足以下任何一个条件的话,rehash 过程就会被激活:

  1. 自然 rehash : ratio >= 1 ,且变量 dict_can_resize 为真。
  2. 强制 rehash : ratio 大于变量 dict_force_resize_ratio (目前版本中, dict_force_resize_ratio 的值为 5 )。

什么时候 dict_can_resize 会为假?

在前面介绍字典的应用时也说到过, 数据库就是字典, 数据库里的哈希类型键也是字典, 当 Redis 使用子进程对数据库执行后台持久化任务时(比如执行 BGSAVE 或 BGREWRITEAOF 时), 为了最大化地利用系统的 copy on write 机制, 程序会暂时将 dict_can_resize 设为假, 避免执行自然 rehash , 从而减少程序对内存的触碰(touch)。

当持久化任务完成之后, dict_can_resize 会重新被设为真。

另一方面, 当字典满足了强制 rehash 的条件时, 即使 dict_can_resize 不为真(有 BGSAVE 或 BGREWRITEAOF 正在执行), 这个字典一样会被 rehash 。

十 Rehash 执行过程

字典的 rehash 操作实际上就是执行以下任务:

  1. 创建一个比 ht[0]->table 更大的 ht[1]->table ;
  2. 将 ht[0]->table 中的所有键值对迁移到 ht[1]->table ;
  3. 将原有 ht[0] 的数据清空,并将 ht[1] 替换为新的 ht[0] ;

经过以上步骤之后, 程序就在不改变原有键值对数据的基础上, 增大了哈希表的大小。

作为例子, 以下四个小节展示了一次对哈希表进行 rehash 的完整过程。

1. 开始 rehash

这个阶段有两个事情要做:

  1. 设置字典的 rehashidx 为 0 ,标识着 rehash 的开始;
  2. 为 ht[1]->table 分配空间,大小至少为 ht[0]->used 的两倍;

这时的字典是这个样子:

digraph rehash_step_one {    // setting    rankdir = LR;    node[shape=record, style = filled];    edge [style = bold];    // nodes    dict [label="dict | type | privdata |<ht> ht[2] | rehashidx: 0 | iterators", fillcolor = "#A8E270"];    ht0 [label="<dictht>dictht |<table> table | size: 4 | sizemask: 3 | used: 4", fillcolor = "#95BBE3"];    ht1 [label="<dictht>dictht |<table> table | size: 8 | sizemask: 7 | used: 0", fillcolor = "#95BBE3"];    bucket [label="<head>dictEntry**\n(bucket) |<table0> 0 |<table1> 1 |<table2> 2 |<table3> 3 ", fillcolor = "#F2F2F2"];    bucket1 [label="<head>dictEntry**\n(bucket) |<table0> 0 |<table1> 1 |<table2> 2 |<table3> 3 |<table4> 4 |<table5> 5 |<table6> 6 |<table7> 7", fillcolor = "#F2F2F2"];    pair_1 [label="<head>dictEntry |{key1 | value1 |<next>next}", fillcolor = "#FADCAD"];    pair_2 [label="<head>dictEntry |{key2 | value2 |<next>next}", fillcolor = "#FADCAD"];    pair_3 [label="<head>dictEntry |{key3 | value3 |<next>next}", fillcolor = "#FADCAD"];    pair_4 [label="<head>dictEntry |{key4 | value4 |<next>next}", fillcolor = "#FADCAD"];    // null for bucket 0    null0 [label="NULL", shape=plaintext];    null1 [label="NULL", shape=plaintext];    null2 [label="NULL", shape=plaintext];    null3 [label="NULL", shape=plaintext];    // null for bucket 1    null10 [label="NULL", shape=plaintext];    null11 [label="NULL", shape=plaintext];    null12 [label="NULL", shape=plaintext];    null13 [label="NULL", shape=plaintext];    null14 [label="NULL", shape=plaintext];    null15 [label="NULL", shape=plaintext];    null16 [label="NULL", shape=plaintext];    null17 [label="NULL", shape=plaintext];    // lines    dict:ht -> ht0:dictht [label="ht[0]"];    dict:ht -> ht1:dictht [label="ht[1]"];    ht0:table -> bucket:head;    ht1:table -> bucket1:head;    bucket:table0 -> pair_1:head; pair_1:next -> null0;    bucket:table1 -> pair_4:head; pair_4:next -> null1;    bucket:table2 -> pair_2:head; pair_2:next -> null2;    bucket:table3 -> pair_3:head; pair_3:next -> null3;    bucket1:table0 -> null10;    bucket1:table1 -> null11;    bucket1:table2 -> null12;    bucket1:table3 -> null13;    bucket1:table4 -> null14;    bucket1:table5 -> null15;    bucket1:table6 -> null16;    bucket1:table7 -> null17;}

2. Rehash 进行中

在这个阶段, ht[0]->table 的节点会被逐渐迁移到 ht[1]->table , 因为 rehash 是分多次进行的(细节在下一节解释), 字典的 rehashidx 变量会记录 rehash 进行到 ht[0] 的哪个索引位置上。

以下是 rehashidx 值为 2 时,字典的样子:

digraph rehash_step_two {    // setting    rankdir = LR;    node[shape=record, style = filled];    edge [style = bold];    // nodes    dict [label="dict | type | privdata |<ht> ht[2] | rehashidx: 2 | iterators", fillcolor = "#A8E270"];    ht0 [label="<dictht>dictht |<table> table | size: 4 | sizemask: 3 | used: 1", fillcolor = "#95BBE3"];    ht1 [label="<dictht>dictht |<table> table | size: 8 | sizemask: 7 | used: 3", fillcolor = "#95BBE3"];    bucket [label="<head>dictEntry**\n(bucket) |<table0> 0 |<table1> 1 |<table2> 2 |<table3> 3 ", fillcolor = "#F2F2F2"];    bucket1 [label="<head>dictEntry**\n(bucket) |<table0> 0 |<table1> 1 |<table2> 2 |<table3> 3 |<table4> 4 |<table5> 5 |<table6> 6 |<table7> 7", fillcolor = "#F2F2F2"];    pair_1 [label="<head>dictEntry |{key1 | value1 |<next>next}", fillcolor = "#FFC1C1"];    pair_2 [label="<head>dictEntry |{key2 | value2 |<next>next}", fillcolor = "#FFC1C1"];    pair_3 [label="<head>dictEntry |{key3 | value3 |<next>next}", fillcolor = "#FADCAD"];    pair_4 [label="<head>dictEntry |{key4 | value4 |<next>next}", fillcolor = "#FFC1C1"];    // null for bucket 0    null0 [label="NULL", shape=plaintext];    null1 [label="NULL", shape=plaintext];    null2 [label="NULL", shape=plaintext];    null3 [label="NULL", shape=plaintext];    // null for bucket 1    null10 [label="NULL", shape=plaintext];    null11 [label="NULL", shape=plaintext];    null12 [label="NULL", shape=plaintext];    null13 [label="NULL", shape=plaintext];    null14 [label="NULL", shape=plaintext];    null15 [label="NULL", shape=plaintext];    null16 [label="NULL", shape=plaintext];    null17 [label="NULL", shape=plaintext];    // lines    dict:ht -> ht0:dictht [label="ht[0]"];    dict:ht -> ht1:dictht [label="ht[1]"];    ht0:table -> bucket:head;    ht1:table -> bucket1:head;    bucket:table0 -> null0;    bucket:table1 -> null1;    bucket:table2 -> null2;    bucket:table3 -> pair_3:head; pair_3:next -> null3;    bucket1:table0 -> null10;    bucket1:table1 -> pair_4:head; pair_4:next -> null11;    bucket1:table2 -> null12;    bucket1:table3 -> pair_2:head; pair_2:next -> null13;    bucket1:table4 -> null14;    bucket1:table5 -> null15;    bucket1:table6 -> pair_1:head; pair_1:next -> null16;    bucket1:table7 -> null17;}

注意除了节点的移动外, 字典的 rehashidx 、 ht[0]->used 和 ht[1]->used 三个属性也产生了变化。

3. 节点迁移完毕

到了这个阶段,所有的节点都已经从 ht[0] 迁移到 ht[1] 了:

digraph rehash_step_three {    // setting    rankdir = LR;    node[shape=record, style = filled];    edge [style = bold];    // nodes    dict [label="dict | type | privdata |<ht> ht[2] | rehashidx: 3 | iterators", fillcolor = "#A8E270"];    ht0 [label="<dictht>dictht |<table> table | size: 4 | sizemask: 3 | used: 0", fillcolor = "#95BBE3"];    ht1 [label="<dictht>dictht |<table> table | size: 8 | sizemask: 7 | used: 4", fillcolor = "#95BBE3"];    bucket [label="<head>dictEntry**\n(bucket) |<table0> 0 |<table1> 1 |<table2> 2 |<table3> 3 ", fillcolor = "#F2F2F2"];    bucket1 [label="<head>dictEntry**\n(bucket) |<table0> 0 |<table1> 1 |<table2> 2 |<table3> 3 |<table4> 4 |<table5> 5 |<table6> 6 |<table7> 7", fillcolor = "#F2F2F2"];    pair_1 [label="<head>dictEntry |{key1 | value1 |<next>next}", fillcolor = "#FFC1C1"];    pair_2 [label="<head>dictEntry |{key2 | value2 |<next>next}", fillcolor = "#FFC1C1"];    pair_3 [label="<head>dictEntry |{key3 | value3 |<next>next}", fillcolor = "#FFC1C1"];    pair_4 [label="<head>dictEntry |{key4 | value4 |<next>next}", fillcolor = "#FFC1C1"];    // null for bucket 0    null0 [label="NULL", shape=plaintext];    null1 [label="NULL", shape=plaintext];    null2 [label="NULL", shape=plaintext];    null3 [label="NULL", shape=plaintext];    // null for bucket 1    null10 [label="NULL", shape=plaintext];    null11 [label="NULL", shape=plaintext];    null12 [label="NULL", shape=plaintext];    null13 [label="NULL", shape=plaintext];    null14 [label="NULL", shape=plaintext];    null15 [label="NULL", shape=plaintext];    null16 [label="NULL", shape=plaintext];    null17 [label="NULL", shape=plaintext];    // lines    dict:ht -> ht0:dictht [label="ht[0]"];    dict:ht -> ht1:dictht [label="ht[1]"];    ht0:table -> bucket:head;    ht1:table -> bucket1:head;    bucket:table0 -> null0;    bucket:table1 -> null1;    bucket:table2 -> null2;    bucket:table3 -> null3;    bucket1:table0 -> pair_3:head; pair_3:next -> null10;    bucket1:table1 -> pair_4:head; pair_4:next -> null11;    bucket1:table2 -> null12;    bucket1:table3 -> pair_2:head; pair_2:next -> null13;    bucket1:table4 -> null14;    bucket1:table5 -> null15;    bucket1:table6 -> pair_1:head; pair_1:next -> null16;    bucket1:table7 -> null17;}

4. Rehash 完毕

在 rehash 的最后阶段,程序会执行以下工作:

  1. 释放 ht[0] 的空间;
  2. 用 ht[1] 来代替 ht[0] ,使原来的 ht[1] 成为新的 ht[0] ;
  3. 创建一个新的空哈希表,并将它设置为 ht[1] ;
  4. 将字典的 rehashidx 属性设置为 -1 ,标识 rehash 已停止;

以下是字典 rehash 完毕之后的样子:

digraph rehash_step_four {    // setting    rankdir = LR;    node[shape=record, style = filled];    edge [style = bold];    // nodes    dict [label="dict | type | privdata |<ht> ht[2] | rehashidx: -1 | iterators", fillcolor = "#A8E270"];    ht0 [label="<dictht>dictht |<table> table | size: 8 | sizemask: 7 | used: 4", fillcolor = "#95BBE3"];    ht3 [label="<dictht>dictht |<table> table | size: 0 | sizemask: 0 | used: 0", fillcolor = "#95BBE3"];    bucket1 [label="<head>dictEntry**\n(bucket) |<table0> 0 |<table1> 1 |<table2> 2 |<table3> 3 |<table4> 4 |<table5> 5 |<table6> 6 |<table7> 7", fillcolor = "#F2F2F2"];    pair_1 [label="<head>dictEntry |{key1 | value1 |<next>next}", fillcolor = "#FFC1C1"];    pair_2 [label="<head>dictEntry |{key2 | value2 |<next>next}", fillcolor = "#FFC1C1"];    pair_3 [label="<head>dictEntry |{key3 | value3 |<next>next}", fillcolor = "#FFC1C1"];    pair_4 [label="<head>dictEntry |{key4 | value4 |<next>next}", fillcolor = "#FFC1C1"];    // null for bucket 1    null10 [label="NULL", shape=plaintext];    null11 [label="NULL", shape=plaintext];    null12 [label="NULL", shape=plaintext];    null13 [label="NULL", shape=plaintext];    null14 [label="NULL", shape=plaintext];    null15 [label="NULL", shape=plaintext];    null16 [label="NULL", shape=plaintext];    null17 [label="NULL", shape=plaintext];    // lines    dict:ht -> ht0:dictht [label="ht[0]"];    dict:ht -> ht3:dictht [label="ht[1]"];    null_bucket [label="NULL", shape=plaintext];    ht3:table -> null_bucket;    ht0:table -> bucket1:head;    bucket1:table0 -> pair_3:head; pair_3:next -> null10;    bucket1:table1 -> pair_4:head; pair_4:next -> null11;    bucket1:table2 -> null12;    bucket1:table3 -> pair_2:head; pair_2:next -> null13;    bucket1:table4 -> null14;    bucket1:table5 -> null15;    bucket1:table6 -> pair_1:head; pair_1:next -> null16;    bucket1:table7 -> null17;}

对比字典 rehash 前后, 新的 ht[0] 空间更大, 并且字典原有的键值对也没有被修改或者删除。

十一 Rehash 执行过程

在上一节,我们了解了字典的 rehash 过程, 需要特别指出的是, rehash 程序并不是在激活之后,就马上执行直到完成的, 而是分多次、渐进式地完成的。

假设这样一个场景:在一个有很多键值对的字典里, 某个用户在添加新键值对时触发了 rehash 过程, 如果这个 rehash 过程必须将所有键值对迁移完毕之后才将结果返回给用户, 这样的处理方式将是非常不友好的。

另一方面, 要求服务器必须阻塞直到 rehash 完成, 这对于 Redis 服务器本身也是不能接受的。

为了解决这个问题, Redis 使用了渐进式(incremental)的 rehash 方式: 通过将 rehash 分散到多个步骤中进行, 从而避免了集中式的计算。

渐进式 rehash 主要由 _dictRehashStep 和 dictRehashMilliseconds 两个函数进行:

  • _dictRehashStep 用于对数据库字典、以及哈希键的字典进行被动 rehash ;
  • dictRehashMilliseconds 则由 Redis 服务器常规任务程序(server cron job)执行,用于对数据库字典进行主动 rehash ;

_dictRehashStep

每次执行 _dictRehashStep , ht[0]->table 哈希表第一个不为空的索引上的所有节点就会全部迁移到 ht[1]->table 。

在 rehash 开始进行之后(d->rehashidx 不为 -1), 每次执行一次添加、查找、删除操作, _dictRehashStep 都会被执行一次:

digraph rehash_step {    node[shape=plaintext, style = filled];    edge [style = bold];    // callers    dictAdd [label="dictAdd", fillcolor = "#A8E270"];    dictFind [label="dictFind", fillcolor = "#A8E270"];    dictDelete [label="dictDelete", fillcolor = "#A8E270"];    dictGetRandomKey [label="dictGetRandomKey", fillcolor = "#A8E270"];    // rehash    rehashing_or_not [shape=diamond, label="正在进行 rehash ?", fillcolor = "#95BBE3"];    _dictRehashStep [label="_dictRehashStep", fillcolor = "#A8E270"];    one_index [label="将 ht[0] 第一个不为空索引上的所有节点迁移至 ht[1]"];    // edge    dictAdd -> rehashing_or_not;    dictFind -> rehashing_or_not;    dictDelete -> rehashing_or_not;    dictGetRandomKey -> rehashing_or_not;    rehashing_or_not -> _dictRehashStep [label="是"];    _dictRehashStep -> one_index;}

因为字典会保持哈希表大小和节点数的比率在一个很小的范围内, 所以每个索引上的节点数量不会很多(从目前版本的 rehash 条件来看,平均只有一个,最多通常也不会超过五个), 所以在执行操作的同时,对单个索引上的节点进行迁移, 几乎不会对响应时间造成影响。

dictRehashMilliseconds

dictRehashMilliseconds 可以在指定的毫秒数内, 对字典进行 rehash 。

当 Redis 的服务器常规任务执行时, dictRehashMilliseconds 会被执行, 在规定的时间内, 尽可能地对数据库字典中那些需要 rehash 的字典进行 rehash , 从而加速数据库字典的 rehash 进程(progress)。


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
C语言是一种广泛使用的编程语言,它具有高效、灵活、可移植性强等特点,被广泛应用于操作系统、嵌入式系统、数据库、编译器等领域的开发。C语言的基本语法包括变量、数据类型、运算符、控制结构(如if语句、循环语句等)、函数、指针等。在编写C程序时,需要注意变量的声明和定义、指针的使用、内存的分配与释放等问题。C语言中常用的数据结构包括: 1. 数组:一种存储同类型数据的结构,可以进行索引访问和修改。 2. 链表:一种存储不同类型数据的结构,每个节点包含数据和指向下一个节点的指针。 3. 栈:一种后进先出(LIFO)的数据结构,可以通过压入(push)和弹出(pop)操作进行数据的存储和取出。 4. 队列:一种先进先出(FIFO)的数据结构,可以通过入队(enqueue)和出队(dequeue)操作进行数据的存储和取出。 5. 树:一种存储具有父子关系的数据结构,可以通过中序遍历、前序遍历和后序遍历等方式进行数据的访问和修改。 6. 图:一种存储具有节点和边关系的数据结构,可以通过广度优先搜索、深度优先搜索等方式进行数据的访问和修改。 这些数据结构在C语言中都有相应的实现方式,可以应用于各种不同的场景。C语言中的各种数据结构都有其优缺点,下面列举一些常见的数据结构的优缺点: 数组: 优点:访问和修改元素的速度非常快,适用于需要频繁读取和修改数据的场合。 缺点:数组的长度是固定的,不适合存储大小不固定的动态数据,另外数组在内存中是连续分配的,当数组较大时可能会导致内存碎片化。 链表: 优点:可以方便地插入和删除元素,适用于需要频繁插入和删除数据的场合。 缺点:访问和修改元素的速度相对较慢,因为需要遍历链表找到指定的节点。 栈: 优点:后进先出(LIFO)的特性使得栈在处理递归和括号匹配等问题时非常方便。 缺点:栈的空间有限,当数据量较大时可能会导致栈溢出。 队列: 优点:先进先出(FIFO)的特性使得

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值