前言
标题字越少,分量越不一样,上篇文章我们已经介绍到了,redis是如何从网络io读取数据到执行命令的过程,今天我们来讲redis第二大基础数据字典
为什么要讲字典?
大家都知道redis 是一个key value, 而大家用得最多的就是set和get操作,那么一个set 操作究竟要经历些什么了,我们来看看。
如果不知道为什么会执行命令请回过头来看这篇文章
redis系列,redis是如何执行命令(一).
struct redisCommand redisCommandTable[] = {
/* Note that we can't flag set as fast, since it may perform an
* implicit DEL of a large key. */
{"set",setCommand,-3,
"write use-memory @string",
0,NULL,1,1,1,0,0,0},
从上篇文章我们知道,redis 经过一些前置判断后,最终会调用setCommand.
上文注释说到,set 并不是一个快速的命令,因为它可能引发删除一个大键,话不多说
我们直接看代码
首先我们来看下整个db的结构
server.h
/* Redis database representation. There are multiple databases identified
* by integers from 0 (the default database) up to the max configured
* database. The database number is the 'id' field in the structure. */
typedef struct redisDb {
// 主要的键值空间,所有的数据都会存在这个dict 里面
dict *dict; /* The keyspace for this DB */
// 键的过期时间,字典的键为键,字典的值为过期事件 UNIX 时间戳
dict *expires; /* Timeout of keys with a timeout set */
//用bl pop 命令会涉及到
dict *blocking_keys; /* Keys with clients waiting for data (BLPOP)*/
dict *ready_keys; /* Blocked keys that received a PUSH */
//跟watch 命令相关的key
dict *watched_keys; /* WATCHED keys for MULTI/EXEC CAS */
//database id
int id; /* Database ID */
//这个在过期那章讲过,会采样统计进来
//平均过期时间
long long avg_ttl; /* Average TTL, just for stats */
//这个之前在过期那章也有讲过,
//当一个expire cycle没有处理完的时候
//会从这个游标位置继续处理
//diction 包含一个entry数组
//entry[expires_cursor]
unsigned long expires_cursor; /* Cursor of the active expire cycle. */
//内存碎片整理相关后续再回过头来看
list *defrag_later; /* List of key names to attempt to defrag one by one, gradually. */
} redisDb;
下面是db的初始化
server.c
//我们的db的初始化
for (j = 0; j < server.dbnum; j++) {
server.db[j].dict = dictCreate(&dbDictType,NULL);
server.db[j].expires = dictCreate(&keyptrDictType,NULL);
server.db[j].expires_cursor = 0;
server.db[j].blocking_keys = dictCreate(&keylistDictType,NULL);
server.db[j].ready_keys = dictCreate(&objectKeyPointerValueDictType,NULL);
server.db[j].watched_keys = dictCreate(&keylistDictType,NULL);
server.db[j].id = j;
server.db[j].avg_ttl = 0;
server.db[j].defrag_later = listCreate();
listSetFreeMethod(server.db[j].defrag_later,(void (*)(void*))sdsfree);
}
set命令的入口
server.c
/* SET key value [NX] [XX] [KEEPTTL] [EX <seconds>] [PX <milliseconds>] */
void setCommand(client *c) {
int j;
robj *expire = NULL;
//默认单位秒
int unit = UNIT_SECONDS;
int flags = OBJ_SET_NO_FLAGS;
//从上篇文章我们知道,argc 是参数个数
// 前两个参数set [key]
for (j = 3; j < c->argc; j++) {
//argv 是robj结构, ptr 指向的是一个char[]
char *a = c->argv[j]->ptr;
robj *next = (j == c->argc-1) ? NULL : c->argv[j+1];
//设置状态位
//nx 和xx 是互斥的,且这里看到是忽略大小写的
//nx not exist的意思, 表示键不存在则写入
if ((a[0] == 'n' || a[0] == 'N') &&
(a[1] == 'x' || a[1] == 'X') && a[2] == '\0' &&
!(flags & OBJ_SET_XX))
{
flags |= OBJ_SET_NX;
}
//xx的逻辑
//xx 就是表示键存在则写入覆盖
else if ((a[0] == 'x' || a[0] == 'X') &&
(a[1] == 'x' || a[1] == 'X') && a[2] == '\0' &&
!(flags & OBJ_SET_NX))
{
flags |= OBJ_SET_XX;
}
//这个意思保持原来键的过期时间
//strcasecmp的是忽略大小写比较
else if (!strcasecmp(c->argv[j]->ptr,"KEEPTTL") &&
!(flags & OBJ_SET_EX) && !(flags & OBJ_SET_PX))
{
flags |= OBJ_SET_KEEPTTL;
}
//ex 是以秒为单位
else if ((a[0] == 'e' || a[0] == 'E') &&
(a[1] == 'x' || a[1] == 'X') && a[2] == '\0' &&
!(flags & OBJ_SET_KEEPTTL) &&
!(flags & OBJ_SET_PX) && next)
{
flags |= OBJ_SET_EX;
unit = UNIT_SECONDS;
expire = next;
//这个j++ 很灵性跳过了,参数值直接指向了下一项参数
j++;
}
//px 是以毫秒为单位,与keepttl 不能同时存在
else if ((a[0] == 'p' || a[0] == 'P') &&
(a[1] == 'x' || a[1] == 'X') && a[2] == '\0' &&
!(flags & OBJ_SET_KEEPTTL) &&
!(flags & OBJ_SET_EX) && next)
{
flags |= OBJ_SET_PX;
unit = UNIT_MILLISECONDS;
expire = next;
j++;
} else {
//走到这里说明了不可解析的参数
addReply(c,shared.syntaxerr);
return;
}
}
//这里是redis key, 做压缩处理,
// 这里埋个坑讲压缩string的结构再回过头来看
c->argv[2] = tryObjectEncoding(c->argv[2]);
setGenericCommand(c,flags,c->argv[1],c->argv[2],expire,unit,NULL,NULL);
}
void setGenericCommand(client *c, int flags, robj *key, robj *val, robj *expire, int unit, robj *ok_reply, robj *abort_reply) {
long long milliseconds = 0; /* initialized to avoid any harmness warning */
//expire 不为空的时候
if (expire) {
// 因为expire 是可以转换成整型,如果转换错误则直接return
// 然后赋值给milliseconds
if (getLongLongFromObjectOrReply(c, expire, &milliseconds, NULL) != C_OK)
return;
// expire 不能为负数
if (milliseconds <= 0) {
addReplyErrorFormat(c,"invalid expire time in %s",c->cmd->name);
return;
}
// 如果是单位seconds 则
if (unit == UNIT_SECONDS) milliseconds *= 1000;
}
// 如果设置了nx 或者xx 那么会先去查询一次key,
// 且key 如果已经过期的情况下,xx是会失败的
// 反之key存在的情况下,nx会失败
if ((flags & OBJ_SET_NX && lookupKeyWrite(c->db,key) != NULL) ||
(flags & OBJ_SET_XX && lookupKeyWrite(c->db,key) == NULL))
{
addReply(c, abort_reply ? abort_reply : shared.null[c->resp]);
return;
}
// 这里开始是真正放入db里面的情况。
// c-db 会在createClient 默认情况为db0,
// 可以用select 来修改db
genericSetKey(c,c->db,key,val,flags & OBJ_SET_KEEPTTL,1);
//每次修改都加1用于rdb 来计数
server.dirty++;
//如果过期时间不为空,则重新设置过期时间
if (expire) setExpire(c,c->db,key,mstime()+milliseconds);
// 这个是跟reids-module相关的事件通知
notifyKeyspaceEvent(NOTIFY_STRING,"set",key,c->db->id);
//过期键的相关通知
if (expire) notifyKeyspaceEvent(NOTIFY_GENERIC,
"expire",key,c->db->id);
addReply(c, ok_reply ? ok_reply : shared.ok);
}
void genericSetKey(client *c, redisDb *db, robj *key, robj *val, int keepttl, int signal) {
//look up key write除了会找这个key 是否在key dictionary
if (lookupKeyWrite(db,key) == NULL) {
//db add
dbAdd(db,key,val);
} else {
//以前存在就覆盖
dbOverwrite(db,key,val);
}
// val的引用+1
incrRefCount(val);
// 如果keepttl 为空,则去除过期时间
//以新的设置为主,这个会与ex px 关键字互斥
// 可以看前面代码就知道
if (!keepttl) removeExpire(db,key);
// 这里跟watched 命令相关,
// 还有跟客户端缓存相关的逻辑
if (signal) signalModifiedKey(c,db,key);
}
/* Add the key to the DB. It's up to the caller to increment the reference
* counter of the value if needed.
*
* The program is aborted if the key already exists. */
void dbAdd(redisDb *db, robj *key, robj *val) {
// 分配一个新的空间,因为旧的空间会被回收
sds copy = sdsdup(key->ptr);
// dic add 一个字典增加一个数据,我们在下面文章开始分析
int retval = dictAdd(db->dict, copy, val);
serverAssertWithInfo(NULL,key,retval == DICT_OK);
if (val->type == OBJ_LIST ||
val->type == OBJ_ZSET ||
val->type == OBJ_STREAM)
//这部分跟后续的数据类型有关,后续回头讲
signalKeyAsReady(db, key);
//跟cluster 相关的
if (server.cluster_enabled) slotToKeyAdd(key->ptr);
}
/* Overwrite an existing key with a new value. Incrementing the reference
* count of the new value is up to the caller.
* This function does not modify the expire time of the existing key.
*
* The program is aborted if the key was not already present. */
void dbOverwrite(redisDb *db, robj *key, robj *val) {
// 找到对应的entry
dictEntry *de = dictFind(db->dict,key->ptr);
// 断言打印堆栈
serverAssertWithInfo(NULL,key,de != NULL);
//这里是值传递哦,意思复制了一份de,
// 不明白要百度下引用和值传递的不同
dictEntry auxentry = *de;
robj *old = dictGetVal(de);
//执行lfu 政策的时候要把之前的的信息传递过来
if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
val->lru = old->lru;
}
//设置新的值
dictSetVal(db->dict, de, val);
//是否设置了惰性删除空间
if (server.lazyfree_lazy_server_del) {
//异步回收旧值空间
freeObjAsync(old);
//这里设置为null ,
// 则下面的方法不会再次执行freeval
dictSetVal(db->dict, &auxentry, NULL);
}
//回收val
dictFreeVal(db->dict, &auxentry);
}
从上面的代码可以看到,redis set命令经过一系列的操作会将数据存入dict 这个struct 里面。所以对于redis 而言其本身就是一个大的字典结构,只是在这个字典结构又扩展其它的数据结构。
为什么不是hashtable 而是dict
因为dict 的概念相对于hashtable而言更为广阔,因为实现字典的方式并不一定是hash,也有可能是数组。
数组实现字典
比如我有一个需求想知道梁山108好汉,每个编号对应的武将那么,因为每个id都是唯一的情况下,我完全可以通过数组索引的方式实现这个的key-value需求,而且时间复杂度为o(1)
前缀树实现字典
比如我们key都是由abcd…组成,且key的长度可控的情况,对于这种需求我们也同样可以用前缀树来完成key-value的需求,而且时间复杂度也是为o(1)
public class TrieTree {
List<String> value;
TrieTree[] trieTrees;
}
以上举出了两个例子来说明,字典不一定需要hash table 来实现,当然在redis里面字典主要结构还是hashtable, 下面我们来讲什么是hashtable
hashtable
hashtable 的主体结构其实也是一个数组,
实现一个hashtable主要有3要素,hash算法,如何解决hash冲突,扩容。
hash 算法
hash算法的目的主要让数据散列起来,这是什么意思了,我们开始说了我们可以用数组去表示梁山108好汉,现在我们问题开始升级了,现在出现了多重宇宙,如果每次多重宇宙都是一起来还好,但现实情况总是随机的,可能来了10000次宋江,而只来了1次林冲,那么如果我们用一个二维数组去表示这些人的话,就出现问题,每次找宋江,最坏情况下我需要遍历10000次。且其它空间也无法得到利用,具体可见下图
那么好的解决方案就是把(宋江)分散开来,那么就需要用到hash算法,常用的hash算法有哪些了,乘法hash,加法hash,混合hash 等等,hash算法必须有两个特点
一, 就是让原本具有顺序性的字符,数字变得更为随机,这样上图那些没有分配的空位就能分配到。再具体代码部分我们会继续去说。
二,计算不能太复杂,比如一些复杂的加密算法就不太适合。因为会耗费大量的计算时间。这里说的太复杂可以理解为cpu 平均计算时间。
hash冲突
hash算法是服务分配我们上图的空位,让我们的空位尽量的均匀,所以即使hash算法足够随机最后也是要按需分配,最后的栏位分配位置等于hashcode对这个数组长度取模。我们对分配同一个bucket上面的行为,称为hash冲突,因为size不可能是一个非常大的数,所以冲突也是常态,产生冲突的解决方案主要有三种
1, 拉链法,redis里面也是使用的拉链法,具体代码我们在下面分析。
大体的方式就是发生冲突时,利用链表的指向性,生成新节点,
拉链法的结构如下:
2, 开放地址法。
开放地址法,数据结构就是一个普通的数组。解决冲突的方式最常用的就是线性探测
关于开放地址法还有二次探测和伪随机探测等等,有其它文章已经有详细讨论这边不继续深入了。
3, rehash法。
使用场景当数组比较稀疏的时候可以使用,就是当冲突的时候就继续hash,直到不冲突才插入。可以想象,在比较稠密的数组里面,可以想象冲突几率会很大,会计算多次。对于插入和查询都是比较大的开销。
总结:
以上三种方式,拉链法是优点是比较多的,好扩容,删除,修改都比较友好,但是会有额外的指针空间消耗,基本场景都适合。
开放地址法适合元素较小的场景,比如java 里面的threadlocal.
缺点是查询效率最坏情况下可能直接到o(n)(如上图5和6连起来,当下次遇到需要插入3的位置的数,直到遍历到6才知道需要扩容,当然扩容策略可以调整,但是这种最情况下在内存节省的情况下无法避免) ,扩容时,内存基本不可复用,需要重新rehash到新的数组。优点是就是一个数组,没有额外的内存花销。
rehash, 就不用多说了,如果rehash的位置都是冲突的情况下,需要进行多次计算。且扩容方面等同开放地址法。
扩容
hash 扩容我们主要以拉链法的背景下来讨论,其余的扩容方式可能会另外写文章来讨论,首先要知道为什么要扩容,扩容的原因是我们的元素存到太过稠密,而导致无可避免的链表长度过长。比如上图举例的宋江的情况,比如我们要存120个元素,而拉链表的竖向长度只有4,那么无可避免会导致横向过长,导致存取速度越来越慢,
避免这种情况,所以我们要把竖向长度变长。但是扩容也不是随便扩容,我们的目的是减少横向的长度,所以我们不能因为扩容又导致横向长度变得更长。所以竖向的长度必须是整数的次方。
拿2的次方举例
可以看到扩容后的元素要么在原来不动,要么跑去了高位,因为2的次数,数的因数组成就是2,根据除法的原理,我们可以把5/4 分解为5/2/2 ,所以对于任何数的2^n 的余数= 2^n+ 2的余数,同理也可以得3的n次幂同样的道理,这样就能保证,扩容的时候以前在不同bucket里面的数,扩容后不会进入到同一个bucket里面去,从而达到扩容的目的
为什么很多开源软件都是用的2的n次方来扩容了,原因是取模操作相对来说比较复杂,取模在计算机里面是一个复杂操作,可以自行百度下,这里不过多去解读,所以对于hash出来的数都是通过一个sizeMask做and操作,sizeMask=size-1,因为是2的次方都是00001000这种,减去1就变成了00000111,与其它数做and操作能达到取模的效果。而且位运算速度显然是要比取模速度快很多
另外以2倍的扩容增长程度也很快了,用3或者4会增加扩容和收容的时间成本,所以选用2倍扩容是一个合适的方案。
对于redis 而言扩容和收容都是一个需要思考比较深的事,为什么了因为redis内部是一个单线程处理,如果扩容和收容花费过多的时间,那么将会给redis 造成大的阻塞,所以redis 采用了一种渐进式的rehash方式来处理
普及完字典的知识后我们开始上讲代码
dict 代码
首先我们看一下redis 定义的结构
//字典的主结构
typedef struct dict {
//字典类型,来适配各种需求
dictType *type;
// privdata 可以用于指向一个方法
// 也可以是一个
void *privdata;
//两个table,主要负责rehash的时候
//字典也能够正常使用
dictht ht[2];
//rehasdidx 可以表示rehash的进度
long rehashidx; /* rehashing not in progress if rehashidx == -1 */
//统计有多少个iterator运行中
unsigned long iterators; /* number of iterators currently running */
} dict;
/* This is our hash table structure. Every dictionary has two of this as we
* implement incremental rehashing, for the old to the new table. */
// 这个hash table的结构,每个字典有两个hashtable,用于rehash
typedef struct dictht {
// 相当于一个entry的指针数组
dictEntry **table;
//entry的指针数组的长度
unsigned long size;
//sizemask = size-1,用于做and操作
//来分配key对应的entry
unsigned long sizemask;
//目前table里面存在元素的个数
unsigned long used;
} dictht;
// dictype 主要定义了一些方法,帮助不同类型
// 的参数可以实现差异化的策略
typedef struct dictType {
// 定义hash 算法,用得比较多hash算法
// siphash,这个主要是字符串的hash
uint64_t (*hashFunction)(const void *key);
// 键重复时执行的方法
void *(*keyDup)(void *privdata, const void *key);
// value重复时执行的方法
void *(*valDup)(void *privdata, const void *obj);
// key 的比较的方法
int (*keyCompare)(void *privdata, const void *key1, const void *key2);
// key 销毁调用
void (*keyDestructor)(void *privdata, void *key);
// value 销毁调用
void (*valDestructor)(void *privdata, void *obj);
} dictType;
typedef struct dictEntry {
//定义一个指针,
//那么key可以是一个整型也可以是一个string
void *key;
//定一个union 可以即为String,也可以是其它
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
//指针指向横向的下一个节点
struct dictEntry *next;
} dictEntry;
具体的结构如图:
dict的初始化:
//初始化字典
dict *dictCreate(dictType *type,
void *privDataPtr)
{
//给字典分配空间
dict *d = zmalloc(sizeof(*d));
//初始化参数
_dictInit(d,type,privDataPtr);
return d;
}
/* Initialize the hash table */
int _dictInit(dict *d, dictType *type,
void *privDataPtr)
{
//初始化hashtable
_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;
}
可以看到dict 初始化的时候,只是对dict整体结构进行了初始化,而dict里面的hashtable并没有急着分配空间
dict的put方法
/* Add an element to the target hash table */
//增加元素到字典
int dictAdd(dict *d, void *key, void *val)
{
dictEntry *entry = dictAddRaw(d,key,NULL);
if (!entry) return DICT_ERR;
//将val 设置到entry
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.
*/
//这是一个low level的api ,当add的时候existing是为空的
//执行覆盖的操作的时候,要先找到对应的entry,然后执行覆盖
dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing)
{
long index;
dictEntry *entry;
dictht *ht;
//判断是否在rehash
if (dictIsRehashing(d)) _dictRehashStep(d);
/* Get the index of the new element, or -1 if
* the element already exists. */
//返回entry数组对应到的index的位置
//dictHashKey是key的hash算法
//-1的时候表示有相同的key存在
//-1 不是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 = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0];
//分配空间
entry = zmalloc(sizeof(*entry));
//将新的entry,做为头节点指向以前的entry
entry->next = ht->table[index];
// 让table[index] 指向新插入的节点
ht->table[index] = entry;
// 使用数+1
ht->used++;
/* Set the hash entry fields. */
//给key 赋值
dictSetKey(d, entry, key);
return entry;
}
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 */
//判断是否需要扩容,要记得初始的时候为0
if (_dictExpandIfNeeded(d) == DICT_ERR)
return -1;
for (table = 0; table <= 1; table++) {
//跟sizemask 做and 操作
//idx 元素所属entry数组的下标
idx = hash & d->ht[table].sizemask;
/* Search if this slot does not already contain the given key */
//通过idx 找到entry
he = d->ht[table].table[idx];
//如果he 不为空则表示出现hash冲突,则需要
while(he) {
// 这里是比较key相等,如果有存在的key则返回-1.
// 然后将让existing 指向he,
// 等待做下一步操作
if (key==he->key || dictCompareKeys(d, key, he->key)) {
if (existing) *existing = he;
return -1;
}
he = he->next;
}
// 可以看到这里如果是正在做rehash
// 则返回的是ht[1]的idx
if (!dictIsRehashing(d)) break;
}
return idx;
}
#define dictIsRehashing(d) ((d)->rehashidx != -1)
put 的操作基本都是key hash->取模->判断空间是否充足->判断key是否已经存在->设值key->设值val。
跟常规hashtable差不多,唯一不同的是设置的会判断是否正在rehash,如果在rehash 就会设值到ht[1]
我们再来看dict的扩容
/* 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. */
//初始化完后第一次插入元素会走到这里
//初始化的第一次插入元素会走到这里
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. */
//dict_can_resize 是一个全局设置,
//当有copy on write事件发生会禁止expand
//当hash表的元素个数大于entry数组长度的时候(前提)
//且能够resize 判断为真
//或者已经超过必须扩容的阈值,默认是元素个数是entry数组
//长度的5倍
if (d->ht[0].used >= d->ht[0].size &&
(dict_can_resize ||
d->ht[0].used/d->ht[0].size > dict_force_resize_ratio))
{
//扩容长度
return dictExpand(d, d->ht[0].used*2);
}
return DICT_OK;
}
* Expand or create the hash table */
/**
* 这个方法仅仅用于分配空间,赋值操作
* 如果dic本身已经有元素是不会
*/
int dictExpand(dict *d, unsigned long size)
{
/* the size is invalid if it is smaller than the number of
* elements already inside the hash table */
//判断hash状态,和size是否合法
if (dictIsRehashing(d) || d->ht[0].used > size)
return DICT_ERR;
dictht n; /* the new hash table */
//size 必须是2的次方
unsigned long realsize = _dictNextPower(size);
/* Rehashing to the same table size is not useful. */
//resize 和现在size相同
if (realsize == d->ht[0].size) return DICT_ERR;
/* Allocate the new hash table and initialize all pointers to NULL */
//开始初始化size
n.size = realsize;
//sizemask 用于做and位运算,等同取模操作
n.sizemask = realsize-1;
//分配空间等于resize*dicEntry的空间
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. */
//如果是初始化,则仅仅对h[0]赋值
if (d->ht[0].table == NULL) {
d->ht[0] = n;
return DICT_OK;
}
/* Prepare a second hash table for incremental rehashing */
// 赋值给ht[1] ,然后dic rehash状态改变
// 不遍历所有元素做rehash操作
d->ht[1] = n;
d->rehashidx = 0;
return DICT_OK;
}
/* Our hash table capability is a power of two */
//保证size 是2的次方
static unsigned long _dictNextPower(unsigned long size)
{
unsigned long i = DICT_HT_INITIAL_SIZE;
if (size >= LONG_MAX) return LONG_MAX + 1LU;
while(1) {
if (i >= size)
return i;
i *= 2;
}
}
可以看到dict的扩容只是空间的扩容,并没有对key做rehash
渐进式rehash
/* This function performs just a step of rehashing, and only if there are
* no safe iterators bound to our hash table. When we have iterators in the
* middle of a rehashing we can't mess with the two hash tables otherwise
* some element can be missed or duplicated.
*
* This function is called by common lookup or update operations in the
* dictionary so that the hash table automatically migrates from H1 to H2
* while it is actively used. */
//如果有遍历器则不做rehash
//可以想象下,遍历器主要扫描
//entry中的其中一个
//这样就会导致扫描出来的数据不准确
//具体iterator 我们在scan
// 这一章节分析
static void _dictRehashStep(dict *d) {
if (d->iterators == 0) dictRehash(d,1);
}
/* 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. */
int dictRehash(dict *d, int n) {
int empty_visits = n*10; /* Max number of empty buckets to visit. */
//只有在rehash才会进入这里
if (!dictIsRehashing(d)) return 0;
//开始遍历旧hashtable里面的元素
//used要大于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 */
assert(d->ht[0].size > (unsigned long)d->rehashidx);
//rehashidx 可以看做rehash的进度
//这里就是开始遍历到不为空的节点
while(d->ht[0].table[d->rehashidx] == NULL) {
d->rehashidx++;
//这里就是控制遍历的次数,跟传进来n有关
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) {
uint64_t h;
nextde = de->next;
/* Get the index in the new hash table */
//需要对新的hashtable 里面重新取模
h = dictHashKey(d, de->key) & d->ht[1].sizemask;
//跟插入流程一样,移入的元素放入头节点
de->next = d->ht[1].table[h];
d->ht[1].table[h] = de;
//used 开始--和++
d->ht[0].used--;
d->ht[1].used++;
de = nextde;
}
//完成后将旧节点设置为null
d->ht[0].table[d->rehashidx] = NULL;
//rehashid 更新,
//下次就会从新的层次继续
//rehash
d->rehashidx++;
}
/* Check if we already rehashed the whole table... */
//当旧的链表全部已经处理完
if (d->ht[0].used == 0) {
//释放内存
zfree(d->ht[0].table);
//ht[0]重新做为主存的hashtable
d->ht[0] = d->ht[1];
//重制ht[1]
_dictReset(&d->ht[1]);
//rehashidx 变为-1, 即当前状态为正常态
d->rehashidx = -1;
return 0;
}
/* More to rehash... */
return 1;
}
可以看到redis 采用这种空间换时间的方式采用渐进式hash的方式,redis为什么要这么做了因为redis是单线程操作,如果一次性rehash的话,当整个hash的元素比较多的时候会对其它客户端命令造成较久的等待,所以redis采用这种方式将rehash的步骤分散到每一次的set或者get操作。
总结:
本节文章通过set命令作为入口,讲到了字典在redis 里面的应用,然后讲解了字典和hashtable的关系,详细的描述了redis是如何来设计这个字典的结构,但是对hash算法还没有提及,但是redis 字典里面用得比较多的hash算法是siphash,其主要也是根据key的特性来设计不同的hash算法,宗旨就是让更多位参与运算,减少hash碰撞。这个也是有一些业界比较公用的算法,后续我们研究深入后再继续讨论。下节我们会再讲解一些和字典相关的操作如scan ,keys, 希望大家能够多关注。