# 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型的取值范围。 ![image-20200513235624188](image/一致性hash长度.png) 2. 然后将 `节点ip` 或 `主机名` 进行计算hash,得到一个int值,放置到圆环上。 针对客户端用户,也根据ip进行hash求值,对应到环上某个位置,按 **顺时针** 方向查找最近的Node节点 ![image-20200514000226609](image/顺时针查找.png) 3. 缩容时。如下图,删除节点3,只会影响到3节点上的数据,并将请求指向到节点4。 可以看出集群节点越多 (集群规模越大),那么受到影响的请求就越少。 针对redis集群,那么就只有一少部分数据会请求数据库。并逐步将节点3上的数据缓存到节点4。 若使用普通hash算法,比如`除留余数法`,那么会导致大部分缓存无法命中。 ![image-20200514000618714](image/一致性hash缩容.png) 4. 扩容时。如下图,之前节点3的部分数据会指向节点5,同理集群越大,影响的范围越小。 一致性哈希算法对于节点的增减都只需重定位环空间中的一小 部分数据,具有较好的容错性和可扩展性。 ![image-20200514001147995](image/一致性hash扩容.png) ### 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”的哈希值,于是形成六个虚拟节点,当客户端被路由到虚拟节点的时候其实是被 路由到该虚拟节点所对应的真实节点。 ![image-20200514001921062](image/一致性hash虚拟节点.png) 具体一致性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生成服务可以如下设计: ![](image/分布式ID-数据库.png) 但还是有缺陷: - **水平扩展十分困难**。 号段初始值,步长是依赖机器数量的,一旦水平扩展就只能在一个足够大数字且还未被发号时,扩容机器,同时部署完毕后摘掉以前的机器并重新初始化步长和初始值。一旦集群规模很大则无法扩展。 - **没有了单调递增特性**:由于初始值不同,导致发号是交叉进行。所以只能趋势递增,但绝大部分系统能满足业务要求。 - **性能有瓶颈**:由于发号是需要通过mysql读写的,`取号= (一次socket通信)+(读/写磁盘)`,怎么看都不划算,只能靠堆机器来解决。 ### 3.4 snowflake雪花算法 是一种以划分命名空间(UUID也算,由于比较常见,所以单独分析)来生成ID的一种算法。 这种方案把64-bit分别划分成多段,分开来标示机器、时间等,比如在snowflake中的64-bit分别表示: ![](image/雪花算法.png) **意味着:在某一个毫秒时刻,某一台机器可以生成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`代理服务一次性获取一个区域的号段,步长给足够大即可。 ![](image/leaf-segment.png) 设计方案:需要一个表来保存`步长`,`当前最大发号数`,`业务编码`。 **业务编码**:是用于区分不同业务,号段是相互隔离的。 **步长**:需要给足够大,使之可以缓存,避免数据库宕机时还有号可派发。 **最大号段值**:用于表示当前号段的区间。 更新语句: 可以看出这天生使用了数据库的事物,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取回下一个号段。** ![](image/leaf-segment优化.png) **采用双buffer的方式,Leaf服务内部有两个号段缓存区segment。当前号段已下发10%时,如果下一个号段未更新,则另启一个更新线程去更新下一个号段。当前号段全部下发完后,如果下个号段准备好了则切换到下个号段为当前segment接着下发,循环往复。** - 推荐设置步长`step` 设置为高峰时段QPS的600倍。比如高峰QPS是2000,那么步长为120w。即理论上DB宕机后极限还能派发10分钟。但实际上不是时刻都在峰QPS值,所以实际上能支撑起码20~30分钟。 - 服务每次取号时,都会监控号段的消耗情况,一旦超过阈值就触发异步重新去DB取新号段。 #### 3.5.2 DB宕机容灾优化 可采用 **一主两从的方式,同时分机房部署**。这个可灵活选择。 Master和Slave之间采用 **半同步方式 **同步数据。这里不讨论mysql主从复制的一致性问题。 ![](image/db容灾.png) 服务调用的时候,根据负载均衡算法会优先调用同机房的Leaf服务。在该IDC内Leaf服务不可用的时候才会选择其他机房的Leaf服务。同时服务治理平台OCTO还提供了针对服务的过载保护、一键截流、动态流量分配等对服务的保护措施。 ### 3.6 Leaf-snowflake雪花算法改进方案 上面的`Leaf-segment`方案 有个问题,就是ID号是可估算的,这不适合订单等商业敏感的业务。 主要是为了当集群节点过多时,手动配置`WorkerID`太过于麻烦,所以借用了`zookeeper持久顺序节点` 来自动注册zk并拿到`WorkerID`。 ![](image/雪花算法改进.png) 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 ![](image/xxl-job.jpeg) 源码构建时,修改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 { } } ```