分布式理论

分布式理论

分布式系统定义及面临的问题

分布式系统定义

分布式系统是一个硬件或者软件分布在不同网络的计算机上,彼此之间仅仅通过消息传递进行通信和协调的系统

分布式环境面临的问题

  • 通信异常

    分布式系统之间通讯是通过网络通讯的,每次通讯网络伴随着不可用的风险(光纤,路由不可用等等),导致分布式系统之间出现网络延迟,消息丢失等等问题

  • 网络分区

    网络分区是网络之间出现网络故障,导致大区与大区之间网络中断,但是子网络直接还能通讯,这样分布式系统就会出现小部分集群情况.这就是网络分区

  • 三态

    三态,就是成功,失败和超时

  • 节点故障

    分布式系统,某台服务器节点出现故障,宕机等等

分布式理论:一致性概念

  • 强一致性

    要求系统写入什么,立即读出来也是什么,用户体验好,但是对服务器性能要求很高

  • 弱一致性

    约束了系统写入成功后,不承诺立即可以读到写的值,也不承诺多久数据能达到一致,会尽量保证数据在某个时间段能一致

  • 最终一致性

    这个是弱一致性的升级版,承诺在一定时间内,数据能够达到一致.

分布式理论:CAP定理

选项概念
C(consistence)一致性,分布式环境中,多个副本数据时刻保持一致,当一个系统在数据一致更新后,保证系统的数据还是一致
A(Availability)可用性,系统提供的服务一致处于可用的状态,每次请求都能快速响应,但不保证数据为最新数据
p(Partition tolerance)分区容错性,分布式系统在遇见网络分区情况下,仍然能提供对外满足一致性与可用性的服务,除非整个网络环境发生故障

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WNiEOHBR-1591258346732)( https://typora-hb.oss-cn-shanghai.aliyuncs.com/typera/image-20200603160218600.png)]

CAP能同时满足吗?答案是不能,因为满足一致性与可用性的话,分区容错性不能满足,其他两种结合都另一种就不能满足,他们之间有一定冲突,对立.所以我们只能根据应用场景的不同选中两个结合放弃一种

组合分析结果
CA满足一致性与可用区,放弃分区容错性.说白了就是将所有数据都放在一个分布式节点上,这样做虽然不会100%的成功.至少不会遇见网络分区带来的影响.放弃P也就是意味着放弃了系统的可扩展性
CP满足一致性与分区容错性,放弃可用性,说白了.就是当系统遇见故障是,为了保证数据的一致性与系统的可用性,我们需要停机修复,那么收影响的就是服务需要等待一段时间.在期间系统无法对外提供政正常服务,即不可用
AP满足可用性与分区容错性,放弃一致性.当系统出现问题或者分区问题时,可以保证系统的可用性,但是系统的数据就在一段时间内出现不一致现象.但是数据会在一段时间后会达到最终一致性.

分布式理论:BASE定理

在cap定理中,一致性,可用性,分区容错性不能同时满足,而分区容错性而言是分布式系统必须的.所以推出了base理论

BASE:全称 basically available(基本可用) soft state(软状态) 和 eventually consistent(最终一致性) 三个短语的简称

BASE理论是对CAP中一致性与可用性权衡的结果.其来源是大型互联网分布式实践的总结.基于CAP定理逐步演化的而来的.其核心思想:既然无法做到强一致性,但是每个应用都可以根据自身的业务特点,采用适当的方式使系统达到最终一致性

  • 基本可用(basically available)

    就是系统出现不可预知的故障时,允许损失一部分的可用性,这里不绝不是等价于系统不可用

    • 响应时间上的损失

      正常系统响应时间0.5S,因为出现故障,响应时间延长到1-2S

    • 功能上损失

      正常情况下,比如双十一犹豫订单数据暴增,为了保证系统的稳定与一致性,部分消费者会被引导到一个降级页面(数据加载失败,请重试)

  • 软状态(soft state)

    什么是软状态?相对于一致性,要求多个节点的数据副本都是一致的,这就是一种硬状态.

    软状态:允许系统中数据存在中间状态,并不认为系统不可用,即允许系统在多个节点的数据副本在同步过程中存在延迟

  • 最终一致性(eventually consistent)

    最终一致性强调的是系统中所有的数据副本,在经过一段时间的同步后,数据能达到一致状态.强调的是系统在一段时间达到一致,而不是实时一致

    最终一致性存在以下五类变种

    • 因果一致性

      节点A更新数据后,通知节点B,节点B之后对该数据进行查询与修改都是基于A更新后的值,于此同时,与A无关的节点C数据访问则没有限制

    • 读已之所写

      节点A更新数据后,只能看到更新后的最新值,而不是旧值,这也是另一种因果一致性

    • 会话一致性

      在一次会话中,系统保证同一个有效会话中现实读已之所写的一致性

    • 单调读一致性

      当节点A读到一个数据的某个值后,不会再读到旧值,只会是当前值和新值

    • 单调写一致性

      保证同一个节点写操作顺序执行

一致性协议:2PC

在分布式系统中,会有多个节点机器,每个节点机器都能保证自己进行的事务操作过程中的结果是失败还是成功,但是无法直接获取其他节点的操作结果.因此当一个事务操作需要跨节点的时候,为了保证事务处理的ACID(原子性,一致性,隔离性,持久性)特性,就需要引入一个协调者组件来统一调度所有分布式的执行逻辑,这些被调度的节点统称为参与者.协调者负责调度参与者行为,并最终决定这些参与者是否把事务真正进行提交.基于这个思想就衍生了二阶段提交与三阶段提价两种协议

2PC阶段一:事务提交请求
  • 事务询问
  • 执行事务
  • 各个参与者向协调者反馈事务的响应
2PC阶段二: 执行事务提交/回滚

成功

  • 发送提交请求
  • 事务提交
  • 反馈事务提交结果
  • 完成事务

回滚

  • 发送回滚请求
  • 事务回滚
  • 反馈事务回滚结果
  • 中断事务

将一个事务处理过程分为投票与执行两个阶段,核心就是每个事务采用先尝试后提交的处理方式,从而保证其原子性和一致性,因此可以将二段提交看成一个强一致性的算法

2PC优缺点

优点

​ 原理简单,实现方便

缺点

  • 同步阻塞

    二阶段提交协议存在明显最大的一个问题就是同步阻塞,在二阶段提交的执行过程中,所有的参与该事务的操作逻辑都处理阻塞状态,也就是说,各个参与者都在等待其他参与者响应的过程.无法进行其他操作.限制了分布式系统的性能

  • 单点问题

    如果协调者在提交事务阶段出行问题,整个流程无法运作,参与者一直处于锁定事务资源的状态,而无法完成事务操作

  • 数据不一致

    当协调者发送事务提交请求时,出现网络故障,导致部分参与者接受到请求提交事务,部分参与者没有.导致数据不一致

  • 过于保守

    当个别参与者出现故障时,协调者只能通过自身的超时机制中断事务

    当协调者出现故障是,参与者只能处理事务锁定状态,占用资源

一致性协议:3PC

三段式提交就是在2PC基础上的改进版,在2PC一阶段一分为二,共组成了CanCommit,PreCommit和doCommit三个阶段组成

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rz3FSvGv-1591258346734)( https://typora-hb.oss-cn-shanghai.aliyuncs.com/typera/image-20200604104103870.png)]

3PC阶段一:CanCommit
  • 事务询问
  • 各个参与者向协调者反馈事务询问的响应
3PC阶段二:PreCommit

协调者得到参与者响应之后,只会有两种结果

  • 执行事务预提交
    • 发送预提价请求
    • 事务预提交
    • 参与者向协调者返回事务执行的结果
  • 中断事务
    • 发送中断请求
    • 中断事务
3PC阶段三:doCommit

成功

  • 发送事务提交请求
  • 事务提交
  • 反馈事务结果
  • 完成事务

失败

  • 发送中断请求
  • 事务回滚
  • 返回事务结果
  • 中断事务

3PC优缺点

优点

​ 比较与2PC,最大的优点就是降低了参与者阻塞范围,其他能够在单点故障后继续达成一致

缺点

​ 如果参与者收到preCommit消息后,出现了网络分区,那么参与者等待超时,会默认进行事务提交,这个必然会出现数据不一致问题

2PC对比3PC

3PC对协调者与参与者都设置了超时机制,其中2PC只有协调者有超时机制,3PC的准备阶段和提交阶段插入了预提交阶段,使PreComit是一个缓冲,保证了最后提交阶段之前参与者的状态是一致的

一致性算法:Paxox

首先有一个很重要的概念叫做提案,最终达成一致的value就是在提案里

  • Proposer: 提案发起者
  • Acceptor:决策者,可以批准提案
  • Learners:最终决策学习者

一致性算法:Raft

使用心跳机制来触发选举

  • 领导者
  • 候选者
  • 跟随者

分布式系统设计策略

  • 心跳检测

  • 高可用设计

    • 主备
    • 互备
    • 集群
  • 容错性

  • 负载均衡

分布式架构网络通讯

基本原理

要实现网络机器之间的通讯,首先的来看看计算机系统网络通讯的基本原理,在底层层面看,网络通讯需要做的就是将流从一台计算机传输到另一台计算机,基于传输协议和网络IO来实现,

传输协议:比较出名的有TCP,UDP等等,TCP,UDP都是基于Socket概念上为某应用场景而扩展出来的传输协议

网络IO:主要有bio,nio,aio三种方式

RPC

prc全程remote procedure call 即远程过程调用

结构

  • 客户端(client)

    服务的调用方

  • 客户端存根 (client stub)

    存放服务器的地址消息,在将客户端的请求参数打包成网络消息,然后通过网络远程发送给服务方

  • 服务端(server)

    真正的服务提供者

  • 服务端存根(server stub)

    接受客户端发送过来的消息,将消息解包,并调用本地的方法

调用过程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eSQs99Rc-1591258346735)( https://typora-hb.oss-cn-shanghai.aliyuncs.com/typera/image-20200604135050483.png)]

Client Client Stub Server Stub Server 1.客户端调用 2.序列化 3.消息发送 4.反序列化 5.调用本地方法 6.服务处理 7.返回处理结果 8.将结果序列化 9.返回消息 10.将结果反序列化 11.返回调用结果 Client Client Stub Server Stub Server

RMI

JAVA RMI是远程方法调用,是java原生支持的远程调用,采用JRMP作为通讯协议

客户端从注册中心获取服务端具体信息
客户端调用服务端执行服务
服务端在注册中心注册服务
Client
Registry 注册中心
Server

案例

  1. 创建远程接口,并继承java.rmi.Remote接口
  2. 实现远程接口,并且继承:UnicastRemoteObject
  3. 创建服务器程序: createRegistry()方法注册远程对象
  4. 创建客户端程序

代码实现

  • 远程接口

    package com.study.hb.io.rmi;
    
    import java.rmi.Remote;
    import java.rmi.RemoteException;
    
    /**
     * 远程服务对象接口必须继承java.rmi.Remote,同时方法必须抛出java.rmi.RemoteException异常
     *
     * @author bo.huang
     * @since 2020/6/4 2:20 下午
     */
    public interface HelloRemote extends Remote {
    
        /**
         * 你好RMI
         *
         * @param user user
         * @return java.lang.String
         * @throws RemoteException 异常
         * @author bo.huang update
         * @date 2020/6/4 2:38 下午
         */
        String sayHello(User user) throws RemoteException;
    }
    
    
  • 引用类

    package com.study.hb.io.rmi;
    
    import lombok.Data;
    
    import java.io.Serializable;
    import java.util.Date;
    
    /**
     * 引用对象必须是可序列化对象
     *
     * @author bo.huang
     * @since 2020/6/4 2:21 下午
     */
    @Data
    public class User implements Serializable {
        /**
         * ID
         */
        private Long id;
        /**
         * 姓名
         */
        private String name;
        /**
         * 创建时间
         */
        private Date createTime;
    
        public User(Long id, String name, Date createTime) {
            this.id = id;
            this.name = name;
            this.createTime = createTime;
        }
    }
    
  • 远端接口实现

    package com.study.hb.io.rmi;
    
    import lombok.extern.java.Log;
    
    import java.rmi.RemoteException;
    import java.rmi.server.UnicastRemoteObject;
    
    /**
     * 远程调用服务对象实现类,必须继承java.rmi.server.UnicastRemoteObject
     *
     * @author bo.huang
     * @since 2020/6/4 2:26 下午
     */
    @Log
    public class HelloRemoterImpl extends UnicastRemoteObject implements HelloRemote {
        /**
         * UnicastRemoteObject的构造方法必须抛出RemoteException异常
         *
         * @throws RemoteException
         */
        public HelloRemoterImpl() throws RemoteException {
        }
    
        @Override
        public String sayHello(User user) throws RemoteException {
            log.info("服务端调用方法,数据:" + user);
            return "success";
        }
    }
    
  • 服务端实现

    package com.study.hb.io.rmi;
    
    import lombok.extern.java.Log;
    
    import java.net.MalformedURLException;
    import java.rmi.AlreadyBoundException;
    import java.rmi.Naming;
    import java.rmi.RemoteException;
    import java.rmi.registry.LocateRegistry;
    
    /**
     * 服务端程序
     *
     * @author bo.huang
     * @since 2020/6/4 2:30 下午
     */
    @Log
    public class Server {
    
        public static void main(String[] args) throws RemoteException, AlreadyBoundException, MalformedURLException {
            //创建一个远程调用对象
            HelloRemote helloRemote = new HelloRemoterImpl();
            //本地主机上的远程对象注册表Registry的实例,并指定端口
            LocateRegistry.createRegistry(9090);
            //将stub引用绑定到服务器地址上
            Naming.bind("rmi://127.0.0.1:9090/zm", helloRemote);
            log.info("====================服务端启动成功,并注册服务");
        }
    }
    
  • 客户端实现

    package com.study.hb.io.rmi;
    
    import lombok.extern.java.Log;
    
    import java.net.MalformedURLException;
    import java.rmi.Naming;
    import java.rmi.NotBoundException;
    import java.rmi.RemoteException;
    import java.util.Date;
    
    /**
     * 客户端程序
     *
     * @author bo.huang
     * @since 2020/6/4 2:34 下午
     */
    @Log
    public class Client {
    
        public static void main(String[] args) throws RemoteException, NotBoundException, MalformedURLException {
            //在RMI服务注册表中查询名字为HelloRemote的对象,并调用方法
            HelloRemote helloRemote = (HelloRemote) Naming.lookup("rmi://127.0.0.1:9090/zm");
            String result = helloRemote.sayHello(new User(1L, "黄波", new Date()));
            log.info("======客户端调用成功,结果:" + result);
        }
    }
    

调用结果

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aXXaY0dK-1591258346737)( https://typora-hb.oss-cn-shanghai.aliyuncs.com/typera/image-20200604144059369.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-t6aQH551-1591258346738)( https://typora-hb.oss-cn-shanghai.aliyuncs.com/typera/image-20200604144109554.png)]

BIO,NIO,AIO

IO:即输入(input)和输出(output),以内存为目标,数据进入内存叫输入,出内存叫输出

同步与异步

同步与异步关注的是消息通讯机制

  • 同步

    就是在发出一个调用时,在没有得到结果之前,该调用就不返回,但是一旦调用返回,就得到返回值了.简而言之就是由调用者主动等待这个结果的调用,通常是同步的,线程安全的

    使用同步IO是,java自己处理IO的读写

  • 异步

    根同步相反,调用在发出之后,这个调用就直接返回了,没有返回结果.被调用者通过状态.通知调用者或者通过回调函数来处理这个调用

    使用异步IO,java将IO读写委托给OS处理

阻塞与非阻塞

阻塞与非阻塞关注的是等待结果时的状态

  • 阻塞

    是指调用结果返回之前,当前线程会被挂起,调用线程置只会在得到结果之后才会返回,线程挂起,就不会给这个线程分配CPU,节约了CPU资源.但是挂起线程的优先级高于其他,所以还是阻塞的

  • 非阻塞

    是指在不能立即得到结果之前,该调用不会阻塞当前线程

BIO

同步阻塞IO

服务器实现模式为一个连接一个线程,即客户端有连接请求服务器时,就启动一个线程进行处理

适用场景:java1.4之前的唯一选择,简单易用,但是资源开销太高

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DxUJyIC3-1591258346738)( https://typora-hb.oss-cn-shanghai.aliyuncs.com/typera/image-20200604151754248.png)]

NIO

同步非阻塞

服务器实现模式为一个请求一个线程,即客户端发送连接请求会注册到多路复用器上,多路复用器轮询到接连有IO请求是才启动一个线程进行处理

适用场景:连接数目多连接比较短的架构,比如聊天服务器,java1.4后开始支持

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OSc9hJ0P-1591258346739)( https://typora-hb.oss-cn-shanghai.aliyuncs.com/typera/image-20200604152050963.png)]

AIO

异步非阻塞IO

服务器实现模式为一个有效请求一个线程,客户端的IO请求有OS先完成了在通知服务器应用去启动线程进行处理

适用场景:连接数目多且连接时间长的架构,例如相册服务器,java7支持

Netty

底层封装了JDK的NIO,Netty是一个异步事件驱动的网络应用框架

案例实现

  • jar引用

    implementation 'io.netty:netty-all:5.0.0.Alpha2'
    
  • 服务端代码

    package com.study.hb.io.netty;
    
    import io.netty.bootstrap.ServerBootstrap;
    import io.netty.channel.*;
    import io.netty.channel.nio.NioEventLoopGroup;
    import io.netty.channel.socket.nio.NioServerSocketChannel;
    import io.netty.channel.socket.nio.NioSocketChannel;
    import io.netty.handler.codec.string.StringDecoder;
    import io.netty.handler.codec.string.StringEncoder;
    import lombok.extern.java.Log;
    
    /**
     * netty服务端
     *
     * @author bo.huang
     * @since 2020/6/4 3:37 下午
     */
    @Log
    public class NettyService {
        public static void main(String[] args) throws InterruptedException {
            //1.创建NioEventLoopGroup的两个实例 workerGroup
            //当前这两个实例代表两个线程池,默认线程数为CPU核心数*2
            //bossGroup接受客户端传过来的请求
            //workerGroup处理请求
            EventLoopGroup bossGroup = new NioEventLoopGroup();
            EventLoopGroup workerGroup = new NioEventLoopGroup();
    
            //2.创建服务器启动辅助类:组装一些必要组件
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.group(bossGroup, workerGroup);
            //channel方法指定服务器监听通道
            serverBootstrap.channel(NioServerSocketChannel.class);
            //设置chanel handler
            serverBootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() {
                @Override
                protected void initChannel(NioSocketChannel ch) throws Exception {
                    //管道
                    ChannelPipeline pipeline = ch.pipeline();
                    //设置编码器
                    pipeline.addLast(new StringEncoder());
                    //设置解码器
                    pipeline.addLast(new StringDecoder());
                    //
                    pipeline.addLast(new SimpleChannelInboundHandler<String>() {
                        @Override
                        protected void messageReceived(ChannelHandlerContext ctx, String msg) throws Exception {
                            log.info("========服务端接口消息:" + msg);
                        }
                    });
                }
            });
            //监听端口
            ChannelFuture future = serverBootstrap.bind(9091).sync();
            log.info("服务器建立连接成功!");
            //会阻塞等待直到服务器的channel关闭
            future.channel().closeFuture().sync();
        }
    }
    
  • 客户端代码

    package com.study.hb.io.netty;
    
    import io.netty.bootstrap.Bootstrap;
    import io.netty.channel.Channel;
    import io.netty.channel.ChannelInitializer;
    import io.netty.channel.ChannelPipeline;
    import io.netty.channel.EventLoopGroup;
    import io.netty.channel.nio.NioEventLoopGroup;
    import io.netty.channel.socket.nio.NioSocketChannel;
    import io.netty.handler.codec.string.StringEncoder;
    
    import java.util.Date;
    
    /**
     * entty客户端
     *
     * @author bo.huang
     * @since 2020/6/4 3:52 下午
     */
    public class NettyClient {
    
        public static void main(String[] args) throws InterruptedException {
            //创建客户端与辅助类
          	EventLoopGroup group = new NioEventLoopGroup();
            Bootstrap clientBootStrap = new Bootstrap();
            
            //组装一些必要组件
            clientBootStrap.group(group);
            clientBootStrap.channel(NioSocketChannel.class);
            clientBootStrap.handler(new ChannelInitializer<Channel>() {
                @Override
                protected void initChannel(Channel ch) throws Exception {
                    ChannelPipeline pipeline = ch.pipeline();
                    pipeline.addLast(new StringEncoder());
                }
            });
            Channel channel = clientBootStrap.connect("127.0.0.1", 9091).channel();
    
            while (true) {
                channel.writeAndFlush(new Date() + "hello entty!!");
                Thread.sleep(2000L);
            }
        }
    }
    

NioEventLoopGroup:这是线程池,默认连接数为CPU核心数*2,bossGroup用于接收客户端传过来的请求,workerGroup将接收到的请求进行后续处理

Bootstrap:辅助类,用来启动组装一些必须配置

group():配置线程池

channel():指定服务器端监听套接字通道

NioSocketChannel:其内部管理一个java NIO中的ServerSocketChannel实例

childHandler():设置业务责任链

ChannelInitializer:继承ChannelInboundHandlerAdapter,用于初始化Channel的ChannelPipeline

当一个新的连接被接收是,一个新的channel将被创建,同事他会被自动的分配到他专属的ChannelPipeline

自定义ChannelHandler组件,处理自定也业务逻辑

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值