SPI为基础架构搭建高拓展高可用的RPC-知识点总结汇总 Posted on 2024-01-03 13:38:44 2024-01-03 13:38:44 by Author 摘要 本文主要是通过网络学习资源和参考Xhy-Rpc的代码,进行实战操作,以SPI(Service Provider Interface)为基础架构来创建一个具有高扩展性和高可用性的RPC(Remote Procedure Call)框架。在整个实践过程中,我获取了许多新知识和经验。 ### 以SPI为基础架构搭建高拓展高可用的RPC-知识点总结汇总 > 本文主要通过网络学习资源和参照[Xhy-Rpc](https://www.bilibili.com/video/BV1th4y1w7wX/?vd_source=884a1f9702167e8936a8d6d773a193ae)的代码,亲自动手实践了一次以SPI为基础架构,构建高拓展性和高可用性的RPC框架。所有在实践过程中获取的知识,学习到的知识以个人博客的形式呈现出来。 ###### 1. RPC是什么以及其作用是什么。 1. RPC(Remote Procedure call)全名为远程过程调用,它是计算机之间通信的方式,主要在分布式系统环境中,客户端可以像调用本地程序或者方法一样调用远程计算机上的程序,它使得程序开发和本地过程调用使用感受是一样的,大大提高了开发的效率。 2. 在理解了这个概念之后,我觉得为什么不直接使用HTTP进行数据的交换,调用远程的方法,为什么还要再使用RPC方式,不是多此一举吗?之后经过网上查阅资料以及在项目实践过程中,得出以下结论。 - HTTP进行传输的时候,每次都需要把头部完整的字段名称传输过来,资源浪费,而且传输的数据是字符串形式,Json格式的传输内容,不安全。相比HTTP,RPC可以定制化操作,规定了第几位是那个字段,传输过程中,消耗的资源也会更少,提高效率,同时,在对数据进行序列化的时候,可以选择多种方式,比如gRpc,二进制形式,Json等形式。 - HTTP是一种客户端和服务端约定好的一种通信格式,RPC是远程调用,对应的是一种本地调用方式。RPC可以使用HTTP协议也可以使用其他协议,但是HTTP通信,一般使用TCP/IP协议。 - RPC应用于分布式服务通信上面,现在许多业务都拆分为微服务架构,但是服务与服务之间需要进行通信,可以选择HTTP进行通信,但是实现起来太麻烦,而使用RPC协议,可以使得开发者不用关注底层的数据通信方式,以及错误处理方式,就像本地调用一样,使得开发更加高效。 ###### 2.知识点总结 本人根据Xhy搭建的一个可扩展以及可用的RPC框架,在自己动手实践过程中,总结了需要学习的知识点有:"Spring Boot相关元注解以及Spring对Bean的初始化过程","Java反射机制","Java动态代理","SPI(Service Provider Interface)","Netty","Java多线程"。以上的知识点都是我个人在这个项目中所用到以及学到的知识点,接下来的知识,我将详细讲解每个知识点在项目中的具体使用。 ###### 3. **Spring 相关知识** 1. 在进行RPC框架开发时,需要自己定义开发过程中用到的注解,那么需要学习Spring框架中的元注解以及作用,以下介绍了主要的四种元注解: - @Target,该注解指定可以修饰哪些程序元素,比如说类,方法,字段或者构造器。如果@Target该元注解的值设置为ElementType.TYPE,@Target修饰的注解可以用来修饰类。具体用法如下。 ```java @Target({ElementType.TYPE}) public @interface ConsumerRpc { } //用@ConsumerRpc可以修饰类。 @ConsumerRpc public class TestClass{ ... } @Target({ElementType.FIELD}) public @interface ConsumerRpcField{ } public class TestField{ //用@ConsumerRpcField可以修饰属性。 @ConsumerRpcField public TestClass class; } ``` - `@Retention`: 用于指定该注解的生命周期,有SOURCE(源码阶段),CLASS(字节码阶段),RUNTIME(运行时阶段)三个阶段。 - **`RetentionPolicy.SOURCE`**: 在源代码阶段保留,注解只保留在源文件(`.java`文件)中,当Java文件编译成`.class`文件后,注解将不再存在。这种类型的注解主要用于通过对源码的阅读或者代码提示的方式来帮助编码,例如`@Override` 就是这种类型的注解。 - **`RetentionPolicy.CLASS`**: 注解在类文件阶段被保留,也就是在编译时会将注解信息嵌入到`.class`文件中,但是在运行时无法通过反射机制获取到。这是默认的策略,如果`@Retention`元注解没有显示指定策略,那么就会使用这种策略。 - **`RetentionPolicy.RUNTIME`**: 这是生命周期最长的一种策略。在运行期也保留,可以通过反射机制获取到这些注解信息,这对于编写某些需要读取注解信息的通用工具类非常有用。例如Spring的一系列注解和Java内置的`@Deprecated`都是运行时注解。 - `@Inherited`: 这个元注解表明一个新的注解被它修饰后,新注解具备了继承性。如果某个类选择了@Inherited修饰的注解修饰,则其子类将自动具备这个注解。 - `@Documented`: 该注解表明这个注解是由javadoc之类的工具文档化的,在默认情况下,javadoc工具会将它处理成注释内容。 - `@Import`是Spring的核心注解之一,用于导入其他配置类。通过`@Import`,可以把标有`@Configuration`的类引入到当前上下文环境。这样我们可以很方便地对一些Bean进行统一的管理。 2. Spring对Bean的加载过程。 - 首先对Bean进行定义阶段,Spring首先会找到所有的Bean定义,这些定义可以来自XML配置,注解以及Java配置类。该阶段,Spring只是获取Bean的定义,还未创建Bean的实例。 - Bean是实例化阶段。该阶段,Spring会通过Bean的定义创建Bean的实例,会通过反射机制或者工厂模式来创建Bean的实例,该阶段,Bean的属性还未赋值,循环依赖也没有解决。 - Bean的初始化阶段,该阶段主要分为以下几步: - 属性填充:该阶段会通过上下文环境,通过自动装配或者显示定义方式将Bean的各种属性填充好,包括其他Bean的引用,该部分可能会存在循环依赖的方式,该部分讲完之后,具体讲讲该部分。 - 如果Bean实现了BeanNameAware接口,Spring会传入Bean的name。BeanNameAware是Spring框架中的一个接口,被用于允许一个Bean实例在被Spring容器初始化之后知道它在容器中的名字。 - 如果Bean实现了BeanFactoryAware接口,Spring会传入`BeanFactory`实例本身。 - 调用BeanPostProcessor前置处理器的postProcessBeforeInitialization方法。 - 如果Bean实现了InitializingBean接口,调用afterPropertiesSet方法。 - 调用自定义初始化方法。 - 调用`BeanPostProcessor`后置处理器的`postProcessAfterInitialization`方法。 - Bean的缓存:对于单例Bean,在初始化之后,其实例会被缓存起来,当再次需要这个Bean时,直接从缓存中获取,不需要重新创建。 - Bean的销毁阶段:对于singleton作用域的Bean,当应用上下文关闭时,相应的destroy方法会被调用。如果实现了`DisposableBean`接口,会调用其`destroy`方法,也可以通过配置指定destroy-method来自定义销毁方法。这个步骤是在Spring IoC容器关闭时执行的。  3. Spring在对Bean初始化阶段导致的循环依赖解决方法。 - 首先Spring在对Bean进行初始化时候,出现循环依赖问题,解决办法为三层缓存的方式。具体的过程如下。 1. 概念:**singletonObjects(一级缓存)**:这个存储结构主要存放的是已经完全**初始化**了的Bean。也就是说,当一个Bean的实例被创建并且被填充了属性,被初始化完成以后,这个Bean的引用就会被存储在这个缓存中。**earlySingletonObjects(二级缓存)**:这个存储结构是为了解决循环引用问题而产生的。它主要存放的是早期暴露出来的Bean,也就是实例化后的,但还未填充属性的Bean。**singletonFactories(三级缓存)**:这个存储结构存放的是Bean的工厂对象,也就是`ObjectFactory`。当有循环依赖发生的时候,这个工厂对象可以生成一个Bean的早期对象引用,并提供给其他的Bean作为依赖。 2. 具体过程:在Spring框架中,在Bean实例化之后,立即将其放入“早期单例对象的缓存”(二级缓存earlySingletonObjects)和“单例工厂的缓存”(三级缓存singletonFactories)中(以上过程为早期的Bean的引用),当Spring进行依赖注入时,它会首先去一级缓存中寻找依赖的Bean,如果在一级缓存中找不到,就会去二级缓存中寻找。然后再次找不到时,这时Spring知道这是因为存在循环依赖的问题,它会从三级缓存中取出ObjectFactory,然后调用其`getObject`方法获取早期的Bean引用,完成属性填充。之后再将其放入一级缓存,并清除二级和三级缓存。 3. 主要的地方:这种解决方式只适用于setter注入,也就是通过set方法注入依赖的情况,因为set方法允许在Bean实例化后再进行属性填充。对于构造器注入,由于在实例化对象时就需要全部的构造参数,因此无法提前暴露未初始化完成的Bean,所以Spring并不能解决通过构造器注入产生的循环依赖问题。 ###### 4. Java反射机制 1. Java反射机制是指程序在运行过程中,访问,检测以及修改本身状态以及行为的一种能力。在运行时动态地获取类的信息、调用对象的方法、操作类的属性等功能。反射机制允许程序在运行时探知类的信息,获取类的属性和方法,动态创建对象,调用方法,以及访问和修改字段等。Java中的反射主要通过`java.lang.reflect`包中的类和接口来实现。 2. 反射机制的核心包括: - `Class`类:`java.lang.Class`类是反射机制的核心,它提供了获取类的各种信息的方法,例如获取类的名称、获取类的方法、获取类的字段等。 - `Field`类:`java.lang.reflect.Field`类用于表示类的字段,可以通过它获取和设置对象的字段值。 - `Method`类:`java.lang.reflect.Method`类用于表示类的方法,可以通过它调用对象的方法。 - `Constructor`类:`java.lang.reflect.Constructor`类用于表示类的构造方法,可以通过它创建对象 3. 反射机制的作用: - **动态加载类**:在运行时,可以通过类的全限定名动态加载类,创建对象。 - **获取类的信息**:可以获取类的属性、方法、构造方法等信息。 - **调用对象的方法**:可以在运行时动态调用对象的方法。 - **操作类的属性**:可以在运行时动态访问和修改对象的属性。 - **实现通用框架**:例如Spring框架就广泛使用了反射机制,通过读取配置文件中的类名,动态地创建对象。 - **实现代理模式**:通过反射可以在运行时动态地生成代理对象,实现代理模式。 ###### 5. Java动态代理 1. Java动态代理的分类 - JDK动态代理:它的实现原理主要是通过反射技术。要求代理对象和目标对象基于同一个接口。通过实现InvocationHandler接口创建自己的调用处理器,然后通过Proxy类的工厂方法创建动态代理类,利用动态代理类的对象作为代理对象,代理对象调用目标方法时,实际上会调用到自定义的InvocationHandler中的invoke方法。通过这种方式,我们可以在调用目标方法前后插入自定义逻辑,为目标对象增加额外的功能。 - Cglib动态代理:它可以在运行时动态地生成给定类的子类,通过方法覆盖的技术可以拦截所有父类方法的调用。Cglib通常用于那些没有实现接口的类,对于无接口的类,CGLib比JDK动态代理更有优势,因为它不关心有无接口这些问题。 2. 两者的区别: - 首先JDK代理,是Java原生提供的代理方式,它要求目标对象必须实现一个或者多个接口,通过Proxy类和InvocationHandler接口实现,通过在invoke()方法里,执行一些额外的扩展。 - 基于Cglib的动态代理,在代理过程中会通过生成目标类的子类作为代理对象,使用方法覆盖的方式,这种方式不需要目标对象实现接口。 ###### 6. SPI(Service Provider Interface) 1. SPI全名是服务提供接口,他是一种服务发现机制,用来被第三方实现或者扩展的API,它可以用来启用框架扩展和替换的组件。SPI的核心思想是解耦,使得接口的定义和接口的实现分离,以实现在面向接口编程中的“可插拔”。 2. 实现机制:当服务的提供者提供了服务接口的一种实现之后,在jar包的META-INF/services/目录下创建一个以服务接口命名的文件,文件内容是该服务接口的具体实现类。在使用SPI机制查找服务时,JVM会在这个目录下搜索文件并加载实现类,进而实例化实现类,这样就完成了接口和实现的动态加载,实现了解耦。在该项目中有许多地方用到SPI这种机制:比如对RPC协议的传输序列化方式(Json, 二进制的,gRpc),注册中心的形式(Zookeeper,Reidis,Nacos),过滤器方式(前置过滤器,后置过滤器)。 3. SPI在Spring boot的具体应用:当Spring Boot启动时,它会查找所有的`META-INF/spring.factories`配置文件,然后将其中`org.springframework.boot.autoconfigure.EnableAutoConfiguration`键对应的配置类都加载到Spring容器中。开发者可以通过在`META-INF/spring.factories`文件中添加自定义的自动配置类。 4. 当Spring Boot应用启动时,`spring.factories`文件中的自动配置类对应的bean会被创建,并载入到ApplicationContext中,从而完成了自动装配的过程。这种方式在Frameworks,如Spring Boot Starters,或开发者自定义自动装配模块时非常有用,也使得Spring Boot可以根据类路径下的类是否存在,来自动配置Spring应用。 ###### 7. Java多线程和线程池 1. 线程的实现方式有两种,一种是继承Thread类,然后重写其run()方法。第二种就是实现Runnable接口,然后将runnable接口的实例传递给一个新的Thread对象。在这两种方法中,一般选择实现Runnable接口,因为一个类继承其他的类,就不能继承Thread类,只能实现Runnable接口。 2. 使用多线程交叉打印数字和字母的实例 ```java private static boolean flag = false; private static int count = 1; public static void main(String[] args) { final Object o = new Object(); // 第一个线程打印数字 new Thread(() -> { synchronized (o) { while (true) { if (flag) { System.out.println(2 * count); count++; flag = false; o.notify(); } else { try { o.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } }).start(); // 第二个线程打印字母 new Thread(() -> { synchronized (o) { while (true) { if (!flag) { System.out.println((char) ('A' + count)); flag = true; o.notify(); } else { try { o.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } }).start(); } } ``` 3. 线程池:Java线程池是Java中的一个并发编程工具,它负责管理一个或多个线程,为线程的执行提供一个合适的运行环境。它具有以下优点:**降低资源消耗:** 通过重复利用已创建的线程,降低线程创建和销毁造成的消耗。**提高响应速度:** 当任务到达时,任务可以不需要等待线程创建就能立即执行。**提高线程的可管理性:** 线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会导致系统负载过大,降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。 4. 线程池的主要参数: - **corePoolSize:**线程池未满,即活动的线程数小于核心线程数时:线程池会立即创建新的线程来处理提交的任务,这个新线程被添加到线程池中。 - **maximumPoolSize:** 最大线程数,表示线程池在极限负载下能够容忍的最大线程数量。一旦超过这个数量,新来的任务就会被拒绝执行。 - **workQueue:** 任务队列,被提交但尚未被执行的任务会在这个队列中等待。任务队列的类型通常是BlockingQueue。 - **keepAliveTime:** 非核心线程的空闲存活时间,如果一个线程的空闲时间超过这个值,那么这个线程就会被回收。 - **handler:** 拒绝策略,当任务太多,无法处理更多任务时采取的策略。拒绝策略有:1,AbortPolicy:直接抛出RejectedExecutionException异常。2,不处理,默认丢弃。3,丢弃队列中最旧的未处理任务。4,调用任务的run方法,直接运行,不经过线程池。 5. Java线程池的具体工作流程: - 首先,将一个实现了Runnable接口或Callable接口的任务提交给线程池。 - **线程池未满,即活动的线程数小于核心线程数时**:线程池会立即创建新的线程来处理提交的任务,这个新线程被添加到线程池中。 - **线程池已满,即活动的线程数已经等于核心线程数,但任务队列未满时**:线程池将会把提交的任务存储在任务队列中以等待被执行。 - **队列已满,即任务队列的容量已经达到最大,并且已经有了核心线程数个活动的线程,但总的线程数还未达到线程池规定的最大线程数时**:线程池会创建新的非核心线程,来处理提交的任务。 - **队列已满且总线程数已经达到线程池规定的最大线程数时**:这时候线程池会采取拒绝策略,处理无法执行的任务。具体的处理办法可能是抛出异常、丢弃任务、丢弃队列中最旧的任务等,具体取决于创建线程池时传入的RejectedExecutionHandler参数。 - **任务执行:** 等待的任务会在有线程可用时开始执行。可能是新创建的线程,也可能是完成任务并返回到线程池的空闲线程。 - **线程管理:** 空闲时间超过某个设置值(keepAliveTime 参数值)的非核心线程会被回收。 - **线程池关闭:** 当调用了线程池的shutdown方法后,线程池将停止接收新任务,然后一旦所有任务都完成执行后,线程池中所有的线程也会退出。 ###### 8. Netty相关的知识 1. Netty是一个Java NIO技术的开源异步事件驱动的网络编程框架,用于快速开发可维护的高性能协议服务器和客户端。 2. Netty的主要组件以及概念: - Channel:通道,代表一个连接,每个Client请对会对应到具体的一个Channel; - ChannelPipeline:责任链,每个Channel都有且仅有一个ChannelPipeline与之对应,里面是各种各样的Handler; - handler:用于处理出入站消息及相应的事件,实现我们自己要的业务逻辑; - EventLoopGroup:I/O线程池,负责处理Channel对应的I/O事件; - ServerBootstrap::服务器端启动辅助对象; - BootStrap:客户端启动辅助对象; - ChannelFuture:代表I/O操作的执行结果,通过事件机制,获取执行结果,通过添加监听器,执行我们想要的操作; 3. Netty线程模型 1. Reactor模型:Reactor 是反应堆的意思,Reactor 模型是指通过一个或多个输入同时传递给服务处理器的服务请求的事件驱动处理模式。 - 服务端程序处理传入多路请求,并将它们同步分派给请求对应的处理线程,Reactor 模式也叫 Dispatcher 模式,即 I/O 多了复用统一监听事件,收到事件后分发(Dispatch 给某进程),是编写高性能网络服务器的必备技术之一。 - Reactor模型中有2个关键组成: - Reactor:Reactor 在一个单独的线程中运行,负责监听和分发事件,分发给适当的处理程序来对 IO 事件做出反应。它就像公司的电话接线员,它接听来自客户的电话并将线路转移到适当的联系人; - Handlers:处理程序执行 I/O 事件要完成的实际事件,类似于客户想要与之交谈的公司中的实际官员。Reactor 通过调度适当的处理程序来响应 I/O 事件,处理程序执行非阻塞操作。  2. Netty 主要基于主从 Reactors 多线程模型(做了一定的修改,其中主从 Reactor 多线程模型有多个 Reactor:MainReactor 负责客户端的连接请求,并将请求转交给 SubReactor;SubReactor 负责相应通道的 IO 读写请求;非 IO 请求(具体逻辑处理)的任务则会直接写入队列,等待 worker threads 进行处理。  以上简单的介绍了一些Netty相关的知识,自己只是初学者,后期会继续学习相关的知识,进行更深入的理解。然后继续更新。 以上知识就是本人在学习以SPI方式搭建RPC项目过程中所要学习的知识,然后通过简单的介绍,增加自己的基础知识,同时积累了这些基础知识以便更好的理解该项目,之后,会将更新博文具体介绍该项目以及以上的知识怎么运用到项目中的。 ``参考文献`` - http://www.52im.net/thread-2043-1-1.html - http://www.52im.net/thread-3207-1-1.html
{{ item.content }}
{{ child.content }}