一致性哈希算法与实现
背景
随着业务系统越来越大,我们需要更高并发、更高存储的缓存,比如如Redis,但是:
单台Redis性能、容量不足以满足需求时,怎么使用Redis集群来进行分布式缓存?
写入数据时,如何决定将内容缓存在集群中的哪一台Redis服务器上,并且查询时能找到这台机器?
随机分配
不合理,想要查询的时候我们自己也不知道在哪,只能通过轮询来获取数据。
hash取模
假如我有3个缓存节点A\B\C,根据对象的 hashCode(object) % 3
缓存到对应的节点。当有一天扩容1台机器后,我们的缓存集群变为了4个,数据变为了根据 hashCode(object) % 4
放入对应节点。这样会出现,绝大部分的缓存都会无法命中。
一致性hash
而consistent hashing就是来把这种「绝大部分」无法命中 变为 「绝大部分」都能命中。
简单的说,在移除 / 添加一个 cache 时,它能够尽可能小的改变已存在 key 映射关系,尽可能的满足单调性的要求。
下面,本文剩下部分重点来讲讲这个一致性哈希算法。
最小数据失效
Hash 算法的一个衡量指标是单调性( Monotonicity ),定义如下:
单调性是指如果已经有一些内容通过哈希分派到了相应的缓存节点中,又有新的缓存节点加入到系统中,哈希的结果应能够保证原有已分配的内容可以被映射到新的节点,而不会被映射到旧的缓冲集合中的其他缓冲区。
容易看到,上面的简单 hash 算法
hashCode(object) % N
难以满足单调性要求。
上面这话有点绕口,我写个简单demo说明:
每个程序员,尤其是JAVA程序员,应该都知道Object基类的hashCode函数,hashCode会返回一个 0~2^32-1次方
之间的数字。
想象一下,将此范围映射到一个圆中,其中包含许多对象(1、2、3、4)和缓存节点(A,B,C),这些对象标记在它们哈希到的点上。
为了找到对象进入哪个缓存节点,我们绕圆顺时针移动直到找到缓存节点。因此,在上图中,我们看到对象1和4属于缓存A,对象2属于缓存B,对象3属于缓存C。请考虑一下如果删除缓存C会发生什么:对象3会归属到缓存A,其他对象映射不变。如果然后在标记的位置添加另一个缓存D,它将获取对象3和4,仅保留属于A的对象1。
数据分布均衡
考量 Hash 算法的另一个指标是平衡性 (Balance),定义如下:
平衡性是指哈希的结果能够尽可能平均分布到所有的缓冲节点中,这样可以使得所有的缓冲空间都得到利用。
而Hash的本质上是随机的,因此缓存之间的对象分布可能不均匀。解决此问题的方法是引入“虚拟节点的概念”。他们是真实缓存节点的副本,因此,无论何时添加缓存,我们都会在圆中为其创建多个“虚拟节点”。
可以在下图看到效果,该图模拟将10000个对象存储在10个缓存中而产生的。
X轴标识缓存节点的副本数。当它很小时,我们会看到对象在整个缓存中的分布是不平衡的,因为标准差很高。而随着副本数量的增加,对象的分布变得更加平衡(标准差大约为平均值的5%-10%是一个比较好的性能与平衡性的点)。
例子
hash环的实现
节点对象
测试代码
返回结果
总结
虽然一致性hash能很好的解决一部分问题,但是也有不好的地方,就是数据迁移问题。因为所有数据都分散到各个机器上,没有很好的聚合,所以对于数据迁移是有一定困难。
还有就是使用什么样的hash算法:
第一代:SHA-1(1993),MD5(1992),CRC(1975),Lookup3(2006)
第二代:MurmurHash(2008)
第三代:CityHash, SpookyHash(2011)
在一致性hash方面,推荐:MurmurHash、FNV、Ketama,具体可以百度、Google查找下资料。
其他场景需要具体问题具体分析选择,毕竟我们一致性hash不仅仅使用在缓存,还有一些场景需要从安全性、性能、实现复杂度等来权衡。
软件设计没有银弹,每一种算法只是解决一种相对应的问题。不要手里拿着锤子,就看什么都是钉子,还是要具体问题具体分析。
版权声明: 本文为 InfoQ 作者【俊俊哥】的原创文章。
原文链接:【http://xie.infoq.cn/article/3f62a120d1d654e0c08a93a03】。文章转载请联系作者。
评论