前言
文章内容输出来源:拉勾教育Java高薪训练营;
在面试的时候,尝尝会被问到分布式一些相关的问题,比如如何确保在分布式环境下 session 一致,分布式 ID 等等。我在拉勾训练营学习的时候,刚好老师有讲到,我就整理下来了。
image-20200703104027902
解决hash 冲突的方法:
开放寻址法:1放进去了,6再来的时候,向前或者向后找空闲位置存放,不好的地⽅,如果数组⻓度定
义好了⽐如10,⻓度不能扩展,来了11个数据,不管Hash冲突不冲突,肯定存不下这么多数据
拉链法:数据⻓度定义好了,怎么存储更多内容呢,算好Hash值,在数组元素存储位置放了⼀个链表.也就是出现重读的,我们就通过链表存储起来,也叫链地址法。hashmap 就是采用在这种结构哒。
nginx 的负载均衡
我们知道 nginx 实现负载均衡有三种方式。轮寻、设置权重、配置 ip_hash 。‘’
其中配置 ip_hash 就使用到了 hash 算法。Nginx的 IP_hash策略可以在客户端ip不变的情况下,将其发出的请求始终路由到同⼀个⽬标服务器上,实现会话粘滞,避免处理session共享问题 。对ip地址或者sessionid进⾏计算哈希值,哈希值与服务器数量进⾏取模运算,得到的值就是当前请求应该被路由到的服务器编号,如此,同⼀个客户端ip发送过来的请求就可以路由到同⼀个⽬标服务器,实现会话粘滞。
分布式存储
以分布式内存数据库Redis为例,集群中有redis1,redis2,redis3 三台Redis服务器那么,在进⾏数据存储时,<key1,value1>数据存储到哪个服务器当中呢?针对key进⾏hash处理hash(key1)%3=index, 使⽤余数index锁定存储的具体服务器节点。
普通hash 简单实现
我们写一个普通hash 的简单实现。
public class GeneralHash {
public static void main(String[] args) {
// 定义客户端IP
String[] clients = new String[]{
"192.168.1.61",
"192.168.1.48",
"192.168.1.44",
"192.168.1.42",
"192.168.1.43",
"192.168.1.73",
"192.168.1.83",
"192.168.1.23"};
//服务器数量
int count =5;
for (String client : clients) {
int hash = Math.abs(client.hashCode());
int index=hash%count;
System.out.println("IP 为:"+client+ "服务器编号为:"+index);
}
}
}
结果:
IP 为:192.168.1.61服务器编号为:2
IP 为:192.168.1.48服务器编号为:2
IP 为:192.168.1.44服务器编号为:3
IP 为:192.168.1.42服务器编号为:1
IP 为:192.168.1.43服务器编号为:2
IP 为:192.168.1.73服务器编号为:0
IP 为:192.168.1.83服务器编号为:1
IP 为:192.168.1.23服务器编号为:0
比如上面的 ip_hash 是利用取模运算,但是如果出现一个服务宕机或者出现扩容和缩容的情况,就会导致重新hash .那么原来的会话就会丢失。
首先有一条一条直线。为 0 到 2 的 32 次方 -1。然后将首尾相连,形成一个闭环,也就是 hash 环。
image-20200703132632412
如下图所示:我们的服务器节点散落在这个环上。当请求的 ip 通过一致性 hash 可以找到离他最近的节点,从而进行访问。
当出现服务宕机,或者缩容时。只会影响部分的 IP 进行重新指向。
同理,增加服务器的时候,也只会影响部分 IP 重新指定。
demo 实现
我们使用 代码来实现一个一致性 hash 算法 借助 SortedMap 的 tailMap(K fromKey)获取一个子集。其所有对象的 key 的值大于等于 fromKey 。然后 firstKey() 获取最小 key
/**
* 无虚拟节点
*/
public class ConsistentHashNoVirtual {
public static void main(String[] args) {
//step 1:将服务器的节点存到 hash 环中。
// 定义服务器节点
String[] servers = new String[]{
"192.168.1.10",
"192.168.1.30",
"192.168.1.50",
"192.168.1.70",
"192.168.1.90",
"192.168.2.10",
"192.168.1.30",
"192.168.2.80"};
//定义一个hash 环
SortedMap<Integer, String> hashServerMap = new TreeMap<>();
for (String server : servers) {
int hash = Math.abs(server.hashCode());
hashServerMap.put(hash,server);
}
//step 2:将客户端的IP映射到 hash 环中。
String[] clients = new String[]{
"10.168.1.10",
"10.168.2.10",
"10.168.3.10",
"10.168.4.10",
"10.168.5.10",
"10.168.6.10",
"10.168.7.10",
"192.168.1.40",
"192.168.1.60",
"192.168.1.80",
"192.168.2.00",
"192.168.2.30",
"192.168.2.50",
"192.168.3.50",
"192.168.4.50",
"192.168.2.90"};
for (String client : clients) {
//step3 针对客户端,找到能够处理当前客户端请求的服务器(哈希环上顺时针最近)
// 根据客户端ip的哈希值去找出哪一个服务器节点能够处理()
int clienthash = Math.abs(client.hashCode());
//tailMap(K fromKey)获取一个子集。其所有对象的 key 的值大于等于 fromKey
SortedMap<Integer, String> tailMap = hashServerMap.tailMap(clienthash);
Integer firstKey = hashServerMap.firstKey();
if(!tailMap.isEmpty()){
firstKey = tailMap.firstKey();
}
System.out.println("客户端:" + client + " 被路由到服务器:" + hashServerMap.get(firstKey));
}
}
}
结果:
问题
从上面的结果来看,这种会存在一问题,也就是可能出现数据倾斜。⼀致性哈希算法在服务节点太少时,容易因为节点分部不均匀⽽造成数据倾斜问题。例如系统中只有两台服务器,其环分布如下,节点2只能负责⾮常⼩的⼀段,⼤量的客户端请求落在了节点1上,这就是数据(请求)倾斜问题。
解决方案
一致性hash 算法引入了虚拟节点机制。为每个服务节点计算多个hash,每个hash 放置一个服务器几点,称为虚拟节点。
具体做法可以在服务器ip或主机名的后⾯增加编号来实现。⽐如,可以为每台服务器计算三个虚拟节点,于是可以分别计算 “节点1的ip#1”、“节点1的ip#2”、“节点1的ip#3”、“节点2的ip#1”、“节点2的ip#2”、“节点2的ip#3”的哈希值,于是形成六个虚拟节点,当客户端被路由到虚拟节点的时候其实是被路由到该虚拟节点所对应的真实节点
demo 实现
我们在上面代码的基础上增加,虚拟节点。这里的 rehash 算法需要实际调整,我这里只是随便写的。
image-20200703164212953
就在原来的基础上加了这部分代码,我们再运行看下。可以看到原来后面这部分就重新分配了。
当我们的服务部署在多台服务器时,如果这些服务器的时间不一致必定会导致各种问题。
所以需要保证集群所在的服务器时间保持一致。
可以联网
#使⽤ ntpdate ⽹络时间同步命令
ntpdate -u ntp.api.bz #从⼀个时间服务器同步时间
不能联网
1、如果有 restrict default ignore,注释掉它
2、添加如下⼏⾏内容
restrict 172.17.0.0 mask 255.255.255.0 nomodify notrap # 放开局
域⽹同步功能,172.17.0.0是你的局域⽹⽹段
server 127.127.1.0 # local clock
fudge 127.127.1.0 stratum 10
3、重启⽣效并配置ntpd服务开机⾃启动
service ntpd restart
chkconfig ntpd on
image-20200703171349839
解决方法:
雪花算法是⼀个算法,基于这个算法可以⽣成ID,⽣成的ID是⼀个long型,那么在Java中⼀个long 型是8个字节,算下来是64bit,如下是使⽤雪花算法⽣成的⼀个ID的⼆进制形式示意
image-20200703173115718
什么是分布式调度
定时任务与消息队列的区别
共同点:
不同点:
定时任务作业是时间驱动,⽽MQ是事件驱动;
时间驱动是不可代替的,⽐如⾦融系统每⽇的利息结算,不是说利息来⼀条(利息到来事件)就算⼀下,⽽往往是通过定时任务批量计算;所以,定时任务作业更倾向于批处理,MQ倾向于逐条处理;
Elastic-Job是当当⽹开源的⼀个分布式调度解决⽅案,基于Quartz⼆次开发的,由两个相互独⽴的⼦项⽬Elastic-Job-Lite和Elastic-Job-Cloud组成。我们要学习的是 Elastic-Job-Lite,它定位为轻量级⽆中⼼化解决⽅案,使⽤Jar包的形式提供分布式任务的协调服务,⽽Elastic-Job-Cloud⼦项⽬需要结合Mesos以及Docker在云环境下使⽤。
Elastic-Job的github地址:
https://github.com/elasticjob
主要功能介绍
引用
<!-- https://mvnrepository.com/artifact/com.dangdang/elastic-job-lite-core
-->
<dependency>
<groupId>com.dangdang</groupId>
<artifactId>elastic-job-lite-core</artifactId>
<version>2.1.5</version>
</dependency>
session 丢失问题
从根本上来说是因为Http协议是⽆状态的协议。客户端和服务端在某次会话中产⽣的数据不会被保留下来,所以第⼆次请求服务端⽆法认识到你曾经来过, Http为什么要设计为⽆状态协议?早期都是静态⻚⾯⽆所谓有⽆状态,后来有动态的内容更丰富,就需要有状态,出现了两种⽤于保持Http状态的技术,那就是Cookie和Session。⽽出现上述不停让登录的问题,分析如下图:
Nginx的 IP_Hash 策略(可以使⽤)
同⼀个客户端IP的请求都会被路由到同⼀个⽬标服务器,也叫做会话粘滞
优点:
配置简单,不⼊侵应⽤,不需要额外修改代码
缺点:
服务器重启Session丢失
存在单点负载⾼的⻛险
单点故障问题
Session复制(不推荐)
多个tomcat之间通过修改配置⽂件,达到Session之间的复制
image-20200705163259385
优点:
不⼊侵应⽤
便于服务器⽔平扩展
能适应各种负载均衡策略
服务器重启或者宕机不会造成Session丢失
缺点:
性能低
内存消耗
不能存储太多数据,否则数据越多越影响性能
延迟性
Session共享,Session集中存储(推荐)
image-20200705163354860
优点:
能适应各种负载均衡策略
服务器重启或者宕机不会造成Session丢失
扩展能⼒强
适合⼤集群数量使⽤
缺点:
对应⽤有⼊侵,引⼊了和Redis的交互代码
这些问题都是面试会别问到的高频问题,所以赶紧 get 吧