假如有一个数组,采用二分查找找某一个元素:
第一次:n个元素里面找;
第二次:n/2个元素里面找;
第三次:n/4;
…………
第k次:n/(2^k-1);
假如第k次已经找到了,刚好是最后一个元素才被找到,那么
n/(2^k-1) = 1 -> k = log2(n+1) ->即log2n
一: hash算法
hash算法是一种高效的寻址法,通过hash值定位到目标位置。
普通hash算法:
就是取模,然后计算服务节点,如分库分表,nginx ip_hash
问题:扩容和缩容都需要重新计算,影响很大
一致性hash:
1到2的32次方-1,形成闭环(因为hash值是32位无符号数值),服务器的hash值落到环上,client端ip也是同样算法落到环上,顺时针最近的服务节点即是访问节点
这样,缩容或扩容,都只会影响小部分请求,比如:
- 节点2宕机,只会影响1-2之间,本应该落到节点2上,2宕机后落到了节点3上。
- 2-3之间增加节点5,只会影响2-5之间,本该落到3的,会落到5上。
代码实现:
public class ConsistentHashNoVirtual { public static void main(String[] args) { //step1 初始化:把服务器节点IP的哈希值对应到哈希环上 // 定义服务器ip String[] tomcatServers = new String[] {"123.111.0.0", "123.101.3.1", "111.20.35.2", "123.98.26.3"}; SortedMap<Integer, String> hashServerMap = new TreeMap<>(); for (String tomcatServer : tomcatServers) { // 求出每⼀个ip的hash值,对应到hash环上,存储hash值与ip的对应关系 int serverHash = Math.abs(tomcatServer.hashCode()); // 存储hash值与ip的对应关系 hashServerMap.put(serverHash, tomcatServer); } //step2 针对客户端IP求出hash值 // 定义客户端IP String[] clients = new String[] {"10.78.12.3", "113.25.63.1", "126.12.3.8"}; for (String client : clients) { int clientHash = Math.abs(client.hashCode()); //step3 针对客户端,找到能够处理当前客户端请求的服务器(哈希环上顺时针最近) // 根据客户端ip的哈希值去找出哪⼀个服务器节点能够处理() //tailMap方法返回hashServerMap中key值>=当前key的值 SortedMap<Integer, String> integerStringSortedMap = hashServerMap.tailMap(clientHash); if (integerStringSortedMap.isEmpty()) { // 取哈希环上的顺时针第⼀台服务器 Integer firstKey = hashServerMap.firstKey(); System.out.println("==========>>>>客户端:" + client + " 被路由到服务器:" + hashServerMap.get(firstKey)); } else { Integer firstKey = integerStringSortedMap.firstKey(); System.out.println("==========>>>>客户端:" + client + " 被路由到服务器:" + hashServerMap.get(firstKey)); } } } }
如果节点太少,异常时会影响很多数据。另外,正常时候也可能出现数据的情况,比如只有节点1和节点2,,那么大部分请求会落到节点1。
这时可以为每个真实节点增加多个虚拟节点,当落在虚拟节点上的请求,让它请求真实的节点:
如下: 对于虚拟节点2#2和2#3的请求,让他请求真实节点2#1。
代码实现:
public class ConsistentHashWithVirtual { public static void main(String[] args) { //step1 初始化:把服务器节点IP的哈希值对应到哈希环上 // 定义服务器ip String[] tomcatServers = new String[] {"123.111.0.0","123.101.3.1","111.20.35.2","123.98.26.3"}; SortedMap<Integer,String> hashServerMap = new TreeMap<>(); // 定义针对每个真实服务器虚拟出来⼏个节点 int virtaulCount = 3; for(String tomcatServer: tomcatServers) { // 求出每⼀个ip的hash值,对应到hash环上,存储hash值与ip的对应关系 int serverHash = Math.abs(tomcatServer.hashCode()); // 存储hash值与ip的对应关系 hashServerMap.put(serverHash,tomcatServer); // 处理虚拟节点 for(int i = 0; i < virtaulCount; i++) { int virtualHash = Math.abs((tomcatServer + "#" + i).hashCode()); hashServerMap.put(virtualHash,"----由虚拟节点"+ i + "映射过来的请求:"+ tomcatServer); } } //step2 针对客户端IP求出hash值 // 定义客户端IP String[] clients = new String[] {"10.78.12.3","113.25.63.1","126.12.3.8"}; for(String client : clients) { int clientHash = Math.abs(client.hashCode()); //step3 针对客户端,找到能够处理当前客户端请求的服务器(哈希环上顺时针最近) // 根据客户端ip的哈希值去找出哪⼀个服务器节点能够处理() SortedMap<Integer, String> integerStringSortedMap = hashServerMap.tailMap(clientHash); if(integerStringSortedMap.isEmpty()) { // 取哈希环上的顺时针第⼀台服务器 Integer firstKey = hashServerMap.firstKey(); System.out.println("==========>>>>客户端:" + client + " 被路由到服务器:" + hashServerMap.get(firstKey)); }else{ Integer firstKey = integerStringSortedMap.firstKey(); //如果是虚拟节点,就会打印出对应的真实节点 System.out.println("==========>>>>客户端:" + client + " 被路由到服务器:" + hashServerMap.get(firstKey)); } } } }
nginx可以使用ngx_http_upstream_consistent_hash模块来配置请求的一致性hash:
consistent_hash $remote_addr
:可以根据客户端
ip
映射
consistent_hash $request_uri
:根据客户端请求的
uri
映射
consistent_hash $args
:根据客户端携带的参数进⾏映
ngx_http_upstream_consistent_hash
模块是⼀个第三⽅模块,需要我们下载安装后使⽤
github
下载
nginx
⼀致性
hash
负载均衡模块
https://github.com/replay/ngx_http_consistent_hash
将下载的压缩包上传到nginx服务器,并解压
提前已经编译安装过
nginx
,此时进⼊当时
nginx
的源码⽬录,执⾏如下命令
./confifigure —add-module=/root/ngx_http_consistent_hash-master
make
make install
二: 时钟同步问题
1、各节点都可以联网,ntpdate -u ntp.api.bz 命令使用网络时间
2、不能联网,将某一台节点作为时间服务器,其他节点来同步;ntpdate 时间服务器ip
三 :分布式id
- java.util包下的UUID:随机无意义字符串,不能排序,数据量大时候查询慢
- 独立数据库表的自增id:有单点故障的隐患,且并发量大的情况下,写入和查询有性能瓶颈;如果使用主从模式,主节点挂了也没法同步
- redis incr自增命令:依赖于redis,如果redis宕机有可能存在数据丢失,造成重复
- SnowFlake 雪花算法构成-64位
- 1、首位0,代表正数
- 2、41位时间戳:41位最大值/一年的ms值,得到69年9个月6天零15小时47分35秒;时间部分计算方式为:当前时间戳-初始时间戳(自己设定开始时间)再往前移22位(64-1-41=22),因为时间戳所在段位是在64位的第2-42位置上
- 3、10位机器id,代表可以部署2^10=1024台机器,一般分为前5位数据中心id:dataCenterId。。后5位机器id:workerId;dataCenterId代表机房数,可以手动设置编号1,2,3等,保证不同就行。workerId可以用ip来区分(比如取ip的最后一段),同一机房内能保证不重复。
dataCenterId需要往前移17位(64-1-41-5=17),workerId需要移动12位(64-1-41-5-5=12),剩下的12位就是序列号了,不用移动。可以根据实际情况设置dataCenterId和workerId。比如,我们的机房就一个,只占1位,那么剩下的9位就是workerId的了,这时候,dataCenterId需要左移21位(64-1-41-1=21)- 4、12位序列号:同一个机房同一台机器在一个ms时间内,可以生成2^12-1=4095个序列号
- 5、注意:避免并发状况,一定要加锁