分布式架构设计

资料参考来源拉钩Java高薪训练营

1、RPC框架设计

1.1、socket编程

1.1.1、服务端

1.创建一个ExecutorService(Executors.newCachedThreadPool())线程池,如果有客户端连接就创建一个线程, 与之通信

2.创建 ServerSocket 对象

3.监听客户端

4.开启新的线程处理executorService.execute

1.1.2、客户端

1.创建 Socket 对象

2.从Socket 连接中取出输出流并发消息

3.从Socket 连接中取出输入流并接收回话

4.关闭Socket

1.2、IO模型

BIO:同步阻塞

服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销

NIO:同步非阻塞

一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有 I/O 请求就进行处理

AIO:异步非阻塞

先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用

1.3、应用场景

BIO: 适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序简单易理解

NIO:适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,弹幕系统,服务器间通讯等。编程比较复杂,JDK1.4 开始支持

AIO:使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用 OS 参与并发操作, 编程比较复杂,JDK7 开始支持。

1.4、NIO编程

1.4.1、概述

  • 三大核心,Channel(通道),Buffer(缓冲区), Selector(选择器)

  • 面向缓冲区编程
  • 非阻塞模式,一个线程来处理多个操作

1.4.2、和BIO比较

项目

BIO

NIO

处理方式

缓冲区

是否阻塞

操作方式

基于字节流和字符流

基于 Channel(通道)和 Buffer(缓冲区)

1.4.3、缓冲区Buffer

        本质上是一个可以读写数据的内存块,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。Channel 提供从网络读取数据的渠道,但是读取或写入的数据都必须经由Buffer.

  • Buffer 类及其子类

  • 常用API

方法

说明

缓冲区对象创建

static ByteBuffer allocate(长度)

创建byte类型的指定长度的缓冲区

static ByteBuffer wrap(byte[] array)

创建一个有内容的byte类型缓冲区

缓冲区对象添加数据

int position()/position(int newPosition)

获得当前要操作的索引/修改当前要操作的索引位置

int limit()/limit(int newLimit)

最多能操作到哪个索引/修改最多能操作的索引位置

int capacity()

返回缓冲区的总长度

int remaining()/boolean hasRemaining()

还有多少能操作索引个数/是否还有能操作
remaining=limit-position

put(byte b)/put(byte[] src)

添加一个字节/添加字节数组

缓冲区对象读取数据

flip()

写切换读模式 limit设置position位置, position设置0

get()

读一个字节

get(byte[] dst)

读多个字节

get(int index)

读指定索引的字节

rewind()

将position设置为0,可以重复读

clear()

切换写模式 position设置为0 , limit 设置为 capacity

array()

将缓冲区转换成字节数组返回

        使用说明:

1. capacity:容量(长度)limit: 界限(最多能读/写到哪里)posotion:位置(读/写哪个索引)

2. 获取缓冲区里面数据之前,需要调用flip方法

3. 再次写数据之前,需要调用clear方法,但是数据还未消失,等再次写入数据,被覆盖了才会消失。

1.4.4、channel

可以读也可以写

可以异步读写

总是基于缓冲区Buffer来读写

        ServerSocketChannel服务端实现步骤:

1. 打开一个服务端通道

2. 绑定对应的端口号

3. 通道默认是阻塞的,需要设置为非阻塞

4. 检查是否有客户端连接 有客户端连接会返回对应的通道

5. 获取客户端传递过来的数据,并把数据放在byteBuffer这个缓冲区中

6. 给客户端回写数据

7. 释放资源

        SocketChannel客户端实现步骤:

1. 打开通道

2. 设置连接IP和端口号

3. 写出数据

4. 读取服务器写回的数据

5. 释放资源

1.4.5、Selector (选择器)

        用一个线程,处理多个客户端连接,Selector 能够检测多个注册的服务端通道上是否有事件发生,如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求。

常用API:

方法

说明

Selector抽象类

Selector.open()

得到一个选择器对象

selector.select()

阻塞监控所有注册的通道,当有对应的事件操作时, 会将SelectionKey放
入集合内部并返回事件数量

selector.select(1000)

阻塞 1000 毫秒,监控所有注册的通道,当有对应的事件操作时, 会将
SelectionKey放入集合内部并返回

selector.selectedKeys()

返回存有SelectionKey的集合

SelectionKey

isAcceptable()

是否是连接继续事件

isConnectable()

是否是连接就绪事件

isReadable()

是否是读就绪事件

isWritable()

是否是写就绪事件

SelectionKey中定义的4种事件

SelectionKey.OP_ACCEPT

接收连接继续事件,表示服务器监听到了客户连接,服务器
可以接收这个连接了

SelectionKey.OP_CONNECT

连接就绪事件,表示客户端与服务器的连接已经建立成功

SelectionKey.OP_READ

读就绪事件,表示通道中已经有了可读的数据,可以执行读操
作了(通道目前有数据,可以进行读操作了)

SelectionKey.OP_WRITE

写就绪事件,表示已经可以向通道写数据了(通道目前可以用
于写操作)

        Selector服务端实现步骤:

1. 打开一个服务端通道

2. 绑定对应的端口号

3. 通道默认是阻塞的,需要设置为非阻塞

4. 创建选择器

5. 将服务端通道注册到选择器上,并指定注册监听的事件为OP_ACCEPT

6. 检查选择器是否有事件

7. 获取事件集合

8. 判断事件是否是客户端连接事件SelectionKey.isAcceptable()

9. 得到客户端通道,并将通道注册到选择器上, 并指定监听事件为OP_READ

10. 判断是否是客户端读就绪事件SelectionKey.isReadable()

11. 得到客户端通道,读取数据到缓冲区

12. 给客户端回写数据

13. 从集合中删除对应的事件, 因为防止二次处理.

1.5、Netty编程

1.5.1、概述

         JBOSS 提供的一个 Java 开源框架。提供异步的、基于事件驱动的网络应用程序框架,用以快速开发高性能、高可靠性的网络 IO 程序。

        基于 NIO 的网络编程框架,使用Netty 可以帮助你快速、简单的开发出一 个网络应用,相当于简化和流程化了 NIO 的开发过程。

        应用广泛,Netty 在互联网领域、大数据分布式计算领域、游戏行业、 通信行业等获得了广泛的应用,如Elasticsearch 、Dubbo。

        优点:

1. 设计优雅,提供阻塞和非阻塞的 Socket;提供灵活可拓展的事件模型;提供高度可定制的线程模型。

2. 具备更高的性能和更大的吞吐量,使用零拷贝技术最小化不必要的内存复制,减少资源的消耗。

3. 提供安全传输特性。

4. 支持多种主流协议;预置多种编解码功能,支持用户开发私有协议。

1.5.2、线程模型

        传统阻塞 I/O 服务模型:每个连接都需要独立的线程完成数据的输入。

        Reactor 模型:多个输入同时传递给服务处理器,并将它们同步分派到相应的处理线程,Reactor 模式也叫 Dispatcher模式。

        根据 Reactor 的数量和处理资源池线程的数量不同,有 3 种典型的实现:

单 Reactor 单线程

单 Reactor 多线程

主从 Reactor 多线程

1.5.3、Netty线程模型

1、简易版

2、进阶版Netty模型

3、详细版Netty模型

1.5.4、核心API介绍

1、ChannelHandler及其实现类

2、ChannelPipeline

ChannelPipeline 是一个 Handler 的集合,它负责处理和拦截 inbound 或者 outbound 的事件和

操作,相当于一个贯穿 Netty 的责任链.

3、ChannelHandlerContext

ChannelFuture close(),关闭通道

ChannelOutboundInvoker flush(),刷新

ChannelFuture writeAndFlush(Object msg) , 将 数 据 写 到 ChannelPipeline 中 当 前

ChannelHandler 的下一个 ChannelHandler 开始处理(出站)

4、ChannelOption 参数

ChannelOption.SO_BACKLOG:用来初始化服务器可连接队列大小。

ChannelOption.SO_KEEPALIVE ,一直保持连接活动状态。(true/false)

5、ChannelFuture表示 Channel 中异步 I/O 操作的结果

Channel channel(),返回当前正在进行 IO 操作的通道

ChannelFuture sync(),等待异步操作执行完毕,将异步改为同步

6、EventLoopGroup和实现类NioEventLoopGroup

public NioEventLoopGroup(),构造方法,创建线程组

public Future<?> shutdownGracefully(),断开连接,关闭线程

7、ServerBootstrap和Bootstrap

ServerBootstrap 是 Netty 中的服务器端启动助手,通过它可以完成服务器端的各种配置;

Bootstrap 是 Netty 中的客户端启动助手

public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup), 该方法用于服务器端,用来设置两个 EventLoop

public B group(EventLoopGroup group) ,该方法用于客户端,用来设置一个 EventLoop

public B channel(Class<? extends C> channelClass),该方法用来设置一个服务器端的通道 实现

public B option(ChannelOption option, T value),用来给 ServerChannel 添加配置

public ServerBootstrap childOption(ChannelOption childOption, T value),用来给接收到的通道添加配置

public ServerBootstrap childHandler(ChannelHandler childHandler),该方法用来设置业务 处理类(自定义的 handler)

public ChannelFuture bind(int inetPort) ,该方法用于服务器端,用来设置占用的端口号

public ChannelFuture connect(String inetHost, int inetPort) ,该方法用于客户端,用来连 接服务器端

8、Unpooled类

Netty 提供的一个专门用来操作缓冲区的工具类

public static ByteBuf copiedBuffer(CharSequence string, Charset charset),通过给定的数据和字符编码返回一个 ByteBuf 对象(类似于 NIO 中的 ByteBuffer 对象)

1.5.5、Netty入门案例

        服务端实现步骤:

1. 创建bossGroup线程组: 处理网络事件--连接事件

2. 创建workerGroup线程组: 处理网络事件--读写事件

3. 创建服务端启动助手

4. 服务端启动助手设置bossGroup线程组和workerGroup线程组

5. 服务端启动助手设置服务端通道实现为NIO

6. 服务端启动助手参数设置:线程队列中等待连接个数,child设置workerGroup活跃状态

7. 服务端启动助手设置childHandler为新创建的通道初始化对象ChannelInitializer

8. 在ChannelInitializer中向pipeline中添加自定义业务处理handler

9. 启动服务端并绑定端口,同时将异步改为同步

ChannelFuture channelFuture = serverBootstrap.bind(9999).sync();

10. 关闭通道和关闭连接池

channelFuture.channel().closeFuture().sync();

bossGroup.shutdownGracefully();

workerGroup.shutdownGracefully();

        客户端实现步骤:

1. 创建线程组

2. 创建客户端启动助手

3. 设置线程组

4. 设置客户端通道实现为NIO

5. 创建一个通道初始化对象

6. 向pipeline中添加自定义业务处理handler

7. 启动客户端,等待连接服务端,同时将异步改为同步

8. 关闭通道和关闭连接池

1.5.6、Future-Listener 机制

ChannelFuture future = bootstrap.bind(9999);

future.addListener(new ChannelFutureListener() {

@Override

public void operationComplete(ChannelFuture future) throws Exception {

if (future.isSuccess()) {

System.out.println("端口绑定成功!");

} else {

System.out.println("端口绑定失败!");

}

}

});

1.6、Netty高级应用

1.6.1、Netty编解码器

1、解码器Decoder:负责处理“入站 InboundHandler”数据。

向pipeline中添加解码器handler

2、编码器Encoder:负责“出站OutboundHandler” 数据。

向pipeline中添加编码器handler

3、编码解码器Codec

向pipeline中添加编解码器handler

1.6.2、websocket

Http协议

客户端与服务器通信,必须要有客户端先发起, 然后服务器返回结果。客户端是主动的,服务器是被动的。 客户端要想实时获取服务端消息就得不断发送长连接到服务端。

WebSocket

实现了多路复用,是全双工通信。在webSocket协议下服务端和客户端可以同时发送信息。 建立了WebSocket连接之后, 服务端可以主动发送信息到客户端。而且信息当中不必再带有head的部分信息

注意:在编写WebSocketHandler时,要加上标注@ChannelHandler.Sharable,来设置通道共享

1.6.3、粘包和拆包

粘包:服务端一次接收到了两个数据包,D1和D2粘合在一起,被称为TCP粘包

拆包:如果D2的数据包比较大, 服务端分两次读取到了两个数据包,第一次读取到了完整的D1包和D2包的部分内容,第二次读取到了D2包的剩余内容,这被称为TCP拆包。

        Netty提供了4种解码器来解决:

固定长度的拆包器 FixedLengthFrameDecoder,每个应用层数据包都拆分成固定长度的大小;

行拆包器 LineBasedFrameDecoder,每个应用层数据包,都以换行符作为分隔符,进行分割拆分;

分隔符拆包器 DelimiterBasedFrameDecoder,每个应用层数据包,都通过自定义的分隔符,进行分割拆分;

基于数据包长度的拆包器 LengthFieldBasedFrameDecoder,将应用层数据包的长度,作为接收端应用层数据包的拆分依据。按照应用层数据包的大小,拆包。

1.6.4、主要源码分析

1、创建线程组

2、bind事件分析

3、initAndRegister

4、newChannel

5、init

6、config().group().register(channel) 方法

7、NioEventLoop的run方法的循环

进入execute,往下走,调用栈如下:

进入run方法,NioEventLoop的run方法的循环

runAllTasks方法

8、register0方法

doRegister

initChannel

9、processSelectedKeysOptimized

processSelectedKey->unsafe.read()->...->channelRead

writeAndFlush->write

1.7、自定义RPC框架

1.7.1、概述

RPC全称为remote procedure call,即远程过程调用。借助RPC可以做到像本地调用一样调用远程服务,是一种进程间的通信方式.

RPC并不是一个具体的技术,而是指整个网络远程调用过程。

在java中RPC框架比较多,常见的有Hessian、gRPC、Dubbo 等,其实对 于RPC框架而言,核心模块就是通讯和序列化

1.7.2、RMI

Java RMI,即远程方法调用(Remote Method Invocation),一种用于实现远程过程调用(RPCRemote procedure call)的Java API, 能直接传输序列化后的Java对象。它的实现依赖于Java虚拟机,因此它仅支持从一个JVM到另一个JVM的调用。

1.7.3、RMI实现RPC

1、服务端


//1.注册Registry实例. 绑定端口

Registry registry = LocateRegistry.createRegistry(9998);

//2.创建远程对象

IUserService userService = new UserServiceImpl();

//3.将远程对象注册到RMI服务器上即(服务端注册表上)

registry.rebind("userService", userService);


2、客户端


//1.获取Registry实例

Registry registry = LocateRegistry.getRegistry("127.0.0.1", 9998);

//2.通过Registry实例查找远程对象

IUserService userService = (IUserService)registry.lookup("userService");


1.7.4、基于Netty实现RPC

1、服务端

创建注解@RpcService

实现类UserServiceImpl,添加注解@RpcService

服务Netty启动类RpcServer

服务业务处理类RpcServerHandler,实现ApplicationContextAware接口,重写setApplicationContext方法,在这个方法中实现:

* 1.将标有@RpcService注解的bean缓存

* 2.接收客户端请求

* 3.根据传递过来的beanName从缓存中查找到对应的bean

* 4.解析请求中的方法名称. 参数类型 参数信息

* 5.反射调用bean的方法

* 6.给客户端进行响应

2、客户端

客户端Netty启动类RpcClient

客户端业务处理类RpcClientHandler

RPC代理类实现功能:

创建代理对象

* 1.封装request请求对象

* 2.创建RpcClient对象

* 3.发送消息

客户端启动类ClientBootStrap,测试

2、分布式理论与分布式架构设计理论

2.1、CAP定理

一致性(Consistency)所有节点访问时都是同一份最新的数据副本;

可用性(Availability)每次请求都能获取到非错的响应,但是不保证获取的数据为最新数据;

分区容错性(Partition tolerance)分布式系统在遇到任何网络分区故障的时候,仍然能够对外提供满足一致性和可用性的服务,除非整个网络环境都发生了故障。

CAP三者不可能同时满足

2.2、BASE理论

        Basically Available(基本可用),Soft state(软状态),和 Eventually consistent(最终一致性)三个短语的缩写 ,Base 理论是对 CAP 中一致性和可用性权衡的结果,其来源于对大型互联网分布式实践的总结,是基于 CAP 定理逐步演化而来的。

        其核心思想是: 既是无法做到强一致性(Strong consistency),但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性(Eventual consistency)。

Basically Available(基本可用):响应时间上的损失/功能上的损失

Soft state(软状态):相对于原子性而言,要求多个节点的数据副本都是一致的,这是一种 “硬状态”。软状态指的是:允许系统中的数据存在中间状态,并认为该状态不会影响系统的整体可用性,即允许系统在多个不同节点的数据副本存在数据延时。

Eventually consistent(最终一致性):软状态,然后不可能一直是软状态,必须有个时间期限。在期限过后,应当保证所有副本保持数据一致性。从而达到数据的最终一致性。

2.3分布式一致性协议

2.3.1、两阶段提交协议(2PC)

要么所有参与进程都提交事务,要么都取消事务,即实现ACID中的原子性(A)的常用手段。

2PC 优点缺点

1. 优点

原理简单

2. 缺点

同步阻塞:当参与者占有公共资源时,其他节点访问公共资源会处于阻塞状态;

单点问题:若协调器出现问题,那么整个二阶段提交流程将无法运转,若协调者是在阶段二中出现问题时,那么其他参与者将会一直处于锁定事务资源的状态中,而无法继续完成事务操作;

数据不一致:局部网络异常或者是协调者在尚未发送完Commit请求之前自身发生了崩溃,导致最终只有部分参与者收到了Commit请求,于是会出现数据不一致的现象;

太过保守:没有完善的容错机制,任意一个结点的失败都会导致整个事务的失败。

2.3.2 三阶段提交协议(3PC)

        3PC,全称 “three phase commit”,是 2PC 的改进版,将 2PC 的 “提交事务请求” 过程一分为二,共形成了由CanCommit、PreCommit和doCommit三个阶段组成的事务处理协议。

多设置了一个PreCommit缓冲阶段保证了在最后提交阶段之前各参与节点的状态是一致的 。

2.3.3、 NWR协议

N:在分布式存储系统中,有多少份备份数据

W:代表一次成功的更新操作要求至少有w份数据写入成功

R: 代表一次成功的读数据操作要求至少有R份数据成功读取

NWR值的不同组合会产生不同的一致性效果,当W+R>N的时候,整个系统对于客户端来讲能保证强一致性

2.3.4 Gossip 协议

        Gossip 协议最终目的是将数据分发到网络中的每一个节点。根据不同的具体应用场景,网络中两个节点之间存在三种通信方式:推送模式、拉取模式、推/拉模式

1. Push

节点 A 将数据 (key,value,version) 及对应的版本号推送给 B 节点,B 节点更新 A 中比自己新的数据

2.Pull

A 仅将数据 key, version 推送给 B,B 将本地比 A 新的数据(Key, value, version)推送给 A,A 更

新本地

3.Push/pull

与 Pull 类似,只是多了一步,A 再将本地比 B 新的数据推送给 B,B 则更新本地

Gossip 协议由于以上的优缺点,所以适合于 AP 场景的数据一致性处理,常见应用有:P2P 网络通信、Redis Cluster、Consul。

2.3.5 Paxos协议

        基于消息传递且具有高度容错特性的一致性算法,是目前公认的解决分布式一致性问题最有效的算法之一.

        Paxos算法需要解决的问题就是如何在一个可能发生上述异常的分布式系统中,快速且正确地在集群内部对某个数据的值达成一致,并且保证不论发生以上任何异常,都不会破坏整个系统的一致性。

        Paxos的版本有: Basic Paxos , Multi Paxos, Fast-Paxos, 具体落地有Raft 和zk的ZAB协议

2.3.6、Raft

1.介绍

        Raft 是一种为了管理复制日志的一致性算法。

        Raft 提供了和Paxos算法相同的功能和性能,但是它的算法结构和Paxos不同。Raft 算法更加容易理解并且更容易构建实际的系统。

        Raft 将一致性算法分解成了3模块:领导人选举、日志复制、安全性

        Raft 算法分为两个阶段,首先是选举过程,然后在选举出来的领导人带领进行正常操作,比如日志复制等。

2.领导人Leader选举

        Raft通过选举一个领导人,然后给予他全部的管理复制日志的责任来实现一致性。

        在Raft中,任何时候一个服务器都可以扮演下面的角色之一:

  • 领导者(leader):处理客户端交互,日志复制等动作,一般一次只有一个领导者
  • 候选者(candidate):候选者就是在选举过程中提名自己的实体,一旦选举成功,则成为领导者
  • 跟随者(follower):类似选民,完全被动的角色,这样的服务器等待被通知投票

        Raft使用心跳机制来触发选举。当server启动时,初始状态都是follower。每一个server都有一个定时器,超时时间为election timeout(一般为150-300ms),如果某server没有超时的情况下收到来自领导者或者候选者的任何消息,定时器重启,如果超时,它就开始一次选举。

        具体选举详情可以查看动画演示就很清楚了:http://thesecretlivesofdata.com/raft/

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值