Dynamic-sized NonBlocking Hash table

在hash表进行resize的过程中,保持Lock-Free是一件非常困难的事。

一个hash表通常由若干个bucket组成,每一个bucket中会存储若干条被散列至此的数据项。当hash表进行resize时,需要将“旧”桶中的数据读出,并且重新散列至另外一个“新”桶中。假设这个过程不是一个原子操作,那么会导致此刻其他的读、写请求的结果发生异常,甚至导致数据丢失的情况发生。

因此,liu等人提出了一个新颖的概念:一个bucket的数据是可以冻结的

这个特点极大地简化了hash表在resize过程中在不同bucket之间转移数据的复杂度。

散列

Dynamic-sized NonBlocking Hash table - 图1

该哈希表的散列与普通的哈希表一致,都是借助散列函数,将用户需要查找、更改的数据散列到某一个哈希桶中,并在哈希桶中进行操作。

由于一个哈希桶的容量是有限的(一般不大于32个数据),因此在哈希桶中进行插入、查找的时间复杂度可以视为是常量的。

扩大

Dynamic-sized NonBlocking Hash table - 图2

当cache中维护的数据量太大时,会发生哈希表扩张的情况。以下两种情况是为“cache中维护的数据量过大”:

  • 整个cache中,数据项(node)的个数超过预定的阈值(默认初始状态下哈希桶的个数为16个,每个桶中可存储32个数据项,即总量的阈值为哈希桶个数乘以每个桶的容量上限);
  • 当cache中出现了数据不平衡的情况。当某些桶的数据量超过了32个数据,即被视作数据发生散列不平衡。当这种不平衡累积值超过预定的阈值(128)个时,就需要进行扩张;

一次扩张的过程为:

  • 计算新哈希表的哈希桶个数(扩大一倍);
  • 创建一个空的哈希表,并将旧的哈希表(主要为所有哈希桶构成的数组)转换一个“过渡期”的哈希表,表中的每个哈希桶都被“冻结”;
  • 后台利用“过渡期”哈希表中的“被冻结”的哈希桶信息对新的哈希表进行内容构建;值得注意的是,在完成新的哈希表构建的整个过程中,哈希表并不是拒绝服务的,所有的读写操作仍然可以进行

哈希表扩张过程中,最小的封锁粒度为哈希桶级别

当有新的读写请求发生时,若被散列之后得到的哈希桶仍然未构建完成,则“主动”进行构建,并将构建后的哈希桶填入新的哈希表中。后台进程构建到该桶时,发现已经被构建了,则无需重复构建。

因此如上图所示,哈希表扩张结束,哈希桶的个数增加了一倍,于此同时仍然可以对外提供读写服务,仅仅需要哈希桶级别的封锁粒度就可以保证所有操作的一致性跟原子性。

构建哈希桶

当哈希表扩张时,构建一个新的哈希桶其实就是将一个旧哈希桶中的数据拆分成两个新的哈希桶。

拆分的规则很简单。由于一次散列的过程为:

  • 利用散列函数对数据项的key值进行计算;
  • 将第一步得到的结果取哈希桶个数的余,得到哈希桶的ID;因此拆分时仅需要将数据项key的散列值对新的哈希桶个数取余即可。

缩小

当哈希表中数据项的个数少于哈希桶的个数时,需要进行收缩。收缩时,哈希桶的个数变为原先的一半,2个旧哈希桶的内容被合并成一个新的哈希桶,过程与扩张类似,在这里不展开详述。