Redis入坑(二)字典

515 阅读5分钟

字典又称散列表,Redis这种非关系型数据库,整个数据库的数据结构可以说是一个超级大的字典。

字典常见命令

// 添加 一个字典类型,key:city ,value :{hangzhou:"xihu"}
127.0.0.1:6379> hset city hangzhou "xihu"
(integer) 1
127.0.0.1:6379> hset city chongqing "huoguo"
(integer) 1
127.0.0.1:6379> hget city hangzhou
"xihu"
127.0.0.1:6379> hget city chongqing
"huoguo"
// 获取key的所有字典
127.0.0.1:6379> hgetall city
1) "hangzhou"
2) "xihu"
3) "chongqing"
4) "huoguo"
//  更新操作,返回0
127.0.0.1:6379> hset city hangzhou "dongporou"
(integer) 0
127.0.0.1:6379> hgetall city
1) "hangzhou"
2) "dongporou"
3) "chongqing"
4) "huoguo"
//  批量添加
127.0.0.1:6379> hmset city chengdu "chuanchuan" shanxi "roujiamo"
OK
// 同样字典也支持自增
127.0.0.1:6379> hset age peter 25
(integer) 1
127.0.0.1:6379> hincrby age peter 1
(integer) 26

数据结构

Redis字典数据结构和Java的HashMap数据结构还是有很大相似之处的。(这里不讨论底层用压缩链表情况)

image.png

typedef struct dictht {
    // 指针数组,即上图ditEntry的数组
    dictEntry **table;
    // table数组大小
    unsigned long size;
    // 掩码 ,size-1
    unsigned long sizemask;
    // 已经存在节点数量
    unsigned long used;
} dictht;

上面结构体中的used属性,是已经存在的节点即(数组+链表)。另外used可能会大于size。而sizemask属性是size-1是为了通过位运算高效地获取索引值,(索引值=Hash值&掩码值)。

typedef struct dictEntry {

    // 键
    void *key;
    // 值 是个共用体
    union {
	// 指针指向具体value地址
        void *val;
	// hash值
        uint64_t u64;
	// 过期是时间
        int64_t s64;
        double d;
    } v;
    // 指针指向 链表的下一个元素
    struct dictEntry *next;
} dictEntry;

但是Redis对字典做了一层封装。

image.png

typedef struct dict {
    dictType *type;
    // 字典的私有数据
    void *privdata;
    dictht ht[2];
    // rehash时表示的状态,-1表示完成,0表示开始,每个元素rehash时完成+1
    long rehashidx;
    // 迭代器
    unsigned long iterators;
} dict;
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;

扩容

  1. 申请一块新内存,如果是初次申请默认容量是4,之后都是当前容量的一倍
  2. 新生申请的内存地址会赋值给ht[1]
  3. 把rehashidx的值由-1改为0。表示要开始进行rehash操作了。
static int dictExpand(dict *ht, unsigned long size) {
    // 定义新的字典
    dict n; 
    // 重新计算扩容后的容量
    unsigned long realsize = _dictNextPower(size), i;
    // 如果当前存在元素还是大于扩容后的容量,返回错误状态
    if (ht->used > size)
        return DICT_ERR;

    _dictInit(&n, ht->type, ht->privdata);
    n.size = realsize;
    n.sizemask = realsize-1;
    n.table = calloc(realsize,sizeof(dictEntry*));

    n.used = ht->used;
    for (i = 0; i < ht->size && ht->used > 0; i++) {
        dictEntry *he, *nextHe;

        if (ht->table[i] == NULL) continue;

        /* For each hash entry on this slot... */
        he = ht->table[i];
        while(he) {
            unsigned int h;
            nextHe = he->next;
           // 重新计算元素索引值
            h = dictHashKey(ht, he->key) & n.sizemask;
            // 使用头插法将元素放到新字典中
            he->next = n.table[h];
            n.table[h] = he;
            ht->used--;
            he = nextHe;
        }
    }
    assert(ht->used == 0);
    free(ht->table);
    *ht = n;
    return DICT_OK;
}

缩容

当使用量不到总空间10%时,则进行缩容

void tryResizeHashTables(int dbid) {
    // 字典内存大小
    if (htNeedsResize(server.db[dbid].dict))
        dictResize(server.db[dbid].dict);
    // key的过期时间
    if (htNeedsResize(server.db[dbid].expires))
        dictResize(server.db[dbid].expires);
}

int dictResize(dict *d)
{
    int minimal;

    if (!dict_can_resize || dictIsRehashing(d)) return DICT_ERR;
    minimal = d->ht[0].used;
    if (minimal < DICT_HT_INITIAL_SIZE)
        minimal = DICT_HT_INITIAL_SIZE;
    // 最后调用的还是扩容方法,但是 实际上是缩容
    return dictExpand(d, minimal);
}

渐进式rehash

如果一个很大字典,里面有上百万个key需要扩容,那么一次性把所有元素移到新的字典中。那redis肯定伤不起。

如果服务正在操作时候,只对当前这个key做个rehash操作,将这个key迁移到新的字典中。

static void _dictRehashStep(dict *d) {
    if (d->iterators == 0) dictRehash(d,1);
}
int dictRehash(dict *d, int n) {
    int empty_visits = n*10; 
    if (!dictIsRehashing(d)) return 0;

    while(n-- && d->ht[0].used != 0) {
        dictEntry *de, *nextde;
        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];
        while(de) {
            uint64_t h;

            nextde = de->next
            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;
        // 每迁移一元素到字典中,rehashidx会自增一
        d->rehashidx++;
    }
    if (d->ht[0].used == 0) {
        zfree(d->ht[0].table);
        d->ht[0] = d->ht[1];
        _dictReset(&d->ht[1]);
        // 如果元素全部迁移完成。rehashidx重新赋值为-1
        d->rehashidx = -1;
        return 0;
    }
    return 1;
}

如果服务处于空闲的话,会批量进行rehash操作,每次100个地迁移。

int dictRehashMilliseconds(dict *d, int ms) {
    long long start = timeInMilliseconds();
    int rehashes = 0;

    while(dictRehash(d,100)) {
        rehashes += 100;
        if (timeInMilliseconds()-start > ms) break;
    }
    return rehashes;
}

引用下redis设计与实现中的渐进式rehash过程

image.png

image.png

image.png

............直到全部完成。

普通迭代器

typedef struct dictIterator {
    // 迭代的字典
    dict *d;
    // 当前迭代到hash表那个索引值
    long index;
    // 表示当前正在迭代的那个hash表,是ht[0]还是ht[1]
    // safe 表示当前创建的是否是安全迭代器
    int table, safe;
    // 当前节点 ,下个节点
    dictEntry *entry, *nextEntry;
    // 字典唯一标识,如果字典发生了改变,这个值也会改变
    long long fingerprint;
} dictIterator;
  • 在遍历时会先判断元素是否是字典存储的,在下面两种情况下会使用压缩链表存储
  1. 哈希对象保存的所有键值对的键和值的字符串长度都小于64字节

  2. 哈希对象保存的键值对数量小于512个

image.png

  • 可以看出普通迭代器将 safe设置为0 image.png
  • 遍历元素

image.png 上图中,在首次遍历时会初始化迭代器的fingerprint。在释放迭代器时,会去比较这个属性,如果不一样就会输出异常。另外一点,entry与nextEntry两个指针分别指向Hash冲突后的两个父子节点。如果在安全模式下,删除了entry节点,nextEntry字段可以保证后续迭代数据不丢失。

安全迭代器

  • 安全迭代器和普通迭代器时同一结构体,但是safe=1,看下获取安全迭代器

image.png

  • 遍历元素 和普通迭代器一样 调用dicNext函数。

  • 每次对字典操作都会调用dicRehashStep函数,确保在迭代时不进行rehash

image.png

  • 在释放迭代器时,会把字典在中的iterators字段进行减一操作,确保迭代后的rehash能正常。

字典一些增删查改的源码就没有记录了,大致设计都和java比较类似