重写Redis字典

转载请注明:http://blog.csdn.net/hel_wor/article/details/50358539

关于Redis的博客和教程网上都很多,比较全面的是黄建宏的Redis设计与实现

看了书,作者是怎么思考的似乎明白了。但抽象出来的结论又好像和实现的方式不同,所以在看了Redis字典的源代码后,我觉得可以试试用Java或者C#来写一写。看懂了书中表达的,但又像实际没看懂,所以决定自己实现一个。

Redis的字典是通过哈希表来实现的,冲突通过链地址法解决,Redis作为内存数据库也就是通过在内存中构建数据结构来保存数据,就如同搭积木一样,从低向上一步一步就把我们想要的样子搭建出来了,当我们打算保存数据到Redis中时,我们传入键和值,对于键,会先通过hash算法计算键的hash值,在Redis中使用的hash算法是MurmurHash2,我们需要一个映射表,把这个hash值和键,值封装成的对象对应起来,或者会有想法对应的时候是否不需要键,直接把hash值和值对应起来就可以了?保存这个键的目的是在有hash冲突时,这个键是作为查找条件用来遍历链表的,如果能保证计算出来的hash值是分布的,那么我们将所有hash值同时除以某个数结果也是相同分布的,而同时除以某个数可以保证我们得到的hash值与未除前的哈希值有相同的效果并且已经限制在一个范围内。在Redis中是通过&操作来完成的。

在自己写的DLL里,实现了哈希表扩容,步进Rahash和强制Rehash,定义了新增和保存的接口DicSave,移除键值DicRemove,获取值DicGet,字典是否为空IsEmpty,主键是否存在IsExist。

这里写图片描述

整个程序的结构就如同上图,Dic存放两个哈希表ht[0],ht[1],1表在Rehash时使用,未Rehash时使用0表,存入的键经过MurmurHash2算法获取到hashKey与哈希表中的masksize&操作得到存入哈希节点索引,要保存到字典中的键和值就存放在hashEntry中,即哈希节点,当发现hash冲突时,把新节点放在最前面,这一切通过指针来完成,也就是链地址法。

在自己实现的时候,才发现下面这个结构的实现挺奇妙的。
这里写图片描述
因为数组,我们保存数据的时候要考虑到数组索引超界的问题,链表的长度是可以扩展的,但这里我们放进数组的不是链表,而是我们自己定义的一个固定大小的类,链表的操作被我们定义在类中的next指针实现了,这样在数组中我们存放了自定义类的地址,在这个地址中有包含了下一个自定义类的地址,这样就把这个结构实现了,并且我们不必将这个结构定义成Array[List(HashEntry)]的格式。虽然我猜测C#中的这里写图片描述可能也是这么实现的,不过是不是得看源码才知道了。

在Dic类中:
这里写图片描述

要说说这个lastRehashOverFlag标志(上次Rehash执行完毕标志),我定义它的是为了避免在单步Rehash时由于步进过小导致上一次Rehash未完成时又ht[0]又满足Rehash条件,这样会导致第一次Rehash时扩容的ht[1]未替换掉ht[0]又创建了一个哈希表ht,有了这个标志可以使下一次rehash延迟到上一次rehash结束后再进行。
但这个标志后来证实没有用处,因为不会出现上面说的情况,扩容是按2倍的比例执行的,如果rehash之前的表size为n,那么rehash扩容后的表size则为2n,在代码中将ht[0]上的值搬移到ht[1]上是以一个数组索引为单位的,也就是说每步rehash会搬移ht[0]上的哈希节点数组的某个索引上的所有值(包括解决hash冲突产生的链表)到ht[1]上,如果这个索引上的值为null,就跳过去找下一个值不为null的数组索引,因此在2倍的条件下,将ht[0]中的数据完成搬到ht[1]中所需的单步rehash次数会<=n,每次新增的时候rehash一次,那么最终在n次新增过程内,rehash是一定能完成的。只有在表扩展的增量小于原来表的size时才需要这个标志。

因为先执行把数据搬移一次到ht[1],再执行rehash过程中键值加到ht[1],链表的实现使得这两个过程不会产生冲突。如果自动rehash开关为关,那么满足强制rehash条件后就要执行强制rehash,这时候主线程会被阻塞到直到rehash完成,还有一种情况,就是上面说的表扩展的增量小于原来表的size,一段时间后used/size的比例会满足强制rehash条件,但代码中没有考虑这一点。

        /// <summary>
        /// 保存成员(添加/修改)
        /// </summary>
        /// <param name="key">主键</param>
        /// <param name="value"></param>
        /// <returns>是否成功</returns>
        public bool DicSave(string key, string value)
        {
            //// 散列比例
            float radio = ht[0].used / float.Parse(ht[0].size.ToString());
            if (autoRehashFlag && radio > 1 && lastRehashOverFlag)
            {
                //// 如果单步Rehash步进过小导致二次散列比例超界,等待第一次超界处理流程执行完
                rehashIndex = 0;
                lastRehashOverFlag = false;
            }

            if (rehashIndex != -1)
            {
                //// 单步Rehash
                if (this.DictRehash(1))
                {
                    Console.WriteLine("单步Rehash完成");
                }
            }
            else
            {
                //// 否则超界后的下一次添加才会扩容 因为比例不再是维持在1左右所有必须加等号
                if (radio >= forceRehashBoundary)
                {
                    //// 强制Rehash
                    rehashIndex = 0;
                    forceExpendHashTableFlag = true;
                    if (this.DictRehash(ht[0].used))
                    {
                        Console.WriteLine("强制Rehash完成");
                    }
                }
            }

            //// 这里逻辑不与上面合并的原因,因为Rehash后rehashIndex已置为-1 但合并的话会导致键值仍会保存到ht[1]保存而非ht[0],而此时ht[1]的哈希节点数组为null,会报异常。
            if (rehashIndex == -1)
            {
                return this.HashEntrySave(ht[0], key, value);
            }
            else
            {
                return this.HashEntrySave(ht[1], key, value);
            }
        }

rehashIndex是用来记录上一次rehash执行到的索引位置,但rehashIndex == sizemask时,就表示Rehash已完成,可以用ht[1]表替换ht[0],并将ht[1]表重置,rehashIndex置为-1, 等待下次Rehash。
rehash代码如下:

        /// <summary>
        /// 重新散列
        /// </summary>
        /// <param name="n">此参数控制Rehash步长</param>
        /// <returns>是否成功</returns>
        private bool DictRehash(int n)
        {
            try
            {
                //// rehashIndex == 0作为扩容标志
                if (rehashIndex == 0)
                {
                    //// 强制扩容
                    if (forceExpendHashTableFlag == true)
                    {
                        ht[1] = HashTable.ForceExpendHashTable(ht[0].used);
                    }
                    else 
                    {
                        ht[1] = HashTable.ExpendHashTable(ht[0].size);
                    }
                }

                while (n-- != 0)
                {
                    //// 只需要处理哈希节点不为空的数组索引
                    while (ht[0].hashEntryArr[rehashIndex] == null)
                    {
                        rehashIndex++;
                    }

                    HashEntry entry = ht[0].hashEntryArr[rehashIndex];
                    HashEntry nextEntry = null;

                    //// 对每个节点都做处理
                    while (entry != null)
                    {
                        //// 每个节点取出来单独处理,保存下一个节点的地址
                        nextEntry = entry.next;
                        entry.next = null;
                        int hashKey = HashTable.DictGenHashFunction(entry.key);
                        if (ht[1].hashEntryArr[hashKey & ht[1].sizemask] == null)
                        {
                            ht[1].hashEntryArr[hashKey & ht[1].sizemask] = entry;
                        }
                        else
                        {
                            HashEntry temp = ht[1].hashEntryArr[hashKey & ht[1].sizemask];
                            entry.next = temp;
                            ht[1].hashEntryArr[hashKey & ht[1].sizemask] = entry;
                        }

                        //// 更新已占用的数值
                        ht[1].used++;
                        ht[0].used--;
                        entry = nextEntry;
                    }

                    //// 处理完一条索引下滑1
                    rehashIndex++;

                    //// 循环结束条件
                    if (ht[0].used == 0)
                    {
                        //// 更新标志  重置哈希表
                        rehashIndex = -1;

                        //// 上次Rehash执行结束
                        lastRehashOverFlag = true;
                        ht[0] = ht[1];
                        ht[1] = null;
                        return true;
                    }
                }

                return true;
            }
            catch (Exception ex)
            {
                throw ex;
            }
        }

异常直接抛出了,其作为组件的作用已经完成。异常就丢给调用方去处理。

增,删,改,查,在写DicRemove处理指针时遇到一个问题。

 public bool DicRemove(string key) 
        {
            int count = 0;
            bool removeFlag = 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值