Java学习

  • 首页
  • 文章归档
  • 默认分类
  • 关于页面

  • 搜索
CAP 分布式 计算机网络 MySQL 源码 备份 Redis

Redis5数据结构-字典dict

发表于 2020-11-10 | 分类于 Redis | 1 | 阅读次数 477

字典又称散列表,是用来存储键值(key-value)对的一种数据结构,在很多高级语言中都有实现,如Java的Map。但是C语言没有这种数据结构,Redis是K-V型数据库,整个数据库是用字典来存储的,对Redis 数据库进行任何增、删、改、查操作,实际就是对字典中的数据进行增、删、改、查操作。


1、数据结构

Java中的HashMap就是典型的字典,这里以JDK1.7的HashMap(只用链表处理哈希冲突)为例:

  1. 对键key进行哈希,获取hash值再通过取余等操作得到索引值,通过索引在数组找到对应元素,如果没有找到元素,说明这个key在字典中不存在,如果找到元素,执行第2步。
  2. 字典中的元素同时存储着键key、值value和发生哈希冲突的元素next,将元素的key和要查找的key进行比较,如果相等则将值value取出,如果不相等,则继续查找next元素的,直到next元素指向NULL,则说明该key在字典中不存在。

在Redis中,字典实现主要依靠于:字典、Hash表和Hash表节点。字典中有两个Hash表,每个Hash表存储若干Hash表节点,每个Hash表节点存储着键值对K-V。

1.1、Hash表

//该结构体一共占用4*8=32字节
typedef struct dictht {
    // 指针数组 里面的指针元素指向Hash表节点
    dictEntry **table;
    // table的长度,该长度初始值为4,哈希表的容量会翻倍扩容,即8,16,32……
    unsigned long size;
    /* 掩码,上面的size-1,这样获得的数字在二进制下为11,111,1111……每一位都为1,方便按位与操作,
    这样:索引值=Hash&sizemask。这样得出结果和Hash%size相等,且按位与操作远比取余快得多。
    */
    unsigned long sizemask;
    // table数组已存储元素个数 包括Hash冲突的链表
    unsigned long used;
} dictht;

1.2、Hash表节点

Hash表中的dictEntry **table中每个指针,就是指向Hash表节点:

// 该结构体一共占用3*8=24字节
typedef struct dictEntry {
    // 键
    void *key;
    //共用体表示值 根据不同场景使用不同字段
    union {
        // 当用于存储整个Redis所有键值对时,使用val表示值
        void *val;
        uint64_t u64;
        // 当用于记录键的过期时间时,使用s64表示过期时间
        int64_t s64;
        double d;
    } v;
    // 指向Hash冲突的元素
    struct dictEntry *next;
} dictEntry;

1.3 字典

// 该结构体一共占用4*8+2*32=96字节
typedef struct dict {
    // 函数指针 每个字典都有对应的操作
    dictType *type;
    // 字典独有字段 配合上面的函数指针进行操作
    void *privdata;
    // 两个Hash表 在Hash扩容缩小时,需要再哈希rehash,这时需要两个哈希表,一般只会使用ht[0]一个哈希表
    dictht ht[2];
    // rehash的标识 默认为-1,表示该字典未进行rehash。进行rehash时,该值表示rehash到了那个索引
    long rehashidx;
    /* 当前运行的迭代器数,当有安全迭代器绑定到该字典时,迭代数+1,暂停rehash操作,迭代结束-1。
    普通的迭代器不会影响该数值*/
    unsigned long iterators;
} dict;

type字段指向dictType结构体,内部一共六个函数指针:

typedef struct dictType {
    // 字典使用的Hash函数
    uint64_t (*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;

2、基本操作

主要也是增删改查操作。

2.1、初始化字典

首先在服务器redis-server启动时,需要一个空字典用于存储整个Redis的键值对:

static dict *dictCreate(dictType *type, void *privDataPtr) {
    // 申请96个字节内存生成一个空字典
    dict *ht = malloc(sizeof(*ht));
    // 结构体初始化赋值
    _dictInit(ht,type,privDataPtr);
    return ht;
}

以下字典初始化赋值操作:

int _dictInit(dict *d, dictType *type,void *privDataPtr){
    /*将一个早已初始化的哈希表重新赋值
    static void _dictReset(dict *ht) {
    ht->table = NULL;
    ht->size = 0;
    ht->sizemask = 0;
    ht->used = 0;
	}
    */
    _dictReset(&d->ht[0]);
    _dictReset(&d->ht[1]);
    d->type = type;
    d->privdata = privDataPtr;
    d->rehashidx = -1;
    d->iterators = 0;
    return DICT_OK;
}

2.2、添加元素

当我们使用redis的客户端,执行set key value的时候,就是在向数据库字典里面尝试添加一对键值对,此时redis服务端会判断key是否存在,如果存在则修改value,否则就新增key和value。

void setKey(redisDb *db, robj *key, robj *val) {
    // 如果没有找到key,就新增
    if (lookupKeyWrite(db,key) == NULL) {
        dbAdd(db,key,val);
    } else {
        // 找到了key,即修改
        dbOverwrite(db,key,val);
    }
    
    incrRefCount(val);
    removeExpire(db,key);
    signalModifiedKey(db,key);
}

2.2.1 查找key

robj *lookupKeyWrite(redisDb *db, robj *key) {
    // 先检查key是否过期
    expireIfNeeded(db,key);
    // 查找key
    return lookupKey(db,key,LOOKUP_NONE);
}

robj *lookupKey(redisDb *db, robj *key, int flags) {
    // 真正查找key的函数
    dictEntry *de = dictFind(db->dict,key->ptr);
    if (de) {
        robj *val = dictGetVal(de);
        if (server.rdb_child_pid == -1 &&
            server.aof_child_pid == -1 &&
            !(flags & LOOKUP_NOTOUCH))
        {
            if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
                updateLFU(val);
            } else {
                val->lru = LRU_CLOCK();
            }
        }
        return val;
    } else {
        return NULL;
    }
}

static dictEntry *dictFind(dict *ht, const void *key) {
    dictEntry *he;
    unsigned int h;
    // 哈希表长度为0 不用查找 肯定没有key 直接返回NULL
    if (ht->size == 0) return NULL;
    // 取得该key对应的索引
    h = dictHashKey(ht, key) & ht->sizemask;
    // 根据索引拿到哈希表节点
    he = ht->table[h];
    // 当取得的哈希表节点不为NULL时
    while(he) {
        // 比较两个键是否相同
        if (dictCompareHashKeys(ht, key, he->key))
            return he;
        // 如果不同 继续往后查找next节点
        he = he->next;
    }
    return NULL;
}

2.2.2 、添加键值对

当客户端输入的key为新key时,添加键值对。

void dbAdd(redisDb *db, robj *key, robj *val) {
    // 赋值key字符串
    sds copy = sdsdup(key->ptr);
    // 真正的添加函数
    int retval = dictAdd(db->dict, copy, val);
    ……
}
// 新增键值对
int dictAdd(dict *d, void *key, void *val)
{
    // 首先添加键,若字典已存在该键返回NULL,否则添加至新节点并返回新节点
    dictEntry *entry = dictAddRaw(d,key,NULL);
	// 键存在 则返回错误 因为新增操作的key应该是不存在于字典中的
    if (!entry) return DICT_ERR;
    // 往节点中添加值
    dictSetVal(d, entry, val);
    return DICT_OK;
}

// dictAddRaw函数 添加键并返回新的节点
dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing)
{
    long index;
    dictEntry *entry;
    dictht *ht;
	// 判断字典是否在处于再哈希状态 是则执行一次再哈希
    if (dictIsRehashing(d)) _dictRehashStep(d);
	// _dictKeyIndex函数找到键返回-1,把原节点存至existing,否则返回新节点索引,也可能会触发哈希表的扩容
    if ((index = _dictKeyIndex(d, key, dictHashKey(d,key), existing)) == -1)
        return NULL;

    // 如果字典处于再哈希状态,那么去ht[1],否则还是使用ht[0]
    ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0];
    // 分配内存 插入对应哈希表 设置键和其它相关字段 
    entry = zmalloc(sizeof(*entry));
    entry->next = ht->table[index];
    ht->table[index] = entry;
    ht->used++;

    dictSetKey(d, entry, key);
    return entry;
}

// 根据键查找哈希表索引的_dictKeyIndex函数
static long _dictKeyIndex(dict *d, const void *key, uint64_t hash, dictEntry **existing)
{
    unsigned long idx, table;
    dictEntry *he;
    if (existing) *existing = NULL;

  	// 判断是否需要扩容
    if (_dictExpandIfNeeded(d) == DICT_ERR)
        return -1;
    for (table = 0; table <= 1; table++) {
        // 通过哈希值和掩码获取索引
        idx = hash & d->ht[table].sizemask;
        // 根据索引获取哈希表节点指针
        he = d->ht[table].table[idx];
        // 如果节点存在继续判断
        while(he) {
            // 判断找到的key和新key或者两个key的哈希值是否相等,
            if (key==he->key || dictCompareKeys(d, key, he->key)) {
                // 如果相等,说明新插入的key已存在吗,返回-1,否则继续往下比较next
                if (existing) *existing = he;
                return -1;
            }
            he = he->next;
        }
        // 如果未进行rehash,那么不需要查找ht[1],直接返回NULL
        if (!dictIsRehashing(d)) break;
    }
    return idx;
}

2.2.3、字典的扩容

当添加新键的时候,可能会触发扩容:

int dictExpand(dict *d, unsigned long size)
{
    // 如果字典处于再哈希状态,或者ht[0]已使用长度大于size,直接返回错误
    if (dictIsRehashing(d) || d->ht[0].used > size)
        return DICT_ERR;
	// 新的哈希表
    dictht n; 
    // 计算新哈希表的长度,必须为2的整数次幂,如果是从0扩容,默认长度为4,否则长度会翻倍
    unsigned long realsize = _dictNextPower(size);
	// 如果新哈希表的长度和原表长度一样 则返回错误
    if (realsize == d->ht[0].size) return DICT_ERR;

    // 分配一个新的哈希表 并初始化各字段
    n.size = realsize;
    n.sizemask = realsize-1;
    n.table = zcalloc(realsize*sizeof(dictEntry*));
    n.used = 0;

    // 如果是第一次扩容,即从0到默认的4,那么直接将ht[0]指向新哈希表
    if (d->ht[0].table == NULL) {
        d->ht[0] = n;
        return DICT_OK;
    }

   // 将ht[1]指向新哈希表,并设置rehashidx = 0,表示需要再哈希
    d->ht[1] = n;
    d->rehashidx = 0;
    return DICT_OK;
}

扩容后需要注意,字典的size和sizemask发生了变化,由于key的索引是通过Hash值和掩码按位与得到的,所以扩容后需要重新计算索引,这一过程就是rehash。

2.3.4、 rehash

准确来说,只要哈希表的容量发生了变化,那么都需要rehash。Redis的rehash主要分为三步:

  1. 为ht[1] 申请空间,当扩容时容量为ht[0]的2倍,当容量利用率不足10%时,将容量缩小为最接近当前已用容量的2的整数次幂,也就是说不论扩容缩容,都要保证哈希表容量的大小一定要是2的整数次幂。申请完空间后,将字典中的rehashidx改为0,表示开始rehash。
  2. 调用dictRehash函数,重新计算ht[0]中每个键的Hash值与索引值,然后依次添加到ht[1]中并删除原键值对。字典中的字段rehashidx改为ht[0]当前正在进行rehash操作的节点的索引值。
  3. rehash操作完成后,清空ht[0],然后调换ht[0]和ht[1],这样ht[0]就是新的完整的哈希表,最后把rehashidx字段改回-1,表示当前没有rehash操作。

Redis作为一款高性能的单进程数据库,需要提供快速的存取服务,当数据库中的键值对数量达到很高的数量级时,直接进行rehash会十分耗费时间,会导致严重的服务不可用。Redis对此做出了自己的优化,具体操作如下:

在执行正删改查操作之前,判断字典是否在rehash,如果在进行则调用dictRehashStep函数进行rehash,每次调用只会对1个节点操作1次。当服务空闲时,若字典也需rehash操作,那么会调用incrementallyRehash函数批量进行rehash操作,每次对100个节点操作,耗时1毫秒。所以,整个rehash操作被分配到多次操作中,对于客户端来说几乎无感知。

2.3、查找元素

命令:get key

根据客户端给出的键查找值。

dictEntry *dictFind(dict *d, const void *key)
{
    dictEntry *he;
    uint64_t h, idx, table;
	// 如果整个字典还是空的,直接返回NULL
    if (d->ht[0].used + d->ht[1].used == 0) return NULL; 
    // 如果字典需要rehash,那么执行1次rehash
    if (dictIsRehashing(d)) _dictRehashStep(d);
    // 计算出key的Hash值
    h = dictHashKey(d, key);
    // 遍历两个哈希表
    for (table = 0; table <= 1; table++) {
        // Hash值按位与掩码,得到索引
        idx = h & d->ht[table].sizemask;
        // 根据索引得到哈希表节点
        he = d->ht[table].table[idx];
        // 如果哈希表节点存在
        while(he) {
            // 比较两个key的值和其哈希值,只要其中一个相等就返回哈希表节点
            if (key==he->key || dictCompareKeys(d, key, he->key))
                return he;
            // 继续查找next节点
            he = he->next;
        }
        // 如果未进行rehash,那么不需要查找ht[1],直接返回NULL
        if (!dictIsRehashing(d)) return NULL;
    }
    return NULL;
}

2.4、修改元素

修改和新增是同一个函数setKey(redisDb *db, robj *key, robj *val)的不同分支,主要函数为dbOverwrite:

void dbOverwrite(redisDb *db, robj *key, robj *val) {
    // 根据键查找节点
    dictEntry *de = dictFind(db->dict,key->ptr);
	
    // 找不到节点就停止更新操作
    serverAssertWithInfo(NULL,key,de != NULL);
    dictEntry auxentry = *de;
    // 获取原val值
    robj *old = dictGetVal(de);
    if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
        val->lru = old->lru;
    }
    // 将新的val设置到节点中
    dictSetVal(db->dict, de, val);

    // 服务是否配置了异步执行删除操作
    if (server.lazyfree_lazy_server_del) {
        // 异步释放原val的内存
        freeObjAsync(old);
        dictSetVal(db->dict, &auxentry, NULL);
    }

    // 释放原val的内存
    dictFreeVal(db->dict, &auxentry);
}

2.5、删除元素

int dictDelete(dict *ht, const void *key) {
    return dictGenericDelete(ht,key,0) ? DICT_OK : DICT_ERR;
}

static dictEntry *dictGenericDelete(dict *d, const void *key, int nofree) {
    uint64_t h, idx;
    dictEntry *he, *prevHe;
    int table;

    if (d->ht[0].used == 0 && d->ht[1].used == 0) return NULL;

    if (dictIsRehashing(d)) _dictRehashStep(d);
    h = dictHashKey(d, key);
	// 根据key查找节点
    for (table = 0; table <= 1; table++) {
        idx = h & d->ht[table].sizemask;
        he = d->ht[table].table[idx];
        prevHe = NULL;
        // 如果节点存在则进行删除
        while(he) {
            // 通过比较后,删除节点,释放内存,调整链表结构
            if (key==he->key || dictCompareKeys(d, key, he->key)) {
                // 根据要删除的元素位置,调整链表
                if (prevHe)
                    prevHe->next = he->next;
                else
                    d->ht[table].table[idx] = he->next;
                if (!nofree) {
                    dictFreeKey(d, he);
                    dictFreeVal(d, he);
                    zfree(he);
                }
                // 将已使用节点数减一
                d->ht[table].used--;
                return he;
            }
            prevHe = he;
            he = he->next;
        }
        if (!dictIsRehashing(d)) break;
    }
    return NULL;
}

注意,删除可能会触发缩容操作,缩容其实调用的是扩容使用的dictExpand(dict *d, unsigned long size)函数,只不过将size变小了。

3、字典的遍历

举例Redis两种遍历的命令:keys和hscan。前者会一次性遍历整个数据库;而后者每次执行只取部分数据,分多次遍历。

3.1、迭代器遍历

迭代器(iterator)有时又称光标(cursor)是程序设计的软件设计模式,可在容器对象(container,例如链表或数组)上遍访的接口,设计人员无需关心容器对象的内存分配的实现细节。

在Redis字典的迭代过程中,如果出现rehash操作,由于数据会先拷贝到另一个哈希表中,那么可能导致同一条数据被多次遍历,想要明白Redis如何解决这个问题,还需要看一下迭代器的实现。

// 整个结构体大小为:5*8+2*4=48字节
typedef struct dictIterator {
    // 迭代的字典
    dict *d;
    // 迭代到的哈希表索引
    long index;
    // table指当前正在迭代的哈希表ht[0]或ht[1],safe表示是否为安全迭代器
    int table, safe;
    // 当前哈希表节点和接下来的哈希表节点
    dictEntry *entry, *nextEntry;
    // 字典的指纹,当字典发生变化时该值也会发生变化
    long long fingerprint;
} dictIterator;

Redis是单进程单线程模式,所以如果执行遍历命令时不修改数据,就不会触发rehash,迭代器根据safe字段分为两种:0表示普通迭代器,1表示安全迭代器。普通迭代器只进行遍历,而安全迭代器遍历数据的同时还可以删除数据。

3.1.1、 普通迭代器

普通迭代器会对字典指纹字段fingerprint,做严格的校验以确保迭代过程中字典结构没有发生变化,读取出的数据也不会重复。

  1. 使用dictGetIterator函数初始化一个普通迭代器:

    dictIterator *dictGetIterator(dict *d)
    {
        dictIterator *iter = zmalloc(sizeof(*iter));
    
        iter->d = d;
        iter->table = 0;
        iter->index = -1;
        // safe = 0表示这是一个普通迭代器
        iter->safe = 0;
        iter->entry = NULL;
        iter->nextEntry = NULL;
        return iter;
    }
    
  2. 循环调用dictNext函数遍历哈希表节点:

    dictEntry *dictNext(dictIterator *iter)
    {
        while (1) {
            // 迭代器entry节点为NULL 取当前哈希表
            if (iter->entry == NULL) {
                dictht *ht = &iter->d->ht[iter->table];
                // 当还迭代索引为-1且迭代的哈希表为ht[0],即第一次开始迭代时
                if (iter->index == -1 && iter->table == 0) {
                    // 如果是安全迭代器,则将字典的iterators字段+1,这样字典会暂停rehash
                    if (iter->safe)
                        iter->d->iterators++;
                    // 如果是普通迭代器,则计算字典的指纹并赋值给fingerprint字段
                    else
                        iter->fingerprint = dictFingerprint(iter->d);
                }
                // 迭代索引+1
                iter->index++;
                // 判断当前哈希表是否迭代到最后
                if (iter->index >= (long) ht->size) {
                    /* 字典正在rehash且迭代的是ht[0],那么开始迭代ht[1]并将迭代索引置为0,否则说明
                    整个字典迭代完毕,结束循环*/
                    if (dictIsRehashing(iter->d) && iter->table == 0) {
                        iter->table++;
                        iter->index = 0;
                        ht = &iter->d->ht[1];
                    } else {
                        break;
                    }
                }
                // 根据索引取出节点
                iter->entry = ht->table[iter->index];
            } else {
                // 如果entry不为NULL,那么取出单链表的下一个节点
                iter->entry = iter->nextEntry;
            }
            // 如果entry不为NULL,nextEntry指向单链表下一个节点,返回当前节点
            if (iter->entry) {
                iter->nextEntry = iter->entry->next;
                return iter->entry;
            }
        }
        return NULL;
    }
    
  3. 利用dictNext函数遍历完,释放迭代器:

    void dictReleaseIterator(dictIterator *iter)
    {
        // 如果迭代器索引不为-1或者迭代器的table不为0
        if (!(iter->index == -1 && iter->table == 0)) {
            // 如果是安全迭代器,就将字典的iterators字段-1,这样可以继续进行rehash
            if (iter->safe)
                iter->d->iterators--;
            else
                /*对于普通迭代器,重新计算字典指纹并与第一次迭代时计算的字典指纹比较。
                如果两个指纹相等,即字典在迭代过程中发生了其它操作,就输出异常并退出程序。*/
                assert(iter->fingerprint == dictFingerprint(iter->d));
        }
        zfree(iter);
    }
    

3.1.2、安全迭代器

安全迭代器的原理和普通迭代器类似,在普通迭代器中已经介绍过,是通过限制字典的rehash操作保证数据的准确。rehash的相关函数如:

static void _dictRehashStep(dict *d) {
    // 只有当前绑定的迭代器为普通迭代器才进行rehash
    if (d->iterators == 0) dictRehash(d,1);
}

所以当字典被安全迭代器遍历时,并不会进行rehash。

3.2、间断遍历

迭代器是完全遍历,当数据量很大时,耗时较长,导致短时间内Redis不可用。Redis另外提供了一个scan操作,即间断遍历。字典的间断遍历由dictScan函数实现:

/*d:迭代的字典 v:迭代开始的游标,即哈希表数组的索引 fn:函数指针,每个遍历到的哈希表节点都要用这个函数处理
 bucketfn:函数指针,整理碎片时使用 private:回调函数fn所需的参数*/
unsigned long dictScan(dict *d,
                       unsigned long v,
                       dictScanFunction *fn,
                       dictScanBucketFunction* bucketfn,
                       void *privdata)
{
    dictht *t0, *t1;
    const dictEntry *de, *next;
    unsigned long m0, m1;

    if (dictSize(d) == 0) return 0;

    if (!dictIsRehashing(d)) {
        t0 = &(d->ht[0]);
        m0 = t0->sizemask;

        /* Emit entries at cursor */
        if (bucketfn) bucketfn(privdata, &t0->table[v & m0]);
        de = t0->table[v & m0];
        while (de) {
            next = de->next;
            fn(privdata, de);
            de = next;
        }

        /* Set unmasked bits so incrementing the reversed cursor
         * operates on the masked bits */
        v |= ~m0;

        /* Increment the reverse cursor */
        v = rev(v);
        v++;
        v = rev(v);

    } else {
        t0 = &d->ht[0];
        t1 = &d->ht[1];

        /* Make sure t0 is the smaller and t1 is the bigger table */
        if (t0->size > t1->size) {
            t0 = &d->ht[1];
            t1 = &d->ht[0];
        }

        m0 = t0->sizemask;
        m1 = t1->sizemask;

        /* Emit entries at cursor */
        if (bucketfn) bucketfn(privdata, &t0->table[v & m0]);
        de = t0->table[v & m0];
        while (de) {
            next = de->next;
            fn(privdata, de);
            de = next;
        }

        /* Iterate over indices in larger table that are the expansion
         * of the index pointed to by the cursor in the smaller table */
        do {
            /* Emit entries at cursor */
            if (bucketfn) bucketfn(privdata, &t1->table[v & m1]);
            de = t1->table[v & m1];
            while (de) {
                next = de->next;
                fn(privdata, de);
                de = next;
            }

            /* Increment the reverse cursor not covered by the smaller mask.*/
            v |= ~m1;
            v = rev(v);
            v++;
            v = rev(v);

            /* Continue while bits covered by mask difference is non-zero */
        } while (v & (m0 ^ m1));
    }

    return v;
}
  • 本文作者: fanchw
  • 本文链接: https://www.fanchw.xyz/archives/redis5数据结构-字典dict
  • 版权声明: 本博客所有文章除特别声明外,均采用CC BY-NC-SA 3.0 许可协议。转载请注明出处!
# CAP # 分布式 # 计算机网络 # MySQL # 源码 # 备份 # Redis
Redis5数据结构-跳跃表
MySQL-字符集和比较规则
  • 文章目录
  • 站点概览
fanchw

fanchw

11 日志
5 分类
7 标签
Creative Commons
© 2023 fanchw
由 Halo 强力驱动
|
主题 - NexT.Pisces v5.1.4
皖ICP备19014634号-1

皖公网安备 34180202000448号