1. 首页
  2. >
  3. 编程技术
  4. >
  5. Java

分布式常见问题分析及解决方案

1、分布式和集群概念

分布式:可以简单理解成将一个独立的系统拆分成很多个子系统,每个子系统可以单独的运行(比如用户登录、订单支付),这个称之为分布式系统。

集群:可以简单理解就是对于任何一个系统至少部署了两台服务器上,两台服务器上的应用完全一致。称之为集群。


2、一致性Hash问题及解决方案

2.1Hash溯源

思考:为啥需要使用Hash?

我们带着问题踏上寻求之旅,要想知道为啥使用Hash先明白Hash都能干啥,Hash算法,可以用作安全加密领域MD5、SHA等加密算法,在数据存储和查找⽅⾯有Hash表等, 以上都应⽤到了Hash算法。

实际上Hash算法较多的应用在数据存储和查找领域,最经典的就是Hash表,它的查询效率⾮常⾼,如果Hash算法设计的很好的话,那么Hash表的数据查询时间复杂度可以接近于O(1)。

我们通过一个例子来理解hash算法,需求是提供一组数据1,5,7,6,3,4,8,对这组数据进行存储,然后随便给一个整数n判断n是否存在于这组数据中。

方法一:我们定义一个集合通过循环来一一对比如果找到就存在找不到就不存在。这种称之为顺序查找法,复杂度为O(N)

方法二:将这组数据有序的存储到容器中通过二分查找法如果可以找到的话就返回true 事件复杂度为O(log2n) 显然复杂度比第一张低很多效率高很多。那么有没有比二分法更好的算法?当然是有的

方法三:我们定义一个数组,长度比这组数据最大值大1,那么我们将这组数据存储到数组中以数组中元素的值作为index存储。那么我们在查找的时候直接通过数组arr[n]的方式来获取这样子复杂度为O(1)。这种称之为“直接寻址法”

分布式常见问题分析及解决方案


直接寻址法的好处就是速度快复杂度为O(1);缺点那么如果有一个很大的数字比如10086的话,就需要创建一个长度为10087的数组来存储很显然这种过度依赖于空间来换取时间的做法不现实。

方法四:我们来通过取模的方法来定位坐标来节省空间,比如我们将所有数值都跟这组数据的个数取模将最终的结果放入到数组中。换组数据比如2,3,4,5,10086,我们在通过N查找的时候判断存在的话就返回true,其实这就是一种简单的hash算法称之为除留余数法。复杂度接近O(1)


分布式常见问题分析及解决方案


如果在来一个数为7也存储在index为2的位置,这种方法称为开发寻址法,那么位置到达上线的话是不能在进行存储的。

方法五,四的基础上出现一个7取模之后也是在index为2上边,这样子的话就会那么就是在数组上创建一个链表来存储Hash冲突的数据,称为拉链法


分布式常见问题分析及解决方案


总结:

其实上边设计到的算法就是一种极其简单的hash算法,当hash算法设计的足够好那么查找的时间复杂度无限接近与O(1)空间复杂度也接近O(n)。

因此Hash表的查找效率取决于Hash算法,hash算法就是尽可能让数据能平均分配,这样子既能够节省空间也能够节省时间。【Hash算法实际上是一门很深的学问,比较复杂,Hash表内部的Hash算法也⼀直在更新,很多数学家也在研究。】

case:在java中经常使用到的HashCode也是通过hash算法计算得来的。

2.2Hash算法运用场景

Hash算法在分布式系统架构中的运用场景,比如在Redis,Hadoop,ES,MySQL中分库分表,Nginx负载均衡等。

归纳Hash算法运用场景

1、请求的负载均衡(比如Nginx的ip_hash策略)

2、分布式存储

1 请求的负载均衡

Nginx的IP_hash策略可以在客户端ip不变的情况下,将其发出的请求始终路由到同一个目标服务器上,实现会话粘滞(同一个客户端ip用于会将流量打到同一台服务器上),避免了Session共享的问题。

思考:如果不使用IP_hash策略,如何实现会话粘滞呢?

我们正常的做法是,可以维护一张表来存储客户端ip和目标服务器ip的一种关系映射表,这么做确实可以解决会话粘滞,但是如果客户端非常多话,会特别浪费内存,还有就是一旦客户端上下线,目标服务器上下线就需要重新维护这种映射表。

因此使用hash算法,我们拿到客户端ip计算hash值,hash值与服务器数量进行取模运算,得到的结果就是当前请求被路由到的服务器的编号,那么同一个客户端ip都会被路由到同一台服务器上,就实现了会话粘滞。【addrlen】

2 分布式存储

以分布式内存数据库Redis为例,集群中有redis1,redis2,redis3 三台Redis服务器那么,在进行数据存储时,<key1,value1>数据存储到哪个服务器当中呢?针对key进行hash处理hash(key1)%3=index, 使⽤余数index锁定存储的具体服务器节点。

2.3普通Hash算法问题

在Nginx负载均衡的ip_hash算法,这种算法就是对ip的前两位进行hash取模运算,但是类似这种hash取模的算法也有问题,如果一旦服务器端集群扩容或者缩容甚至宕机的话,必然会导致一些客户端的请求不能路由到原来的服务器上,那么就会导致session失效的问题。


分布式常见问题分析及解决方案


因此引入了一致性hash算法

2.4一致性Hash算法

2.4.1一致性Hash算法原理

一致性Hash算法思路:


分布式常见问题分析及解决方案


一条直线,直线的开头和结尾分别定为0和2的32次方-1,这相当于一个地址,对于这样一条线,弯过来构成一个圆环,这样的一个圆环称为Hash环。我们把服务器的ip或者主机名通过计算hash值,对应的hash环上,那么针对客户端用户,也根据他们的ip进行Hash求值,对应的环上某个位置, 按照顺时针的方向找最近的服务器节点。


分布式常见问题分析及解决方案


2.4.2针对缩容缩容,一致性Hash解决思路

缩容

上图中,假如将服务器3下线,服务器3下线后,原来路由到3的客户端重新路由到服务器4,对于其他客户端没有影响只是这⼀⼩部分受影响(请求的迁移达到了最⼩,这样的算法对分布式集群来说⾮常合适的,避免了⼤量请求迁移 )


分布式常见问题分析及解决方案


扩容

增加服务器5之后,原来路由到3的部分客户端路由到新增服务器5上,对于其他客户端没有影响只是这⼀⼩部分受影响(请求的迁移达到了最⼩,这样的算法对分布式集群来说⾮常合适的,避免了⼤量请求迁移)


分布式常见问题分析及解决方案


2.4.3虚拟节点方案


分布式常见问题分析及解决方案

一致性Hash存在的问题,如果服务器比较少的话,比如两台服务器,在进行ipHash取值分配到Hash圆环上分配的不均匀的话,这个时候就导致两台服务器的流量不均匀,一台服务器压力比较大,这就是我们说的数据(请求)倾斜问题。

为了解决这种数据倾斜问题,一致性Hash算法引入了虚拟节点机制,即对每个服务器节点计算多个Hash,每个计算的结果用来存放当前这个服务器的节点,我们称为虚拟节点。

具体做法可以在服务器ip或主机名的后⾯增加编号来实现。⽐如,可以为每台服务器计算三个虚拟节点,于是可以分别计算 “节点1的ip#1”、“节点1的ip#2”、“节点1的ip#3”、“节点2的ip#1”、“节点2的ip#2”、“节点2的ip#3”的哈希值,于是形成六个虚拟节点,当客户端被路由到虚拟节点的时候其实是被路由到该虚拟节点所对应的真实节点。


分布式常见问题分析及解决方案


总结:虚拟节点,在服务器比较少的情况下通过给真实的服务器ip添加编号再次进行hash取值运算来分配到Hash圆环上达到尽可能的将服务器平均分配,来解决数据倾斜问题。

2.4.4手写一致性hash算法

1、一致性Hash算法实现

public static void main(String[] args) {     String[] serverIps = {"192.168.10.153", "192.168.10.03", "192.168.1.45"};      //对ip进行hascode取值 并且放入到hash环内 使用有序的map的存储关系     SortedMap<Integer, String> serverMap = new TreeMap<>();     for (String serverIp : serverIps) {         int serverIpHashCode = Math.abs(serverIp.hashCode());         //存储映射HashCode和ip的映射关系         serverMap.put(serverIpHashCode, serverIp);     }      //自定义客户端ip     String[] clientIps = {"10.102.23.2", "15,45,122,6", "49,36,10.,10"};     for (String clientIp : clientIps) {         //对客户端的ip可进行hash取模运算         int clientHashcode = Math.abs(clientIp.hashCode());         //使用sortedMap中的tailMap获取到大于当前值的ip         SortedMap<Integer, String> sortedMap = serverMap.tailMap(clientHashcode);          //判断是否为空 如果是空就取原来的ip的第一个否则取这个里边的第一个         String serverIP = null;         if (sortedMap.isEmpty()) {             serverIP = serverMap.get(serverMap.firstKey());         } else {             serverIP = sortedMap.get(sortedMap.firstKey());         }         System.out.println("clientIp:" + clientIp + "被路由到了:" + serverIP + "上");     } } 2、一致性Hash+虚拟节点实现 	实现思路就是在一致性Hash在初始化的时候添加上虚拟节点的映射关系。 在这里只截取了代码片段。 String[] serverIps = {"192.168.10.153", "192.168.10.03", "192.168.1.45"};  int virualCount = 3;  //对ip进行hascode取值 并且放入到hash环内 使用有序的map的存储关系 SortedMap<Integer, String> serverMap = new TreeMap<>(); for (String serverIp : serverIps) {     int serverIpHashCode = Math.abs(serverIp.hashCode());     //存储映射HashCode和ip的映射关系     serverMap.put(serverIpHashCode, serverIp);      for (int i = 0; i < virualCount; i++) {         int virtualServerIpHashCode = Math.abs((serverIp + "#" + i).hashCode());         serverMap.put(virtualServerIpHashCode, serverIp);     } }

2.5Nginx中使用一致性Hash算法

1、安装

Nginx本身是没有一致性Hash模块的需要去下载安装上,下载一致性Hash地址https://github.com/replay/ngx_http_consistent_hash 下载好之后直接上传到跟nginx安装包同一目录下,比如/opt/。

解压缩安装包,进入到nginx安装包的根目录下【我的 /opt/nginx-1.19.6】,执行命令安装一致性hash模块 ./configure --add-module=/opt/ngx_http_consistent_hash-master 然后从新对nginx进行编译安装 make make install。

2、使用

一致性hash三种使用配置方案

consistent_hash $remote_addr:可以根据客户端ip映射

consistent_hash $request_uri:根据客户端请求的uri映射

consistent_hash $args:根据客户端携带的参数进行映


具体配置如下图:


分布式常见问题分析及解决方案

3、集群时钟同步问题

假如一个集群中时间不同步的话,可能在存储的时候时间就不一致,就会导致数据记录有问题。

解决方案:

场景一:所有机器都可以联网

ntpdate -u ntp.api.bz 这个命令将系统与互联网时钟进行同步。当然也可以使用Linux的定时任务每隔一段时间将时间与互联网进行同步。

场景二:部分机器能连接到互联网

首先选中一台能连接到互联网的机器将这台机器配置为时间服务器

1、如果有 restrict default ignore,注释掉它2、添加如下⼏行内容restrict 172.17.0 mask 255.255.255.0 nomodify notrap # 放开局 需要输入你自己的网段域⽹同步功能,172.17.0.0是你的局域⽹⽹段server 127.127.1.0 # local clockfudge 127.127.1.0 stratum 103、重启⽣效并配置ntpd服务开机⾃启动service ntpd restartchkconfig ntpd on

然后其他所有的服务器都跟这台服务器进行时钟同步命令:ntpdate 172.17.0.17

场景三:所有机器都不能联网

思路跟场景二的思路是一样的,手动设置一台服务器的时钟 并配置成时钟服务器,其他机器与该台服务器进行同步即可。


4、分布式ID

在分布式系统中需要使用唯一的id来当作表示数据的唯一 ,如果使用普通的mysql自增的话可能会导致重复。

方案一:使用UUID,直接使用JDK自带的工具来生成一个UUID

方案二:通过雪花算法,根据事件戳+机器id+随机数

方案三:使用Redis自增长来获取一个唯一id Redis Incr

4.1雪花算法

原理


分布式常见问题分析及解决方案


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) {         IdWorker worker = new IdWorker(21,10,0);         for (int i = 0; i < 100; i++) {             System.out.println(worker.nextId());         }     } }


5、分布式任务调度

5.1什么是分布式调度任务

1、运行在分布式集群环境下的调度任务(同⼀个定时任务程序部署多份,只应该有⼀个定时任务在执行)

2、分布式调度—>定时任务的分布式—>定时任务的拆分(即为把⼀个⼤的作业任务拆分为多个⼩的作业任务,同时执行)


分布式常见问题分析及解决方案


在分布式任务执行的时候绝对不允许多个机器同时执行。

5.2定时任务和消息队列的区别

共同点

1、异步处理:⽐如注册、下单事件

2、应⽤解耦:不管定时任务作业还是MQ都可以作为两个应⽤之间的⻮轮实现应⽤解耦,这个⻮轮可以中转数据,当然单体服务不需要考虑这些,服务拆分的时候往往都会考虑流量削峰。双⼗⼀的时候,任务作业和MQ都可以⽤来扛流量,后端系统根据服务能⼒定时处理订单或者从MQ抓取订单抓取到⼀个订单到来事件的话触发处理,对于前端⽤户来说看到的结果是已经下单成功了,下单是不受任何影响的。

区别

1、定时任务作业是时间驱动,⽽MQ是事件驱动;

2、时间驱动是不可代替的,⽐如⾦融系统每⽇的利息结算,不是说利息来⼀条(利息到来事件)就算⼀下,⽽往往是通过定时任务批量计算;

所以,定时任务作业更倾向于批处理,MQ倾向于逐条处理;

5.3 分布式调度之Quartz

定时任务的实现⽅式有多种。早期没有定时任务框架的时候,我们会使⽤JDK中的Timer机制和多线程机

制(Runnable+线程休眠)来实现定时或者间隔⼀段时间执行某⼀段程序;后来有了定时任务框架,⽐如⼤名鼎鼎的Quartz任务调度框架,使⽤时间表达式(包括:秒、分、时、⽇、周、年)配置某⼀个任务什么时间去执

Quartz使用Demo


POM

<dependency>     <groupId>org.quartz-scheduler</groupId>     <artifactId>quartz</artifactId>     <version>2.3.2</version> </dependency>

启动类

public class QuartzMain {      // 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);         jobBuilder.withIdentity("jobName", "myJob");         JobDetail jobDetail = jobBuilder.build();         return jobDetail;     }   //3、创建作业任务时间触发器(类似于公交⻋出⻋时间表)  public static Trigger createTrigger() {       // 创建时间触发器,按⽇历调度         CronTrigger trigger = TriggerBuilder.newTrigger()                 .withIdentity("triggerName", "myTrigger")                 .startNow()                 //设置corn表达式                 .withSchedule(CronScheduleBuilder.cronSchedule("0/2 * * * * ? "))                 .build();         return trigger;     }      // 定时任务作业主调度程序     public static void main(String[] args) throws SchedulerException {         // 创建⼀个作业任务调度器(类似于公交调度站)         Scheduler scheduler = QuartzMain.createScheduler();         // 创建⼀个作业任务(类似于⼀辆公交⻋)         JobDetail job = QuartzMain.createJob();         // 创建⼀个作业任务时间触发器(类似于公交⻋出⻋时间表)         Trigger trigger = QuartzMain.createTrigger();         // 使⽤调度器按照时间触发器执行这个作业任务         scheduler.scheduleJob(job, trigger);         scheduler.start();     } }


业务执行类

public class DemoJob implements Job {     @Override     public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {         System.out.println("定时任务执行");     } }

6、Elastic-Job

6.1Elastic-jon简介

Elastic-Job是当当⽹开源的⼀个分布式调度解决⽅案,基于Quartz⼆次开发的,由两个相互独⽴的⼦项⽬Elastic-Job-Lite和Elastic-Job-Cloud组成。这里主要介绍 Elastic-Job-Lite,它定位为轻量级无中心化解决⽅案,使⽤Jar包的形式提供分布式任务的协调服务,⽽Elastic-Job-Cloud⼦项⽬需要结合Mesos以及Docker在云环境下使⽤。

Elastic-Job的github地址:https://github.com/apache/shardingsphere-elasticjob-example

主要功能

1、分布式调度协调,在分布式环境中,任务能够按指定的调度策略执行,并且能够避免同⼀任务多实重复执行

2、丰富的调度策略 基于成熟的定时任务作业框架Quartz cron表达式执行定时任务

3、弹性扩容缩容 当集群中增加某⼀个实例,它应当也能够被选举并执行任务;当集群减少⼀个实例时,它所执行的任务能被转移到别的实例来执行。

4、失效转移 某实例在任务执行失败后,会被转移到其他实例执行

5、错过执行作业重触发 若因某种原因导致作业错过执行,⾃动记录错过执行的作业,并在上次作业

完成后⾃动触发。

6、支持并行调度支持任务分片,任务分片是指将⼀个任务分为多个小任务项在多个实例同时执行。

7、作业分片⼀致性 当任务被分片后,保证同⼀分片在分布式环境中仅⼀个执行实例。

6.2Elastic-Job-Life应用

Elastic-Job依赖于Zookeeper因此在使用的时候需要部署一个ZK。

1、安装ZK

1、下载连接 https://downloads.apache.org/zookeeper/zookeeper-3.5.9/apache-zookeeper-3.5.9-bin.tar.gz 上传到服务器上,进行解压

2、进⼊conf⽬录,cp zoo_sample.cfg zoo.cfg

3、进⼊bin⽬录,启动zk服务

启动命令 ./zkServer.sh start

查看状态 ./zkServer.sh status

关闭命令 ./zkServer.sh stop


2、代码编写

POM

<!--elasticJob应用--> <dependency>     <groupId>com.dangdang</groupId>     <artifactId>elastic-job-lite-core</artifactId>     <version>2.1.5</version> </dependency>


Job执行业务类

public class DemoJob implements SimpleJob {     //模拟不同的编号     @Override     public void execute(ShardingContext shardingContext) {         //执行你的业务逻辑         System.out.println("我是分布式ElasticJob");     } }


Job启动类

public class ElasticJobMain {      public static void main(String[] args) {         // 配置注册中心zookeeper,zookeeper协调调度,不能让任务重复执行,通过命名空间分类管理任务,对应到zookeeper的⽬录         ZookeeperConfiguration zookeeperConfiguration = new ZookeeperConfiguration("192.168.159.128:2181","demo-job");         CoordinatorRegistryCenter coordinatorRegistryCenter = new ZookeeperRegistryCenter(zookeeperConfiguration);         coordinatorRegistryCenter.init();          // 配置任务         JobCoreConfiguration jobCoreConfiguration = JobCoreConfiguration                 .newBuilder("demo-job","*/2 * * * * ?",1)                 .build();         SimpleJobConfiguration simpleJobConfiguration = new SimpleJobConfiguration(                 jobCoreConfiguration,                 DemoJob.class.getName()         );          // 启动任务         new JobScheduler(                 coordinatorRegistryCenter,                 LiteJobConfiguration.newBuilder(simpleJobConfiguration).build()         //初始化 就是进行启动         ).init();     } }


启动两个main来观察分布式的执行情况,假如一个当家了任务会分配到另外一个上继续执行。


6.3Elastic-Job分片、弹性扩容

如何理解轻量级和去中心化?


分布式常见问题分析及解决方案


1、任务分片

⼀个很大的非常耗时的Job任务,⽐如:⼀次要处理⼀亿的数据,那这⼀亿的数据存储在数据库中,如果⽤⼀个作业节点处理⼀亿数据要很久,在互联⽹领域是不太能接受的,互联⽹领域更希望机器的增加去横向扩展处理能力。所以,ElasticJob可以把作业分为多个的task(每⼀个task就是⼀个任务分片),每⼀个task交给具体的⼀个机器实例去处理(⼀个机器实例是可以处理多个task的),但是具体每个task执行什么逻辑由我们⾃⼰来指定。


分布式常见问题分析及解决方案


Strategy策略定义这些分片项怎么去分配到各个机器上去,默认是平均去分,可以定制,比如某⼀个机

器负载比较高或者预配置比较高,那么就可以写策略。分片和作业本身是通过⼀个注册中心协调的,因

为在分布式环境下,状态数据肯定集中到⼀点,才可以在分布式中沟通。

总结 : 分片意思就是将一个很大的任务,按照业务逻辑水平拆分,通过传递业务参数来分别执行。


分片代码配置

设置业务参数


分布式常见问题分析及解决方案


获取业务参数


分布式常见问题分析及解决方案


2、弹性扩容


分布式常见问题分析及解决方案


新增加⼀个运行实例app3,它会⾃动注册到注册中心,注册中心发现新的服务上线,注册中心会通知

ElasticJob 进行重新分片,那么总得分片项有多少,那么就可以搞多少个实例机器,⽐如完全可以分1000片

最多就可以有多少app实例,机器能成的主,完全可以分1000片那么就可以搞1000台机器⼀起执行作业。

注意:

1)分片项也是⼀个JOB配置,修改配置,重新分片,在下⼀次定时运行之前会重新调⽤分片算法,那么这个分片算法的结果就是:哪台机器运行哪⼀个分片,这个结果存储到zk中的,主节点会把分片给分好放到注册中心去,然后执行节点从注册中心获取信息(执行节点在定时任务开启的时候获取相应的分片)。

2)如果所有的节点挂掉值剩下⼀个节点,所有分片都会指向剩下的⼀个节点,这也是ElasticJob的⾼可⽤。

总结弹性扩容:可以动态的添加机器,任务分片会自动触发分配。


7、Session一致性问题分析

7.1Session一致性问题

如果我们在Nginx服务器上没有配置一致性Hash策略采用轮训策略的话,那么在使用Session的时候由于机器不同导致Session不能进行共享,就不能通过Session来共享数据,最典型的案例就是用户登录的案例。


分布式常见问题分析及解决方案


从根本上来说是因为Http协议是无状态的协议。客户端和服务端在某次会话中产生的数据不会被保留下来,所以第⼆次请求服务端无法认识到你曾经来过, Http为什么要设计为无状态协议?早期都是静态⻚⾯无所谓有无状态,后来有动态的内容更丰富,就需要有状态,出现了两种⽤于保持Http状态的技术,那就是Cookie和Session。⽽出现上述不停让登录的问题。


分布式常见问题分析及解决方案


7.2Session一致性解决方案

1、就是在Nginx中使用IP_Hash策略或者配置一致性Hash策略

优点:1、置简单,不⼊侵应⽤,不需要额外修改代码

缺点:1、服务器重启Session丢失;2、存在单点负载⾼的⻛险;3、单点故障问题

2、Session复制(不推荐)也即,多个tomcat之间通过修改配置⽂件,达到Session之间的复制

优点:1、不⼊侵应⽤;2、便于服务器⽔平扩展;3、能适应各种负载均衡策略;4、服务器重启或者宕机不会造成Session丢失。

缺点:1、性能低;2、内存消;3、不能存储太多数据,否则数据越多越影响性能;4、延迟性

3、通过Session共享将Session集中存储,比如使用Redis中间件

优点:1、能适应各种负载均衡策略;2、服务器重启或者宕机不会造成Session丢失;3、扩展能力强

适合⼤集群数量使用

缺点:1、对应⽤有⼊侵,引⼊了和Redis的交互代码

7.3Session共享

Session共享就是通过Redis将Session集中存储。


分布式常见问题分析及解决方案


使用Demo【SpringBoot项目】

POM

<dependency>     <groupId>org.springframework.boot</groupId>     <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency>     <groupId>org.springframework.session</groupId>     <artifactId>spring-session-data-redis</artifactId> </dependency>

配置Redis

spring.redis.database=0

spring.redis.host=127.0.0.1

spring.redis.port=6379


启动类

启动类上贴上@EnableRedisHttpSession

其原理可以通过源码知道使用的是过滤器对请求进行拦截将session存放到redis中。