redis的底层数据结构--dict

Redis的KV存储结构

Redis内存数据库,最底层是一个redisDb;

 

redisDb 整体使用 dict字典 来存储键值对KV;
字典中的每一项,使用dictEntry ,代表KV键值;类似于HashMap中的键值对Entry。

 

why dict/map?

dict是一种用于维护key和value映射关系的数据结构,与很多编程语言中的Map类似。
为什么dict/map 这么受欢迎呢?
因为dict/map实现了key和value的映射,通过key查询value是效率非常高的操作,时间复杂度是O(C),C是常数,在没有冲突/碰撞的情况下,可以达到O(1)。

dict本质上是为了解决算法中的查找问题(Searching),一般查找问题的解法分为两个大类:一个是基于各种平衡树,一个是基于哈希表。

  • 平衡树,如二叉搜索树、红黑树,使用的是“二分思想”;
    如果需要实现排序,则可使用平衡树,如:用大顶堆实现TreeMap;
  • 哈希表,如Java中的Map,Python中的字典dict,使用的是“映射思想”;

我们平常使用的各种Map或dict,大都是基于哈希表实现的。在不要求数据有序存储,且能保持较低的哈希值冲突概率的前提下,基于哈希表的查找性能能做到非常高效,接近O(1),而且容易实现。

Redis dict的应用

字典dict 在 Redis 中的应用广泛, 使用频率可以说和 SDS 以及双端链表不相上下, 基本上各个功能模块都有用到字典的地方。

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

  • 实现数据库键空间(key space);
  • 用作 hash 键的底层实现之一;

以下两个小节分别介绍这两种用途。


Redis数据库键空间(key space)

Redis 是一个键值对数据库服务器,服务器中每个数据库都由 redisDB 结构表示(默认16个库)。其中,redisDB 结构的 dict 字典保存了数据库中所有的键值对,这个字典被称为键空间(key space)。

 

可以认为,Redis默认16个库,这16个库在各自的键空间(key space)中;其实就通过键空间(key space)实现了隔离。而键空间(key space)底层是dict实现的。

键空间(key space)除了实现了16个库的隔离,还能基于键空间通知(Keyspace Notifications) 实现某些事件的订阅通知,如某个key过期的时间,某个key的value变更事件。

键空间通知(Keyspace Notifications),是因为键空间(key space)实现了16个库的隔离,而我们执行Redis命令最终都是落在其中一个库上,当有事件发生在某个库上时,该库对应的键空间(key space)就能基于pub/sub发布订阅,实现事件“广播”。

 

dict 用作 hash 键的底层实现

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

  • 压缩列表ziplist ;
  • 字典dict;

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

压缩链表转成字典(ziplist->dict)的条件

同时满足以下两个条件,hash 键才会使用ziplist:
1、key和value 长度都小于64
2、键值对数小于512

该配置 在redis.conf

hash-max-ziplist-entries 512
hash-max-ziplist-value 64

如何实现字典dict/映射

dict,又称字典(dictionary)或映射(map),是集合的一种;这种集合中每个元素都是KV键值对。
它是根据关键字值(key)而直接进行访问KV键值对的数据结构。也就是说,它通过把关键字值映射到一个位置来访问记录,以加快查找的速度。这个映射函数称为哈希函数(也称为散列函数)。
因此通常我们称字典dict,也叫哈希表。

映射过程,通常使用hash算法实现,因此也称映射过程为哈希化,存放记录的数组叫做散列表、或hash表。

哈希化之后难免会产生一个问题,那就是对不同的关键字,可能得到同一个散列地址,即不同的key散列到同一个数组下标,这种现象称为冲突,那么我们该如何去处理冲突呢?
最常用的就是链地址法,也常被称为拉链法,就是在冲突的下标处,维护一个链表,所有映射到该下标的记录,都添加到该链表上。

Redis字典dict如何实现的?

Redis字典dict,也是采用哈希表,本质就是数组+链表。
也是众多编程语言实现Map的首选方式,如Java中的HashMap。

Redis字典dict 的底层实现,其实和Java中的ConcurrentHashMap思想非常相似。
就是用数组+链表实现了分布式哈希表。当不同的关键字、散列到数组相同的位置,就拉链,用链表维护冲突的记录。当冲突记录越来越多、链表越来越长,遍历列表的效率就会降低,此时需要考虑将链表的长度变短

将链表的长度变短,一个最直接有效的方式就是扩容数组。将数组+链表结构中的数组扩容,数组变长、对应数组下标就增多了;将原数组中所有非空的索引下标、搬运到扩容后的新数组,经过重新散列,自然就把冲突的链表变短了。

如果你对Java的HashMap或ConcurrentHashMap 底层实现原理比较了解,那么对Redis字典dict的底层实现,也能很快上手。

dict.h 给出了这个字典dict的定义:

/*
 * 字典
 *
 * 每个字典使用两个哈希表,用于实现渐进式 rehash
 */
typedef struct dict {

    // 特定于类型的处理函数
    dictType *type;

    // 类型处理函数的私有数据
    void *privdata;

    // 哈希表(2 个)
    dictht ht[2];

    // 记录 rehash 进度的标志,值为 -1 表示 rehash 未进行
    int rehashidx;

    // 当前正在运作的安全迭代器数量
    int iterators;

} dict;

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的结构。一个dict由如下若干项组成:

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

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

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

dictht(dict hash table)哈希表

dictht 是字典 dict 哈希表的缩写,即dict hash table。
dict.h/dictht 类型定义:

 

/*
 * 哈希表
 */
typedef struct dictht {

    // 哈希表节点指针数组(俗称桶,bucket)
    dictEntry **table;

    // 指针数组的大小
    unsigned long size;

    // 指针数组的长度掩码,用于计算索引值
    unsigned long sizemask;

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

} dictht;


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

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

    // 链往后继节点
    struct dictEntry *next;
} dictEntry;

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的比值就是装载因子。这个比值越大,哈希值冲突概率越高。

Redis dictht的负载因子
我们知道当HashMap中由于Hash冲突(负载因子)超过某个阈值时,出于链表性能的考虑、会进行扩容,Redis dict也是一样。

一个dictht 哈希表里,核心就是一个dictEntry数组,同时用size记录了数组大小,用used记录了所有记录数。

dictht的负载因子,就是used与size的比值,也称装载因子(load factor)。这个比值越大,哈希值冲突概率越高。当比值[默认]超过5,会强制进行rehash。


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

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

 

渐进式rehash:

扩展或收缩哈希表需要将 ht[0] 里面的所有键值对 rehash 到 ht[1] 里面, 但是, 这个 rehash 动作并不是一次性、集中式地完成的, 而是分多次、渐进式地完成的。

这样做的原因在于, 如果 ht[0] 里只保存着四个键值对, 那么服务器可以在瞬间就将这些键值对全部 rehash 到 ht[1] ; 但是, 如果哈希表里保存的键值对数量不是四个, 而是四百万、四千万甚至四亿个键值对, 那么要一次性将这些键值对全部 rehash 到 ht[1] 的话, 庞大的计算量可能会导致服务器在一段时间内停止服务。

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

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

  1. 为 ht[1] 分配空间, 让字典同时持有 ht[0] 和 ht[1] 两个哈希表。
  2. 在字典中维持一个索引计数器变量 rehashidx , 并将它的值设置为 0 , 表示 rehash 工作正式开始。
  3. 在 rehash 进行期间, 每次对字典执行添加、删除、查找或者更新操作时, 程序除了执行指定的操作以外, 还会顺带将 ht[0] 哈希表在 rehashidx 索引上的所有键值对 rehash 到 ht[1] , 当 rehash 工作完成之后, 程序将 rehashidx 属性的值增一。
  4. 随着字典操作的不断执行, 最终在某个时间点上, ht[0] 的所有键值对都会被 rehash 至 ht[1] , 这时程序将 rehashidx 属性的值设为 -1 , 表示 rehash 操作已完成。

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

图 4-12 至图 4-17 展示了一次完整的渐进式 rehash 过程, 注意观察在整个 rehash 过程中, 字典的 rehashidx 属性是如何变化的。

 

digraph {      label = "\n 图 4-12    准备开始 rehash";      rankdir = LR;      node [shape = record];      // 字典      dict [label = " <head> dict | type | privdata | <ht> ht | rehashidx \n -1 "];      // 哈希表      dictht0 [label = " <head> dictht | <table> table | <size> size \n 4 | <sizemask> sizemask \n 3 | <used> used \n 4"];      dictht1 [label = " <head> dictht | <table> table | <size> size \n 8 | <sizemask> sizemask \n 7 | <used> used \n 0"];      table0 [label = " <head> dictEntry*[4] | <0> 0 | <1> 1 | <2> 2 | <3> 3 "];      table1 [label = " <head> dictEntry*[8] | <0> 0 | <1> 1 | <2> 2 | ... | <7> 7 "];      // 哈希表节点      kv0 [label = " <head> dictEntry | { k0 | v0 } "];     kv1 [label = " <head> dictEntry | { k1 | v1 } "];     kv2 [label = " <head> dictEntry | { k2 | v2 } "];     kv3 [label = " <head> dictEntry | { k3 | v3 } "];      //      node [shape = plaintext, label = "NULL"];      //      dict:ht -> dictht0:head [label = "ht[0]"];     dict:ht -> dictht1:head [label = "ht[1]"];      dictht0:table -> table0:head;     dictht1:table -> table1:head;      table0:0 -> kv2:head -> null0;     table0:1 -> kv0:head -> null1;     table0:2 -> kv3:head -> null2;     table0:3 -> kv1:head -> null3;      table1:0 -> null10;     table1:1 -> null11;     table1:2 -> null12;     table1:7 -> null17;  }

digraph {      label = "\n 图 4-13    rehash 索引 0 上的键值对";      rankdir = LR;      node [shape = record];      // 字典      dict [label = " <head> dict | type | privdata | <ht> ht | rehashidx \n 0 "];      // 哈希表      dictht0 [label = " <head> dictht | <table> table | <size> size \n 4 | <sizemask> sizemask \n 3 | <used> used \n 3"];      dictht1 [label = " <head> dictht | <table> table | <size> size \n 8 | <sizemask> sizemask \n 7 | <used> used \n 1"];      table0 [label = " <head> dictEntry*[4] | <0> 0 | <1> 1 | <2> 2 | <3> 3 "];      table1 [label = " <head> dictEntry*[8] | ... | <4> 4 | ... "];      // 哈希表节点      kv0 [label = " <head> dictEntry | { k0 | v0 } "];     kv1 [label = " <head> dictEntry | { k1 | v1 } "];     kv2 [label = " <head> dictEntry | { k2 | v2 } "];     kv3 [label = " <head> dictEntry | { k3 | v3 } "];      //      node [shape = plaintext, label = "NULL"];      //      dict:ht -> dictht0:head [label = "ht[0]"];     dict:ht -> dictht1:head [label = "ht[1]"];      dictht0:table -> table0:head;     dictht1:table -> table1:head;      table0:0 -> null0;     table0:1 -> kv0:head -> null1;     table0:2 -> kv3:head -> null2;     table0:3 -> kv1:head -> null3;      table1:4 -> kv2:head -> null14  }

 

digraph {      label = "\n 图 4-14    rehash 索引 1 上的键值对";      rankdir = LR;      node [shape = record];      // 字典      dict [label = " <head> dict | type | privdata | <ht> ht | rehashidx \n 1 "];      // 哈希表      dictht0 [label = " <head> dictht | <table> table | <size> size \n 4 | <sizemask> sizemask \n 3 | <used> used \n 2"];      dictht1 [label = " <head> dictht | <table> table | <size> size \n 8 | <sizemask> sizemask \n 7 | <used> used \n 2"];      table0 [label = " <head> dictEntry*[4] | <0> 0 | <1> 1 | <2> 2 | <3> 3 "];      table1 [label = " <head> dictEntry*[8] | ... | <4> 4 | <5> 5 | ... "];      // 哈希表节点      kv0 [label = " <head> dictEntry | { k0 | v0 } "];     kv1 [label = " <head> dictEntry | { k1 | v1 } "];     kv2 [label = " <head> dictEntry | { k2 | v2 } "];     kv3 [label = " <head> dictEntry | { k3 | v3 } "];      //      node [shape = plaintext, label = "NULL"];      //      dict:ht -> dictht0:head [label = "ht[0]"];     dict:ht -> dictht1:head [label = "ht[1]"];      dictht0:table -> table0:head;     dictht1:table -> table1:head;      table0:0 -> null0;     table0:1 -> null1;     table0:2 -> kv3:head -> null2;     table0:3 -> kv1:head -> null3;      table1:4 -> kv2:head -> null14     table1:5 -> kv0:head -> null15;  }

digraph {      label = "\n 图 4-15    rehash 索引 2 上的键值对";      rankdir = LR;      node [shape = record];      // 字典      dict [label = " <head> dict | type | privdata | <ht> ht | rehashidx \n 2 "];      // 哈希表      dictht0 [label = " <head> dictht | <table> table | <size> size \n 4 | <sizemask> sizemask \n 3 | <used> used \n 1"];      dictht1 [label = " <head> dictht | <table> table | <size> size \n 8 | <sizemask> sizemask \n 7 | <used> used \n 3"];      table0 [label = " <head> dictEntry*[4] | <0> 0 | <1> 1 | <2> 2 | <3> 3 "];      table1 [label = " <head> dictEntry*[8] | ... | <1> 1 | ... | <4> 4 | <5> 5 | ... "];      // 哈希表节点      kv0 [label = " <head> dictEntry | { k0 | v0 } "];     kv1 [label = " <head> dictEntry | { k1 | v1 } "];     kv2 [label = " <head> dictEntry | { k2 | v2 } "];     kv3 [label = " <head> dictEntry | { k3 | v3 } "];      //      node [shape = plaintext, label = "NULL"];      //      dict:ht -> dictht0:head [label = "ht[0]"];     dict:ht -> dictht1:head [label = "ht[1]"];      dictht0:table -> table0:head;     dictht1:table -> table1:head;      table0:0 -> null0;     table0:1 -> null1;     table0:2 -> null2;     table0:3 -> kv1:head -> null3;      table1:1 -> kv3:head -> null11;     table1:4 -> kv2:head -> null14     table1:5 -> kv0:head -> null15;  }

digraph {      label = "\n 图 4-16    rehash 索引 3 上的键值对";      rankdir = LR;      node [shape = record];      // 字典      dict [label = " <head> dict | type | privdata | <ht> ht | rehashidx \n 3 "];      // 哈希表      dictht0 [label = " <head> dictht | <table> table | <size> size \n 4 | <sizemask> sizemask \n 3 | <used> used \n 0"];      dictht1 [label = " <head> dictht | <table> table | <size> size \n 8 | <sizemask> sizemask \n 7 | <used> used \n 4"];      table0 [label = " <head> dictEntry*[4] | <0> 0 | <1> 1 | <2> 2 | <3> 3 "];      table1 [label = " <head> dictEntry*[8] | ... | <1> 1 | ... | <4> 4 | <5> 5 | ... | <7> 7 "];      // 哈希表节点      kv0 [label = " <head> dictEntry | { k0 | v0 } "];     kv1 [label = " <head> dictEntry | { k1 | v1 } "];     kv2 [label = " <head> dictEntry | { k2 | v2 } "];     kv3 [label = " <head> dictEntry | { k3 | v3 } "];      //      node [shape = plaintext, label = "NULL"];      //      dict:ht -> dictht0:head [label = "ht[0]"];     dict:ht -> dictht1:head [label = "ht[1]"];      dictht0:table -> table0:head;     dictht1:table -> table1:head;      table0:0 -> null0;     table0:1 -> null1;     table0:2 -> null2;     table0:3 -> null3;      table1:1 -> kv3:head -> null11;     table1:4 -> kv2:head -> null14     table1:5 -> kv0:head -> null15;     table1:7 -> kv1:head -> null17;  }

 

digraph {      label = "\n 图 4-17    rehash 执行完毕";      rankdir = LR;      node [shape = record];      // 字典      dict [label = " <head> dict | type | privdata | <ht> ht | rehashidx \n -1 "];      // 哈希表      dictht0 [label = " <head> dictht | <table> table | <size> size \n 8 | <sizemask> sizemask \n 7 | <used> used \n 4"];      dictht1 [label = " <head> dictht | <table> table | <size> size \n 0 | <sizemask> sizemask \n 0 | <used> used \n 0"];      table0 [label = " <head> dictEntry*[8] | ... | <1> 1 | ... | <4> 4 | <5> 5 | ... | <7> 7 "];      table1 [label = "NULL", shape = plaintext];      // 哈希表节点      kv0 [label = " <head> dictEntry | { k0 | v0 } "];     kv1 [label = " <head> dictEntry | { k1 | v1 } "];     kv2 [label = " <head> dictEntry | { k2 | v2 } "];     kv3 [label = " <head> dictEntry | { k3 | v3 } "];      //      node [shape = plaintext, label = "NULL"];      //      dict:ht -> dictht0:head [label = "ht[0]"];     dict:ht -> dictht1:head [label = "ht[1]"];      dictht0:table -> table0:head;     dictht1:table -> table1;      table0:1 -> kv3:head -> null11;     table0:4 -> kv2:head -> null14;     table0:5 -> kv0:head -> null15;     table0:7 -> kv1:head -> null17;  }

因为在进行渐进式 rehash 的过程中, 字典会同时使用 ht[0] 和 ht[1] 两个哈希表, 所以在渐进式 rehash 进行期间, 字典的删除(delete)、查找(find)、更新(update)等操作会在两个哈希表上进行: 比如说, 要在字典里面查找一个键的话, 程序会先在 ht[0] 里面进行查找, 如果没找到的话, 就会继续到 ht[1] 里面进行查找, 诸如此类。

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值