# homework-2-2
**Repository Path**: beiwei/homework-2-2
## Basic Information
- **Project Name**: homework-2-2
- **Description**: springsession+nginx
- **Primary Language**: Java
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2020-05-16
- **Last Updated**: 2020-12-19
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# 分布式和集群
```yaml
分布式: 指的是服务的拆分,并通过rpc调用。
集群: 指的是多个实例副本,单体应用的多副本也算集群。
```
## 一 一致性hash算法
分布式中大量使用`Hash`算法,因为它具有高效的查询效率。通过在使用数组,进行hash映射,最好时时间复杂度接近`O(1)`。
应用领域:
```yaml
加密: md5, sha
负载均衡: 比如nginx的ip_hash策略
分布式存储: 比如redis集群, 数据需要映射到哪一台节点上
```
### 1.1 常见的hash算法
| hash算法 | 说明 |
| ---------- | ------------------------------------------------------------ |
| 直接寻址法 | **直接把数据和数组的下标绑定到一起**,`f(x)=x`。
缺点是浪费空间,比如 1,5,7,6,3,4,8,12306。 |
| 除留余数法 | **求模**。`f(x) = a*x + b`
缺点是会出现**hash冲突**。 |
| 开放寻址法 | **出现hash冲突,则向前或后查找空余位置**。
缺点是数组一旦初始化,则无法扩展。 |
| 拉链法 | **数组+链表**。解决了hash冲突,同时也解决了数组初始化后无法扩展的问题。 |
普通hash算法存在的问题:
hash在负载均衡 和 分布式存储 中有重要作用。但问题是无法解决 **扩容,缩容** 所带来的映射不一致。
比如1:nginx负载到后端10个tomcat集群,采用的是节点数量取模(`除留余数法`) ,如果其中几个节点下线,那么hash映射则会不一致。
比如2:redis集群,将value存入哪个集群节点也是同理。
所以引出了**一致性hash算法**
### 1.2 一致性hash算法
1. 首先是 0~232 - 1 围成一个闭环,这个值正好是int型的取值范围。

2. 然后将 `节点ip` 或 `主机名` 进行计算hash,得到一个int值,放置到圆环上。
针对客户端用户,也根据ip进行hash求值,对应到环上某个位置,按 **顺时针** 方向查找最近的Node节点

3. 缩容时。如下图,删除节点3,只会影响到3节点上的数据,并将请求指向到节点4。
可以看出集群节点越多 (集群规模越大),那么受到影响的请求就越少。
针对redis集群,那么就只有一少部分数据会请求数据库。并逐步将节点3上的数据缓存到节点4。
若使用普通hash算法,比如`除留余数法`,那么会导致大部分缓存无法命中。

4. 扩容时。如下图,之前节点3的部分数据会指向节点5,同理集群越大,影响的范围越小。
一致性哈希算法对于节点的增减都只需重定位环空间中的一小 部分数据,具有较好的容错性和可扩展性。

### 1.3 一致性hash的虚拟节点
由于hash算法是固定的,节点太少时,通过Node的IP来计算hash可能导致节点分布不均,从而导致**数据(请求)倾斜**。
为了解决这种数据倾斜问题,一致性哈希算法引入了虚拟节点机制,即对每一个服务节点计算多个哈希,每个计算结果位置都放置一个此服务节点,称为虚拟节点。
具体做法可以在服务器ip或主机名的后面增加编号来实现。比如,可以为每台服务器计算三个虚拟节 点,于是可以分别计算 “节点1的ip#1”、“节点1的ip#2”、“节点1的ip#3”、“节点2的ip#1”、“节点2的 ip#2”、“节点2的ip#3”的哈希值,于是形成六个虚拟节点,当客户端被路由到虚拟节点的时候其实是被 路由到该虚拟节点所对应的真实节点。

具体一致性hash的实现请看代码:
```java
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 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 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.4 **Nginx** 配置一致性**Hash**负载均衡策略
首先下载安装一致性hash负载均衡模块 (由于nginx已提前编译安装过了)。
```bash
git clone https://github.com/replay/ngx_http_consistent_hash
./configure —add-module=/root/ngx_http_consistent_hash-master
make
make install
```
修改`nginx.conf`配置文件的负载均衡算法
```bash
upstream myService {
#consistent_hash $remote_addr;
#consistent_hash $args;
consistent_hash $request_uri;
server localhost:8080; #实例1
server localhost:8081; #实例2
server localhost:8082; #实例3
server localhost:8083; #实例4
}
```
## 二 集群时钟同步
时钟不同步会导致很多问题,下面简单罗列几个:
- 下单时间不一致,会导致后期大数据报表统计有误差
- etcd集群时钟不同步,会导致集群出错
- jwt,若签发,验签时间不一致,甚至验签在签发之前了,那么会报错
- elk收集的日志时间错乱,无法快速定位线上问题。
### 2.1 集群所有节点都可访问公网
那么可以每台机器可以设置`crond` linux定时任务,从公网授时服务api定期同步时钟。
测试授时服务是否可用:
```bash
# 查看是否安装
rpm -qa| grep ntpdate
# 安装ntp
yum install -y ntp
# 将时间改为早上8点
date -s '08:00:00'
# 从授时服务时钟同步
ntpdate -u ntp.api.bz #从一个时间服务器同步时间(上海)
```
额外说明下`crond`服务
```bash
# 安装crontab
yum install crontabs
# 启动服务
service crond start
# 关闭服务
service crond stop
# 重启服务
service crond restart
# 重新载入配置
service crond reload
# 查看状态
service crond status
# 编辑crontab
crontab -e
# 列出全部信息
crontab -l
# 每小时同步一次时钟
* */1 * * * ntpdate -u ntp.api.bz
```
具体`crontab`语法自行查询。
### 2.2 集群只有一台可访问公网
那么将可访问公网的那台机器A作为ntp服务器。
- 把A配置为时间服务器(修改`/etc/ntp.conf`文件)
```bash
vim /etc/ntp.conf
#注释掉
#restrict default ignore
#放开局域网同步功能,172.17.0.0是你的局域网网段
restrict 172.17.0.0 mask 255.255.255.0 nomodify notrap
#和本地硬件进行时钟同步
server 127.127.1.0 # local clock
fudge 127.127.1.0 stratum 10
#重启ntp
service ntpd restart
#ntp开机启动
chkconfig ntpd on
```
当然机器A需要从公网的授时服务同步时钟
```bash
crontab -e
* */1 * * * ntpdate -u ntp.api.bz
service crond reload
```
- 其他节点从A服务器同步时钟
```bash
# 编辑crontab
crontab -e
# 每小时同步一次时钟
* */1 * * * ntpdate -u
# 重启crond
service crond restart
```
**当然若所有节点都无法访问公网,那么随便选一台作为基准,其他同步这个基准时钟即可**
## 三 分布式ID方案
为何需要分布式id?由于对数据分库分表后需要有一个唯一ID来标识一条数据或消息,单表的自增id无法满足。
### 3.1 需保证的前提
| 前提条件 | 说明 |
| ---------- | ------------------------------------------------------------ |
| 全局唯一性 | 不能出现重复的ID号,既然是唯一标识,最基本的要求。 |
| 趋势递增 | InnoDB引擎中使用的是聚集索引,数据结构时B+树。
为保证插入性能(涉及到磁盘预读取,内存页的概念),必须是有序的。 |
| 单调递增 | `趋势递增` 是站在索引写入性能的角度,`单调递增` 是站在业务角度。 |
| 安全性 | id不能连续,这个不强制,但某些场合是需要一个无规则的id,避免泄漏商业信息。
比如订单号连续,app抓包是能获取一天的单量。 |
| 可用性5个9 | (1-99.999%)*365*24*60=5.26分钟,连续运行1年时间里最多可能的业务中断时间是5.26分钟。 |
参考:
[MySQL索引背后的数据结构及算法原理](http://blog.codinglabs.org/articles/theory-of-mysql-index.html)
[美团点评分布式ID生成系统-Leaf](https://tech.meituan.com/2017/04/21/mt-leaf.html)
### 3.2 UUID
标准型式包含32个16进制数字,以连字号分为五段,形式为8-4-4-4-12的36个字符。
业界一共有5种方式生成UUID,详情见IETF发布的UUID规范 [A Universally Unique IDentifier (UUID) URN Namespace](http://www.ietf.org/rfc/rfc4122.txt)
优点:
- **性能非常高**。本地生成,没有网络消耗。重复的几率几乎没有。
缺点:
- **不易于存储**:UUID太长,16字节128位,通常以36长度的字符串表示,很多场景不适用。
- **信息不安全**:基于MAC地址生成UUID的算法可能会造成MAC地址泄露,这个漏洞曾被用于寻找梅丽莎病毒的制作者位置。
- **对索引不利**:索引官方建议越短越好,这个和InnoDB引擎的聚集索引(B+树)的原理有关,每个节点的字节数有限,索引越大,出度就越小。意味着树的高度会变大,查询次数变多。
同时UUID是无规则的,对数据的插入删除的效率有很大影响。
### 3.3 数据库生成
比如mysql,利用数据库自增以及自增步长:
```bash
#自增
auto_increment_increment
#步长
auto_increment_offset
```
先`insert`,在`select`, 即`select last_insert_id()`方式获取单调递增ID号。
优点:
- 非常简单,利用现有数据库系统的功能实现,成本小。
- ID号单调自增,可以实现一些对ID有特殊要求的业务。
缺点:
- 强依赖DB,当DB异常时整个系统不可用。就算配置主从复置增加可用性,但主从切换时的不一致可能导致重复发号。
- ID发号性能瓶颈限制在单台MySQL的读写性能。
为了解决msql性能和重复发号的问题,可用如下方案:
假设5台机器,每台的初始值相差为5,且步长即为机器数量:
```yaml
机器1: {offset: 1, increment: 5} #{1,5,10,15...}
机器2: {offset: 2, increment: 5} #{2,6,11,16...}
机器3: {offset: 3, increment: 5} #{3,7,12,17...}
机器4: {offset: 4, increment: 5} #{4,8,13,18...}
机器5: {offset: 5, increment: 5} #{5,9,14,19...}
```
2台机器具体实现可以如下:
```yaml
TicketServer1:
auto-increment-increment: 2
auto-increment-offset: 1
TicketServer2:
auto-increment-increment: 2
auto-increment-offset: 2
```
具体的分布式ID生成服务可以如下设计:

但还是有缺陷:
- **水平扩展十分困难**。 号段初始值,步长是依赖机器数量的,一旦水平扩展就只能在一个足够大数字且还未被发号时,扩容机器,同时部署完毕后摘掉以前的机器并重新初始化步长和初始值。一旦集群规模很大则无法扩展。
- **没有了单调递增特性**:由于初始值不同,导致发号是交叉进行。所以只能趋势递增,但绝大部分系统能满足业务要求。
- **性能有瓶颈**:由于发号是需要通过mysql读写的,`取号= (一次socket通信)+(读/写磁盘)`,怎么看都不划算,只能靠堆机器来解决。
### 3.4 snowflake雪花算法
是一种以划分命名空间(UUID也算,由于比较常见,所以单独分析)来生成ID的一种算法。
这种方案把64-bit分别划分成多段,分开来标示机器、时间等,比如在snowflake中的64-bit分别表示:

**意味着:在某一个毫秒时刻,某一台机器可以生成4096个ID号**
> 41bit可以表示(1L<<41)/(1000 * 3600 * 24 * 365)=69年的时间;
>
> 10bit可以表示1024台机器。当然可以自行分组。
>
> 12bit自增序号可表示4096个ID, 那么理论 QPS=4096*1024 =409.6w/s
优点:
- 毫秒数在高位,自增序列在低位,整个ID都是趋势递增的。
- 不依赖第三方数据库,以服务的方式部署,稳定性更高,效率更高。
- bit位的分配可自行根据业务调节长度,很灵活。
缺点:
强制依赖服务器时钟,一旦时钟回拨,将导致重复发号。
### 3.5 Leaf-segment数据库改进方案
上述数据库的改进方案缺点是每次取号,都将`取号= (一次socket通信)+(读/写磁盘)`,所以性能低下。
那么改进为通过一个`proxy`代理服务一次性获取一个区域的号段,步长给足够大即可。

设计方案:需要一个表来保存`步长`,`当前最大发号数`,`业务编码`。
**业务编码**:是用于区分不同业务,号段是相互隔离的。
**步长**:需要给足够大,使之可以缓存,避免数据库宕机时还有号可派发。
**最大号段值**:用于表示当前号段的区间。
更新语句:
可以看出这天生使用了数据库的事物,mysql的排他锁功能。
```sql
Begin
UPDATE table SET max_id=max_id+step WHERE biz_tag=xxx
SELECT tag, max_id, step FROM table WHERE biz_tag=xxx
Commit
```
优点:
- 符合趋势递增和单调递增。
- 8byte 64位满足主键索引的长度要求。
- 容灾性高,就算DB宕机也可通过缓存继续发号,比如`step`足够大可支持半小时的发号。
- 自定义`max_id`的大小与步长`step`,非常灵活。
缺点:
- ID号不够随机,因为这是自增方式,当然也可以再加入子步长来解决。
- tg999数据会出现偶尔的尖刺,主要是缓存的号派发完毕后需要去更新DB取号段,可能发生网络发生抖动导致系统响应变慢。
- DB宕机则整个服务不可用。
#### 3.5.1 网络抖动优化
**针对取号可能发生网络抖动,那么我们可以在号段消费到某个阈值时,就异步去DB取回下一个号段。**

**采用双buffer的方式,Leaf服务内部有两个号段缓存区segment。当前号段已下发10%时,如果下一个号段未更新,则另启一个更新线程去更新下一个号段。当前号段全部下发完后,如果下个号段准备好了则切换到下个号段为当前segment接着下发,循环往复。**
- 推荐设置步长`step` 设置为高峰时段QPS的600倍。比如高峰QPS是2000,那么步长为120w。即理论上DB宕机后极限还能派发10分钟。但实际上不是时刻都在峰QPS值,所以实际上能支撑起码20~30分钟。
- 服务每次取号时,都会监控号段的消耗情况,一旦超过阈值就触发异步重新去DB取新号段。
#### 3.5.2 DB宕机容灾优化
可采用 **一主两从的方式,同时分机房部署**。这个可灵活选择。
Master和Slave之间采用 **半同步方式 **同步数据。这里不讨论mysql主从复制的一致性问题。

服务调用的时候,根据负载均衡算法会优先调用同机房的Leaf服务。在该IDC内Leaf服务不可用的时候才会选择其他机房的Leaf服务。同时服务治理平台OCTO还提供了针对服务的过载保护、一键截流、动态流量分配等对服务的保护措施。
### 3.6 Leaf-snowflake雪花算法改进方案
上面的`Leaf-segment`方案 有个问题,就是ID号是可估算的,这不适合订单等商业敏感的业务。
主要是为了当集群节点过多时,手动配置`WorkerID`太过于麻烦,所以借用了`zookeeper持久顺序节点` 来自动注册zk并拿到`WorkerID`。

1) 逻辑
- 启动Leaf-snowflake服务,连接Zookeeper,在leaf_forever父节点下检查自己是否已经注册过(是否有该顺序子节点)。
- 如果有注册过直接取回自己的workerID(zk顺序节点生成的int类型ID号),启动服务。
- 如果没有注册过,就在该父节点下面创建一个持久顺序节点,创建成功后取回顺序号当做自己的workerID号,启动服务。
2) 解决了如下几个问题:
- 弱依赖ZooKeeper。会在本机文件系统上缓存一个workerID文件,保证zk重启或出现问题时也能正常启动。
- 时钟回拨问题。由于雪花算法强依赖时钟,所以需要处理时钟回拨的重复发号问题。
- 通过临时节点(运行的`Leaf-snowflake`节点)获取IP+Port,rpc获取所有服务器几点的时钟,
`abs( 系统时间-sum(time)/nodeSize ) < 阈值` 则认为时钟准确,正常启动,同时写临时节点`leaf_temporary/${self}`维持租约。否则报警。
- 运行时时钟回拨,则停止提供服务并返回错误,每隔一段时间检测时钟是否正常并恢复服务。
或者直接摘除服务并报警,手工处理时钟。
## 四 分布式调度
### 4.1 Quartz核心
核心的流程如下:
- **调度器 **
类似公交车调度站,或者机场塔台。
接口:`SchedulerFactory`,两个实现:`DirectSchedulerFactory`,`StdSchedulerFactory`
- **具体任务**
类似公交出行,飞机入跑道起飞。
接口:`JobDetail`,一个实现:`JobDetailImpl`。 它持有一个具体的`Job`接口实现。
- **时间触发器**
类似发车时刻表。Quartz叫做`Trigger`触发器。
接口:`Trigger`,子接口:有7个分别对应不同类型
- **调度器进行调度**
即调度站进行调度,下达具体任务和时刻表。比如早上6点开始发车。
`Date scheduleJob(JobDetail var1, Trigger var2)`
| 核心构件 | 说明 |
| ------------- | ------------------------------------------------------------ |
| **Job** | `void execute(JobExecutionContext context)`
拥有一个`execute`执行方法。代表执行的具体内容。 |
| **Scheduler** | 任务调度器。它需要注册两个东西:`JobDetail具体任务` + `Trigger调度配置`
`DirectSchedulerFactory`:自行指定调度器参数
`StdSchedulerFactory`:读取classpath下`quartz.properties`,没有则使用默认。 |
| **JobDetail** | 具体任务。实现类:`JobDetailImpl`,持有一个具体`Job`实现。
由`JobBuilder`进行构建。 |
| **Trigger** | 触发器。由`TriggerBuilder`进行构建。常用的实现如下四种:
`SimpleTrigger`(常用,时间间隔重复执行)
`CronTrigger`(常用)
`CalendarIntervalTrigger`
`DailyTimeIntervalTrigger` |
Trigger触发器是核心,这里说明一下:
| 触发器类型 | 说明 |
| ------------------------ | ------------------------------------------------------------ |
| SimpleTrigger | 从某一个时间开始,以一定的时间间隔来执行任务。
`repeatInterval`:重复时间间隔
`repeatCount`:重复次数,注意启动的时候会执行一次,并不计入次数。 |
| CronTrigger | 使用cron表达式。所有的时间类型都支持。 |
| CalendarIntervalTrigger | 和`SimpleTrigger` 类似。但是支持了`intervalUnit`。
可以指定 **秒,分钟,小时,天,月,年,星期** |
| DailyTimeIntervalTrigger | 和`SimpleTrigger` 类似。但是主要是按天执行。
`startTimeOfDay`:每天开始时间
`endTimeOfDay`:每天结束时间
`daysOfWeek`:需要执行的星期
`interval`:执行间隔
`intervalUnit`:执行间隔的单位(秒,分钟,小时,天,月,年,星期)
`repeatCount`:重复次数 |
所以我们常用的还是**CronTrigger**,它支持所有类型。
最后说一下`crontab` 表达式
```yaml
#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 可选项
# 注意 * 和 ? 在 "日","星期" 两个域上只能互斥使用, 比如 每个月20号并不知道是周几,每周五也不知道是几号
*: 表示匹配任意值,表示"每"的含义
?: 只在"天","周"才能使用,表示"不清楚含义",因为只有这两个域有冲突
-: 表示范围。这个范围内每个时刻都会触发一次。比如在 Second域配置30-40,表示30~40秒内每秒都执行一次
/: 表示每隔多久触发。 秒域配置5/10,表示每隔10秒执行一次,即5,15,25,35,45,55都触发一次
,: 枚举。如上面"/" 的例子,只是配置枚举太多的话会比较啰嗦,少的话可以使用,也经常使用在"日","星期"
L: 表示最后一天,只能用在"日","星期"
W: 表示工作日[周一, 周五],只能用在"星期"
#例子
"0 0 0 20 * ?" 每月20号凌晨执行一次
"0 0 10,14,16 * * ?" 每天上午10点,下午2点,4点
"0 0/30 9-17 * * ?" 朝九晚五工作时间内每半小时
"0 0 12 ? * WED" 表示每个星期三中午12点
"0 0 12 * * ?" 每天中午12点触发
"0 15 10 ? * *" 每天上午10:15触发
"0 15 10 * * ?" 每天上午10:15触发
"0 15 10 * * ? *" 每天上午10:15触发
"0 15 10 * * ? 2005" 2005年的每天上午10:15触发
"0 * 14 * * ?" 在每天下午2点到下午2:59期间的每1分钟触发
"0 0/5 14 * * ?" 在每天下午2点到下午2:55期间的每5分钟触发
"0 0/5 14,18 * * ?" 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发
"0 0-5 14 * * ?" 在每天下午2点到下午2:05期间的每1分钟触发
"0 10,44 14 ? 3 WED" 每年三月的星期三的下午2:10和2:44触发
"0 15 10 ? * MON-FRI" 周一至周五的上午10:15触发
"0 15 10 15 * ?" 每月15日上午10:15触发
"0 15 10 L * ?" 每月最后一日的上午10:15触发
"0 15 10 ? * 6L" 每月的最后一个星期五上午10:15触发
"0 15 10 ? * 6L 2002-2005" 2002年至2005年的每月的最后一个星期五上午10:15触发
"0 15 10 ? * 6#3" 每月的第三个星期五上午10:15触发
"0 0 2 1 * ? *" 表示在每月的1日的凌晨2点调度任务
"0 15 10 ? * MON-FRI" 表示周一到周五每天上午10:15执行作业
"0 15 10 ? 6L 2002-2006" 表示200-2006年的每个月的最后一个星期五上午10:15执行作业
"0 0 0 10 12 ? 2009" 代表:2009年12月10日0点0分0秒执行(星期几:'?'代表忽略)
"0 0 0 10 12 ? *" 代表:每年12月10日0点0分0秒执行
"0 0 0 10 * ?" 代表:每月10日0点0分0秒执行
"0 0 1 1 * ?" 代表:每月1号1点0分0秒执行
"0 0 1 1 3,6,9 ?" 代表:3月 6月 9月,1号1点0分0秒执行
```
### 4.2 分布式调度框架
分布式调度框架有很多
**elastic-job , xxl-job ,quartz , saturn, opencron , antares, airflow...**
#### 4.2.1 elastic-job
https://github.com/elasticjob
当当网基于quartz 二次开发。有两个子项目:
```yaml
Elastic-Job-Lite: 轻量级无中心化
Elastic-Job-Cloud: Mesos + Docker。自身没有实现中心式调度,借用了开源的Mesos
```
特点:
- 以Quartz为基础
- 使用zookeeper做协调,无须单独部署调度中心 **无中心化**
- 支持任务的分片
- 支持弹性扩缩容 , 水平扩展
- 失效转移,容错处理
- 提供运维界面,可以管理作业和注册中心
核心代码:
```java
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();
}
}
```
具体任务:
```java
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);
// 获取分片参数
// 0=bachelor,1=master,2=doctor
String shardingParameter = shardingContext.getShardingParameter();
}
}
```
**使用十分简单,引入jar依赖,和zookeeper即可**
```xml
com.dangdang
elastic-job-lite-core
2.1.5
```
#### 4.2.2 xxl-job
目前已到2.2.0: https://gitee.com/xuxueli0323/xxl-job

源码构建时,修改maven仓库为阿里源
```xml
aliyun
https://maven.aliyun.com/repository/public
true
false
aliyun-plugin
https://maven.aliyun.com/repository/public
true
false
```
模块
| 模块 | 说明 |
| ------------------------ | -------------------------------- |
| xxl-job-admin | 调度中心 |
| xxl-job-core | 核心。执行器依赖此模块即可 |
| xxl-job-executor-samples | 示例模块。里面有springboot的示例 |
1) `xxl-job-admin`调度中心配置
```properties
### web
server.port=8080
server.servlet.context-path=/xxl-job-admin
### actuator
management.server.servlet.context-path=/actuator
management.health.mail.enabled=false
### resources
spring.mvc.servlet.load-on-startup=0
spring.mvc.static-path-pattern=/static/**
spring.resources.static-locations=classpath:/static/
### freemarker
spring.freemarker.templateLoaderPath=classpath:/templates/
spring.freemarker.suffix=.ftl
spring.freemarker.charset=UTF-8
spring.freemarker.request-context-attribute=request
spring.freemarker.settings.number_format=0.##########
### mybatis
mybatis.mapper-locations=classpath:/mybatis-mapper/*Mapper.xml
#mybatis.type-aliases-package=com.xxl.job.admin.core.model
### xxl-job, datasource
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/xxl_job?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=1234
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
### datasource-pool
spring.datasource.type=com.zaxxer.hikari.HikariDataSource
spring.datasource.hikari.minimum-idle=10
spring.datasource.hikari.maximum-pool-size=30
spring.datasource.hikari.auto-commit=true
spring.datasource.hikari.idle-timeout=30000
spring.datasource.hikari.pool-name=HikariCP
spring.datasource.hikari.max-lifetime=900000
spring.datasource.hikari.connection-timeout=10000
spring.datasource.hikari.connection-test-query=SELECT 1
### xxl-job, email
spring.mail.host=smtp.qq.com
spring.mail.port=25
spring.mail.username=xxx@qq.com
spring.mail.password=xxx
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true
spring.mail.properties.mail.smtp.starttls.required=true
spring.mail.properties.mail.smtp.socketFactory.class=javax.net.ssl.SSLSocketFactory
### xxl-job, access token
xxl.job.accessToken=
### xxl-job, i18n (default is zh_CN, and you can choose "zh_CN", "zh_TC" and "en")
xxl.job.i18n=zh_CN
## xxl-job, triggerpool max size
xxl.job.triggerpool.fast.max=200
xxl.job.triggerpool.slow.max=100
### xxl-job, log retention days
xxl.job.logretentiondays=30
```
2) 执行器配置
```properties
# web port
server.port=8081
# no web
#spring.main.web-environment=false
# log config
logging.config=classpath:logback.xml
### 调度中心部署跟地址 [选填]:如调度中心集群部署存在多个地址则用逗号分隔。
### 执行器将会使用该地址进行"执行器心跳注册"和"任务结果回调";为空则关闭自动注册;
xxl.job.admin.addresses=http://127.0.0.1:8080/xxl-job-admin
### 执行器通讯TOKEN [选填]:非空时启用;
xxl.job.accessToken=
### 执行器AppName [选填]:执行器心跳注册分组依据;为空则关闭自动注册
xxl.job.executor.appname=xxl-job-executor-sample
### 执行器注册 [选填]:优先使用该配置作为注册地址,为空时使用内嵌服务 ”IP:PORT“ 作为注册地址。
### 从而更灵活的支持容器类型执行器动态IP和动态映射端口问题。
xxl.job.executor.address=
### 执行器IP [选填]:默认为空表示自动获取IP,多网卡时可手动设置指定IP,该IP不会绑定Host仅作为通讯实用;
### 地址信息用于 "执行器注册" 和 "调度中心请求并触发任务";
xxl.job.executor.ip=
### 执行器端口号 [选填]:小于等于0则自动获取;默认端口为9999,单机部署多个执行器时,注意要配置不同执行器端口;
xxl.job.executor.port=9999
### 执行器运行日志文件存储磁盘路径 [选填] :需要对该路径拥有读写权限;为空则使用默认路径;
xxl.job.executor.logpath=logs/xxl-job/jobhandler
### x执行器日志文件保存天数 [选填] : 过期日志自动清理, 限制值大于等于3时生效; 否则, 如-1, 关闭自动清理功能;
xxl.job.executor.logretentiondays=30
```
## 五 Session共享
一般有三种处理方式:
- tomcat cluster集群模式 (**不推荐**)
使用组播进行session复制,有延迟,效率低,占内存资源,这是老一代解决方案
- nginx ip_hash (**一般也不使用**)
使用了nginx的L4 四层ip-hash负载。但重启会丢失session,单点负载高,且有单点负载故障。
- redis+spring-session (**主流/推荐**)
除了需要编写一些额外代码,没有缺点。
1) 引入jar
`spring-session` 极大简化了session管理的开发工作,只需引入一个注解即可。
```xml
org.springframework.boot
spring-boot-starter-data-redis
org.springframework.session
spring-session-data-redis
```
2) 添加配置
```yaml
spring:
redis:
database: 0
host: 127.0.0.1
port: 6379
```
3) 添加注解
```java
@EnableCaching
@EnableRedisHttpSession
```
4) 源码解析
**spring-session** 本质是使用了`Filter`过滤器,利用过滤器来保存session。
利用`HttpServletRequestWrapper`,实现自己的` getSession()`方法,接管创建和管理Session数据的工作。
**原理**:
- 浏览器初次访问。client请求不携带任何标识,server无法找到对应的session,所以会新建session对象。
当服务器进行响应的时候,服务器会将session标识放到响应头的Set-Cookie中,以key-value的形式返回。
如: `JSESSIONID=7F149950097E7B5B41B390436497CD21`
- 浏览器再次访问。会携带此cookie `JSESSIONID=7F149950097E7B5B41B390436497CD21`,server会根据此ID找到对应的`HttpSession`对象,里面可以保存你想要保存的用户等信息。
- 浏览器关闭。session会话结束,但由于不是长链接,所以`HttpSession`会有一个过期时间 (可指定)。
一般目前使用redis来保存`Session`对象,我们则可以使用这个对象来保存一些用户权限等验证信息,用于登录和接口权限验证。
部署nginx时,由于使用nginx docker镜像,且本机不能使用80端口,所以nginx启动命令如下:
```bash
docker run -itd --name nginx -p 9000:80 nginx
```
修改nginx的配置
```bash
upstream springsession {
server 192.168.0.101:8080;
server 192.168.0.101:8081;
}
server {
listen 80;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html;
#注意此处必须是docker映射的宿主机端口,否则重定向会使用80端口
proxy_set_header Host $host:9000;
proxy_pass http://springsession;
}
location /favicon.ico {
}
}
```