Tars源码分析---内存哈希表的实现

前言

在cpp/util中实现了基于内存的哈希表(hashmap.(cpp,h))。该哈希表支持dump到文件,以及从文件load到内存。在实现上,它基于传统的开链哈希,分别为每个哈希桶维护一个block链表,相同哈希值的kv对散列存储到相同的block链表中。链表中每个block存储具体的一个kv数据。由于hashmap使用TC_MemMultiChunkAllocator进行内存管理,因此它以块为单位使用内存(当然,支持不同大小的块)。hashmap通过在block中维护chunk链表来支持数据操作(主要是set)的灵活性。为了更好和磁盘读写对接(dump,load),hashmap使用的所有内存都属于一块连续的内存空间。除此之外,为了对外提供灵活的会写,备份功能,hashmap还对block建立了多维的管理链表(_iSet,_iGet,….)。

内存结构


这里写图片描述

如上图为hashmap实现的逻辑视图。一个简单的开链哈希算法,并通过tagMapHead对哈希中的block提供多种类型的管理功能。kv值编码成一个二进制序列存储在block-chunk链表中(chunk非必须),kv的编解码使用TC_PackOut/TC_PackIn来完成(tc_Pack.cpp)。当然,hashmap支持只有key的存储,类似STL中的set集合。

为了方便地支持将哈希表dump到文件或者从文件load进入内存,hashmap的实现内存是在一整块连续的内存空间中。如下图所示:

这里写图片描述

可以参看以下函数的实现:

void TC_HashMap::create(void *pAddr, size_t iSize);

这给内存管理带来了麻烦。hashmap在实现时是依赖于tars中的块内存管理器TC_MemMultiChunkAllocator来完成的。TC_MemMultiChunkAllocator支持对一整块大内存进行分块管理,分块大小具有多样性,上层用户通过allocate和dellocate向TC_MemMultiChunkAllocator发出管理请求。不过需要注意都是TC_MemMultiChunkAllocator并不支持想操作系统申请或者注销内存,它只负责对分配好的大块内存的管理。

基于这样的物理存储格式,在实现时块地址就可以简化为相对于_pHead的偏移量。因此就可以简单地实现将复杂链表结构dump到文件中。

功能实现

hashmap对外提供了set,get,find等功能接口。为了更灵活地支持这些功能,hashmap在实现时对block进行了复杂的管理,特别是针对set接口。由于set接口在实现时可能涉及到内存不够,此时需要对哈希表中的旧数据进行清理淘汰(当然可以设置不清理,一旦满就只读)。实现时应当淘汰最长时间没有被访问到的数据块,这就涉及到对用户访问的记录,这些都是记录在tagMapHead的管理链表中。除了hashmap中的冲突链表,它还按照各个block被访问的性质对block设计了两个组织链表,分别是

_iSet链表
_iGet链表

可以从hashmap的内存逻辑实现图中看到。顾名思义,这两个链表分别是根据block被set(get)操作访问的时间顺序链接起来的。对一个block的get操作会使该block被移动到_iGet链表的头部,对一个block的的set操作会同时影响两个链表。

基于这两个链表,hashmap还实现了备份和回写的管理:

size_t _iBackupTail;
size_t _iSyncTail; 

本质就是两个指针,分别指向最旧的需要回写或者备份的block。根据上面-iSet和_iGet链表的插入原则,越新的block会越接近链表头,因此备份或者回写都是沿着block的prev指针进行的。不过回写和备份的具体过程一般是由上层的用户代码完成,hashmap只是提供了数据指针。

修改数据块:
hashmap的头部除了tagMapHead之外,还有一个修改数据块头部:

    struct tagModifyHead
    {
        char            _cModifyStatus;         /**修改状态: 0:目前没有人修改, 1: 开始准备修改, 2:修改完毕, 没有copy到内存中*/
        size_t          _iNowIndex;             /**更新到目前的索引, 不能操作10个*/
        tagModifyData   _stModifyData[20];      /**一次最多20次修改*/
    }__attribute__((packed));

这个数据结构主要是用来缓存对hashmap的修改,每次调用update对hashmap进行操作时,修改都会暂存在这个数据修改块中。只有调用doUpdate时才会进行真正的修改。这给撤销修改提供了一个缓冲余地。

TC_HashMapCompact

这是对hashmap的一个紧凑性的实现。主要是通过改变管理结构中的数据类型来降低管理结构的内存占用率,比如将size_t类型的管理字段变位uint_32类型的字段等等,当然主要是针对64bit的优化。可以比较hashmap和TC_HashMapCompact的tagBlockHead、tagChunkHead等的字段类型一下。

TC_Multi_HashMap

TC_Multi_HashMap是基于内存的多key的hashmap。在TC_Multi_HashMap中有主key(mainKey)和辅key(UK)(主key+辅key => 联合主key),主key存储在tagBlockHead->_iMainKey地址指向的地址对应的MainKey数据结构中。辅key和value的存储和前面介绍的hashmap一样,存储在一起。TC_Multi_HashMap还单独对主键构建了一个哈希索引区:

TC_MemVector<tagMainKeyHashItem>    _hashMainKey;

而联合主键区即为hashmap的:

TC_MemVector<tagHashItem>   _hash;

Mainkey中包含指向数据块的指针:

uint32_t    _iAddr; 

在构建block时根据mainkey和UK的连接所得的哈希找到哈希桶。并将主key连入到主key哈希索引区。

总结

本文简单介绍了tars底层库的hashmap的实现机制。基本实现原理是开链哈希,只是为了提供一些类似磁盘读写,旧数据淘汰,备份等功能而使得实现具有了一定的复杂性。后面还介绍了基于hashmap的一些变种实现,包括紧凑哈希和多键哈希。

util里面还对红黑树进行实现,支持和hashmap基本相同的功能以及对外也提供基本一致的接口,因此底层的存储机制以及思想基本思想大体一致。只是平衡树算法在查找,插入等操作的实现上和哈希算法不同。

猜你喜欢

转载自blog.csdn.net/Swartz2015/article/details/80775061