Redis 字典 dict 用途有两个,首先实现数据库键空间(Key space),Redis 是一个保存键值对的数据库,数据库的键值对由字典保存,每个数据库都有一个对应的字典, 这个字典被称之为键空间(key space)。
其次字典 dict 还作为 Hash 类型键的底层实现之一,当 Hash 类型的键使用 hashtable 编码时,也使用 dict 进行存储。
Redis 采用链地址法解决 key hash 冲突问题,以头插的方式插入到冲突节点的前面。
单机 Redis 默认有 16 个 redisDb ,通过 select(n) 选择使用哪个 redisDb cluster 模式下只有一个 redisDb,每个 redisDb 里面都有一个字典 dict,用于保存 set 进来的键值对。
typedef struct redisDb { dict *dict; /* The keyspace for this DB */ 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 */ dict *watched_keys; /* WATCHED keys for MULTI/EXEC CAS */ int id; /* Database ID */ long long avg_ttl; /* Average TTL, just for stats */ list *defrag_later; /* List of key names to attempt to defrag one by one, gradually. */ } redisDb;
字典 dict 里面包含两个 dictht,ht[0],ht[1],ht[1] 用于渐进式 rehash;
typedef struct dict { dictType *type; // 字典类型 void *privdata; // 私有数据 dictht ht[2]; // 哈希表[两个] long rehashidx; // 记录rehash 进度的标志,值为-1表示rehash未进行 unsigned long iterators; // 当前正在迭代的迭代器数 } 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;
哈希表 dictht 里面包含一个 table,里面的节点就是哈希表节点 dicEntry,dictEntry 里面保存着元素的地址 *val,和指向下一个 dictEntry 的指针,这个指针将哈希值相同的键值对连接在一起。
typedef struct dictht { dictEntry **table; // 哈希表数组 unsigned long size; // 哈希表的大小 unsigned long sizemask; // 哈希表大小掩码 unsigned long used; // 哈希表现有节点的数量 } dictht; typedef struct dictEntry { void *key; // 键定义 // 值定义 union { void *val; // 自定义类型 uint64_t u64; // 无符号整形 int64_t s64; // 有符号整形 double d; // 浮点型 } v; struct dictEntry *next; //指向下一个哈希表节点 } dictEntry;
所以 Redis Key 的分布,可以用下图表示:
当哈希表 dict 需要扩展或者收缩时,Redis 会进行 rehash 操作,即将 h[0] 里面的元素移到 h[1]
rehash 的过程不是一次性的,而是将 rehash 所需的计算工作分摊到每次对字典进行增删改查的操作上。
字典维护 rehashidx 字段,从 0 开始,每次将 rehashidx 上面的键值对 rehash 到 ht[1],然后 rehashidx++,最终 ht[0] 所有键值对移到 ht[1],那么 rehashidx = -1,表示 rehash 完成。
渐进式 rehash 过程中,Redis 会同时使用 ht[0] 和 ht[1] 两张表,delete,find,update 操作会在两个哈希表上运行。rehash 过程中,新增的键值对保存到 ht[1],保证 rehash 执行完成时,ht[0] 为空表,然后将 ht[1] = ht[0]。
负载因子 = 哈希表已保存节点数量 / 哈希表大小
load_factor = ht[0].used / ht[0].size
当服务器没有执行 BGSAVE or BGREWRITEAOF 命令时,若负载因子 >=1,哈希表将会执行扩展操作;
当服务器 执行 BGSAVE or BGREWRITEAOF 命令时,若负载因子 >= 5,哈希表将会执行扩展操作。