Redis 数据结构(三):字典

一、数据结构 🪜

在这里插入图片描述

typedef struct dictEntry {
	// key
	void *key;
	// 这里采用了 union 类型,节省内存
	union {
		void *val;
		uint64_t u64;
		int64_t s64;
		double d;
	} v;
	// 下一个节点, 采用链地址法。针对新节点采用头插法。
	struct dictEntry *next;
} dictEntry;

// 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;

// 每个 hashtable 都会有两个,将旧的复制到新的
type struct dictht {
	dictEntry **table;
	unsigned long size; //hash表的大小
	unsigned long sizemask; //mask计算索引值
	unsigned long used; //表示已经使用的个数
} dictht;

typedef struct dict {
	dictType *type;
	void *privdata;
	// 新的 dictht 和旧的 dictht,一般只会用 0,ht[1]哈希表只会对 ht[0] 哈希表进行 rehash 操作。
	dictht ht[2];
	//如果 rehashindex == -1 表示不会进行 rehash
	long rehashindex;
	unsigned long iterators;
} dict;

二、重点解释 🌈

2.1 dictht 扩容

// 对齐 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;
	}
}

2.2 rehashindex

  • dict 中的 rehashindex 标记是否在进行 rehash,默认为 -1 表示未进行,n 表示当前 dt[0]中第n 个 tb 实例。
// 等于 -1;
#define dictIsRehashing(d) ((d) -> rehashidx != -1)
//不等于 -1;
if(!dictIsRehashing(d)) return 0;

while(n-- && d->ht[0].used != 0) {
	dictEntry *de, *nextde;
	assert(d->ht[0].size > (unsigned long)d -> rehashidex);
	// 空桶的话,就直接跳过,如果超过 10*N 个空桶,此时也先切换,不能长时间占用
	while(d->ht[0].table[d->rehashidx] == NULL) {
		d->rehashidx++;
		if (--empty_visits == 0) return 1;
	}
	// 表明该桶不为空
	de = d->ht[0].table[d->rehashidx];

2.3 哈希算法

在 redis 中 hash 默认使用的是 siphash 算法。计算哈希值和索引值方法如下:

  1. 使用字典设置的哈希函数,计算键key 的哈希值
    hash = dict->type->hashFunction(key);
    
  2. 使用哈希表的 sizemark 属性和第一步得到的哈希值,计算索引值
    //取模运算
    index = hash & dict->ht[x].sizemark;
    

2.3.1

  • 哈希冲突
    redis 解决哈希冲突的方法时链地址法,而且采用的是头插法。
  • 哈希扩容
    采用链地址法来解决冲突,如果数据量较大,大量的冲突会导致某个桶上的链表非常长,不利于数据查询,因此需要根据负载因子来判断当前是否需要扩容,见函数 _dictExpandIfNeeded。其中 resize 还与是否正在进行持久化有关。
  • 缩容
    redis 中进行 resize 的条件是由两处决定的即 htNeedResize 和 dictResize。
    1. 超过了初始值且填充率小于 10%,这说明需要扩容。
      int htNeedsResize(dict *dict){
      	long long size,used;
      	size = dictSlots(dict);
      	used = dictSize(dict);
      	return (size > DICT_HT_INITIAL_SIZE && (used*100/size < HASHTABLE_MIN_FILL));
      }
      
      发生的时机主要是在每次数据移除和 serverCron 定时任务中:
      intsetTYpeRemove(robj *setobj,sds value){
      	long long llval;
      	if(setobj-encoding == OBJ_ENCODING_HT){
      		if(dictDelete(setobj->ptr.value) == DICT_OK) {
      			if(htNeedsResize(setobj->ptr)) dictResize(setobj->ptr);
      			return 1;
      		}
      	}	
      }
      

2.3.2 rehash 优化

  • rehash过程,ht[0中会存在空节点,为了防止阻塞,每次限定如果连续访问 10*N(N表示步长)个空节点后,直接返回。见dictRehash函数
  • 渐进式hash。是指 rehash 操作不是一次性、集中式完成的。
    • 第一类:正常操作见dictRehash函数,但是对于 Redis 而言,如果 Hash 表的 key 太多,这样可能导致 rehash 操作需要长时间进行,阻塞服务器,所以 Redis 本身将 rehash 操作分散在了后续的每次增删改查中(以桶为单位)。
    • 第二类: 针对第一类间接式 rehash,存在一个问题:如果 A 服务器长时间处于空闲状态,导致哈希表长期使用 0 和 1 号两个表。为解决这个问题,在 serverCron 定时函数中,每次拿出 1ms 时间来执行 Rehash 操作,每次步长 100,但需要开启 activerehashing。

2.3.3 rehash 注意点

  1. rehash 过程 dictAdd,只插入 ht[1]中,确保 ht[0]只减不增。
  2. rehash 过程 dictFind 和 dictDelete,需要涉及到 0 和 1 两个表。
  3. 字典默认的 hash 算法是 SipHash。
  4. Redis 在持久化时,服务器执行扩展操作所需要的负载因子并不相同,默认为 5.
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值