前言
课程内容
-
第⼀部分:⼀致性Hash算法
-
第⼆部分:集群时钟同步问题
-
第三部分:分布式ID解决⽅案
-
第四部分:分布式调度问题(定时任务的分布式)
-
第五部分:Session共享(⼀致性)问题
分布式和集群介绍
分布式和集群是不⼀样的,分布式⼀定是集群,但是集群不⼀定是分布式(因为集群就是多个实例⼀起⼯作,分布式将⼀个系统拆分之后那就是多个实例;集群并不⼀定是分布式,因为复制型的集群不是拆分⽽是复制)
第⼀部分 ⼀致性Hash算法
Hash算法,⽐如说在安全加密领域MD5、SHA等加密算法,在数据存储和查找⽅⾯有Hash表等, 以上都应⽤到了Hash算法。
为什么需要使⽤Hash?
Hash算法较多的应⽤在数据存储和查找领域,最经典的就是Hash表,它的查询效率⾮常之⾼,其中的哈希算法如果设计的⽐较ok的话,那么Hash表的数据查询时间复杂度可以接近于O(1),示例
需求:提供⼀组数据 1,5,7,6,3,4,8,对这组数据进⾏存储,然后随便给定⼀个数n,请你判断n是否存在于刚才的数据集中?
list:List[1,5,7,6,3,4,8]
// 通过循环判断来实现
for(int element: list) {
if(element == n) {
如果相等,说明n存在于数据集中
}}
以上这种⽅法叫做顺序查找法 :这种⽅式我们是通过循环来完成,⽐较原始,效率也不⾼
⼆分查找:排序之后折半查找,相对于顺序查找法会提⾼⼀些效率,但是效率也并不是特别好我能否不循环!不⼆分!⽽是通过⼀次查询就把数据n从数据集中查询出来???可以!
定义⼀个数组,数组⻓度⼤于等于数据集⻓度,此处⻓度为9,数据1就存储在下标为1的位置,3就存储在下标为3的元素位置,,,依次类推。
这个时候,我想看下5存在与否,只需要判断list.get(5) array[5] 是否为空,如果为空,代表5不存在于数据集,如果不为空代表5在数据集当中,通过⼀次查找就达到了⽬的,时间复杂度为O(1)。
这种⽅式叫做“直接寻址法”:直接把数据和数组的下标绑定到⼀起,查找的时候,直接array[n]就取出了数据
优点:速度快,⼀次查找得到结果
缺点:
1)浪费空间,⽐如 1,5,7,6,3,4,8,12306 ,最⼤值12306 ,按照上述⽅式需要定义⼀个⽐如⻓度为12307的数组,但是只存储零星的⼏个数据,其他位置空间都浪费着
2)数据如:1,5,7,6,3,4,8,1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,2最⼤值12,⽐如开辟13个空间,存储不了这么多内容
现在,换⼀种设计,如果数据是3,5,7,12306,⼀共4个数据,我们开辟任意个空间,⽐如5个,那么具体数据存储到哪个位置呢,我们可以对数据进⾏求模(对空间位置数5),根据求模余数确定存储位置的下标,⽐如3%5=3,就可以把3这个数据放到下标为3的位置上,12306%5=1,就把12306这个数据存储到下标为1的位置上
上⾯对数据求模 (数据%空间位置数) 他就是⼀个hash算法,只不过这是⼀种⽐较普通⼜简单的hash算法,这种构造Hash算法的⽅式叫做除留余数法
如果数据是1,6,7,8,把这4个数据存储到上⾯的数组中
在此基础上采⽤开放寻址法(了解)
开放寻址法:1放进去了,6再来的时候,向前或者向后找空闲位置存放,不好的地⽅,如果数组⻓度定义好了⽐如10,⻓度不能扩展,来了11个数据,不管Hash冲突不冲突,肯定存不下这么多数据
拉链法:数据⻓度定义好了,怎么存储更多内容呢,算好Hash值,在数组元素存储位置放了⼀个链表
如果Hash算法设计的⽐较好的话,那么查询效率会更接近于O(1),如果Hash算法设计的⽐较low,那么查询效率就会很低了
所以,Hash表的查询效率⾼不⾼取决于Hash算法,hash算法能够让数据平均分布,既能够节省空间⼜能提⾼查询效率。Hash算法的研究是很深的⼀⻔学问,⽐较复杂,⻓久以来,Hash表内部的Hash算法也⼀直在更新,很多数学家也在研究。
除留余数法 3%5
线性构造Hash算法
直接寻址法也是⼀种构造Hash的⽅式,只不过更简单,表达式:H(key)=key
⽐如H(key)=a*key + b(a,b是常量)
hashcode其实也是通过⼀个Hash算法得来的
第 1 节 Hash算法应⽤场景
Hash算法在分布式集群架构中的应⽤场景
Hash算法在很多分布式集群产品中都有应⽤,⽐如分布式集群架构Redis、Hadoop、ElasticSearch,Mysql分库分表,Nginx负载均衡等
主要的应⽤场景归纳起来两个
请求的负载均衡(⽐如nginx的ip_hash策略)
Nginx的IP_hash策略可以在客户端ip不变的情况下,将其发出的请求始终路由到同⼀个⽬标服务
器上,实现会话粘滞,避免处理session共享问题
如果没有IP_hash策略,那么如何实现会话粘滞?
可以维护⼀张映射表,存储客户端IP或者sessionid与具体⽬标服务器的映射关系
<ip,tomcat1>
缺点
1)那么,在客户端很多的情况下,映射表⾮常⼤,浪费内存空间
2)客户端上下线,⽬标服务器上下线,都会导致重新维护映射表,映射表维护成本很⼤
如果使⽤哈希算法,事情就简单很多,我们可以对ip地址或者sessionid进⾏计算哈希值,哈希值与服务器数量进⾏取模运算,得到的值就是当前请求应该被路由到的服务器编号,如此,同⼀个客户端ip发送过来的请求就可以路由到同⼀个⽬标服务器,实现会话粘滞。
分布式存储
以分布式内存数据库Redis为例,集群中有redis1,redis2,redis3 三台Redis服务器
那么,在进⾏数据存储时,<key1,value1>数据存储到哪个服务器当中呢?针对key进⾏hash处理hash(key1)%3=index, 使⽤余数index锁定存储的具体服务器节点
第 2 节 普通Hash算法存在的问题
普通Hash算法存在⼀个问题,以ip_hash为例,假定下载⽤户ip固定没有发⽣改变,现在tomcat3出现了问题,down机了,服务器数量由3个变为了2个,之前所有的求模都需要重新计算。
如果在真实⽣产情况下,后台服务器很多台,客户端也有很多,那么影响是很⼤的,缩容和扩容都会存在这样的问题,⼤量⽤户的请求会被路由到其他的⽬标服务器处理,⽤户在原来服务器中的会话都会丢失。
第 3 节 ⼀致性Hash算法
⼀致性哈希算法思路如下:
⾸先有⼀条直线,直线开头和结尾分别定为为1和2的32次⽅减1,这相当于⼀个地址,对于这样⼀条线,弯过来构成⼀个圆环形成闭环,这样的⼀个圆环称为hash环。我们把服务器的ip或者主机名求hash值然后对应到hash环上,那么针对客户端⽤户,也根据它的ip进⾏hash求值,对应到环上某个位置,然后如何确定⼀个客户端路由到哪个服务器处理呢?按照顺时针⽅向找最近的服务器节点
假如将服务器3下线,服务器3下线后,原来路由到3的客户端重新路由到服务器4,对于其他客户端没有影响只是这⼀⼩部分受影响(请求的迁移达到了最⼩,这样的算法对分布式集群来说⾮常合适的,避免了⼤量请求迁移 )
增加服务器5之后,原来路由到3的部分客户端路由到新增服务器5上,对于其他客户端没有影响只是这⼀⼩部分受影响(请求的迁移达到了最⼩,这样的算法对分布式集群来说⾮常合适的,避免了⼤量请求迁移 )
1)如前所述,每⼀台服务器负责⼀段,⼀致性哈希算法对于节点的增减都只需重定位环空间中的⼀⼩部分数据,具有较好的容错性和可扩展性。但是,⼀致性哈希算法在服务节点太少时,容易因为节点分部不均匀⽽造成数据倾斜问题。例如系统中只有两台服务器,其环分布如下,节点2只能负责⾮常⼩的⼀段,⼤量的客户端请求落在了节点1上,这就是数据(请求)倾斜问题
2)为了解决这种数据倾斜问题,⼀致性哈希算法引⼊了虚拟节点机制,即对每⼀个服务节点计算多个哈希,每个计算结果位置都放置⼀个此服务节点,称为虚拟节点。具体做法可以在服务器ip或主机名的后⾯增加编号来实现。⽐如,可以为每台服务器计算三个虚拟节点,于是可以分别计算 “节点1的ip#1”、“节点1的ip#2”、“节点1的ip#3”、“节点2的ip#1”、“节点2的ip#2”、“节点2的ip#3”的哈希值,于是形成六个虚拟节点,当客户端被路由到虚拟节点的时候其实是被路由到该虚拟节点所对应的真实节点
第 4 节 ⼿写实现⼀致性Hash算法
4.1 普通Hash算法实现
代码实现
package config;
/**
* @author lane
* @date 2021年05月12日 下午7:52
*/
public class GeneralHash {
public static void main(String[] args) {
//定义客户端
String[] clients = new String[]{"10.78.12.3","113.25.63.1","126.12.3.8"};
//定义服务器的数量0,1,2
int serverCount = 3;
//根据ip的hash对服务器数量取模获取其index为对应的服务器
for (String ip:clients) {
//取hash值
int i = ip.hashCode();
//取绝对值
int abs = Math.abs(i);
//对应的服务器
int index = abs % serverCount;
System.out.println("客户端IP为:"+ip+"路由的服务器是:"+index);
}
}
}
打印结果
#服务器3台时
客户端IP为:10.78.12.3路由的服务器是:1
客户端IP为:113.25.63.1路由的服务器是:2
客户端IP为:126.12.3.8路由的服务器是:1
#服务器2台时
客户端IP为:10.78.12.3路由的服务器是:0
客户端IP为:113.25.63.1路由的服务器是:0
客户端IP为:126.12.3.8路由的服务器是:1
#服务器5台时
客户端IP为:10.78.12.3路由的服务器是:4
客户端IP为:113.25.63.1路由的服务器是:0
客户端IP为:126.12.3.8路由的服务器是:1
4.2 一致性Hash算法实现(不带虚拟节点)
代码实现
package config;
import java.util.SortedMap;
import java.util.TreeMap;
/**
* 一致性哈希没有虚拟节点
* @author lane
* @date 2021年05月13日 上午11:12
*/
public class ConsisiHashNoVirtual {
public static void main(String[] args) {
//1. 初始化服务器节点到哈希环上
//定义服务器
String[] servers = new String[]{"102.12.0.11","172.127.1.22","126.162.2.33","132.123.3.44"};
//初始化sortedMap,是一个有序到map,tail方法可以获取大于某个key的map集合
SortedMap<Integer,String> serverMap = new TreeMap<>();
//对应到哈希环上
for (String server: servers) {
//求哈希
int serverHash = server.hashCode();
//绝对值
serverHash = Math.abs(serverHash);
//存入sortedMap当中
serverMap.put(serverHash,server);
}
//2. 定义客户端
String[] clients = new String[]{"10.78.12.3","113.25.63.1","126.12.3.8"};
//3. 获取客户端的哈希,并获取对应的服务器
for (String client:clients) {
int clientHash = Math.abs(client.hashCode());
//获取大于此key的客户端map
SortedMap<Integer, String> tailMap = serverMap.tailMap(clientHash);
//判断是否为空,若是则为服务器端第一个节点服务器
//否则就是大于此key的map第一个节点服务器
if (tailMap.isEmpty()){
// 取哈希环上的顺时针第⼀台服务器
Integer firstKey = serverMap.firstKey();
System.out.println("客户端IP为:"+client+"对应的服务器为:"+serverMap.get(firstKey));
}else{
Integer firstKey = tailMap.firstKey();
System.out.println("客户端IP为:"+client+"对应的服务器为:"+tailMap.get(firstKey));
}
}
}
}
测试结果
#4台服务器
客户端IP为:10.78.12.3对应的服务器为:132.123.3.44
客户端IP为:113.25.63.1对应的服务器为:102.12.0.11
客户端IP为:126.12.3.8对应的服务器为:132.123.3.44
#3台服务器
客户端IP为:10.78.12.3对应的服务器为:126.162.2.33
客户端IP为:113.25.63.1对应的服务器为:102.12.0.11
客户端IP为:126.12.3.8对应的服务器为:126.162.2.33
#5台服务器
客户端IP为:10.78.12.3对应的服务器为:81.68.211.19
客户端IP为:113.25.63.1对应的服务器为:102.12.0.11
客户端IP为:126.12.3.8对应的服务器为:111.18.211.20
4.3 ⼀致性Hash算法实现(含虚拟节点)
主要改变
就是在添加服务器节点的时候每个服务器添加了3个虚拟节点的哈希,对应服务器节点,这样其它地方就不需要改变,这些虚拟节点的key依然对应真实的服务器
代码实现
package config;
import java.util.SortedMap;
import java.util.TreeMap;
/**
* 一致性哈希有虚拟节点,主要就是添加服务器节点的时候添加虚拟节点
* @author lane
* @date 2021年05月13日 上午11:12
*/
public class ConsisiHashWithVirtual {
public static void main(String[] args) {
//1. 初始化服务器节点到哈希环上
//定义服务器
String[] servers = new String[]{"102.12.0.11","171.127.1.22","106.162.2.33"};
//初始化sortedMap,是一个有序到map,tail方法可以获取大于某个key的map集合
SortedMap<Integer,String> serverMap = new TreeMap<>();
//对应到哈希环上
for (String server: servers) {
//求哈希
int serverHash = server.hashCode();
//绝对值
serverHash = Math.abs(serverHash);
//存入sortedMap当中
serverMap.put(serverHash,server);
//给每个服务器节点添加3个虚拟节点
for (int i = 0; i < 3; i++) {
int virtualHash = Math.abs((server+"#"+i).hashCode());
serverMap.put(virtualHash,"虚拟节点映射的服务器:"+server);
}
}
//2. 定义客户端
String[] clients = new String[]{"10.78.12.3","113.25.63.1","126.12.3.8"};
//3. 获取客户端的哈希,并获取对应的服务器
for (String client:clients) {
int clientHash = Math.abs(client.hashCode());
//获取大于此key的客户端map
SortedMap<Integer, String> tailMap = serverMap.tailMap(clientHash);
//判断是否为空,若是则为服务器端第一个节点服务器
//否则就是大于此key的map第一个节点服务器
if (tailMap.isEmpty()){
// 取哈希环上的顺时针第⼀台服务器
Integer firstKey = serverMap.firstKey();
System.out.println("客户端IP为:"+client+"对应的服务器为:"+serverMap.get(firstKey));
}else{
Integer firstKey = tailMap.firstKey();
System.out.println("客户端IP为:"+client+"对应的服务器为:"+tailMap.get(firstKey));
}
}
}
}
测试结果
#3台服务器每个3个虚拟节点,3个客户端
客户端IP为:10.78.12.3对应的服务器为:106.162.2.33
客户端IP为:113.25.63.1对应的服务器为:102.12.0.11
客户端IP为:126.12.3.8对应的服务器为:虚拟节点映射的服务器:106.162.2.33
第 5 节 Nginx 配置⼀致性Hash负载均衡策略
5.1 简单介绍
ngx_http_upstream_consistent_hash 模块是⼀个负载均衡器,使⽤⼀个内部⼀致性hash算法来选择合适的后端节点。
该模块可以根据配置参数采取不同的⽅式将请求均匀映射到后端机器,
consistent_hash $remote_addr:可以根据客户端ip映射
consistent_hash $request_uri:根据客户端请求的uri映射
consistent_hash $args:根据客户端携带的参数进⾏映
ngx_http_upstream_consistent_hash 模块是⼀个第三⽅模块,需要我们下载安装后使⽤
5.2 安装Nginx第三方⼀致性hash负载均衡
下载
github下载nginx⼀致性hash负载均衡模块 https://github.com/replay/ngx_http_consistent_hash
将下载的压缩包上传到nginx服务器,并解压
我们已经编译安装过nginx,此时进⼊当时nginx的源码⽬录,执⾏如下命令
#添加一致性哈希模块在nginx编译前的文件下root/nginx1.7/
./configure --add-module=/root/ngx_http_consistent_hash-master
#编译
make
make install
#配置在/use/local/nginx/conf/nginx_conf
consistent_hash $remote_addr;#可以根据客户端ip映射
consistent_hash $request_uri;#根据客户端请求的uri映射
consistent_hash $args; #根据客户端携带的参数进⾏映
具体命令
➜ nginx-1.17.8 ./configure --add-module=/root/ngx_http_consistent_hash-master
➜ nginx-1.17.8 make
➜ nginx-1.17.8 make install
➜ nginx-1.17.8 cd /usr/local/nginx/conf
➜ conf vim nginx.conf
➜ conf cd ../sbin
➜ sbin ./nginx -s stop
➜ sbin ./nginx
vim添加内容如下
实现效果同一个ip访问每次都是同一个tomcat了
第⼆部分 集群时钟同步问题
第 1 节 时钟不同步导致的问题
时钟此处指服务器时间,如果集群中各个服务器时钟不⼀致势必导致⼀系列问题,试想 “集群是各个服务器⼀起团队化作战,⼤家⼯作都不在⼀个点上,岂不乱了套!”
举⼀个例⼦,电商⽹站业务中,新增⼀条订单,那么势必会在订单表中增加了⼀条记录,该条记录中应该会有“下单时间”这样的字段,往往我们会在程序中获取当前系统时间插⼊到数据库或者直接从数据库服务器获取时间。那我们的订单⼦系统是集群化部署,或者我们的数据库也是分库分表的集群化部署,然⽽他们的系统时钟缺不⼀致,⽐如有⼀台服务器的时间是昨天,那么这个时候下单时间就成了昨天,那我们的数据将会混乱!如下
第 2 节 集群时钟同步配置
2.1 分布式集群中各个服务器节点都可以连接互联⽹
思路:
#查看服务器的时间
date
#修改时间
date -s '08:00:00'
#同步最新时间
ntpdate -u ntp.api.bz
windows有计划任务
Linux也有定时任务,crond,可以使⽤linux的定时任务,每隔10分钟执⾏⼀次ntpdate命令
2.2 0个会1个节点可以访问互联⽹
具体实现
1)选取集群中的⼀个服务器节点A(172.17.0.17)作为时间服务器(整个集群时间从这台服务器同步,如果这台服务器能够访问互联⽹,可以让这台服务器和⽹络时间保持同步,如果不能就⼿动设置⼀个时间)
-
⾸先设置好A的时间
-
把A配置为时间服务器(修改/etc/ntp.conf⽂件)
1、如果有 restrict default ignore,注释掉它 2、添加如下⼏⾏内容 # 放开局域⽹同步功能,172.17.0.0是你的局域⽹⽹段 restrict 172.17.0.0 mask 255.255.255.0 nomodify notrap # local clock 本地的时钟同步 server 127.127.1.0 fudge 127.127.1.0 stratum 10 3、重启⽣效并配置ntpd服务开机⾃启动 service ntpd restart #开机自启动 chkconfig ntpd on
-
集群中其他节点就可以从A服务器同步时间了
ntpdate 172.17.0.17
第三部分 分布式ID解决⽅案
第 1 节 为什么需要分布式ID
分布式集群环境下的全局唯⼀ID必须统一才行,不然会造成ID重复问题
第 2 节 常见的一致性ID生成方式
2.1 UUID(可以使用)
UUID 是指Universally Unique Identifier,翻译为中⽂是通⽤唯⼀识别码
产⽣重复 UUID 并造成错误的情况⾮常低,是故⼤可不必考虑此问题。一般的生产环境可以使用。
Java中得到⼀个UUID,可以使⽤java.util包提供的⽅法
缺点就是不够直观,没有规律
package identify;
import java.util.UUID;
/**
* @author lane
* @date 2021年05月13日 下午5:50
*/
public class GenerateUUID {
public static void main(String[] args) {
String uuid = UUID.randomUUID().toString();
for (int i = 0; i <10 ; i++) {
uuid = UUID.randomUUID().toString();
System.out.println("UUID为:"+uuid);
}
}
}
具体效果
UUID为:bbcd619a-e5a4-4137-8d16-cc6f8a9608ea
UUID为:6865f8c3-583d-4642-bd09-09050c902fa4
UUID为:8ee3d9a4-5a7b-4b3b-92b4-1db362763a82
UUID为:b7957b06-6ecc-478a-bea1-639dd66ec247
UUID为:e133e587-e9ea-4acb-92e3-19a7c5b87d41
UUID为:a286d27b-4d05-4b58-acf7-604dbb868229
UUID为:6efa9e8f-a646-4ffc-9a35-485f943869b9
UUID为:3aaaa236-a669-4503-84f0-fd388daa15c7
UUID为:c5c89b0d-a544-45f1-97d2-3b524eb3e4e8
UUID为:e4627b65-0f3e-4df6-9ad7-9015f7a36c4c
2.2 独⽴数据库的⾃增ID
⽐如A表分表为A1表和A2表,那么肯定不能让A1表和A2表的ID⾃增,那么ID怎么获取呢?
我们可以单独的创建⼀个Mysql数据库,在这个数据库中创建⼀张表,这张表的ID设置为⾃增,其他地⽅需要全局唯⼀ID的时候,就模拟向这个Mysql数据库的这张表中模拟插⼊⼀条记录,此时ID会⾃增,然后我们可以通过Mysql的select last_insert_id() 获取到刚刚这张表中⾃增⽣成的ID。
⽐如,我们创建了⼀个数据库实例global_id_generator,在其中创建了⼀个数据表,表结构如下:
-- ----------------------------
-- Table structure for DISTRIBUTE_ID
-- ----------------------------
DROP TABLE IF EXISTS `DISTRIBUTE_ID`;
CREATE TABLE `DISTRIBUTE_ID` (
`id` bigint(32) NOT NULL AUTO_INCREMENT COMMENT '主键',
`createtime` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
当分布式集群环境中哪个应⽤需要获取⼀个全局唯⼀的分布式ID的时候,就可以使⽤代码连接这个数据库实例,执⾏如下sql语句即可。
insert into DISTRIBUTE_ID(createtime) values(NOW());
select LAST_INSERT_ID();
注意:
1)这⾥的createtime字段⽆实际意义,是为了随便插⼊⼀条数据以⾄于能够⾃增id。
2)使⽤独⽴的Mysql实例⽣成分布式id,虽然可⾏,但是性能和可靠性都不够好,因为你需要代码连接到数据库才能获取到id,性能⽆法保障,另外mysql数据库实例挂掉了,那么就⽆法获取分布式id了。
3)有⼀些开发者⼜针对上述的情况将⽤于⽣成分布式id的mysql数据库设计成了⼀个集群架构,那么其实这种⽅式现在基本不⽤,因为过于麻烦了。
SnowFlake 雪花算法(推荐)
雪花算法是Twitter推出的⼀个⽤于⽣成分布式ID的策略。
雪花算法是⼀个算法,基于这个算法可以⽣成ID,⽣成的ID是⼀个long型,那么在Java中⼀个long型是8个字节,算下来是64bit,如下是使⽤雪花算法⽣成的⼀个ID的⼆进制形式示意:
另外,⼀些互联⽹公司也基于上述的⽅案封装了⼀些分布式ID⽣成器,⽐如滴滴的tinyid(基于数据库实现)、百度的uidgenerator(基于SnowFlake)和美团的leaf(基于数据库和SnowFlake)等。
代码如下
package identify;
/**
* 官方推出,Scala编程语言来实现的
* Java前辈用Java语言实现了雪花算法
* @author lane
* @date 2021年05月13日 下午6:07
*/
public class IdWorker{
//下面两个每个5位,加起来就是10位的工作机器id
private long workerId; //工作id
private long datacenterId; //数据id
//12位的序列号
private long sequence;
public IdWorker(long workerId, long datacenterId, long sequence){
// sanity check for workerId
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0",maxWorkerId));
}
if (datacenterId > maxDatacenterId || datacenterId < 0) {
throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0",maxDatacenterId));
}
System.out.printf("worker starting. timestamp left shift %d, datacenter id bits %d, worker id bits %d, sequence bits %d, workerid %d",
timestampLeftShift, datacenterIdBits, workerIdBits, sequenceBits, workerId);
this.workerId = workerId;
this.datacenterId = datacenterId;
this.sequence = sequence;
}
//初始时间戳
private long twepoch = 1288834974657L;
//长度为5位
private long workerIdBits = 5L;
private long datacenterIdBits = 5L;
//最大值
private long maxWorkerId = -1L ^ (-1L << workerIdBits);
private long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
//序列号id长度
private long sequenceBits = 12L;
//序列号最大值
private long sequenceMask = -1L ^ (-1L << sequenceBits);
//工作id需要左移的位数,12位
private long workerIdShift = sequenceBits;
//数据id需要左移位数 12+5=17位
private long datacenterIdShift = sequenceBits + workerIdBits;
//时间戳需要左移位数 12+5+5=22位
private long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
//上次时间戳,初始值为负数
private long lastTimestamp = -1L;
public long getWorkerId(){
return workerId;
}
public long getDatacenterId(){
return datacenterId;
}
public long getTimestamp(){
return System.currentTimeMillis();
}
//下一个ID生成算法
public synchronized long nextId() {
long timestamp = timeGen();
//获取当前时间戳如果小于上次时间戳,则表示时间戳获取出现异常
if (timestamp < lastTimestamp) {
System.err.printf("clock is moving backwards. Rejecting requests until %d.", lastTimestamp);
throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds",
lastTimestamp - timestamp));
}
//获取当前时间戳如果等于上次时间戳
//说明:还处在同一毫秒内,则在序列号加1;否则序列号赋值为0,从0开始。
if (lastTimestamp == timestamp) { // 0 - 4095
sequence = (sequence + 1) & sequenceMask;
if (sequence == 0) {
timestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0;
}
//将上次时间戳值刷新
lastTimestamp = timestamp;
/**
* 返回结果:
* (timestamp - twepoch) << timestampLeftShift) 表示将时间戳减去初始时间戳,再左移相应位数
* (datacenterId << datacenterIdShift) 表示将数据id左移相应位数
* (workerId << workerIdShift) 表示将工作id左移相应位数
* | 是按位或运算符,例如:x | y,只有当x,y都为0的时候结果才为0,其它情况结果都为1。
* 因为个部分只有相应位上的值有意义,其它位上都是0,所以将各部分的值进行 | 运算就能得到最终拼接好的id
*/
return ((timestamp - twepoch) << timestampLeftShift) |
(datacenterId << datacenterIdShift) |
(workerId << workerIdShift) |
sequence;
}
//获取时间戳,并与上次时间戳比较
private long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
//获取系统时间戳
private long timeGen(){
return System.currentTimeMillis();
}
public static void main(String[] args) {
//注意前两个参数的取值范围为5位2进制
IdWorker worker = new IdWorker(21,10,0);
for (int i = 0; i < 10; i++) {
System.out.println(worker.nextId());
}
}
}
打印信息
1392784219517046784
1392784219517046785
1392784219517046786
1392784219517046787
1392784219517046788
1392784219517046789
1392784219517046790
1392784219517046791
1392784219517046792
1392784219517046793
2.3 借助Redis的Incr命令获取全局唯⼀ID(推荐)
Redis Incr 命令将 key 中储存的数字值增⼀。如果 key 不存在,那么 key 的值会先被初始化为 0,然后再执⾏ INCR 操作。
代码实现
package identify;
import redis.clients.jedis.Jedis;
/**
* @author lane
* @date 2021年05月13日 下午7:14
*/
public class GenerateRedisId {
public static void main(String[] args) {
Jedis jedis = new Jedis("81.68.211.196",6379);
try {
for (int i = 0; i < 10; i++) {
Long id = jedis.incr("id");
System.out.println("使用redis自动生成ID:" + id);
}
}finally {
if (null != jedis) {
jedis.close();
}
}
}
}
打印效果
使用redis自动生成ID:1
使用redis自动生成ID:2
使用redis自动生成ID:3
使用redis自动生成ID:4
使用redis自动生成ID:5
使用redis自动生成ID:6
使用redis自动生成ID:7
使用redis自动生成ID:8
使用redis自动生成ID:9
使用redis自动生成ID:10
第四部分 分布式调度问题
简单理解
调度—>定时任务
分布式调度—>在分布式集群环境下定时任务这件事
Elastic-job(当当⽹开源的分布式调度框架)
第 1 节 定时任务的场景
定时任务形式:每隔⼀定时间/特定某⼀时刻执⾏
例如:
- 订单审核、出库
- 订单超时⾃动取消、⽀付退款
- 礼券同步、⽣成、发放作业
- 物流信息推送、抓取作业、退换货处理作业
- 数据积压监控、⽇志监控、服务可⽤性探测作业
- 定时备份数据
- ⾦融系统每天的定时结算
- 数据归档、清理作业
- 报表、离线数据分析作业
第 2 节 什么是分布式调度
什么是分布式任务调度?有两层含义
1)运⾏在分布式集群环境下的调度任务(同⼀个定时任务程序部署多份,只应该有⼀个定时任务在执⾏)
2)分布式调度—>定时任务的分布式—>定时任务的拆分(即为把⼀个⼤的作业任务拆分为多个⼩的作业任务,同时执⾏)
第 3 节 定时任务与消息队列的区别
3.1 共同点
异步处理
⽐如注册、下单事件
应⽤解耦
不管定时任务作业还是MQ都可以作为两个应⽤之间的⻮轮实现应⽤解耦,这个⻮轮可以中转数据,当然单体服务不需要考虑这些,服务拆分的时候往往都会考虑
流量削峰
双⼗⼀的时候,任务作业和MQ都可以⽤来扛流量,后端系统根据服务能⼒定时处理订单或者从MQ抓取订单抓取到⼀个订单到来,事件触发处理,对于前端⽤户来说看到的结果是已经下单成功了,下单是不受任何影响的
3.2 本质不同
定时任务作业是时间驱动,⽽MQ是事件驱动;
时间驱动是不可代替的
定时任务作业更倾向于批处理,MQ倾向于逐条处理;
⽐如⾦融系统每⽇的利息结算,不是说利息来⼀条(利息到来事件)就算⼀下,⽽往往是通过定时任务批量计算;
第 4 节 定时任务的实现⽅式
定时任务的实现⽅式有多种。早期没有定时任务框架的时候,我们会使⽤JDK中的Timer机制和多线程机制(Runnable+线程休眠)来实现定时或者间隔⼀段时间执⾏某⼀段程序;后来有了定时任务框架,⽐如⼤名鼎鼎的Quartz任务调度框架,使⽤时间表达式(包括:秒、分、时、⽇、周、年)配置某⼀个任务什么时间去执⾏:
4.1 任务调度框架Quartz(不推荐)
引入依赖
<!--引入定时任务的jar包-->
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.3.2</version>
</dependency>
代码实现类
package quartz;
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
/**
* @author lane
* @date 2021年05月14日 上午10:51
*/
public class QuartzDemo {
//1. 创建定时任务调度器(类似于公交调度站)
public static Scheduler createScheduler() throws SchedulerException {
SchedulerFactory schedulerFactory = new StdSchedulerFactory();
Scheduler scheduler = schedulerFactory.getScheduler();
return scheduler;
}
//2. 创建任务 类似与公交运输
public static JobDetail createJob(){
JobBuilder jobBuilder = JobBuilder.newJob(JobDemo.class);
jobBuilder.withIdentity("jobName","myJob");
JobDetail jobDetail = jobBuilder.build();
return jobDetail;
}
/**
* 3、创建作业任务时间触发器(类似于公交车出车时间表)
* cron表达式由七个位置组成,空格分隔
* 1、Seconds(秒) 0~59
* 2、Minutes(分) 0~59
* 3、Hours(小时) 0~23
* 4、Day of Month(天)1~31,注意有的月份不足31天
* 5、Month(月) 0~11,或者 JAN,FEB,MAR,APR,MAY,JUN,JUL,AUG,SEP,OCT,NOV,DEC
* 6、Day of Week(周) 1~7,1=SUN或者 SUN,MON,TUE,WEB,THU,FRI,SAT
* 7、Year(年)1970~2099 可选项
*示例:
* 0 0 11 * * ? 每天的11点触发执行一次
* 0 30 10 1 * ? 每月1号上午10点半触发执行一次
*/
public static Trigger createTrigger(){
CronTrigger trigger = TriggerBuilder.newTrigger()
.withIdentity("triggerName", "myTrigger")
.startNow() //2s触发一次
.withSchedule(CronScheduleBuilder.cronSchedule("*/2 * * * * ?"))
.build();
return trigger;
}
//4. 主程序执行
public static void main(String[] args) {
try {
//1. 创建任务调度主程序
Scheduler scheduler = QuartzDemo.createScheduler();
//2. 创建任务
JobDetail jobDetail = QuartzDemo.createJob();
//3. 创建任务触发器
Trigger trigger = QuartzDemo.createTrigger();
//4. 执行调度任务
scheduler.scheduleJob(jobDetail,trigger);
scheduler.start();
} catch (SchedulerException e) {
e.printStackTrace();
}
}
}
任务类
package quartz;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import java.util.Calendar;
/**
* @author lane
* @date 2021年05月14日 下午2:36
*/
public class JobDemo implements Job {
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
System.out.println("执行定时任务"+ Calendar.getInstance().getTime());
}
}
实现效果
执行定时任务Fri May 14 14:40:18 CST 2021
执行定时任务Fri May 14 14:40:20 CST 2021
执行定时任务Fri May 14 14:40:22 CST 2021
执行定时任务Fri May 14 14:40:24 CST 2021
执行定时任务Fri May 14 14:40:26 CST 2021
执行定时任务Fri May 14 14:40:28 CST 2021
以上,是回顾⼀下任务调度框架Quartz的⼤致⽤法,那么在分布式架构环境中使⽤Quartz已经不能更好的满⾜我们需求,我们可以使⽤专业的分布式调度框架,这⾥我们推荐使⽤Elastic-job。
第 5 节 分布式调度框架Elastic-Job
5.1 Elastic-Job介绍
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
主要功能介绍
- 分布式调度协调
- 在分布式环境中,任务能够按指定的调度策略执⾏,并且能够避免同⼀任务多实例重复执⾏
- 丰富的调度策略 基于成熟的定时任务作业框架Quartz cron表达式执⾏定时任务
- 弹性扩容缩容 当集群中增加某⼀个实例,它应当也能够被选举并执⾏任务;当集群减少⼀个实例时,它所执⾏的任务能被转移到别的实例来执⾏。
- 失效转移 某实例在任务执⾏失败后,会被转移到其他实例执⾏
- 错过执⾏作业重触发 若因某种原因导致作业错过执⾏,⾃动记录错过执⾏的作业,并在上次作业完成后⾃动触发。
- ⽀持并⾏调度 ⽀持任务分⽚,任务分⽚是指将⼀个任务分为多个⼩任务项在多个实例同时执⾏。
- 作业分⽚⼀致性 当任务被分⽚后,保证同⼀分⽚在分布式环境中仅⼀个执⾏实例。
5.2 Elastic-Job-Lite应⽤
jar包(API) + 安装zk软件
Elastic-Job依赖于Zookeeper进⾏分布式协调,所以需要安装Zookeeper软件(3.4.6版本以上),关于Zookeeper,此处我们不做详解,在阶段三会有深度学习,我们此处需要明⽩Zookeeper的本质功能:
安装Zookeeper(此处单例配置)
1)我们使⽤3.4.10版本,在linux平台解压下载的zookeeper-3.4.10.tar.gz
2)进⼊conf⽬录,cp zoo_sample.cfg zoo.cfg
- 进⼊bin⽬录,启动zk服务,默认端口2181
#进⼊bin⽬录,启动zk服务
#启动
./zkServer.sh start
#停⽌
./zkServer.sh stop
#查看状态
./zkServer.sh status
Zookeeper的树形节点结构图
5.2 Elastic-Job-Lite代码实现
定时任务实例
需求:每隔两秒钟执⾏⼀次定时任务(resume表中未归档的数据归档到resume_bak表中,每次归档1条记录)
1)resume_bak和resume表结构完全⼀样
2)resume表中数据归档之后不删除,只将state置为"已归档"
sql语句
CREATE TABLE `resume` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
`sex` varchar(255) DEFAULT NULL,
`phone` varchar(255) DEFAULT NULL,
`address` varchar(255) DEFAULT NULL,
`education` varchar(255) DEFAULT NULL,
`state` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1001 DEFAULT CHARSET=utf8;
SET FOREIGN_KEY_CHECKS = 1;
CREATE TABLE `resume_bak` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
`sex` varchar(255) DEFAULT NULL,
`phone` varchar(255) DEFAULT NULL,
`address` varchar(255) DEFAULT NULL,
`education` varchar(255) DEFAULT NULL,
`state` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1001 DEFAULT CHARSET=utf8;
SET FOREIGN_KEY_CHECKS = 1;
INSERT INTO resume VALUES (1,'张三','男','153','beijing','doctor','未归档');
INSERT INTO resume VALUES (2,'张三2','男','153','beijing','doctor','未归档');
INSERT INTO resume VALUES (3,'张三3','男','153','beijing','doctor','未归档');
INSERT INTO resume VALUES (4,'张三4','男','153','shanghai','doctor','未归档');
INSERT INTO resume VALUES (5,'张三5','男','153','guangzhou','bachelor','未归档');
INSERT INTO resume VALUES (6,'张三6','男','153','shenzhen','master','未归档');
依赖实现
<!--引入elastic-job jar-->
<dependency>
<groupId>com.dangdang</groupId>
<artifactId>elastic-job-lite-core</artifactId>
<version>2.1.5</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.46</version>
</dependency>
JDBC工具类
package elasticjob;
import java.sql.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class JdbcUtil {
//url
private static String url = "jdbc:mysql://localhost:3306/job?characterEncoding=utf8&useSSL=false";
//user
private static String user = "root";
//password
private static String password = "root";
//驱动程序类
private static String driver = "com.mysql.jdbc.Driver";
static {
try {
Class.forName(driver);
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public static Connection getConnection() {
try {
return DriverManager.getConnection(url, user, password);
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
}
public static void close(ResultSet rs, PreparedStatement ps, Connection con) {
if (rs != null) {
try {
rs.close();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
if (ps != null) {
try {
ps.close();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
if (con != null) {
try {
con.close();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
}
}
}
/***
* DML操作(增删改)
* 1.获取连接数据库对象
* 2.预处理
* 3.执行更新操作
* @param sql
* @param obj
*/
//调用者只需传入一个sql语句,和一个Object数组。该数组存储的是SQL语句中的占位符
public static void executeUpdate(String sql,Object...obj) {
Connection con = getConnection();//调用getConnection()方法连接数据库
PreparedStatement ps = null;
try {
ps = con.prepareStatement(sql);//预处理
for (int i = 0; i < obj.length; i++) {//预处理声明占位符
ps.setObject(i + 1, obj[i]);
}
ps.executeUpdate();//执行更新操作
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
close(null, ps, con);//调用close()方法关闭资源
}
}
/***
* DQL查询
* Result获取数据集
*
* @param sql
* @param obj
* @return
*/
public static List<Map<String,Object>> executeQuery(String sql, Object...obj) {
Connection con = getConnection();
ResultSet rs = null;
PreparedStatement ps = null;
try {
ps = con.prepareStatement(sql);
for (int i = 0; i < obj.length; i++) {
ps.setObject(i + 1, obj[i]);
}
rs = ps.executeQuery();
//new 一个空的list集合用来存放查询结果
List<Map<String, Object>> list = new ArrayList<>();
//获取结果集的列数
int count = rs.getMetaData().getColumnCount();
//对结果集遍历每一条数据是一个Map集合,列是k,值是v
while (rs.next()) {
//一个空的map集合,用来存放每一行数据
Map<String, Object> map = new HashMap<String, Object>();
for (int i = 0; i < count; i++) {
Object ob = rs.getObject(i + 1);//获取值
String key = rs.getMetaData().getColumnName(i + 1);//获取k即列名
map.put(key, ob);
}
list.add(map);
}
return list;
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
close(rs, ps, con);
}
return null;
}
}
定时任务业务逻辑处理类ArchivieJob
package elasticjob;
import com.dangdang.ddframe.job.api.ShardingContext;
import com.dangdang.ddframe.job.api.simple.SimpleJob;
import java.util.List;
import java.util.Map;
/**
* ElasticJobLite定时任务业务逻辑处理类
*/
public class ArchivieJob implements SimpleJob {
/**
* 需求:resume表中未归档的数据归档到resume_bak表中,每次归档1条记录
* execute方法中写我们的业务逻辑(execute方法每次定时任务执行都会执行一次)
* @param shardingContext
*/
@Override
public void execute(ShardingContext shardingContext) {
int shardingItem = shardingContext.getShardingItem();
System.out.println("=====>>>>当前分片:" + shardingItem);
// 获取分片参数
String shardingParameter = shardingContext.getShardingParameter(); // 0=bachelor,1=master,2=doctor
// 1 从resume表中查询出1条记录(未归档)
String selectSql = "select * from resume where state='未归档' and education='"+ shardingParameter +"' limit 1";
List<Map<String, Object>> list = JdbcUtil.executeQuery(selectSql);
if(list == null || list.size() ==0 ) {
System.out.println("数据已经处理完毕!!!!!!");
return;
}
// 2 "未归档"更改为"已归档"
Map<String, Object> stringObjectMap = list.get(0);
long id = (long) stringObjectMap.get("id");
String name = (String) stringObjectMap.get("name");
String education = (String) stringObjectMap.get("education");
System.out.println("=======>>>>id:" + id + " name:" + name + " education:" + education);
String updateSql = "update resume set state='已归档' where id=?";
JdbcUtil.executeUpdate(updateSql,id);
// 3 归档这条记录,把这条记录插入到resume_bak表
String insertSql = "insert into resume_bak select * from resume where id=?";
JdbcUtil.executeUpdate(insertSql,id);
}
}
分布式调度定时任务核心
package elasticjob;
import com.dangdang.ddframe.job.config.JobCoreConfiguration;
import com.dangdang.ddframe.job.config.simple.SimpleJobConfiguration;
import com.dangdang.ddframe.job.lite.api.JobScheduler;
import com.dangdang.ddframe.job.lite.config.LiteJobConfiguration;
import com.dangdang.ddframe.job.reg.base.CoordinatorRegistryCenter;
import com.dangdang.ddframe.job.reg.zookeeper.ZookeeperConfiguration;
import com.dangdang.ddframe.job.reg.zookeeper.ZookeeperRegistryCenter;
/**
* 分布式调度定时任务核心方法
* @author lane
* @date 2021/5/15 上午11:13
*/
public class ElasticJobMain {
public static void main(String[] args) {
// 配置分布式协调服务(注册中心)Zookeeper
//zk配置
ZookeeperConfiguration zookeeperConfiguration = new ZookeeperConfiguration("localhost:2181","data-archive-job");
//创建注册中心对象
CoordinatorRegistryCenter coordinatorRegistryCenter = new ZookeeperRegistryCenter(zookeeperConfiguration);
//注册中心对象初始化
coordinatorRegistryCenter.init();
// 配置任务(时间事件、定时任务业务逻辑、调度器)
JobCoreConfiguration jobCoreConfiguration = JobCoreConfiguration
//设置每2秒执行1次,分片个数
.newBuilder("archive-job", "*/2 * * * * ?", 3)
//分片执行逻辑,按类型执行0号执行bachelor,1号执行master,2号执行doctor
.shardingItemParameters("0=bachelor,1=master,2=doctor").build();
//关联任务配置
SimpleJobConfiguration simpleJobConfiguration = new SimpleJobConfiguration(jobCoreConfiguration,ArchivieJob.class.getName());
//关联调度
JobScheduler jobScheduler = new JobScheduler(coordinatorRegistryCenter, LiteJobConfiguration.newBuilder(simpleJobConfiguration).overwrite(true).build());
//初始化
jobScheduler.init();
}
}
单片实现效果
保证了同一时间只有一个任务在执行
#当只有一个main线程执行的时候
=======>>>>id:1 name:张三 education:doctor
=======>>>>id:2 name:张三2 education:doctor
=======>>>>id:3 name:张三3 education:doctor
=======>>>>id:4 name:张三4 education:doctor
=======>>>>id:5 name:张三5 education:master
=======>>>>id:6 name:张三6 education:master
#当有两个个main线程执行的时候
#main1开始执行
=======>>>>id:204 name:张三4 education:doctor
=======>>>>id:205 name:张三5 education:master
=======>>>>id:206 name:张三6 education:master
=======>>>>id:207 name:张三7 education:master
#main2开始接着main1执行,main1不再执行
=======>>>>id:208 name:张三8 education:master
=======>>>>id:209 name:张三9 education:bachelor
=======>>>>id:210 name:张三10 education:bachelor
=======>>>>id:211 name:张三11 education:bachelor
=======>>>>id:212 name:张三12 education:bachelor
#当main2挂掉的时候,main1继续执行
=======>>>>id:207 name:张三7 education:master
=======>>>>id:309 name:张三9 education:bachelor
=======>>>>id:310 name:张三10 education:bachelor
=======>>>>id:311 name:张三11 education:bachelor
=======>>>>id:312 name:张三12 education:bachelor
分三片实现效果
#1个main执行的时候
=====>>>>当前分片:2
=====>>>>当前分片:1
=====>>>>当前分片:0
=======>>>>id:5 name:张三5 education:master
=======>>>>id:1 name:张三 education:doctor
=======>>>>id:9 name:张三9 education:bachelor
#2个main执行的时候
#main1
=====>>>>当前分片:1
=======>>>>id:1020 name:张三7 education:master
=====>>>>当前分片:1
=======>>>>id:1021 name:张三8 education:master
=====>>>>当前分片:1
=======>>>>id:1022 name:张三9 education:master
#main2
=====>>>>当前分片:2
=====>>>>当前分片:0
=======>>>>id:2002 name:张三2 education:doctor
=======>>>>id:1013 name:张三12 education:bachelor
#3个main执行的时候,分工明确,执行效率大幅度上升
#main1
=====>>>>当前分片:2
=======>>>>id:2901 name:张三 education:doctor
=====>>>>当前分片:2
=======>>>>id:2902 name:张三2 education:doctor
#main2
=====>>>>当前分片:1
=======>>>>id:1132 name:张三7 education:master
=====>>>>当前分片:1
=======>>>>id:1133 name:张三8 education:master
#main3
=====>>>>当前分片:0
=======>>>>id:1135 name:张三10 education:bachelor
=====>>>>当前分片:0
=======>>>>id:1136 name:张三11 education:bachelor
#挂掉两个main之后执行效果
=====>>>>当前分片:0
=====>>>>当前分片:2
=====>>>>当前分片:1
=======>>>>id:2811 name:张三11 education:bachelor
=======>>>>id:2905 name:张三5 education:master
=======>>>>id:5302 name:张三2 education:doctor
还是要使用连接池啊,每次执行sql也都关闭连接了,不知道几个连接没及时关闭,mysql就直接挂掉了
mysql实际测试
参考mysql连接数
mysql> show status like 'Threads%';
+-------------------+-------+
| Variable_name | Value |
+-------------------+-------+
| Threads_cached | 32 |
| Threads_connected | 10 |
| Threads_created | 50 |
| Threads_rejected | 0 |
| Threads_running | 1 |
+-------------------+-------+
5 rows in set (0.00 sec)
Threads_connected :这个数值指的是打开的连接数.
Threads_running :这个数值指的是激活的连接数,这个数值一般远低于connected数值.
Threads_connected 跟show processlist结果相同,表示当前连接数。准确的来说,Threads_running是代表当前并发数
如下图所示 当mysql连接的关闭速度小于创建速度的时候会到达mysql默认的最大连接数151,此时项目出现错误连接过多
,当连接关闭之后小于151,就又可以重新建立新的连接了
连接个数 6–>146–>152–>53
可以设置最大连接数set global max_connections=1000;
#查看最大连接数,默认是151
show variables like '%max_connection%';
#重新设置最大连接数
set global max_connections=1000;
#在/etc/my.cnf里面设置数据库的最大连接数
#max_connections = 1000
#查看连接数
show status like 'Threads%';
#查看当前用户的连接100之内
show processlist;
#查看当前用户所有的连接
show full processlist;
测试
1)可先启动⼀个进程,然后再启动⼀个进程(两个进程模拟分布式环境下,通⼀个定时任务部署了两份在⼯作)
2)两个进程逐个启动,观察现象
3)关闭其中执⾏的进程,观察现象
5.4 Elastic-Job-Lite轻量级去中⼼化的特点
如何理解轻量级和去中⼼化
Leader节点选举机制
每个Elastic-Job的任务执⾏实例App作为Zookeeper的客户端来操作ZooKeeper的znode
(1)多个实例同时创建/leader节点
(2)/leader节点只能创建⼀个,后创建的会失败,创建成功的实例会被选为leader节点,执⾏任务
5.5 任务分⽚
⼀个⼤的⾮常耗时的作业Job,⽐如:⼀次要处理⼀亿的数据,那这⼀亿的数据存储在数据库中,如果⽤⼀个作业节点处理⼀亿数据要很久,在互联⽹领域是不太能接受的,互联⽹领域更希望机器的增加去横向扩展处理能⼒。所以,ElasticJob可以把作业分为多个的task(每⼀个task就是⼀个任务分⽚),每⼀个task交给具体的⼀个机器实例去处理(⼀个机器实例是可以处理多个task的),但是具体每个task执⾏什么逻辑由我们⾃⼰来指定。
Strategy策略定义这些分⽚项怎么去分配到各个机器上去,默认是平均去分,可以定制,⽐如某⼀个机器负载 ⽐较⾼或者预配置⽐较⾼,那么就可以写策略。分⽚和作业本身是通过⼀个注册中⼼协调的,因为在分布式环境下,状态数据肯定集中到⼀点,才可以在分布式中沟通。
分⽚代码
5.6 弹性扩容
新增加⼀个运⾏实例app3,它会⾃动注册到注册中⼼,注册中⼼发现新的服务上线,注册中⼼会通知ElasticJob 进⾏重新分⽚,那么总得分⽚项有多少,那么就可以搞多少个实例机器,⽐如完全可以分1000⽚最多就可以有多少app实例,那么就可以搞1000台机器⼀起执⾏作业
注意:
1)分⽚项也是⼀个JOB配置,修改配置,重新分⽚,在下⼀次定时运⾏之前会重新调⽤分⽚算法,那么这个分⽚算法的结果就是:哪台机器运⾏哪⼀⽚,这个结果存储到zk中的,主节点会把分⽚给分好放到注册中⼼去,然后执⾏节点从注册中⼼获取信息(执⾏节点在定时任务开启的时候获取相应的分⽚)。
2)如果所有的节点挂掉值剩下⼀个节点,所有分⽚都会指向剩下的⼀个节点,这也是ElasticJob的⾼可⽤。
第五部分 Session共享问题
主要是分布式情况下登录如何 保证用户只需要登录一次,而不是每个服务器都要登录Session共享及Session保持或者叫做Session⼀致性
第 1 节 Session问题原因分析
出现这个问题的原因,从根本上来说是因为Http协议是⽆状态的协议。客户端和服务端在某次会话中产⽣的数据不会被保留下来,所以第⼆次请求服务端⽆法认识到你曾经来过, Http为什么要设计为⽆状态协议?早期都是静态⻚⾯⽆所谓有⽆状态,后来有动态的内容更丰富,就需要有状态,出现了两种⽤于保持Http状态的技术,那就是Cookie和Session。⽽出现上述不停让登录的问题,分析如下图:
第 2 节 解决Session⼀致性的⽅案
2.1 Nginx的 IP_Hash 策略(可以使⽤)
同⼀个客户端IP的请求都会被路由到同⼀个⽬标服务器,也叫做会话粘滞
优点:
- 配置简单,不⼊侵应⽤,不需要额外修改代码
缺点:
-
服务器重启Session丢失
-
存在单点负载⾼的⻛险
-
单点故障问题
2.2 Session复制(不推荐)
也即,多个tomcat之间通过修改配置⽂件,达到Session之间的复制
优点:
-
不⼊侵应⽤
-
便于服务器⽔平扩展
-
能适应各种负载均衡策略
-
服务器重启或者宕机不会造成Session丢失
缺点:
-
性能低
-
内存消耗
-
不能存储太多数据,否则数据越多越影响性能
-
延迟性
2.3 Session共享集中存储(推荐)
Session的本质就是缓存,那Session数据为什么不交给专业的缓存中间件呢?⽐如Redis
优点:
-
能适应各种负载均衡策略
-
服务器重启或者宕机不会造成Session丢失
-
扩展能⼒强
-
适合⼤集群数量使⽤
-
能适应各种负载均衡策略
缺点:
- 对应⽤有⼊侵,引⼊了和Redis的交互代码
代码实现
1)引⼊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>
2)配置redis
spring.redis.database=0
spring.redis.host=127.0.0.1
spring.redis.port=6379
3)添加注解
/**
* 打war包要继承SpringBootServletInitializer实现方法configure
* @author lane
* @date 2021/5/15 下午4:03
*/
@SpringBootApplication
@EnableCaching //这个2个注解
@EnableRedisHttpSession
public class NginxLoginProjectApplication extends SpringBootServletInitializer {
2.4 ssion共享集中存储之源代码实现
整体架构springboot项目
依赖xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.galaxy</groupId>
<artifactId>nginx-login-project</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>nginx-login-project</name>
<description>Demo project for Spring Boot</description>
<packaging>war</packaging>
<properties>
<java.version>11</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<scope>provided</scope>
</dependency>
<!-- 添加jstl标签库依赖模块 -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
</dependency>
<!--添加tomcat依赖模块.-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
<!-- 使用jsp引擎,springboot内置tomcat没有此依赖 -->
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<scope>provided</scope>
</dependency>
<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>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>1.4.2.RELEASE</version>
</plugin>
<!-- 配置打war包-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<configuration>
<warSourceExcludes>src/main/resources/**</warSourceExcludes>
<warName>nginxloginproject</warName>
</configuration>
</plugin>
</plugins>
<!-- 配置打jar包 < resources>将webapp打包进target/classes 目录,其中< targetPath>META-INF/resources</ targetPath>
指定将webapp打包到target/classes 目录的META-INF/resources 目录下,必须这样配置,否则会出错 -->
<!--<resources>
<resource>
<directory>src/main/webapp</directory>
<targetPath>META-INF/resources</targetPath>
</resource>
<resource>
<directory>src/main/resources</directory>
</resource>
</resources>-->
</build>
</project>
properties文件
#spring.http.encoding.force-response=true
server.port=8090
#jsp 支持
spring.mvc.view.suffix=.jsp
spring.mvc.view.prefix=/WEB-INF/jsp/
#关闭默认模板引擎
spring.thymeleaf.cache=false
spring.thymeleaf.enabled=false
spring.redis.database=0
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.password=
controller文件
登陆controller
package com.galaxy.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
/**
* @author lane
* @date 2021年05月15日 下午3:15
*/
@Controller
@RequestMapping("/login")
public class LoginController {
//只是作为跳转登陆方法
@RequestMapping("/toLogin")
public String toLogin() {
System.out.println("=================>跳转到登陆页面");
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName("login");
return "login";
}
//判断用户是否正确,是则登陆成功,否则跳转登陆页面
@RequestMapping("/judge")
public String JudgeUser(String username, String password, HttpSession session){
if ("admin".equals(username)&&"admin".equals(password)){
System.out.println("登陆成功,跳转主页");
//用户信息放入session当中
session.setAttribute("username",username+System.currentTimeMillis());
return "redirect:/home/result";
}else{
System.out.println("登陆失败,跳转登陆页");
return "redirect:toLogin";
}
}
}
成功页面controller
package com.galaxy.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* @author lane
* @date 2021年05月15日 下午3:28
*/
@Controller
@RequestMapping("/home")
public class HomeController {
@RequestMapping("/result")
public String homePage(){
return "result";
}
}
拦截器
具体的拦截器文件
package com.galaxy.interceptor;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.util.Enumeration;
/**
* @author lane
* @date 2021年05月15日 下午3:36
*/
public class RequestInterceptor implements HandlerInterceptor {
/**
* handle执行之前拦截
* 判断是否在session中存储了用户信息
*
* @author lane
* @date 2021/5/15 下午3:46
* @param request
* @param response
* @param handler
* @return boolean
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HttpSession session = request.getSession();
//获取sessionID
String sessionId = session.getId();
//获取session中的信息
Enumeration<String> attributeNames = session.getAttributeNames();
while (attributeNames.hasMoreElements()){
// 获取session键值
String name = attributeNames.nextElement().toString();
Object attribute = session.getAttribute(name);
System.out.println("------" + name + ":" + attribute +"--------\n");
}
Object username = session.getAttribute("username");
System.out.println("当前用户为:"+username);
System.out.println("当前URL为:"+request.getRequestURI());
if (username==null){
System.out.println("返回登陆页面");
response.sendRedirect(request.getContextPath() +"/login/toLogin");
return false;
}else {
return true;
}
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}
配置拦截器文件
package com.galaxy.interceptor;
import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
/**
* 注册拦截器到配置中心
* 定义拦截规则
* @author lane
* @date 2021年05月15日 下午3:33
*/
@Configuration
public class MyWebAppConfigurer extends WebMvcConfigurerAdapter {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new RequestInterceptor())
.addPathPatterns("/**")
.excludePathPatterns("/login/**","/error");
super.addInterceptors(registry);
}
}
springboot启动类
package com.galaxy;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
/**
* 打war包要继承SpringBootServletInitializer实现方法configure
* @author lane
* @date 2021/5/15 下午4:03
*/
@SpringBootApplication
@EnableCaching
@EnableRedisHttpSession
public class NginxLoginProjectApplication extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
return builder.sources(NginxLoginProjectApplication.class);
}
public static void main(String[] args) {
SpringApplication.run(NginxLoginProjectApplication.class, args);
}
}
前端页面
login.jsp
<%@ page isELIgnored="false" contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%>
<html>
<head>
<title>系统登录</title>
<style>
div{
width:300px;
height:100px;
position: absolute;
top:50%;
left:50%;
margin-top: -50px ;
margin-left:-150px;
}
</style>
</head>
<body>
<h2>我是服务器:${pageContext.request.localPort}</h2>
<h2>当前sessionId:${pageContext.session.id}</h2>
<div>
<form method="post" action="${pageContext.request.contextPath}/login/judge">
<table>
<tr>
<td>用户名:</td>
<td><input type="text" name="username"></td>
</tr>
<tr>
<td>密码:</td>
<td><input type="password" name="password"> <input type="submit" value="登录"></td>
</tr>
</table>
</form>
</div>
</body>
</html>
结果页result
<%@ page isELIgnored="false" contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%>
<html>
<head>
</head>
<body>
<h1>欢迎登录xxx系统!</h1><br/>
<h2>我是服务器:${pageContext.request.localPort}</h2>
<h2>当前sessionId:${pageContext.session.id}</h2>
</body>
</html>
Nginx配置文件
#server同级
upstream myserver{
server 127.0.0.1:8081;
server 127.0.0.1:8082;
}
server {
listen 9090;
server_name localhost;
#charset koi8-r;
#access_log logs/host.access.log main;
location / {
proxy_pass http://myserver/;
# root html;
# index index.html index.htm;
}
测试结果
两台tomcat轮询访问,session保持一致,不需要重复登陆
2.5 ssion共享集中存储之源码示意(了解)
该注解可以创建⼀个过滤器使得SpringSession替代HttpSession发挥作⽤,找到那个过滤器!
观察其⽗类,⽗类中有Filter
这个Filter就是SpringSession最核⼼的地⽅
在过滤器中将HttpServletRequest包装
本质就是⼀个HtppRequest,拥有同样的⽅法,找getSession
回到SessionRepositoryFilter的doFilterInternal⽅法
原理示意图
结语
参考应癫老师的笔记,自己会看一遍再复制过来,排版
代码手打的,如果有不懂的知识点百度之后再补充进来
注意网络安全,redis不要开放公网端口访问,要设置密码。
学习期间我的云服务被入侵,腾讯云的工程师解决不了,只能重装系统,具体参考我linux文章[Linux从零开始]