记得一年前分享过一篇《一致性 Hash 算法分析》,当时只是分析了这个算法的实现原理、解决了什么问题等。java
但没有实际实现一个这样的算法,毕竟要加深印象还得本身撸一遍,因而本次就当前的一个路由需求来着手实现一次。git
看过《为本身搭建一个分布式 IM(即时通信) 系统》的朋友应该对其中的登陆逻辑有所印象。github
先给新来的朋友简单介绍下 cim 是干啥的:
其中有一个场景是在客户端登陆成功后须要从可用的服务端列表中选择一台服务节点返回给客户端使用。面试
而这个选择的过程就是一个负载策略的过程;初版本作的比较简单,默认只支持轮询的方式。算法
虽然够用,但不够优雅😏。设计模式
所以个人规划是内置多种路由策略供使用者根据本身的场景选择,同时提供简单的 API 供用户自定义本身的路由策略。数组
先来看看一致性 Hash 算法的一些特色:服务器
0 ~ 2^32-1
大小的环。根据这些客观条件咱们很容易想到经过自定义一个有序数组来模拟这个环。数据结构
这样咱们的流程以下:分布式
先不考虑排序所消耗的时间,单看这个路由的时间复杂度:
O(1)
。O(N)
。理论讲完了来看看具体实践。
我自定义了一个类:SortArrayMap
他的使用方法及结果以下:
可见最终会按照 key
的大小进行排序,同时传入 hashcode = 101
时会按照顺时针找到 hashcode = 1000
这个节点进行返回。
下面来看看具体的实现。
成员变量和构造函数以下:
其中最核心的就是一个 Node
数组,用它来存放服务节点的 hashcode
以及 value
值。
其中的内部类 Node
结构以下:
写入数据的方法以下:
相信看过 ArrayList
的源码应该有印象,这里的写入逻辑和它很像。
可是存放时是按照写入顺序存放的,遍历时天然不会有序;所以提供了一个 Sort
方法,能够把其中的数据按照 key
其实也就是 hashcode
进行排序。
排序也比较简单,使用了 Arrays
这个数组工具进行排序,它实际上是使用了一个 TimSort
的排序算法,效率仍是比较高的。
最后则须要按照一致性 Hash 的标准顺时针查找对应的节点:
代码仍是比较简单清晰的;遍历数组若是找到比当前 key 大的就返回,没有查到就取第一个。
这样就基本实现了一致性 Hash 的要求。
ps:这里并不包含具体的 hash 方法以及虚拟节点等功能(具体实现请看下文),这个能够由使用者来定,SortArrayMap 可做为一个底层的数据结构,提供有序 Map 的能力,使用场景也不局限于一致性 Hash 算法中。
SortArrayMap
虽然说是实现了一致性 hash 的功能,但效率还不够高,主要体如今 sort
排序处。
下图是目前主流排序算法的时间复杂度:
最好的也就是 O(N)
了。
这里彻底能够换一个思路,不用对数据进行排序;而是在写入的时候就排好顺序,只是这样会下降写入的效率。
好比二叉查找树,这样的数据结构 jdk
里有现成的实现;好比 TreeMap
就是使用红黑树来实现的,默认状况下它会对 key 进行天然排序。
来看看使用 TreeMap
如何来达到一样的效果。
运行结果:
127.0.0.1000
效果和上文使用 SortArrayMap
是一致的。
只使用了 TreeMap 的一些 API:
TreeMap
能够保证 key 的天然排序。tailMap
能够获取比当前 key 大的部分数据。Map
的第一个节点,一样也实现了环形结构。ps:这里一样也没有 hash 方法以及虚拟节点(具体实现请看下文),由于 TreeMap 和 SortArrayMap 同样都是做为基础数据结构来使用的。
为了方便你们选择哪个数据结构,我用 TreeMap
和 SortArrayMap
分别写入了一百万条数据来对比。
先是 SortArrayMap
:
耗时 2237 毫秒。
TreeMap:
耗时 1316毫秒。
结果是快了将近一倍,因此仍是推荐使用 TreeMap
来进行实现,毕竟它不须要额外的排序损耗。
下面来看看在 cim
这个应用中是如何具体使用的,其中也包括上文提到的虚拟节点以及 hash 算法。
在应用的时候考虑到就算是一致性 hash 算法都有多种实现,为了方便其使用者扩展本身的一致性 hash 算法所以我定义了一个抽象类;其中定义了一些模板方法,这样你们只须要在子类中进行不一样的实现便可完成本身的算法。
AbstractConsistentHash,这个抽象类的主要方法以下:
add
方法天然是写入数据的。sort
方法用于排序,但子类也不必定须要重写,好比 TreeMap
这样自带排序的容器就不用。getFirstNodeValue
获取节点。process
则是面向客户端的,最终只须要调用这个方法便可返回一个节点。下面咱们来看看利用 SortArrayMap
以及 AbstractConsistentHash
是如何实现的。
就是实现了几个抽象方法,逻辑和上文是同样的,只是抽取到了不一样的方法中。
只是在 add 方法中新增了几个虚拟节点,相信你们也看得明白。
把虚拟节点的控制放到子类而没有放到抽象类中也是为了灵活性考虑,可能不一样的实现对虚拟节点的数量要求也不同,因此不如自定义的好。
可是 hash
方法确是放到了抽象类中,子类不用重写;由于这是一个基本功能,只须要有一个公共算法能够保证他散列地足够均匀便可。
所以在 AbstractConsistentHash
中定义了 hash 方法。
这里的算法摘抄自 xxl_job,网上也有其余不一样的实现,好比
FNV1_32_HASH
等;实现不一样可是目的都同样。
这样对于使用者来讲就很是简单了:
他只须要构建一个服务列表,而后把当前的客户端信息传入 process
方法中便可得到一个一致性 hash 算法的返回。
一样的对于想经过 TreeMap
来实现也是同样的套路:
他这里不须要重写 sort 方法,由于自身写入时已经排好序了。
而在使用时对于客户端来讲只需求修改一个实现类,其余的啥都不用改就能够了。
运行的效果也是同样的。
这样你们想自定义本身的算法时只须要继承 AbstractConsistentHash
重写相关方法便可,客户端代码无须改动。
但其实对于 cim
来讲真正的扩展性是对路由算法来讲的,好比它须要支持轮询、hash、一致性hash、随机、LRU等。
只是一致性 hash 也有多种实现,他们的关系就以下图:
应用还须要知足对这一类路由策略的灵活支持,好比我也想自定义一个随机的策略。
所以定义了一个接口:RouteHandle
public interface RouteHandle { /** * 再一批服务器里进行路由 * @param values * @param key * @return */ String routeServer(List<String> values,String key) ; }
其中只有一个方法,也就是路由方法;入参分别是服务列表以及客户端信息便可。
而对于一致性 hash 算法来讲也是只须要实现这个接口,同时在这个接口中选择使用 SortArrayMapConsistentHash
仍是 TreeMapConsistentHash
便可。
这里还有一个 setHash
的方法,入参是 AbstractConsistentHash;这就是用于客户端指定须要使用具体的那种数据结构。
而对于以前就存在的轮询策略来讲也是一样的实现 RouteHandle
接口。
这里我只是把以前的代码搬过来了而已。
接下来看看客户端究竟是如何使用以及如何选择使用哪一种算法。
为了使客户端代码几乎不动,我将这个选择的过程放入了配置文件。
RouteHandle
接口的轮询策略的全限定名。RouteHandle
接口的一致性 hash 算法的全限定名。SortArrayMapConsistentHash
仍是 TreeMapConsistentHash
或是自定义的其余方案。AbstractConsistentHash
的全限定名。无论这里的策略如何改变,在使用处依然保持不变。
只须要注入 RouteHandle
,调用它的 routeServer
方法。
@Autowired private RouteHandle routeHandle ; String server = routeHandle.routeServer(serverCache.getAll(),String.valueOf(loginReqVO.getUserId()));
既然使用了注入,那其实这个策略切换的过程就在建立 RouteHandle bean
的时候完成的。
也比较简单,须要读取以前的配置文件来动态生成具体的实现类,主要是利用反射完成的。
这样处理以后就比较灵活了,好比想新建一个随机的路由策略也是一样的套路;到时候只须要修改配置便可。
感兴趣的朋友也可提交 PR 来新增更多的路由策略。
但愿看到这里的朋友能对这个算法有所理解,同时对一些设计模式在实际的使用也能有所帮助。
相信在金三银四的面试过程当中仍是能让面试官眼前一亮的,毕竟根据我这段时间的面试过程来看听过这个名词的都在少数😂(可能也是和候选人都在 1~3 年这个层级有关)。
以上全部源码:
https://github.com/crossoverJie/cim
若是本文对你有所帮助还请不吝转发。