字典
字典在Redis中主要用在数据库和哈希键功能上,其他也有。
当一个哈希键包含的键值对比较多,又或者键值对中的元素都是比较长的字符串时,Redis就会使用字典作为哈希键的底层实现。(《Redis设计与实现》)
特点
来之《Redis设计与实现》
- Redis中的字典使用哈希表作为底层实现,每个字典带有两个哈希表,一个平时使用,另一个仅在进行rehash时使用。
- 当字典被用作数据库的底层实现,或者哈希键的底层实现时,Redis使用MurmurHash2算法来计算键的哈希值。
- 哈希表使用链地址法来解决键冲突,被分配到同一个索引上的多个键值对会连接成一个单向链表。
- 在对哈希表进行扩展或者收缩操作时,程序需要将现有哈希表包含的所有键值对rehash到新哈希表里面,并且这个rehash过程并不是一次性地完成的,而是渐进式地完成的。
rehash(重新散列)
来之《Redis设计与实现》
rehash的原因
随着操作的不断执行,哈希表保存的键值对会逐渐地增多或者减少,为了让哈希表的负载因子(loadfactor)维持在一个合理的范围之内,当哈希表保存的键值对数量太多或者太少时,程序需要对哈希表的大小进行相应的扩展或者收缩。
哈希表的扩展/收缩条件
哈希表负载因子计算公式:
负载因子 = 哈希表已经保存节点数量/哈希表大小
# load_factor = ht[0].used / ht[0].size
扩展
当以下条件任意一个被满足,Redis会自动对哈希表进行扩展操作:
- 服务器目前没有在执行
BGSAVE
命令或者BGREWRITEAOF
命令,并且哈希表的负载因子大于等于1。 - 服务器目前正在执行
BGSAVE
命令或者BGREWRITEAOF
命令,并且哈希表的负载因子大于等于5。
在BGSAVE
命令或者BGREWRITEAOF
期间,提高扩展的负载因子是因为,在这两个命令期间,过程中,Redis需要创建当前服务器进程的子进程,而大多数操作系统都采用写时复制(copy-on-write)技术来优化子进程的使用效率,提高扩展的负载因子可以可以避免不必要的内存写入操作,最大限度地节约内存。
收缩
当哈希表的负载因子小于0.1
时,Redis会自动对哈希表进行收缩操作。
rehash的步骤
-
为字典的ht[1]哈希表分配空间,这个哈希表的空间大小取决于要执行的操作,以及ht[0]当前包含的键值对数量(也即是ht[0].used属性的值):
- 如果执行的是扩展操作,那么ht[1]的大小为第一个大于等于ht[0].used*2的2^n(2的n次方幂);
- 如果执行的是收缩操作,那么ht[1]的大小为第一个大于等于ht[0].used的2^n。
-
将保存在ht[0]中的所有键值对rehash到ht[1]上面:rehash指的是重新计算键的哈希值和索引值,然后将键值对放置到ht[1]哈希表的指定位置上。
-
当ht[0]包含的所有键值对都迁移到了ht[1]之后(ht[0]变为空表),释放ht[0],将ht[1]设置为ht[0],并在ht[1]新创建一个空白哈希表,为下一次rehash做准备。
渐进式rehash
来之《Redis设计与实现》
rehash的动作并不是一次性,集中式地完成的,而是分多次、渐进式地完成。
原因
如果哈希表中保存的键值对数量太大,要一次性将ht[0]
中的键值对全部rehash到ht[1]
的话,可能会导致服务器在一段时间内停止服务。
步骤
- 为ht[1]分配空间,让字典同时持有ht[0]和ht[1]两个哈希表。
- 在字典中维持一个索引计数器变量rehashidx,并将它的值设置为0,表示rehash工作正式开始。
- 在rehash进行期间,每次对字典执行添加、删除、查找或者更新操作时,程序除了执行指定的操作以外,还会顺带将ht[0]哈希表在rehashidx索引上的所有键值对rehash到ht[1],当rehash工作完成之后,程序将rehashidx属性的值增一。
- 随着字典操作的不断执行,最终在某个时间点上,ht[0]的所有键值对都会被rehash至ht[1],这时程序将rehashidx属性的值设为-1,表示rehash操作已完成。
PS: 在渐进rehash期间,字典的删除(delete)、查找(find)、更新(update)等操作,都会现在ht[0]进行,没找到的话再在ht[1]上进行。
PPS: 另外,在渐进式rehash执行期间,新添加到字典的键值对一律会被保存到ht[1]里面,而ht[0]则不再进行任何添加操作,这一措施保证了ht[0]包含的键值对数量会只减不增,并随着rehash操作的执行而最终变成空表。
代码结构
// dict.h
// 哈希表节点
typedef struct dictEntry {
void *key; // 键
// 值
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
// 指向下一个哈希表节点,形成链表
// 用以解决键冲突
struct dictEntry *next;
} dictEntry;
/* This is our hash table structure. Every dictionary has two of this as we
* implement incremental rehashing, for the old to the new table. */
// 哈希表的结构定义
typedef struct dictht {
dictEntry **table; // 哈希表的数组
unsigned long size; // 哈希表的大小
unsigned long sizemask; // 哈希表大小掩码,用于计算索引,值为 size-1
unsigned long used; // 哈希表已有节点的数量
} dictht;
// 字典结构
typedef struct dict {
dictType *type; // 类型特定函数,保存了用于操作特定类型键值对的函数
void *privdata; // 私有数据,保存了需要传给类型特定函数的可选参数
dictht ht[2]; // 哈希表,一般情况下字典只是用ht[0],ht[1]只会在进行rehash时使用
long rehashidx; /* rehashing not in progress if rehashidx == -1 */ // 当rehash不在进行时,值为 -1
unsigned long iterators; /* number of iterators currently running */ // 字典上正在执行的迭代器数量
} dict;
// 记录操作字典类型特定函数的结构体
typedef struct dictType {
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;
/* If safe is set to 1 this is a safe iterator, that means, you can call
* dictAdd, dictFind, and other functions against the dictionary even while
* iterating. Otherwise it is a non safe iterator, and only dictNext()
* should be called while iterating. */
// 字典迭代器
typedef struct dictIterator {
dict *d; // 指向指点
long index;
int table, safe; // 标记是否安全迭代,如果是1,则是安全迭代,可以同时执行dictAdd, dictFind等操作
dictEntry *entry, *nextEntry; // 下一个哈希节点的指针
/* unsafe iterator fingerprint for misuse detection. */
long long fingerprint;
} dictIterator;
部分代码解析
-
dict *dictCreate(dictType *type, void *privDataPtr)
初始化创建一个字典,type
为类型操作函数的集合,privDataPtr
为操作函数所需要的参数:// dict.c /* Create a new hash table */ dict *dictCreate(dictType *type, void *privDataPtr) { // 为字典申请内存 dict *d = zmalloc(sizeof(*d)); // 对字典d进行初始化 _dictInit(d,type,privDataPtr); return d; } /* Initialize the hash table */ // 字典初始化函数 int _dictInit(dict *d, dictType *type, void *privDataPtr) { //将两个哈希表的数据都重置为没数据的状态 _dictReset(&d->ht[0]); _dictReset(&d->ht[1]); //对各个属性进行赋值 d->type = type; d->privdata = privDataPtr; d->rehashidx = -1; d->iterators = 0; return DICT_OK; } /* Reset a hash table already initialized with ht_init(). * NOTE: This function should only be called by ht_destroy(). */ // 将重置哈希表的数据 static void _dictReset(dictht *ht) { ht->table = NULL; ht->size = 0; ht->sizemask = 0; ht->used = 0; }
-
int dictAdd(dict *d, void *key, void *val)
将键key
值val
加入到字典d
中:/* Add an element to the target hash table */ // 往字典d中添加键值对key, val,成功返回DICT_OK,失败返回DICT_ERR // 如果key已经存在,将会返回DICT_ERR int dictAdd(dict *d, void *key, void *val) { // 将 key 添加到字典中,并返回对应的哈希表节点 dictEntry *entry = dictAddRaw(d,key,NULL); if (!entry) return DICT_ERR; // 将 val 添加到对应的哈希表节点当中 dictSetVal(d, entry, val); return DICT_OK; } /* Low level add or find: * This function adds the entry but instead of setting a value returns the * dictEntry structure to the user, that will make sure to fill the value * field as he wishes. * * This function is also directly exposed to the user API to be called * mainly in order to store non-pointers inside the hash value, example: * * entry = dictAddRaw(dict,mykey,NULL); * if (entry != NULL) dictSetSignedIntegerVal(entry,1000); * * Return values: * * If key already exists NULL is returned, and "*existing" is populated * with the existing entry if existing is not NULL. * * If key was added, the hash entry is returned to be manipulated by the caller. */ // 底层接口 // 往字典d中添加键值对key, val // 如果返回结果是NULL,表示添加失败;成功添加将会返回对应key的节点 // 如果key已经存在于字典,也会返回NULL,但是会将已经存在的节点写入"*existing"中 dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing) { long index; dictEntry *entry; dictht *ht; // 如果字典正在渐进rehash,执行一步rehash动作 if (dictIsRehashing(d)) _dictRehashStep(d); /* Get the index of the new element, or -1 if * the element already exists. */ // 计算出 key 的 hash值,并获取对应的索引位置 if ((index = _dictKeyIndex(d, key, dictHashKey(d,key), existing)) == -1) return NULL; /* Allocate the memory and store the new entry. * Insert the element in top, with the assumption that in a database * system it is more likely that recently added entries are accessed * more frequently. */ // 如果字典正在渐进 rehash ,则使用ht[1],否则使用 ht[0] ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0]; // 为hash表节点申请内存 entry = zmalloc(sizeof(*entry)); // 将 新hash表节点entry的next指针 指向 hash表中index位置 // 防止键冲突 entry->next = ht->table[index]; // 将 hash表中的index位置 指向 新的hash表节点entry ht->table[index] = entry; // 节点加1 ht->used++; /* Set the hash entry fields. */ // 将 key 设入到 节点entry 中 dictSetKey(d, entry, key); return entry; } /* Returns the index of a free slot that can be populated with * a hash entry for the given 'key'. * If the key already exists, -1 is returned * and the optional output parameter may be filled. * * Note that if we are in the process of rehashing the hash table, the * index is always returned in the context of the second (new) hash table. */ // 通过参数`hash`获取一个索引位置 // 如果key已经存在,将会返回 -1, 同时已经存在的节点写入"*existing"中 static long _dictKeyIndex(dict *d, const void *key, uint64_t hash, dictEntry **existing) { unsigned long idx, table; dictEntry *he; if (existing) *existing = NULL; /* Expand the hash table if needed */ // 通过_dictExpandIfNeeded()做hash表扩展检查 if (_dictExpandIfNeeded(d) == DICT_ERR) return -1; for (table = 0; table <= 1; table++) { // 在两个表ht[0], ht[1]中查找 // 通过hash表大小掩码 和 key的hash值, 计算出索引 idx idx = hash & d->ht[table].sizemask; /* Search if this slot does not already contain the given key */ // 在该索引 idx 下,检查key是否已经存在 he = d->ht[table].table[idx]; while(he) { if (key==he->key || dictCompareKeys(d, key, he->key)) { // 如果key已经存在,返回-1,且将节点写入"*existing"中 if (existing) *existing = he; return -1; } he = he->next; } // 如果不是正在rehash表,是不用检查ht[1]的 if (!dictIsRehashing(d)) break; } return idx; } /* Expand the hash table if needed */ // 如果有需要,对字典进行扩展 static int _dictExpandIfNeeded(dict *d) { /* Incremental rehashing already in progress. Return. */ // 如果已经在rehash了,则直接返回 if (dictIsRehashing(d)) return DICT_OK; /* If the hash table is empty expand it to the initial size. */ // 如果默认hash表 ht[0] 是空的,则扩展为默认大小 if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE); /* If we reached the 1:1 ratio, and we are allowed to resize the hash * table (global setting) or we should avoid it but the ratio between * elements/buckets is over the "safe" threshold, we resize doubling * the number of buckets. */ // 如果哈希表的已用节点数 >= 哈希表的大小,即负载因子大于1, // 并且以下条件任一个为真: // 1) dict_can_resize 为真(应该是系统设置是否允许字典改变大小的标识位) // 2) d->ht[0].used/d->ht[0].size > dict_force_resize_ratio(dict_force_resize_ratio的值为5,表示负载因子大于5) // 则调用dictExpand 函数对hash表进行扩展 if (d->ht[0].used >= d->ht[0].size && (dict_can_resize || d->ht[0].used/d->ht[0].size > dict_force_resize_ratio)) { // // 扩展大小为已使用节点数的2倍 return dictExpand(d, d->ht[0].used*2); } return DICT_OK; }
-
int dictExpand(dict *d, unsigned long size)
扩展或者创建hash表到指定的size
大小:/* Expand or create the hash table */ int dictExpand(dict *d, unsigned long size) { // 新的hash表n dictht n; /* the new hash table */ // 通过_dictNextPower()获取实际的大小,因为参数传入的size可能不是一个2的幂 unsigned long realsize = _dictNextPower(size); /* the size is invalid if it is smaller than the number of * elements already inside the hash table */ // 如果 字典d 正在rehash // 或者 // d->ht[0]已经使用的节点数大于指定的size // 将会返回错误 if (dictIsRehashing(d) || d->ht[0].used > size) return DICT_ERR; /* Rehashing to the same table size is not useful. */ // hash表d->ht[0]的大小已经是将要扩展的实际大小,无需扩展,返回错误 if (realsize == d->ht[0].size) return DICT_ERR; /* Allocate the new hash table and initialize all pointers to NULL */ // 为新的 hash表n 赋值 n.size = realsize; n.sizemask = realsize-1; // 申请相应的内存 n.table = zcalloc(realsize*sizeof(dictEntry*)); n.used = 0; /* Is this the first initialization? If so it's not really a rehashing * we just set the first hash table so that it can accept keys. */ // 如果 hash表d->ht[0].table 是NULL的,直接将d->ht[0]指向 n if (d->ht[0].table == NULL) { d->ht[0] = n; return DICT_OK; } /* Prepare a second hash table for incremental rehashing */ // 将 d->ht[1] 指向 n,并将 d->rehashidx 设置为0,表明 字典d 要rehash d->ht[1] = n; d->rehashidx = 0; return DICT_OK; } /* Our hash table capability is a power of two */ // 计算hash表的实际大小,大小是一个2的幂 static unsigned long _dictNextPower(unsigned long size) { // 默认最小的hash表大小 // DICT_HT_INITIAL_SIZE(4) unsigned long i = DICT_HT_INITIAL_SIZE; // LONG_MAX 是long的最大数 // 如果指定的size >= LONG_MAX // 将会返回 LONG_MAX + 1UL的大小 if (size >= LONG_MAX) return LONG_MAX + 1LU; // 每次 i * 2,直至i刚好大于或等于参数size while(1) { if (i >= size) return i; i *= 2; } }
-
int dictRehash(dict *d, int n)
渐进式rehash函数,d
是要rehash的字典,n
是要执行的步数:/* Performs N steps of incremental rehashing. Returns 1 if there are still * keys to move from the old to the new hash table, otherwise 0 is returned. * * Note that a rehashing step consists in moving a bucket (that may have more * than one key as we use chaining) from the old to the new hash table, however * since part of the hash table may be composed of empty spaces, it is not * guaranteed that this function will rehash even a single bucket, since it * will visit at max N*10 empty buckets in total, otherwise the amount of * work it does would be unbound and the function may block for a long time. */ // 渐进式rehash的步骤,执行 参数n 步, // 返回1表示还有key需要从旧hash表(d->ht[0])移动到新hash表(d->ht[1]) // 返回0表示已经完成 // bucket表示哈希表一个槽位 int dictRehash(dict *d, int n) { // 访问的空bucket的最大数量 int empty_visits = n*10; /* Max number of empty buckets to visit. */ if (!dictIsRehashing(d)) return 0; // 循环,直至n到0,或者哈希表ht[0]中没有节点 while(n-- && d->ht[0].used != 0) { dictEntry *de, *nextde; /* Note that rehashidx can't overflow as we are sure there are more * elements because ht[0].used != 0 */ // 断言判断 哈希表的大小ht[0].size 要大于 rehashidx // rehashidx是用来记录 rehash 在 ht[0] 中,操作到哪个位置 assert(d->ht[0].size > (unsigned long)d->rehashidx); while(d->ht[0].table[d->rehashidx] == NULL) { // 当遇到空的bucket,rehashidx自增1 d->rehashidx++; // 如果 访问空bucket次数empty_visits 达到了0,返回1 // 表示还需要继续rehash if (--empty_visits == 0) return 1; } de = d->ht[0].table[d->rehashidx]; /* Move all the keys in this bucket from the old to the new hash HT */ while(de) { // 当de是不为空的时候,将该bucket下所有的hash节点移到ht[1] uint64_t h; nextde = de->next; /* Get the index in the new hash table */ 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; } // 将 ht[0].table[d->rehashidx] 指向 NULL // 因为如果 ht[0].table[d->rehashidx] 有节点,在上面循环中已将移动到了ht[1] d->ht[0].table[d->rehashidx] = NULL; // rehashidx自增1 d->rehashidx++; } /* Check if we already rehashed the whole table... */ // 检查是否将整个ht[0]的节点转移到了ht[1] // 如果是,则rehash完成,函数返回0 if (d->ht[0].used == 0) { zfree(d->ht[0].table); d->ht[0] = d->ht[1]; _dictReset(&d->ht[1]); d->rehashidx = -1; return 0; } /* More to rehash... */ return 1; }
字典API
参考之《Redis设计与实现》
函数 | 作用 |
---|---|
dict *dictCreate(dictType *type, void *privDataPtr) | 初始化创建一个字典,type 为类型操作函数的集合,privDataPtr 为操作函数所需要的参数 |
int dictExpand(dict *d, unsigned long size) | 扩展或者创建hash表到指定的size 大小 |
int dictAdd(dict *d, void *key, void *val) | 将键key 值val 加入到字典d 中 |
dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing) | 往字典d 中添加键值对key , val ;如果返回结果是NULL ,表示添加失败;成功添加将会返回对应key 的节点;如果key 已经存在于字典,也会返回NULL,但是会将已经存在的节点写入*existing 中 |
dictEntry *dictAddOrFind(dict *d, void *key) | 在字典d 添加或者查找key ,如果已经存在则返回已经存在的节点,如果不存在则创建一个新节点并加入字典d 中 |
int dictReplace(dict *d, void *key, void *val) | 在字典d 中使用值val 添加或者覆盖键key ;如果键key 已经存在则返回0;如果是新加入的键则返回1; |
int dictDelete(dict *d, const void *key) | 在字典d 中删除键key ,如果键存在且删除成功则返回DICT_OK(0) ,如果键不存在则返回DICT_ERR(1) |
dictEntry *dictUnlink(dict *ht, const void *key) | 在字典d 中删除key ,但是不释放该hash节点,而是将其返回。如果key 不存在,则返回NULL |
void dictFreeUnlinkedEntry(dict *d, dictEntry *he) | 释放字典d 中的 已经取消绑定的hash节点he ,调用前应该先调用dictUnlink() |
void dictRelease(dict *d) | 清除并释放字典d |
dictEntry * dictFind(dict *d, const void *key) | 在字典d 中按key 查找节点,并将节点返回 |
void *dictFetchValue(dict *d, const void *key) | 在字典d 中按key 查找节点,并返回节点中的值 |
int dictResize(dict *d) | 调整字典d 的大小,使得负载因子 <= 1 (尽量接近) |
dictIterator *dictGetIterator(dict *d) | 获得字典d 的迭代器 |
dictIterator *dictGetSafeIterator(dict *d) | 获得一个安全的字典d 的迭代器 |
dictEntry *dictNext(dictIterator *iter) | 使用迭代器iter 获取下一个hash节点 |
void dictReleaseIterator(dictIterator *iter) | 释放迭代器iter |
dictEntry *dictGetRandomKey(dict *d) | 随机获取字典d 中的一个节点 |
unsigned int dictGetSomeKeys(dict *d, dictEntry **des, unsigned int count) | 在字典d 中随机获取指定个数count 的节点,并存进**des 中(函数不保证**des 中的节点个数一定满足count ,实际的节点个数是函数的返回结果) |
void dictGetStats(char *buf, size_t bufsize, dict *d) | 调试用,取得dict当前状态,会将结果写入buf 中 |
uint64_t dictGenHashFunction(const void *key, int len) | 计算key 的hash值 |
uint64_t dictGenCaseHashFunction(const unsigned char *buf, int len) | 不区分大小写计算key 的hash值 |
void dictEmpty(dict *d, void(callback)(void*)) | 清空字典d 并在完成后调用回调函数 |
void dictEnableResize(void) | 打开dict调整大小开关,一个全局设置,将全局变量dict_can_resize 设为1 |
void dictDisableResize(void) | 关闭dict调整大小开关,一个全局设置,将全局变量dict_can_resize 设为0 |
int dictRehash(dict *d, int n) | 渐进式rehash函数,对字典d 执行n 步渐进式rehash |
int dictRehashMilliseconds(dict *d, int ms) | 对字典d 执行指定毫秒ms 的rehash |
void dictSetHashFunctionSeed(uint8_t *seed) | 设置hash种子 |
uint8_t *dictGetHashFunctionSeed(void) | 取得hash种子 |
unsigned long dictScan(dict *d, unsigned long v, dictScanFunction *fn, dictScanBucketFunction *bucketfn, void *privdata) | 遍历字典d |
uint64_t dictGetHash(dict *d, const void *key) | 在字典d 中获取/计算key 的hash值 |
dictEntry **dictFindEntryRefByPtrAndHash(dict *d, const void *oldptr, uint64_t hash) | 根据指针*oldptr 和hash值hash 在字典d 中查找hash节点 |