分布式解决方案

分布式协调工具Zookeeper:
 Java语言编写开源框架
 ZK应用场景:
  1.注册中心->一般都是集群版本,consul\eureka\zk\redis高可用
  2.分布式配置中心->动态管理配置文件信息
  3.消息中间件->事件通知,类似发布订阅功能
  4.解决分布式事务->全局协调者
  5.分布式锁
  6.选举策略(哨兵机制)
  7.本地动态负载均衡(Dubbo服务负载均衡原理)->服务注册临时节点,客户端从zk获取父节点,得到服务器信息,本地使用负载均衡算法去访问得到的服务器
  8.消息中间件集群管理

 存储结构:
  以节点方式进行存储,类似XML树状结构(目录结构)
  节点路径(节点名称),不能重复,唯一

 节点类型:
  永久Persistent:创建节点永久的持久化在硬盘上
  临时Ephemeral:当前节点和会话连接保持,如果连接断开,那该节点也会被删除(分布式锁原理)

 节点事件通知:
  每个节点都有事件通知,对该节点发生增删改都会有事件通知。类似于消息中间件

分布式锁:
 1.产生原因,因为服务器产生集群
 2.在单台服务器上如何生成订单,保证唯一性
  UUID+时间戳,集群中无法保证唯一性
  大型电商,使用redis,提前生成一定量订单号存放redis中,客户下单就直接取对应订单号就行,redis是单线程的,可以保证唯一性,当redis里面订单快用完的时候,会自动继续补上
 3.需要使用锁的客户端在zk上同时去创建临时节点,创建成功则表示获取到锁,使用zk节点路径名称的唯一性,保证已有一个客户端能拿到锁。同时没有获取到锁的其他客户端,都去监听这个节点(可以监听节点的删除和改变),一旦获取到锁的客户端做完操作就删除节点表示释放锁,其他客户端收到消息就会再一起去创建节点,大家一起重新去获取锁。

分布式Session:
 分布式Session一致性:
  集群服务器Session共享的问题
 作用:服务器端与客户端保存整个通讯的会话基本信息
 应用场景:登陆流程做法(登陆成功后,获取userId存放session中,下次再获取,就直接从session获取),防表单重复提交
 类似于Token也可做到

分布式网站跨域问题:
 属于浏览器安全策略问题
 一定要端口号和域名保持一致。请求可以访问,但是获取不到结果。
 两个项目之间使用ajax实现通讯,如果浏览器访问的域名地址与ajax访问的地址不一致的情况下,默认情况下浏览器会有安全机制致使无法获取到返回,即为跨域问题
 解决方案:
  1.使用jsonp解决,但是只支持get请求,不支持post.浏览器生成一个随机数,传给服务端,服务端返回时也把随机数返回过来,有点麻烦
  2.使用HttpClient进行转发,效率低,会发送两次求情。前端调用自己接口,自己服务端使用httpclient调用目的域名获取返回给前端,发送了两次请求
  3.设置响应头允许跨域,setHeader。response.setHeader("Access-Control-Allow-Origin", "*");所有域名都允许跨域,放在过滤器中。小项目快速解决问题
  4.使用Nginx搭建API接口网关。A项目里直接调用www.ddd.com/b/***就行了
 配置nginx:

server {
    listen       80;
    server_name  www.ddd.com;
    ###A项目
    location /a {
        proxy_pass   http://a.ddd.com:8080/;
        index  index.html index.htm;
    }
    ###B项目
    location /b {
        proxy_pass   http://b.ddd.com:8081/;
        index  index.html index.htm;
    }
}

  5.使用zuul微服务搭建API接口网关。同nginx,就是在springcloud里新增Zuul服务,配置下网关,然后用zuul访问,微服务通讯不会产生跨域问题

ZK集群选举环境搭建:
 为什么:服务器集群保证高并发,注册中心集群为了保证系统高可用,进行服务治理,必须集群,实时的控制服务。服务从注册中心获取到相应服务地址后,把其缓存再jvm缓存一份,并设置个监听,如果节点改变则重新更新
 搭建:
  安装JDK,因为ZK是Java写的,所以需要jdk环境
  配置环境变量

修改profile文件
vi /etc/profile
export JAVA_HOME=/usr/local/jdk1.8.0_181
export ZOOKEEPER_HOME=/usr/local/zookeeper
export CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar
export PATH=$JAVA_HOME/bin:$ZOOKEEPER_HOME/bin:$PATH
刷新profile文件
source /etc/profile

  安装ZK:

进入目录
cd /usr/local/zookeeper/conf
mv zoo_sample.cfg zoo.cfg

vi zoo.cfg
dataDir=/usr/local/zookeeper/data
server.0=192.168.212.154:2888:3888
server.1=192.168.212.156:2888:3888
server.2=192.168.212.157:2888:3888

  服务器标识配置:

  创建文件夹

mkdir /usr/local/zookeeper/data
创建文件myid并填写内容为0
vi myid (内容为服务器标识 : 0)

  再在另外两台服务上搭建ZK

  server.1=192.168.212.156:2888:3888
  server.2=192.168.212.157:2888:3888
  修改相应的myid

  springboot需要自己配置ZK版本号,没有集成

分布式任务调度平台:
 传统定时任务调度(单点系统)方案:
  1、使用Thread方式(线程睡眠时间就是周期,最简单);
  2、TimerTask方式(单线程,任务会相互影响,且执行周期任务时依赖系统时间):

timer.scheduleAtFixedRate(new TimerTask() {
        @Override
        public void run() {...}
    }, delay天数, period秒数);

  3、ScheduledExecutorService方式(线程池,多线程并发执行,任务互不影响,基于时间的延迟,不会由于系统时间的改变发生执行变化):周期执行和使用Callable延迟执行。

ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor();
// 第二个参数为首次执行的延时时间,第三个参数为定时执行的间隔时间
service.scheduleAtFixedRate(new Runnable() {
        public void run() {...}
    }, 1, 1, TimeUnit.SECONDS);

  4、使用Quartz:

//实现Job接口,创建任务类
public class MyJob implements Job {
	public void execute(JobExecutionContext context) throws JobExecutionException {}}	

//1.创建Scheduler的工厂
SchedulerFactory sf = new StdSchedulerFactory();
//2.从工厂中获取调度器实例
Scheduler scheduler = sf.getScheduler();
//3.创建JobDetail
JobDetail jb = JobBuilder.newJob(MyJob.class).........
//4.创建Trigger触发器
Trigger t......
//5.注册任务和定时器
scheduler.scheduleJob(jb, t);
//6.启动 调度器
scheduler.start();

  需要使用crontab表达式

  定时任务需要考虑高并发,以备在同一时间点执行多个任务:如果定时任务宕机怎么办?使用心跳检测监控自动重启,补偿机制(每个任务打一个小标记),定时任务代码终端突然报错,使用日志记录错误,跳过继续执行。

  5.分布式中定时任务,就要保证定时Job幂等性
   1.使用ZK实现分布式锁(效率差),要执行就去zk创节点,就避免其他执行了
   2.配置文件中加上启动Job的开关,设置一个启动,其他不启动
   3.XXL-Job 轻量级分布式任务调度平台
   a.支持Job集群,保证幂等,Job负载均衡轮询机制
   b.支持Job补偿,失败自动重试,多次失败则发邮件
   c.Job日志记录
   d.动态配置定时规则
   XXL-Job原理:执行器(定时Job实际执行的服务地址)和任务管理(配置任务定时规则、路由策略、运行模式等)

分布式配置中心携程Apollo:
 1.特点:
  a.吃内存,对外部依赖少
  b.基于springBoot和SpringCloud开发,提供Java和.Net原生客户端
  c.统一管理不同环境environment、不同集群cluster、不同命名空间namespace的配置
  d.配置修改实时生效(热发布,客户端于服务保持长连接),也可灰度发布(点了发布支队部分应用实例生效,没问题后再推给所有实例)
  e.版本发布管理,配置发布都有版本概念,支持回滚
  f.权限管理、发布审核、操作审计日志
  g.客户端可以配置信息监控,看到配饰被那些实例使用

 2.原理:
  核心模块:ConfigService(服务于Apollo客户端,提供配置获取和推送接口)、AdminService(服务Portal,提供配置管理和修改发布接口)、Client(为应用获取配置,支持实时更新)、Portal(配置管理界面)
  辅助服务发现模块:Eureka(ConfigService/AdminService注册实例并定期报心跳,便于Client和Portal发现)、MetaService(Client和Portal用其获取Service服务列表)、MginxLB(软负载路由)

  图片来自网络https://github.com/ctripcorp/apollo,侵删

  支持.NET:

 图片来自网络https://github.com/ctripcorp/apollo,侵删

 3.快速搭建:
  1.安装JDK环境1.8
  2.安装Mysql数据库5.7+,创建ApolloPortalDB和ApolloConfigDB数据库
  3.编辑demo.sh启动脚本,配置上面新建的两个数据库的连接信息
  4.启动脚本demo.sh (service和portal日志文件地址)

 4.应用服务器配置:
  1.引入jar包,maven库里没有,得自己导入本地maven库。可以使用biuld.bat
  2.配置application.yml文件
  3.修改环境:修改/opt/settings/server.properties(Mac/Linux)或C:\opt\settings\server.properties(Windows)文件,设置:env=DEV
  4.创建apollo-env.properties文件
  5.在META-INF文件夹创建app.properties,指定appid

分布式事务:
 事务即一个操作序列
 产生背景:
  传统项目情况下->多数据源,一个项目中,有两个JDBC连接(使用分包或者注解)
  微服务项目情况下->每个服务都有自己独立的数据库(数据源),互不影响,即自己的本地事务,当两个服务相互通讯的时候,但是两个本地事务互不影响,就出现分布式事务问题
  本地事务的有效范围在同一个JDBC里里面
  用户充值,会触发余额增减的抵扣,可能出现事务问题,比如充值在调用余额增减抵扣的时候,已经调用了,但是充值的下一步出问题了,就会发生余额增减抵扣发生,但是充值失败
 CAP原理:一致性(分布式系统中所有数据备份在同一时刻具有同样的值,所有节点同一时刻的数据都是最新的数据副本)、可用性(响应性能好,在故障下,服务能在有限的时间内处理完成并进行响应)、分区容忍性(数据必须存放在多节点,以免程序出现节点之间不连通导致分区,分区之间数据无法相互访问,所以服务无法容忍单节点)
 总结:要保证分区容忍性,就要把数据复制多个节点,数据要复制存放在多个节点就会带来一致性问题,那为了保证数据一致性,那每次的写操作都要等待所有的节点写成功服务才能使用,那这段时间的服务可能出现可用性问题。三角关系,太极八卦啊
 ACID理论:数据库管理系统中事务的四个特性,保证强一致性,即原子性、一致性、隔离性、持久性
 BASE理论(是对CAP中一致性和可用性权衡的结果):
  基本可用:系统出现不可预知的故障,但是还能用,损失部分可用性,响应时间上的损失,或者功能上的损失,对部分不重要的服务进行降级处理
  软状态:允许系统中的数据存在中间状态,并任务改状态不影响系统的整体可用性,即允许系统在多个节点的数据副本存在数据延时
  最终一致性:软状态不能一直存在,否则会降低系统的可行性,必须有个时间限制,在期限过后,就要保证所有副本保持数据一致性,达到数据的最终一致性。时间期限取决于网络延时、系统负载、数据复制方案设计等
 最终一致性又分为5种类型:因果一致性、读己之所写、会话一致性、单调读一致性、单调写一致性

 柔性事务(满足BASE,两阶段型、补偿型、异步确认型、最大努力通知型)与刚性事务(满足ACID)

 传统项目解决分布式事务问题:JTA+Atomic

 两阶段提交协议:
  准备阶段->协调者询问所有参与者是否可以执行提交事务,参与者执行事务但不提交(undo和redo信息写入日志)锁定事务,完成后再相应协调者成功还是失败
  提交阶段->a.回滚=协调者收到参与者失败或超时消息,直接向所有参与者节点发出回滚请求,参与者利用写入的undo回滚并向协调者发送回滚完成消息,协调者接受到多有参与者回滚消息,取消事务完成;b.提交=同回滚,协调者发出提交请求,参与者提交并发提交完成消息给协调者,协调者收到所有参与者反馈的成功,完成事务
  缺陷:1.执行中,所有参与节点都是事务阻塞型的,会造成同步阻塞问题;2.单点故障,协调者宕机的话,不知道事务现在具体的状态;3.数据不一致,在发送提交请求时,网络故障会导致部分参与节点提交数据,部分没提交;

 三阶段提交协议:相比于两阶段,增加了个协调者询问参与者可行性的过程,确保尽可能早的发现无法执行操作而需要终止的行为,但并不能发现所有这种行文,只是减少发生问题的情况。设置超时时间,一旦超过时间,参与者和协调者都默认继续提交事务,默认成功,可能发生不一致问题,但不会发生阻塞和永久锁定资源的问题

 LCN:
  1.发起方和参与者都要注册到事务协调者中,简历一个长连接(减少宽带传输,但是较占内存)
  2.事务发起方调事务参与方之前,会向事务协调者TxManager申请创建一个事务分组ID
  3.事务发起方调用事务参与方的时候,会在请求头中存放该事务的分组ID
  4.事务参与者收到对应事务的分组ID,执行完业务代码,会采用假关闭,不会提交事务
  5.事务发起方调用参与方后,后面执行都没问题,发起方根据事务分组ID通知事务协调者MxManager,然后事务协调者根据分组ID通知其他所有参与方

 基于可靠消息服务解决分布式事务:

  场景:一个请求,需要在A、B两系统里分别进行事务操作,且两个事务操作必须保证原子性,即在同一个事务中
  系统A:任务A->消息中间件->系统B:任务B
  1.系统A在处理任务A前,向RabbitMQ发送一条消息
  2.RabbitMQ收到消息后将该条消息持久化,但是并不投递。同时下游的系统B并不知道该条消息的存在
  3.系统A收到RabbitMQ的确认应答后,开始处理任务A
  4.任务A处理完成,系统A向RabbitMQ发送commit请求。如果任务A处理失败,则进入回滚流程,系统A就向RabbitMQ发送Rollback请求。发完请求系统A相当于事务处理过程结束。
  5.RabbitMQ收到Commit指令后,将该消息投递给系统B,触发B任务执行。如果收到的是Rollback指令,则直接将该消息丢弃,不投递给系统B。
  6.任务B执行完成后,系统B向RabbitMQ返回一个确认应答,表示该消息已经成功消费。为防止B没有返回应答,RabbitMQ需要使用消息重试(补偿)机制
  7.分布式事务处理完成
  事务处理存在一定的时间差,期间系统数据是不一致的,这段时间过后,最终实现一致性,满足了BASE理论
  上游系统和RabbitMQ之间采用异步通信是为了提高系统并发度,但是必须防止commit和rollback指令在传输中丢失
  超时询问机制:Commit和Rollback指令都又可能在传输的过程中丢失,所以必须有一个超时询问机制,即系统A必须提供一个超时事务询问接口供RabbitMQ调用,当RabbitMQ收到系统A发送的事务型消息后便开始计时,如果超时了还没收到系统A发来的commit或rolback指令的话,就调用此接口询问该系统目前的状态->提交、回滚、处理中
  RabbitMQ和下游系统之间采用同步通信,降低系统复杂度

 

posted @ 2020-06-08 16:37  shuG214xin  阅读(265)  评论(0编辑  收藏  举报