分库分表的利器-一致性哈希算法

随着工作经验的提升,接触到了更多的性能问题,最近接触到一个订单中心的场景,预估数据量是千万级别,那首要解决的是数据如何分布的问题了


分布式数据存储存在着很多的问题,稍不注意就会踩到坑里,需要注意的问题有

数据分散的平均性

简单来说,就是把所有的数据尽可能的平均分散到各个区,避免数据量过少带来的资源浪费和数据量过多带来的性能问题,比如按照时间进行分表是常见的策略,但是对于抢票订单显然不合适,春运时提交的抢票订单和平时没有可比性

数据获取的方便性

由于数据存在不同的表甚至不同的数据源中,如何定位数据所在的位置和对批量数据的操作成为了没有完美解决方案的事情,在数据存储上一般我们认为单个用户的数据应该储存在相近的位置,也就是单库,有些没有用户标识的业务就会要求调用方冗余存储,或者本身临时存储,使用后情况关系,批量操作会命中多个数据分布,分布式事务又变成了麻烦的事情

横向扩展的能力

数据每天都是在不断扩展的,随着业务的爆发增长或者事件的推移,第一次的分布存储可能会显得不够用,性能逐渐下滑,需要对横向扩展做好准备和方案

数据库的可用性

如果采用多数据源的方案,数据源越多,发生故障的几率越大,数据库A故障之后,如果能够做到感知,那么也应该关注如何让本来分布到A的数据选择正常的数据源进行操作

一致性哈希算法

如果你没有接触过这个算法,那我先说一句让你转变观念的话,平时你所使用的hashMap是根据键值的hash值确定数据存储的位置,然而一致性哈希则经历了两步,先使用hash算法和一次取模运算,确认数据存储的位置,再由节点来决定哪一段的数据是属于他的。

官方说法:

定义:一致哈希 是一种特殊的哈希算法。在使用一致哈希算法后,哈希表槽位数(大小)的改变平均只需要对 K/n 个关键字重新映射,其中 K是关键字的数量, n是槽位数量。然而在传统的哈希表中,添加或删除一个槽位的几乎需要对所有关键字进行重新映射。

需求:在使用n台缓存服务器时,一种常用的负载均衡方式是,对资源o的请求使用 hash(o)=o mod n来映射到某一台缓存服务器。当增加或减少一台缓存服务器时这种方式可能会改变所有资源对应的hash值,也就是所有的缓存都失效了,这会使得缓存服务器大量集中地向原始内容服务器更新缓存。因此需要一致哈希算法来避免这样的问题。 一致哈希尽可能使同一个资源映射到同一台缓存服务器。这种方式要求增加一台缓存服务器时,新的服务器尽量分担存储其他所有服务器的缓存资源。减少一台缓存服务器时,其他所有服务器也可以尽量分担存储它的缓存资源。 一致哈希算法的主要思想是将每个缓存服务器与一个或多个哈希值域区间关联起来,其中区间边界通过计算缓存服务器对应的哈希值来决定。(定义区间的哈希函数不一定和计算缓存服务器哈希值的函数相同,但是两个函数的返回值的范围需要匹配。)如果一个缓存服务器被移除,则它所对应的区间会被并入到邻近的区间,其他的缓存服务器不需要任何改变。

实现:一致哈希将每个对象映射到圆环边上的一个点,系统再将可用的节点机器映射到圆环的不同位置。查找某个对象对应的机器时,需要用一致哈希算法计算得到对象对应圆环边上位置,沿着圆环边上查找直到遇到某个节点机器,这台机器即为对象应该保存的位置。 当删除一台节点机器时,这台机器上保存的所有对象都要移动到下一台机器。添加一台机器到圆环边上某个点时,这个点的下一台机器需要将这个节点前对应的对象移动到新机器上。 更改对象在节点机器上的分布可以通过调整节点机器的位置来实现。

以上来源于维基百科

如何实现

一个最简单的方法,利用hashcode对2^32进行取模,可以使数据能均匀的分散在2^32种可能上,然后定义Node,散列在这些可能性上,当一个数据分配到这个一致性哈希表之后,让他往下进行寻找,找到的第一个Node就是他所属于的节点,如果找不到节点,那他就属于第一个节点,所以一致性哈希解构是一个环状解构

如何评价一致性哈希算法的好坏

  1. 平衡性(Balance):平衡性是指哈希的结果能够尽可能分布到所有的缓冲中去,这样可以使得所有的缓冲空间都得到利用。很多哈希算法都能够满足这一条件。
  2. 单调性(Monotonicity):单调性是指如果已经有一些内容通过哈希分派到了相应的缓冲中,又有新的缓冲加入到系统中。哈希的结果应能够保证原有已分配的内容可以被映射到原有的或者新的缓冲中去,而不会被映射到旧的缓冲集合中的其他缓冲区。
  3. 分散性(Spread):在分布式环境中,终端有可能看不到所有的缓冲,而是只能看到其中的一部分。当终端希望通过哈希过程将内容映射到缓冲上时,由于不同终端所见的缓冲范围有可能不同,从而导致哈希的结果不一致,最终的结果是相同的内容被不同的终端映射到不同的缓冲区中。这种情况显然是应该避免的,因为它导致相同内容被存储到不同缓冲中去,降低了系统存储的效率。分散性的定义就是上述情况发生的严重程度。好的哈希算法应能够尽量避免不一致的情况发生,也就是尽量降低分散性。
  4. 负载(Load):负载问题实际上是从另一个角度看待分散性问题。既然不同的终端可能将相同的内容映射到不同的缓冲区中,那么对于一个特定的缓冲区而言,也可能被不同的用户映射为不同 的内容。与分散性一样,这种情况也是应当避免的,因此好的哈希算法应能够尽量降低缓冲的负荷。

数据库如何避坑

由于数据分布不均或者数据量爆发增长,如果扩展Node是一个很头疼的问题,从官方说明上来看,添加或删除一个槽位的几乎需要对所有关键字进行重新映射,这是我们无法接受的,一个常见的做法是使用虚拟节点,在一开始使用的时候,就将数据定义为很多个Node(32、64或者更多),而某个数据源又对应着多个node,这样后期扩展的时候可以根据实际情况让这个数据源对于的多个node分裂。
而另一个问题是数据库的故障,这里可以让故障的数据源交出自己持有的Node,将这些Node交给下一个数据源管理,当然这里涉及的内容过于复杂,还需要支持多写多读,而且只有个别的场景符合,不建议大家为了保证可用性进行尝试,需要有大牛坐镇。

轮子

一个合适的轮子能让我们变得更快,这里推荐Google的guava包,他给我们提供了一种使用方式

1
Hashing.consistentHash(id, buckets)

传入id和总的Node数就能得到对于的值,效率非常高,另外推荐大家看一下guava,他提供了很多集合、哈希、并发、缓存、IO等等的工具,1看实现,2看适用场景,真的是受益匪浅。