其中有一个场景是在客户端登陆成功后须要从可用的服务端列表中选择一台服务节点返回给客户端使用。面试
而这个选择的过程就是一个负载策略的过程;初版本作的比较简单,默认只支持轮询的方式。算法
虽然够用,但不够优雅。设计模式
所以个人规划是内置多种路由策略供使用者根据本身的场景选择,同时提供简单的 API 供用户自定义本身的路由策略。数组
先来看看一致性 Hash 算法的一些特色:服务器
根据这些客观条件咱们很容易想到经过自定义一个有序数组来模拟这个环。数据结构
这样咱们的流程以下:函数
先不考虑排序所消耗的时间,单看这个路由的时间复杂度:工具
理论讲完了来看看具体实践。性能
我自定义了一个类:SortArrayMapspa
他的使用方法及结果以下:
可见最终会按照 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:
ps:这里一样也没有 hash 方法以及虚拟节点(具体实现请看下文),由于 TreeMap 和 SortArrayMap
同样都是做为基础数据结构来使用的。
性能对比
为了方便你们选择哪个数据结构,我用 TreeMap 和 SortArrayMap 分别写入了一百万条数据来对比。
先是 SortArrayMap:
耗时 2237 毫秒。
TreeMap:
耗时 1316毫秒。
结果是快了将近一倍,因此仍是推荐使用 TreeMap 来进行实现,毕竟它不须要额外的排序损耗。
下面来看看在 cim 这个应用中是如何具体使用的,其中也包括上文提到的虚拟节点以及 hash 算法。
模板方法
在应用的时候考虑到就算是一致性 hash 算法都有多种实现,为了方便其使用者扩展本身的一致性 hash 算法所以我定义了一个抽象类;其中定义了一些模板方法,这样你们只须要在子类中进行不一样的实现便可完成本身的算法。
AbstractConsistentHash,这个抽象类的主要方法以下:
下面咱们来看看利用 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 {
/**
*/
String routeServer(List<String> values,String key) ;
}
其中只有一个方法,也就是路由方法;入参分别是服务列表以及客户端信息便可。
而对于一致性 hash 算法来讲也是只须要实现这个接口,同时在这个接口中选择使用 SortArrayMapConsistentHash 仍是 TreeMapConsistentHash 便可。
这里还有一个 setHash 的方法,入参是 AbstractConsistentHash;这就是用于客户端指定须要使用具体的那种数据结构。
而对于以前就存在的轮询策略来讲也是一样的实现 RouteHandle 接口。
这里我只是把以前的代码搬过来了而已。
接下来看看客户端究竟是如何使用以及如何选择使用哪一种算法。
为了使客户端代码几乎不动,我将这个选择的过程放入了配置文件。
无论这里的策略如何改变,在使用处依然保持不变。
只须要注入 RouteHandle,调用它的 routeServer 方法。
@Autowired
private RouteHandle routeHandle ;
String server =
routeHandle.routeServer(serverCache.getAll(),String.valueOf(loginReqVO.getUserId()));
既然使用了注入,那其实这个策略切换的过程就在建立 RouteHandle bean 的时候完成的。
也比较简单,须要读取以前的配置文件来动态生成具体的实现类,主要是利用反射完成的。
这样处理以后就比较灵活了,好比想新建一个随机的路由策略也是一样的套路;到时候只须要修改配置便可。
感兴趣的朋友也可提交 PR 来新增更多的路由策略。
但愿看到这里的朋友能对这个算法有所理解,同时对一些设计模式在实际的使用也能有所帮助。
相信在金三银四的面试过程当中仍是能让面试官眼前一亮的,毕竟根据我这段时间的面试过程来看听过这个名词的都在少数(可能也是和候选人都在 1~3 年这个层级有关)。