分布式和集群
分布式和集群是不一样的,分布式一定是集群,但是集群不⼀定是分布式(因为集群就是多个实例⼀起工作,分布式将⼀个系统拆分之后那就是多个实例;集群并不⼀定是分布式,因为复制型的集群不是拆分而是复制)
第一部分 一致性Hash算法
1.1 什么是Hash算法
百度百科对Hash的解释:
Hash算法简介
- Hash算法可以将一个数据转换为一个标志,这个标志和源数据的每一个字节都有十分紧密的关系。Hash算法还具有一个特点,就是很难找到逆向规律。
- Hash算法是一个广义的算法,也可以认为是一种思想,使用Hash算法可以提高存储空间的利用率,可以
提高数据的查询效率
,也可以做数字签名来保障数据传递的安全性
。所以Hash算法被广泛地应用在互联网应用中。 - Hash算法也被称为散列算法,Hash算法虽然被称为算法,但实际上它更像是一种思想。Hash算法没有一个固定的公式,只要符合散列思想的算法都可以被称为是Hash算法。
1.2 Hash算法的特点
一个优秀的 hash 算法,将能实现:
- 正向快速:给定明文和 hash 算法,在有限时间和有限资源内能计算出 hash 值。
- 逆向困难:给定(若干) hash 值,在有限时间内很难(基本不可能)逆推出明文。
- 输入敏感:原始输入信息修改一点信息,产生的 hash 值看起来应该都有很大不同。
- 冲突避免:很难找到两段内容不同的明文,使得它们的 hash 值一致(发生冲突)。即对于任意两个不同的数据块,其hash值相同的可能性极小;对于一个给定的数据块,找到和它hash值相同的数据块极为困难。
1.3 Hash算法应用场景
Hash算法在分布式集群架构中的应用场景:Hash算法在很多分布式集群产品中都有应用,比如分布式集群架构Redis、Hadoop、ElasticSearch,Mysql分库分表,Nginx负载均衡等。
主要的应用场景归纳起来两个:
(1)请求的负载均衡(比如nginx的ip_hash策略)
-
Nginx的IP_hash策略可以在客户端ip不变的情况下,将其发出的请求始终路由到同⼀个目标服务器上,实现会话粘滞,避免处理session共享问题。
-
如果没有IP_hash策略,那么如何实现会话粘滞?
可以维护⼀张映射表,存储客户端IP或者sessionid与具体⽬标服务器的映射关系 <ip,tomcat1>
缺点
1)那么,在客户端很多的情况下,映射表非常大,浪费内存空间
2)客户端上下线、目标服务器上下线,都会导致重新维护映射表,映射表维护成本很大
如果使用哈希算法,事情就简单很多,我们可以对ip地址或者 sessionid 进行计算哈希值,哈希值与服务器数量进行取模运算,得到的值就是当前请求应该被路由到的服务器编号,如此,同⼀个客户端ip发送过来的请求就可以路由到同⼀个目标服务器,实现会话粘滞。
(2)分布式存储
以分布式内存数据库Redis为例,集群中有redis1,redis2,redis3 三台Redis服务器。那么,在进行数据存储时,<key1,value1>数据存储到哪个服务器当中呢?
针对key进行 Hash 处理:hash(key1)%3=index,使用余数 index 锁定存储的具体服务器节点
1.4 普通Hash算法存在的问题
普通Hash算法存在⼀个问题,以ip_hash为例,假定下载用户ip固定没有发生改变,现在tomcat3出现了问题,down机了,服务器数量由3个变为了2个,之前所有的求模都需要重新计算。
如果在真实生产情况下,后台服务器很多台,客户端也有很多,那么影响是很大的,缩容和扩容都会存在这样的问题,大量用户的请求会被路由到其他的目标服务器处理,用户在原来服务器中的会话都会丢失。
1.5 ⼀致性Hash算法
(1)⼀致性哈希算法思路如下:
首先有⼀条直线,直线开头和结尾分别定为为1和2的32次方减1,这相当于⼀个地址。
对于这样⼀条线,弯过来构成⼀个圆环形成闭环,这样的⼀个圆环称为hash环。我们把服务器的ip或者主机名求hash值然后对应到hash环上,那么针对客户端用户,也根据它的ip进行hash求值,对应到环上某个位置,然后如何确定⼀个客户端路由到哪个服务器处理呢?按照顺时针方向找最近的服务器节点。
(2)一致性Hash算法缩容和扩容分析:
假如将服务器3下线,服务器3下线后,原来路由到3的客户端重新路由到服务器4,对于其他客户端没有影响只是这一小部分受影响(请求的迁移达到了最小,这样的算法对分布式集群来说非常合适的,避免了大量请求迁移 )
增加服务器5之后,原来路由到3的部分客户端路由到新增服务器5上,对于其他客户端没有影响只是这一小部分受影响(请求的迁移达到了最小,这样的算法对分布式集群来说非常合适的,避免了大量请求迁移 )
(3)一致性Hash算法+虚拟节点方案:
- 如前所述,每一台服务器负责⼀段,⼀致性哈希算法对于节点的增减都只需重定位环空间中的一小部分数据,具有较好的容错性和可扩展性。
但是,一致性哈希算法在服务节点太少时,容易因为节点分部不均匀而造成数据倾斜问题。例如系统中只有两台服务器,其环分布如下,节点2只能负责非常小的⼀段,大量的客户端请求落在了节点1上,这就是数据(请求)倾斜问题 - 为了解决这种数据倾斜问题,⼀致性哈希算法引入了虚拟节点机制,即对每⼀个服务节点计算多个哈希,每个计算结果位置都放置⼀个此服务节点,称为虚拟节点。
具体做法可以在服务器ip或主机名的后面增加编号来实现。比如,可以为每台服务器计算三个虚拟节点,于是可以分别计算 “节点1的ip#1”、“节点1的ip#2”、“节点1的ip#3”、“节点2的ip#1”、“节点2的ip#2”、“节点2的ip#3”的哈希值,于是形成六个虚拟节点,当客户端被路由到虚拟节点的时候其实是被路由到该虚拟节点所对应的真实节点。
1.6 手写实现一致性Hash算法
1.6.1 普通Hash算法实现
public class GeneralHash {
public static void main(String[] args) {
// 定义客户端IP
String[] clients = new String[]{"10.78.12.3", "113.25.63.1", "126.12.3.8"};
// 定义服务器数量
int serverCount = 3; // (编号对应0,1,2)
// hash(ip) % node_counts = index
//根据index锁定应该路由到的tomcat服务器
for (String client : clients) {
System.out.println(client.hashCode());
int hash = Math.abs(client.hashCode());
int index = hash % serverCount;
System.out.println("客户端:" + client + " 被路由到服务器编号为:" + index);
}
}
}
1.6.2 一致性Hash算法实现(不含虚拟节点)
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的哈希值去找出哪⼀个服务器节点能够处理()
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.6.3 一致性Hash算法实现(含虚拟节点)
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));
}
}
}
}
1.7 Nginx 配置一致性Hash负载均衡策略
- ngx_http_upstream_consistent_hash 模块是⼀个负载均衡器,使用一个内部一致性hash算法来选择合适的后端节点。
- 该模块可以根据配置参数采取不同的方式将请求均匀映射到后端机器
- consistent_hash $remote_addr:可以根据客户端ip映射
- consistent_hash $request_uri:根据客户端请求的uri映射
- consistent_hash $args:根据客户端携带的参数进行映
- ngx_http_upstream_consistent_hash 模块是⼀个第三方模块,需要我们下载安装后使用
(1)github下载nginx⼀致性hash负载均衡模块 https://github.com/replay/ngx_http_consistent_hash
(2)将下载的压缩包上传到nginx服务器,并解压
(3)我们已经编译安装过nginx,此时进⼊当时nginx的源码目录,执行如下命令
- ./configure —add-module=/root/ngx_http_consistent_hash-master
- make
- make install
(4)Nginx就可以使用啦,在nginx.conf
文件中配置
第二部分 集群时钟同步问题
第 1 节 时钟不同步导致的问题
时钟此处指服务器时间,如果集群中各个服务器时钟不一致势必导致一系列问题,试想 “集群是各个服务器⼀起团队化作战,大家工作都不在一个点上,岂不乱了套!”
举一个例子:电商网站业务中,新增⼀条订单,那么势必会在订单表中增加了⼀条记录,该条记录中应该会有“下单时间”这样的字段,往往我们会在程序中获取当前系统时间插⼊到数据库或者直接从数据库服务器获取时间。那我们的订单子系统是集群化部署,或者我们的数据库也是分库分表的集群化部署,然而他们的系统时钟缺不⼀致,比如有一台服务器的时间是昨天,那么这个时候下单时间就成了昨天,那我们的数据将会混乱!如下
第 2 节 集群时钟同步配置
集群时钟同步思路:
(1)分布式集群中各个服务器节点都可以连接互联网
操作方式:
#使⽤ ntpdate ⽹络时间同步命令
ntpdate -u ntp.api.bz #从⼀个时间服务器同步时间
- windows有计划任务
- Linux也有定时任务,crond,可以使用Linux的定时任务,每隔10分钟执行一次
ntpdate
命令
(2)分布式集群中某一个服务器节点可以访问互联网或者所有节点都不能够访问互联网
操作方式:
选取集群中的⼀个服务器节点A(172.17.0.17)作为时间服务器(整个集群时间从这台服务器同步,如果这台服务器能够访问互联网,可以让这台服务器和网络时间保持同步,如果不能就手动设置一个时间)
- 首先设置好A的时间
- 把A配置为时间服务器(修改
/etc/ntp.conf
文件)
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
- 集群中其它节点就可以从A服务器同步时间了
ntpdate 172.17.0.17
第三部分 分布式ID问题及解决方案
为什么需要分布式ID(分布式集群环境下的全局唯⼀ID)
3.1 UUID【可以用】
- UUID 是指Universally Unique Identifier,翻译为中文是通用唯一识别码
- 产生重复 UUID 并造成错误的情况非常低,是故大可不必考虑此问题。
- Java中得到⼀个UUID,可以使⽤java.util 包提供的方法
public class MyTest {
public static void main(String[] args) {
System.out.println(java.util.UUID.randomUUID().toString());
}
}
3.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();
注意:
- 这里的createtime字段无实际意义,是为了随便插入⼀条数据以至于能够自增id。
- 使用独立的Mysql实例生成分布式id,虽然可行,但是性能和可靠性都不够好,因为你需要代码连接到数据库才能获取到id,性能无法保障,另外mysql数据库实例挂掉了,那么就无法获取分布式id了。
- 有⼀些开发者又针对上述的情况将用于生成分布式id的mysql数据库设计成了⼀个集群架构,那么其实这种方式现在基本不用,因为过于麻烦了。
3.3 SnowFlake 雪花算法(可以用,推荐)
- 雪花算法是Twitter推出的⼀个用于生成分布式ID的策略。
- 雪花算法是⼀个算法,基于这个算法可以生成ID,生成的ID是⼀个long型,那么在Java中⼀个long型是8个字节,算下来是64bit,如下是使用雪花算法生成的⼀个ID的⼆进制形式示意:
另外,⼀切互联网公司也基于上述的方案封装了⼀些分布式ID生成器,比如滴滴的tinyid(基于数据库实现)、百度的uidgenerator(基于SnowFlake)和美团的leaf(基于数据库和SnowFlake)等。
3.4 借助Redis的Incr命令获取全局唯一ID(推荐)
Redis Incr 命令将 key 中储存的数字值增一。如果 key 不存在,那么 key 的值会先被初始化为 0,然后再执行 INCR 操作。
第四部分 分布式调度问题
- 调度—>定时任务
- 分布式调度—>在分布式集群环境下定时任务这件事。Elastic-job(当当网开源的分布式调度框架)
第 1 节 定时任务的场景
定时任务形式:每隔⼀定时间/特定某⼀时刻执行
例如:
- 订单审核、出库
- 订单超时自动取消、支付退款
- 礼券同步、生成、发放作业
- 物流信息推送、抓取作业、退换货处理作业
- 数据积压监控、日志监控、服务可用性探测作业
- 定时备份数据
- 金融系统每天的定时结算
- 数据归档、清理作业
- 报表、离线数据分析作业
第 2 节 什么是分布式调度
什么是分布式任务调度?有两层含义
- 运行在分布式集群环境下的调度任务(同⼀个定时任务程序部署多份,只应该有⼀个定时任务在执行)
- 分布式调度—>定时任务的分布式—>定时任务的拆分(即为把一个大的作业任务拆分为多个小的作业任务,同时执行)
第 3 节 定时任务与消息队列的区别
(1)共同点
- 异步处理
比如注册、下单事件 - 应用解耦
不管定时任务作业还是MQ都可以作为两个应用之间的齿轮实现应用解耦,这个齿轮可以中转数据,当然单体服务不需要考虑这些,服务拆分的时候往往都会考虑 - 流量削峰
双十一的时候,任务作业和MQ都可以用来扛流量,后端系统根据服务能力定时处理订单或者从MQ抓取订单抓取到⼀个订单到来事件的话触发处理,对于前端用户来说看到的结果是已经下单成功了,下单是不受任何影响的。
(2)本质不同
- 定时任务作业是时间驱动,而MQ是事件驱动;
- 时间驱动是不可代替的,比如⾦融系统每日的利息结算,不是说利息来⼀条(利息到来事件)就算⼀下,而往往是通过定时任务批量计算;
所以,定时任务作业更倾向于批处理,MQ倾向于逐条处理;
第 4 节 定时任务的实现方式
定时任务的实现方式有多种。早期没有定时任务框架的时候,我们会使用JDK中的Timer机制 和 多线程机制(Runnable+线程休眠) 来实现定时或者间隔⼀段时间执行某⼀段程序;后来有了定时任务框架,比如大名鼎鼎的Quartz
任务调度框架,使用时间表达式(包括:秒、分、时、日、周、年)配置某⼀个任务什么时间去执行:
任务调度框架Quartz简单回顾:
(1)引入jar
<!--任务调度框架quartz-->
<!-- https://mvnrepository.com/artifact/org.quartz-scheduler/quartz -->
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.3.2</version>
</dependency>
(2)定时任务作业主调度程序
public class QuartzMan {
// 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(DemoJob.class); // TODO 自定义任务类
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 cronTrigger = TriggerBuilder.newTrigger()
.withIdentity("triggerName","myTrigger")
.startNow()
.withSchedule(CronScheduleBuilder.cronSchedule("*/2 * * * * ?")).build();
return cronTrigger;
}
/**
* main函数中开启定时任务
* @param args
*/
public static void main(String[] args) throws SchedulerException {
// 1、创建任务调度器(好比公交调度站)
Scheduler scheduler = QuartzMan.createScheduler();
// 2、创建一个任务(好比某一个公交车的出行)
JobDetail job = QuartzMan.createJob();
// 3、创建任务的时间触发器(好比这个公交车的出行时间表)
Trigger trigger = QuartzMan.createTrigger();
// 4、使用任务调度器根据时间触发器执行我们的任务
scheduler.scheduleJob(job,trigger);
scheduler.start();
}
}
(3)定义⼀个job,需实现Job接口
public class DemoJob implements Job {
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
System.out.println("我是一个定时任务执行逻辑");
}
}
以上,是回顾⼀下任务调度框架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的本质功能:存储+通知。
(1)引入Jar包
<!-- 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>
(2)定时任务实例
需求:每隔两秒钟执行一次定时任务(resume表中未归档的数据归档到resume_bak表中,每次归档1条记录)
1)resume_bak和resume表结构完全⼀样
2)resume表中数据归档之后不删除,只将state置为"已归档"
数据表结构
-- ----------------------------
-- Table structure for resume
-- ----------------------------
DROP TABLE IF EXISTS `resume`;
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=1 DEFAULT CHARSET=utf8;
SET FOREIGN_KEY_CHECKS = 1;
定时任务类
/**
* 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);
}
}
主类
public class ElasticJobMain {
public static void main(String[] args) {
// 配置分布式协调服务(注册中心)Zookeeper
ZookeeperConfiguration zookeeperConfiguration = new ZookeeperConfiguration("localhost:2181", "data-archive-job");
CoordinatorRegistryCenter coordinatorRegistryCenter = new ZookeeperRegistryCenter(zookeeperConfiguration);
coordinatorRegistryCenter.init();
// 配置任务(时间事件、定时任务业务逻辑、调度器)
JobCoreConfiguration jobCoreConfiguration = JobCoreConfiguration
.newBuilder("archive-job", "*/2 * * * * ?", 3)
.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();
}
}
JdbcUtil工具类
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 = "123456";
//驱动程序类
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;
}
}
(3)测试
- 可先启动⼀个进程,然后再启动⼀个进程(两个进程模拟分布式环境下,通⼀个定时任务部署了两份在工作)
- 两个进程逐个启动,观察现象
- 关闭其中执行的进程,观察现象
(4)Leader节点选举机制
每个Elastic-Job的任务执行实例App作为Zookeeper的客户端来操作ZooKeeper的znode
1)多个实例同时创建/leader节点
2)/leader节点只能创建⼀个,后创建的会失败,创建成功的实例会被选为leader节点,执行任务
5.4 Elastic-Job-Lite轻量级去中心化的特点
如何理解轻量级和去中心化?
5.5 任务分片
一个大的非常耗时的作业Job,比如:⼀次要处理⼀亿的数据,那这⼀亿的数据存储在数据库中,如果用⼀个作业节点处理⼀亿数据要很久,在互联网领域是不太能接受的,互联网领域更希望机器的增加去横向扩展处理能力。所以,ElasticJob可以把作业分为多个的task(每⼀个task就是⼀个任务分片),每⼀个task交给具体的⼀个机器实例去处理(⼀个机器实例是可以处理多个task的),但是具体每个task执行什么逻辑由我们自己来指定。
Strategy策略定义这些分片项怎么去分配到各个机器上去,默认是平均去分,可以定制,比如某⼀个机器负载比较高或者预配置比较⾼,那么就可以写策略。分片和作业本身是通过⼀个注册中心协调的,因为在分布式环境下,状态数据肯定集中到⼀点,才可以在分布式中沟通。
分片代码
5.6 弹性扩容
新增加⼀个运行实例app3,它会自动注册到注册中心,注册中心发现新的服务上线,注册中心会通知ElasticJob 进行重新分片。
注意:
- 分片项也是⼀个JOB配置,修改配置,重新分片,在下⼀次定时运行之前会重新调用分片算法,那么这个分片算法的结果就是:哪台机器运行哪⼀个一片,这个结果存储到zk中的,主节点会把分片给分好放到注册中心去,然后执行节点从注册中心获取信息(执行节点在定时任务开启的时候获取相应的分片)。
- 如果所有的节点挂掉,只剩下⼀个节点,所有分片都会指向剩下的⼀个节点,这也是ElasticJob的高可用。
第五部分 Session共享问题
Session共享及Session保持或者叫做Session⼀致性
第 1 节 Session问题原因分析
出现这个问题的原因,从根本上来说是因为Http协议是无状态的协议。客户端和服务端在某次会话中产生的数据不会被保留下来,所以第⼆次请求服务端无法认识到你曾经来过,Http为什么要设计为无状态协议?早期都是静态页面无所谓有无状态,后来有动态的内容更丰富,就需要有状态,出现了两种用于保持Http状态的技术,那就是Cookie和Session。而出现上述不停让登录的问题,分析如下图:
场景:nginx默认轮询策略
第 2 节 解决Session⼀致性的⽅案
5.2.1 Nginx的 IP_Hash 策略(可以使用)
同⼀个客户端IP的请求都会被路由到同⼀个目标服务器,也叫做会话粘滞
优点:
- 配置简单,不入侵应用,不需要额外修改代码
缺点:
- 服务器重启Session丢失
- 存在单点负载高的风险
- 单点故障问题
5.2.2 Session复制(不推荐)
也即,多个tomcat之间通过修改配置文件,达到Session之间的复制
优点:
- 不入侵应用
- 便于服务器水平扩展
- 能适应各种负载均衡策略
- 服务器重启或者宕机不会造成Session丢失
缺点:
- 性能低
- 内存消耗
- 不能存储太多数据,否则数据越多越影响性能
- 延迟性
5.2.3 Session共享,Session集中存储(推荐)
Session的本质就是缓存,那Session数据为什么不交给专业的缓存中间件呢?比如Redis
优点:
- 能适应各种负载均衡策略
- 服务器重启或者宕机不会造成Session丢失
- 扩展能力强
- 适合大集群数量使用
缺点:
对应用有入侵,引入了和Redis的交互代码
Spring Session使得基于Redis的Session共享应用起来非常之简单
(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)添加注解