场景
以分布式缓存为例,我们需要在多台机器上分别存储不同的缓存,从而降低单一机器的负担。最常见的做法是通过hash算法,计算缓存key的哈希值,然后再对机器数量取余,从而将缓存分散到不同的机器上,以达到同一份缓存每次都会访问固定机器的目标。
但是在增加一台机器,或者减少一条机器的时候,对机器取余,就会造成大量的缓存不能访问原来的机器,很可能会引起缓存雪崩效应,进而造成服务器宕机。
一致性hash算法
图来源: https://www.cnblogs.com/williamjie/p/9477852.html
图应该是比较直观的,环上的大圆表示机器,小圆表示数据。机器(node节点)hash之后,映射到环上一个固定的位置。数据也同样,hash之后,顺时针选取最近的机器节点存储数据。图中,添加node5节点(机器),映射到的位置再node2和node4节点之间,索引node2和node5之间的数据都需要发生变化,具体是: 原来存储的是node4节点,现在存储的是node5节点,因为对这部分数据而言,顺时针更靠近node5节点,所以存储到node5中。
一种可能和一致性hash差不多的算法 -- lemon algorithm
假设我们的集群中有N台机器,我们的第一个变量firstMod为大于N的2的m次方,第二个变量secondMod为firstMod的一半。比如我们有8台机器,那么大于8的2的幂次方就是16,secondMod则为8;如果我们有5台机器,那么firstMod=8,secondMod=4.
还是以场景中的常规做法为例,我们计算缓存key的hash值之后,对“机器”取余,当然此处的“机器”,不是真实的机器数量,否则我也不会写这篇文章了。我们用缓存key的hash值对firstMod取余,从而映射到不同的机器。比如缓存key计算出的hash值为23,那么以8台机器为例,firstMod=16,取余为7,那么就将这份缓存存储到7号机器上。对于余数为0的,固定存储到1号机器上。
可能有的同学发现了,当缓存key为25时,以8台机器为例,firstMod=16,取余为9,没有对应的9号机器。所以我们的第二步就是用余数和最大的机器号进行比较,如果大于最大的机器号,即没有机器可以存储,那么就用余数再对secondMod取余,这样便一定有可对应的机器。还是以8台机器为例,hash值为25,对firstMod=16取余为9,大于最大的机器号8,再取余为1,所以映射到1号机器上。有没有可能对secondMod取余没有相应的机器呢?因为我们的firstMod为最靠近且大于机器数量machine的2的m次方,所以
firstMod < (machine << 1),故secondMod肯定小于machine,所以一定可以映射到。
上图以8台机器扩容到9台机器为例,需要搬移的数据仅hash值为9的,其与均不变,也就是变了1/16,这个大概和一致性hash差不多,最重要的是,不会引起缓存雪崩,而又分布比较均匀。上图中mod8一列,红色标记的为需要二次取余的hash值。X表示本次扩容需要搬移的数据。下面为几张扩容示意图:
总结
计算量仅为两次取余和一次比较,所以性能上不会有问题。对于分布均匀的问题,前面我们提到余数为0(整除),固定存储到1号机器上,但是这样1号机器上压力比较大,初步设想是可以存到secondMod机器上,secondMod对应的机器也是一定存在的,这样相对分散一点。