Redis数据结构之Dict字典

本文深入解析Redis中的字典数据结构,字典用于存储键值对,采用哈希表实现,包含两个哈希表ht[0]和ht[1]以支持渐进式rehash。哈希表节点由dictEntry组成,键值对通过键的哈希值计算索引,负载因子控制字典扩展和收缩。此外,字典还包括类型特定函数dictType和私有数据,用于处理不同类型的键值对操作。rehash过程分为多个步骤,确保在不影响性能的同时完成键值对的迁移。
摘要由CSDN通过智能技术生成

简介

字典(Dict),是一种用于保存键值对(key-value pair)的抽象数据结构,其应用于将一个键(key)映射为一个值,从而形成关联性。

在字典中,每个键都是独一无二的,通过独一无二的特性来获取、更新、删除与之关联的值。
键的独一无二特性取决于哈希函数的随机性,当我们实现自己的字典时,哈希函数是实现的一大关键。

字典这种结构通常内置在各个高级语言,比如c++的Map、java的Map,都是字典。

字典在redis中的应用场景

Redis的实现语言为C,因此Redis的作者自己实现了字典这个数据结构。

redis数据库

Redis的数据库采用字典作为底层实现,其增、删、改、改操作都是通过字典进行操作的。

typedef struct redisDb {
    // 数据库键空间,保存着数据库中的所有键值对
    dict *dict;                 /* The keyspace for this DB */
    // 键的过期时间,字典的键为键,字典的值为过期事件 UNIX 时间戳
    dict *expires;              /* Timeout of keys with a timeout set */
    // 正处于阻塞状态的键
    dict *blocking_keys;        /* Keys with clients waiting for data (BLPOP) */
    // 可以解除阻塞的键
    dict *ready_keys;           /* Blocked keys that received a PUSH */
    // 正在被 WATCH 命令监视的键
    dict *watched_keys;         /* WATCHED keys for MULTI/EXEC CAS */
    struct evictionPoolEntry *eviction_pool;    /* Eviction pool of keys */
    // 数据库号码
    int id;                     /* Database ID */
    // 数据库的键的平均 TTL ,统计信息
    long long avg_ttl;          /* Average TTL, just for stats */
} redisDb;

redis作为nosql,其数据库结构 redisDb 采用dict来存储数据库中的所有键值对。

通过源码搜索下dict类型,便可以知道redis在哪些部分使用到了dict,redis的源码地址:
github

字典的实现

接下来讲解redis里的字典实现,在后面的内容介绍 c++ 以及 java的内置数据结构

redis的字典采用哈希表,哈希表由多个节点组成,每个节点保存字典中的一个键值对。
字典实现对应源码文件为:dict.h、dict.c

哈希表
字典结构图,对应的源码:

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;

字典由两个哈希表组成,ht[0] 表示当前存储的键值对,而可以将ht[1]视为用于rehash时进行扩展或收缩的临时数据,介绍rehash时再介绍其用法。

哈希表 dictht

哈希表结构源码如下:

/*
 * 哈希表
 *
 * 每个字典都使用两个哈希表,从而实现渐进式 rehash 。
 */
typedef struct dictht {
    
    // 哈希表节点数组
    dictEntry **table;

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

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

} dictht;

1、table是一个数组,数组的每个元素为dictEntry结构的指针,每个dictEntry即保存着一个键值对。
2、size是数组的大小,采用无符号long类型
3、used是已存有的键值对数量
4、sizemask用于计算键值对的索引,因为数组下标从0开始,所以其值等于size-1

哈希表节点dictEntry

节点结构的源码如下:

typedef struct dictEntry {
    
    // 键
    void *key;

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

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

} dictEntry;

1、键值对的值可以是指针,指向redis内的其他结构对象,也可以是一个uint64_t、int64_t整数
2、next指针用于指向下一个键值对节点,是解决哈希冲突的最常见方式:链地址法。当k1的哈希值与k0哈希值一致时,其保存的哈希节点在相同的表索引位置。因为dictEntry节点组成的链表没有指向链表表尾的指针,所以为了速度考虑,总是将新节点添加在链表的表头:假设[0]节点为k0,现在新增k1

k1->next = dictEntry[0];
dictEntry[0] = k1
字典类型特定函数dictType和私有数据privdata

1、type属性为dictType结构,每个dictType结构保存了一簇用于操作特定类型键值对的函数,Redis会为用途不同的字典设置不同的类型特定函数,用于针对不同类型的键值对实现多态
2、private属性保存了需要传给那些类型特定函数的可选参数

其源码如下:

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;

当redis创建字典时,可以设置函数指针到dictType结构中

哈希算法

redis计算哈希值和索引值的方法:

hash = dict->type->hashFunction(key);

index = hash & dict->ht[0].sizemask;

hash算法用于根据键值对计算键值对节点的位置,redis采用的是MurmurHash2算法。

rehash

哈希表的键值对会增加或者减少,为了让哈希表的负载因子(load factor)维持在一个合理的范围,哈希表需要进行扩展或者收缩。

负载因子:哈希表已保存节点数量/哈希表大小
load_factor = ht[0].used / ht[0].size

1、先为ht[1]分配空间:如果是扩展操作,ht[1]的大小等于ht[0].used * 2 的 2^n 、 如果是收缩操作,h1[1]的大小等于ht[0].used 的 2^n
2、将ht[0]的键值对rehash(重新计算节点位置)放到ht[1]
3、ht[0]在迁移玩所有数据后为空表,即可释放,同时将ht[1]设置成ht[0],并在ht[1]新建一个空白哈希表。

扩展操作:
1、服务器没有执行BGSAVE或者BGREWRITEAOF命令,且负载因子大于等于1
2、正在执行BGSAVE或者BGREWRITEAOF命令,且负载因子大于等于5

负载因子不同时因为执行BGSAVE或者BGREWRITEAOF时,需要创建子进程,提高负载因子可以避免在子进程存在时进行扩展,优化内存使用情况

收缩操作:
负载因子小于0.1

渐进式rehash

rehash执行并不是一次性完成的,为了提高性能,采用渐进式的方式多次rehash。
1、在字典中维持一个索引计数器变量rehashidx,其值为0时表示正在执行rehash。
2、rehash期间,对字典操作时,程序除了执行指定命令(键值对的新增是在ht[1]表上操作),会顺带将ht[0]表在rehashidx索引上的所有键值对rehash到ht[1]上,执行成功后rehashidx值加1
4、ht[0]的所有键值对操作完成后,rehashidx置为-1
5、在rehash期间,键值对的查找会现在ht[0]表上查找,再到ht[1]查找。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值