redis数据结构-字典

1、字典数据结构

1.1 哈希表数据结构
typedef struct dictht { //哈希表
    dictEntry **table;  //存放一个数组的地址,数组存放着哈希表节点dictEntry的地址。
    unsigned long size; //哈希表table的大小
    unsigned long sizemask; //用于将哈希值映射到table的位置索引。它的值总是等于(size-1)。
    unsigned long used; //记录哈希表已有的节点(键值对)数量。
} dictht;
  • hash函数可以将任意字符串转换成整型数,使其可以当作数组下标使用
  • table ,是存放节点指针的数组。如dictEntry [sizemask],存放的是最后一个节点的地址,通过next地址构成单链表解决哈希冲突
1.2 字典数据结构
typedef struct dict {
    dictType *type; //指向dictType结构,dictType结构中包含自定义的函数,这些函数使得key和value能够存储任何类型的数据。
    void *privdata; //私有数据,保存着dictType结构中函数的参数。
    dictht ht[2];       //两张哈希表。
    long rehashidx; //rehash的标记,rehashidx==-1,表示没在进行rehash
    int iterators; //正在迭代的迭代器数量
} dict;
  • ht包含两张哈希表,正常状态下,数据存储在ht[0]中,当发生扩缩容时,使用ht[1]过渡数据,扩缩容结束后,ht[1]恢复到初始状态。
  • rehashidx默认值为-1,当发生rehash,即在扩缩容时,需要将ht[0]数据迁移到ht[1]数据时,rehashidx值为索引值

2、字典扩缩容

2.1 扩缩容条件
  • 已使用节点数和字典大小之间的比率超过 dict_force_resize_ratio,默认值为5,则进行扩容
  • 已使用节点数和字典大小之间的比率小于 HASHTABLE_MIN_FILL,默认值为10(%),则进行缩容
  • 扩容,ht[1]的大小为第一个大于等于ht[0].used* 2的2^n; 缩容,ht[1]的大小为第一个大于等于ht[0].used的 2^n
2.2 扩容流程
  • 申请一块新内存,初次申请时默认容量大小为4个dictEntry;非初次申请时,申请内存的大小则为当前Hash表容量的一倍。
  • 把新申请的内存地址赋值给ht[1],并把字典的rehashidx标识由-1改为0,表示之后需要进行rehash操作
2.3 渐进式rehash
  • 重新计算ht[0]中每个键的Hash值与索引值(重新计算就叫rehash),依次添加到新的Hash表ht[1],并把老Hash表中该键值对删除。把字典中字段rehashidx字段修改为Hash表ht[0]中正在进行rehash操作节点的索引值。
  • rehash操作后,清空 ht[0],然后对调一下ht[1]与ht[0]的值,并把字典中rehashidx字段标识为-1。
2.4 渐进式rehash场景
  • 执行插入、删除、查找、修改等操作前,都先判断当前字典rehash操作是否在进行中,进行中则进行rehash操作(每次只对1个节点进行rehash操作,共执行1次)
  • 当服务空闲时,如果当前字典也需要进行rehsh操作,则会进行批量rehash操作(每次对100个节点进行rehash操作,共执行1毫秒)。在经历N次rehash操作后,整个ht[0]的数据都会迁移到ht[1]中,这样做的好处就把是本应集中处理的时间分散到了上百万、千万、亿次操作中,所以其耗时可忽略不计。

3、迭代器

3.1 迭代器数据结构

迭代器主要用于遍历字典,遍历数据的原则为:不重复出现数据、不遗漏任何数据

typedef struct dictIterator {
    dict *d;                    //被迭代的字典
    long index;                 //迭代器当前所指向的哈希表索引位置
    int table, safe;            //table表示正迭代的哈希表号码,ht[0]或ht[1]。safe表示这个迭代器是否安全。
    dictEntry *entry, *nextEntry;   //entry指向当前迭代的哈希表节点,nextEntry则指向当前节点的下一个节点。
    /* unsafe iterator fingerprint for misuse detection. */
    long long fingerprint;      //避免不安全迭代器的指纹标记
} dictIterator;
3.2 普通迭代器
  • 迭代器初始化,safe字段为0
  • fingerprint用来表示字典的指纹,通过对字典ht[0]、ht[1]两张哈希的地址、大小、已用大小进行指纹计算。在迭代器第一次迭代和释放时,会计算这两个状态时的指纹是否一致,不一致,会抛出异常
  • 普通迭代器,只遍历数据,不允许在遍历的过程中,发生字典的删除、新增操作
  • 当发生rehash操作时,普通迭代器可能会出现重复数据
3.3 安全迭代器
  • 迭代器初始化,safe字段为1
  • 安全迭代器开始迭代时,字典的iterators值会+ 1,表示当前安全迭代器数目,当该值不为0时,不允许进行rehash操作。
static void _dictRehashStep(dict *d) {
    if (d->iterators == 0) dictRehash(d,1); //没有迭代器,进行1步rehash
}

4、discan间断遍历字典

4.1 索引更新方式

通常情况下,遍历情况下的索引更新方式为从0递增到size-1,可以保证所有数据不会遗漏。但在discan遍历方式中,索引更新方式采用了另外一种策略,从高位开始递增。代码如下,如长度为8的情况,索引顺序为000-100-010-110-001-101-011-111

	v |= ~m1;//m1为哈希表掩码
    v = rev(v);//二进制逆转
    v++;
    v = rev(v);//二进制逆转

	static unsigned long rev(unsigned long v) { //翻转
    unsigned long s = 8 * sizeof(v); // 位数bit size; must be power of 2
    unsigned long mask = ~0;    //s个位全为1
    while ((s >>= 1) > 0) {
        mask ^= (mask << s);
        v = ((v >> s) & mask) | ((v << s) & ~mask);
    }
    return v;
	}
4.2 遍历方式

针对遍历,我们要考虑三种情况,未进行扩缩容,迭代中正在进行rehash,迭代过程中已完成扩缩容。

  1. 未进行扩缩容,则只需按照当前索引更新方式遍历哈希表ht[0]即可。
  2. 迭代过程中已完成扩缩容,即仍只需要遍历哈希表ht[0]即可。实例如:假设hash表大小为4,进行第3次迭代时,hash表扩容到了8。此时我们发现,迭代只进行6次就完成了,顺序为0、2、1、5、3、7,扩容后少遍历了4、6,因为游标为0、2的数据在扩容前已经迭代完,而Hash表大小从4扩容至8,再经过rehash后,游标为0、2的数据可能会分布在0|42|6上,因此扩容后的游标4、6不需要再迭代。
  3. 迭代过程中正在rehash,则需要遍历哈希表ht[0]和ht[1],假设size更小的为t[0],大的为t[1],则根据当前的索引值,遍历t[0]上的数据,还需要遍历t[1]该索引值高位为1的数据。t[1]遍历的 条件为while(v & (m0 ^ m1))。实例如:ht[0]大小为8,ht[1]大小为32,当前索引为011,则在ht[1]需要遍历的位置顺序为00011、10011、01011、11011。高位逐渐+1的过程。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值