1. ⼀致性Hash算法
- ⼀致性哈希算法思路如下:
⾸先有⼀条直线,直线开头和结尾分别定为为1和2的32次⽅减1,这相当于⼀个地址,对于这样⼀条线,弯过来构成⼀个圆环形成闭环,这样的⼀个圆环称为hash环。我们把服务器的ip或者主机名求hash值然后对应到hash环上,那么针对客户端⽤户,也根据它的ip进⾏hash求值,对应到环上某个位置,然后如何确定⼀个客户端路由到哪个服务器处理呢?按照顺时针⽅向找最近的服务器节点
- ⼀致性哈希算法在服务节点太少时,容易因为节点分部不均匀⽽造成数据倾斜问题。例如系统中只有两台服务器,其环分布如下,节点2只能负责⾮常⼩的⼀段,⼤量的客户端请求落在了节点1上,这就是数据(请求)倾斜问题
- 为了解决这种数据倾斜问题,⼀致性哈希算法引⼊了虚拟节点机制,即对每⼀个服务节点计算多个哈希,每个计算结果位置都放置⼀个此服务节点,称为虚拟节点。具体做法可以在服务器ip或主机名的后⾯增加编号来实现。⽐如,可以为每台服务器计算三个虚拟节点,于是可以分别计算 “节点1的ip#1”、“节点1的ip#2”、“节点1的ip#3”、“节点2的ip#1”、“节点2的ip#2”、“节点2的ip#3”的哈希值,于是形成六个虚拟节点,当客户端被路由到虚拟节点的时候其实是被路由到该虚拟节点所对应的真实节点
- 一致性Hash算法(含虚拟节点)
public class ConsistentHashWithVirtual {
public static void main(String[] args) {
// 1.初始化:把服务器节点IP的哈希值对应到哈希环上
//定义服务器ip
String[] tomcatServers = new String[]{"123.111.0.0","123.101.3.1","111.20.35.2","123.98.26.3"};
//SortedMap接口主要提供有序的Map实现。TreeMap实现了SortedMap接口,保证了有序性。默认的排序是根据key值进行升序排序
SortedMap<Integer, String> hashServerMap = new TreeMap<Integer, String>();
// 2.定义针对每个真实服务器虚拟出来几个节点
int virtualCount = 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 < virtualCount; i++) {
int virtualHash = Math.abs((tomcatServer + "#" + i).hashCode());
//存储hash值与虚拟ip的对应关系
hashServerMap.put(virtualHash,"----由虚拟节点"+ tomcatServer + "#" + i + "映射过来的请求到真实服务器:"+ tomcatServer);
}
}
//定义客户端IP
String[] clients = new String[]{"10.78.12.3","113.25.63.1","126.12.3.8"};
for (String client : clients) {
// 3.针对客户端IP求出哈希值
int clientHash = Math.abs(client.hashCode());
// 4.针对客户端,找到能够处理当前客户端请求的服务器(哈希环上顺时针最近服务器)
//根据客户端ip的哈希值去找出哪一个服务器节点能够处理
SortedMap<Integer, String> map = hashServerMap.tailMap(clientHash);//tailMap(K fromKey) 获取一个子集。其所有对象的 key 的值大于等于 fromKey
if (map.isEmpty()) {
//如果map是空的,说明要取哈希环上的顺时针第一台服务器
Integer firstKey = hashServerMap.firstKey();//fisrtKey() 获取第一个(排在最低的)对象的 Key
System.out.println("==========>>>>客户端:" + client + " 被路由到服务器:" + hashServerMap.get(firstKey));
} else {
//如果map不空,取哈希环上顺时针最近服务器
Integer firstKey = map.firstKey();
System.out.println("==========>>>>客户端:" + client + " 被路由到服务器:" + hashServerMap.get(firstKey));
}
}
}
}
- Nginx 配置⼀致性Hash负载均衡策略
需要下载安装ngx_http_upstream_consistent_hash 模块,是⼀个第三⽅模块
2. 集群时钟同步问题
- 时钟此处指服务器时间,如果集群中各个服务器时钟不⼀致势必导致⼀系列问题,试想 “集群是各个服务器⼀起团队化作战,⼤家⼯作都不在⼀个点上,岂不乱了套!”
集群时钟同步思路
- 分布式集群中各个服务器节点都可以连接互联⽹
#使⽤ ntpdate ⽹络时间同步命令
ntpdate -u ntp.api.bz #从⼀个时间服务器同步时间
- 分布式集群中某⼀个服务器节点可以访问互联⽹或者所有节点都不能够访问互联⽹思路:
选取集群中的⼀个服务器节点A(172.17.0.17)作为时间服务器(整个集群时间从这台服务器同步,如果这台服务器能够访问互联⽹,可以让这台服务器和⽹络时间保持同步,如果不 能就⼿动设置⼀个时间)
3. 分布式ID解决⽅案
- SnowFlake 雪花算法(可以⽤,推荐)
雪花算法是Twitter推出的⼀个⽤于⽣成分布式ID的策略。
雪花算法是⼀个算法,基于这个算法可以⽣成ID,⽣成的ID是⼀个long型,那么在Java中⼀个long型是8个字节,算下来是64bit,如下是使⽤雪花算法⽣成的⼀个ID的⼆进制形式示意:
- 借助Redis的Incr命令获取全局唯⼀ID(推荐)
Redis Incr 命令将 key 中储存的数字值增⼀。如果 key 不存在,那么 key 的值会先被初始化为 0,然后再执⾏ INCR 操作。
4. 分布式调度问题
- 什么是分布式任务调度?有两层含义
- 运⾏在分布式集群环境下的调度任务(同⼀个定时任务程序部署多份,只应该有⼀个定时任务在执
⾏) - 分布式调度—>定时任务的分布式—>定时任务的拆分(即为把⼀个⼤的作业任务拆分为多个⼩的作
业任务,同时执⾏)
- 定时任务与消息队列的区别
- 共同点
- 异步处理; ⽐如注册、下单事件
- 应⽤解耦; 不管定时任务作业还是MQ都可以作为两个应⽤之间的⻮轮实现应⽤解耦,这个⻮轮可以中转数据,当然单体服务不需要考虑这些,服务拆分的时候往往都会考虑
- 流量削峰; 双⼗⼀的时候,任务作业和MQ都可以⽤来扛流量,后端系统根据服务能⼒定时处理订单或者从MQ抓取订单抓取到⼀个订单到来事件的话触发处理,对于前端⽤户来说看到的结果是已经 下单成功了,下单是不受任何影响的
- 本质不同
定时任务作业是时间驱动,⽽MQ是事件驱动; 时间驱动是不可代替的,⽐如⾦融系统每⽇的利息结算,不是说利息来⼀条(利息到来事件)就算⼀下,⽽往往是通过定时任务批量计算; 所以,定时任务作业更倾向于批处理,MQ倾向于逐条处理;
- 分布式调度框架Elastic-Job
主要功能介绍
- 分布式调度协调; 在分布式环境中,任务能够按指定的调度策略执⾏,并且能够避免同⼀任务多实例重复执⾏
- 丰富的调度策略;基于成熟的定时任务作业框架Quartz cron表达式执⾏定时任务 弹性扩容缩容
当集群中增加某⼀个实例,它应当也能够被选举并执⾏任务;当集群减少⼀个实例 时,它所执⾏的任务能被转移到别的实例来执⾏。- 失效转移;某实例在任务执⾏失败后,会被转移到其他实例执⾏
- 错过执⾏作业重触发; 若因某种原因导致作业错过执⾏,⾃动记录错过执⾏的作业,并在上次作业完成后⾃动触发。
- ⽀持并⾏调度; ⽀持任务分⽚,任务分⽚是指将⼀个任务分为多个⼩任务项在多个实例同时执⾏。
- 作业分⽚⼀致性;当任务被分⽚后,保证同⼀分⽚在分布式环境中仅⼀个执⾏实例。
- 需要结合zk作为分布式协调
- 自定义任务类
public class Demojob implements SimpleJob {
private static long i = 0;
/**
* execute方法中写我们的业务逻辑
* execute方法每次定时任务执行都会执行一次
*/
@Override
public void execute(ShardingContext shardingContext) {
//获取分片编号
int shardingItem = shardingContext.getShardingItem();
//获取分片参数
String shardingParameter = shardingContext.getShardingParameter();
System.out.println("我是一个定时任务执行逻辑-" + i + ",当前分片-" + shardingItem + ",分片参数-" + shardingParameter);
i += 1;
}
}
- 主类
public class ElasticJobMain {
public static void main(String[] args) {
// 1.配置分布式协调服务(注册中心)zookeeper
ZookeeperConfiguration zookeeperConfiguration = new ZookeeperConfiguration("192.168.0.106:2181", "elastic-job");
CoordinatorRegistryCenter coordinatorRegistryCenter = new ZookeeperRegistryCenter(zookeeperConfiguration);
coordinatorRegistryCenter.init();
// 2.配置任务(时间事件、定时任务业务逻辑、调度器),和Quartz步骤一致,只不过对象不同
//配置时间事件
JobCoreConfiguration jobCoreConfiguration = JobCoreConfiguration.newBuilder("myElastic", "*/1 * * * * ?", 3).shardingItemParameters("0=分片0,1=分片1,2=分片2").build();
//将配置的时间事件和业务类相关联
SimpleJobConfiguration simpleJobConfiguration = new SimpleJobConfiguration(jobCoreConfiguration, Demojob.class.getName());
//配置调度器,把任务执行起来
JobScheduler jobScheduler = new JobScheduler(coordinatorRegistryCenter, LiteJobConfiguration.newBuilder(simpleJobConfiguration).overwrite(true).build());
jobScheduler.init();
}
}
5. Session共享问题
- Session问题原因分析
出现这个问题的原因,从根本上来说是因为Http协议是⽆状态的协议。客户端和服务端在某次会话中产⽣的数据不会被保留下来,所以第⼆次请求服务端⽆法认识到你曾经来过, Http为什么要设计为⽆状态协议?早期都是静态⻚⾯⽆所谓有⽆状态,后来有动态的内容更丰富,就需要有状态,出现了两种⽤于保持Http状态的技术,那就是Cookie和Session。⽽出现上述不停让登录的问题,分析如下图: - 场景:nginx默认轮询策略
- 解决Session⼀致性的⽅案
- Nginx的 IP_Hash 策略(可以使⽤)
同⼀个客户端IP的请求都会被路由到同⼀个⽬标服务器 - 结合Redis。Session共享,Session集中存储(推荐)
- 引入jar
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
- 配置redis
spring.redis.database=0
spring.redis.host=127.0.0.1
spring.redis.port=6379
- 在核心自动类上添加注解
@EbableRedisHttpSession