2024面试offer收割宝典字节篇

1.IO 模型有哪些,讲讲你理解的 nio ,他和 bio,aio 的区别是啥, 谈谈 reactor 模型。

IO 模型主要包括以下几种:
1. 阻塞 I/O (BIO):

  • 当一个线程调用 read()write() 系统调用时,如果数据没有准备好或者缓冲区已满,则该线程会被操作系统阻塞,直到有数据可读或写入完成。
  • 在Java中传统的java.io包中的类如SocketServerSocket就是基于BIO模型实现的。

2. 非阻塞 I/O (NIO):

  • Java NIO(New Input/Output)在Java 1.4版本引入,提供了非阻塞的I/O能力。
  • 使用NIO,当调用channel.read()channel.write()方法时,若当前无数据可读或缓冲区满不能立即写入,不会阻塞线程,而是会返回一个状态值(通常是0,表示没有数据可读或没有足够的空间写入)。
  • NIO支持单线程处理多个Channel(通道),通过Selector可以监听多个Channel的事件,一旦某个Channel准备就绪进行读写操作,Selector就会通知相应的线程去处理。
  • NIO的核心组件包括Buffer(缓冲区)、Channel(通道)和Selector(选择器)。

3. 异步 I/O (AIO, NIO.2 或 AIO in Java):

  • 异步非阻塞I/O在Java 7中引入,也被称为NIO.2。
  • 在AIO模式下,当发起一个读写请求后,应用程序可以继续执行其他任务,而无需等待操作结果。
  • 当数据准备完毕或操作完成时,操作系统会回调预先注册的CompletionHandler(完成处理器)来处理数据或处理完成后的逻辑。
  • 这种模型极大地提高了系统并发处理能力和资源利用率,特别适合于高并发、低延迟的应用场景。

Reactor 模型是一种设计模式,常用于处理大量并发I/O请求。它通常配合NIO或AIO使用,在服务器端能够同时监听多个连接请求,并且根据不同的事件类型分发给相应的处理器进行处理。基本结构包括:

  • Dispatcher:负责监听并分发事件到对应的Handler。
  • Handlers:处理实际的I/O事件,如读取数据、写入响应等。
  • Event Demultiplexer:在Java NIO中对应的是Selector,它可以监控多个Channel上的事件,并在事件发生时通知Dispatcher

总结来说,BIO、NIO与AIO的主要区别在于线程模型和阻塞行为上

  • BIO是一个连接一个线程,容易造成线程资源浪费,性能瓶颈明显。
  • NIO在一个线程内可以管理多个连接,通过非阻塞方式减少无效等待时间,但程序需要自行轮询检查数据是否准备好。
  • AIO则更进一步,由操作系统直接负责数据的读写操作,并在完成后通知应用,从而避免了不必要的上下文切换,理论上能提供最高的并发性能。

 


2.反射的原理,反射创建类实例的三种方式是什么。

 反射是Java中的一项重要特性,它允许运行中的Java程序对自身进行检查并且可以直接操作程序的各种属性和功能

在运行时,Java反射机制可以加载类、获取类的信息(如类名、方法、字段等)、实例化对象以及调用方法等。
反射创建类实例的三种主要方式如下:

1. 通过无参构造函数创建:

   // 假设我们有一个名为Book的类
   Class<?> bookClass = Class.forName("com.example.Book");
   Constructor<?> constructor = bookClass.getConstructor();  // 获取无参构造器
   Book book = (Book) constructor.newInstance();  // 调用无参构造器创建实例
   

2. 通过带参数的构造函数创建:

   Class<?> bookClass = Class.forName("com.example.Book");
   Constructor<Book> constructor = bookClass.getDeclaredConstructor(String.class, String.class);  // 获取带有String类型参数的私有构造器
   constructor.setAccessible(true);  // 如果构造器是私有的,需要设置为可访问
   Book book = constructor.newInstance("The Title", "The Author");  // 使用指定参数创建实例
   

3. 通过Class的newInstance()方法创建:

   // 注意:这种方式仅适用于具有默认(无参或public)构造函数的类
   Class<Book> bookClass = Book.class;
   try {
       Book book = bookClass.newInstance();
   } catch (InstantiationException | IllegalAccessException e) {
       // 处理异常,例如如果类是抽象类或者没有默认构造函数
       e.printStackTrace();
   }
   

 上述代码展示了如何通过反射API动态地根据类信息创建类的实例。需要注意的是,对于非public的构造函数或其他成员,可能需要调用setAccessible(true)来绕过访问控制检查


3.反射中,Class.forName 和 ClassLoader 区别 。

在Java反射机制中,Class.forName() 方法和 ClassLoader 类加载器都用于动态加载类,但它们之间存在一些区别:
1. Class.forName():

  • 这是一个静态方法,位于 java.lang.Class 类中。
  • 它接受一个字符串参数,该参数是完全限定类名(包括包名)。
  • 当调用 Class.forName(className) 时,它会按照当前线程的上下文类加载器(context class loader)自动寻找并加载指定名称的类,并初始化该类。如果类还未被加载过,则执行类加载过程,包括验证、准备、解析和初始化阶段。
  • 如果类中有静态初始化块或静态字段会被初始化。
  • 通常抛出 ClassNotFoundException,如果找不到指定的类。

2. ClassLoader.loadClass() 或 ClassLoader.findSystemClass() 等相关方法:

  • ClassLoader 是 Java 中用来加载类的抽象类,每个类在运行时都有一个与之关联的类加载器。
  • 使用自定义的 ClassLoader 实例或者系统的默认类加载器,可以更灵活地控制类的加载过程,例如实现自己的类加载逻辑、支持热加载等高级功能。
  • 调用如 ClassLoader.loadClass(className) 不会立即初始化类,仅完成类的加载过程,即验证、准备、解析阶段,而不会执行类的初始化(构造静态变量和执行静态初始化块)。
  • 可以通过调用 Class.forName(className, initialize, classLoader) 来控制是否进行初始化操作,其中 initialize 参数为布尔值。
  • 同样可能抛出 ClassNotFoundException

总结来说,两者的主要区别在于:

  • Class.forName() 默认情况下包含了完整的类加载和初始化过程,更加直观易用,常用于简单场景下的类加载。
  • ClassLoader 提供了更底层和灵活的类加载能力,可以根据需要定制加载策略,且默认情况下不进行类的初始化。

4.描述动态代理的几种实现方式,分别说出相应的优缺点。

在Java中,动态代理有两种主要的实现方式:JDK动态代理和CGLIB代理
1. JDK动态代理:

  • 原理:基于Java反射机制实现。它通过InvocationHandler接口创建一个代理类,该代理类在运行时继承自Proxy类,并实现了与目标对象相同的接口。
  • 优点:
  • 由于是基于接口的,所以可以很方便地为一组具有相同接口的目标对象生成代理,适用范围广。
  • 实现代理逻辑比较简单,只需要实现InvocationHandler接口即可。
  • 不需要预先知道所有要代理的方法,代理逻辑更加灵活。
  • 缺点:
  • 需要目标类实现接口,对于没有接口的类无法进行代理。
  • 由于使用了反射,在性能上相比于直接调用有一定损耗。
  • 功能相对简单,不能针对类的成员变量进行操作。

2. CGLIB代理:

  • 原理:基于ASM字节码生成框架,能够在运行期对字节码进行修改或动态生成新的类。CGLIB代理生成的代理类是被代理类的一个子类。
  • 优点:
  • 不依赖于接口,能够实现代理没有实现任何接口的目标类,适用性更广泛。
  • 相比于JDK动态代理,CGLIB代理功能更加强大,可以拦截方法、修改方法等。
  • 缺点:
  • CGLIB代理会对类的所有方法进行增强,即使不希望增强的方法也会被代理,这可能造成额外的性能开销。
  • 如果目标类做了final修饰或者有final方法,则无法进行代理。
  • CGLIB库不是Java标准库的一部分,需要单独引入依赖。
  • 字节码操作技术相比反射更为复杂,如果出现问题调试起来较为困难。

总结来说,JDK动态代理更适合接口较多且不需要对非接口方法进行增强的情况,而CGLIB代理则在不需要接口或需要对类的所有方法进行增强时更加适用。在实际应用中,如Spring框架会根据目标对象是否有接口智能选择使用哪种动态代理策略。


5.动态代理与 cglib 实现的区别。

JDK动态代理实现:

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class JdkProxyExample {

    interface MyInterface {
        void doSomething();
    }

    static class TargetClass implements MyInterface {
        @Override
        public void doSomething() {
            System.out.println("Target: Doing something...");
        }
    }

    static class LoggingInvocationHandler implements InvocationHandler {
        private final Object target;

        public LoggingInvocationHandler(Object target) {
            this.target = target;
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            System.out.println("Before method call: " + method.getName());
            Object result = method.invoke(target, args);
            System.out.println("After method call: " + method.getName());
            return result;
        }

        public static MyInterface createLoggingProxy(TargetClass target) {
            return (MyInterface) Proxy.newProxyInstance(
                    TargetClass.class.getClassLoader(),
                    new Class<?>[]{MyInterface.class},
                    new LoggingInvocationHandler(target));
        }
    }

    public static void main(String[] args) {
        MyInterface target = new TargetClass();
        MyInterface proxy = LoggingInvocationHandler.createLoggingProxy(new TargetClass());

        proxy.doSomething();  // 输出日志并调用目标方法
    }
}

 在上述例子中,LoggingInvocationHandler实现了InvocationHandler接口,当代理对象的方法被调用时,会触发invoke方法,在该方法中添加了前后打印日志的逻辑,并实际调用了目标类的方法。

 

 CGLIB代理实现:

首先需要引入CGLIB库(例如使用Spring框架中的CGLIB代理支持):

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

public class CglibProxyExample {

    static class TargetClass {
        public void doSomething() {
            System.out.println("Target: Doing something...");
        }
    }

    static class LoggingMethodInterceptor implements MethodInterceptor {
        @Override
        public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
            System.out.println("Before method call: " + method.getName());
            Object result = proxy.invokeSuper(obj, args);  // 调用父类(即目标类)的方法
            System.out.println("After method call: " + method.getName());
            return result;
        }

        public static TargetClass createLoggingProxy(TargetClass target) {
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(TargetClass.class);
            enhancer.setCallback(new LoggingMethodInterceptor());
            return (TargetClass) enhancer.create();
        }
    }

    public static void main(String[] args) {
        TargetClass target = new TargetClass();
        TargetClass proxy = LoggingMethodInterceptor.createLoggingProxy(new TargetClass());

        proxy.doSomething();  // 输出日志并调用目标方法
    }
}

在这个CGLIB代理的例子中,我们创建了一个LoggingMethodInterceptor类,它实现了MethodInterceptor接口。当代理对象的方法被调用时,会触发intercept方法,在这个方法里同样添加了日志输出,并通过MethodProxy调用了目标类的实际方法。CGLIB库通过字节码增强技术动态生成了继承自TargetClass的新类作为代理对象。


6.为什么 CGlib 方式可以对接口实现代理。

实际上,CGLIB代理并不直接对接口实现动态代理,而是通过生成被代理类的子类来实现代理功能

因为Java中所有的类都隐式地继承了java.lang.Object,所以即使目标类没有实现任何接口,CGLIB仍然可以通过创建一个子类的方式来覆盖(或增强)父类的方法。


具体来说,CGLIB使用ASM库在运行时动态生成一个新的字节码文件,这个新生成的类是目标类的子类,并且重写了所有finalprivate的方法,在这些方法内插入代理逻辑。

因此,无论目标类是否实现了接口,只要它不是final类,CGLIB都可以为其创建代理对象。

与之对比,JDK动态代理则要求目标类必须至少实现一个接口,因为它创建的是一个实现了相同接口的新代理类,并通过InvocationHandler调用接口中的方法实现动态代理。


7.final 的用途。

在Java中,final关键字有以下几个主要用途:
1. 修饰变量(成员变量和局部变量):

  • 成员变量:声明为final的成员变量必须在声明时或构造函数中初始化。一旦赋值后,就不能再改变其值,即它是一个常量。
       public class MyClass {
           public final String CONSTANT = "This is a constant value";
       }
       
  • 局部变量:如果一个局部变量(如方法内的变量)被声明为final,则在其生命周期内只能赋值一次。这对于多线程环境下不可变对象的安全共享非常有用,例如在匿名内部类中引用外部变量。

2. 修饰方法:

  • 当方法被声明为final时,表明该方法不能被子类重写。这有助于保护方法的行为不被修改,保证代码的稳定性和可预测性。

3. 修饰类:

  • 声明一个类为final意味着这个类不能被继承。这样做可以防止其他人对你的类进行扩展,确保了类的设计完整性,并且可能带来性能上的微小提升,因为编译器知道不会有子类存在,因此能做出一些优化。

总结来说,final关键字主要用于提供封装和限制更改的能力,以增强程序的健壮性和安全性。


8.简述 Mybatis 的插件运行原理,以及如何编写一个插件。

Mybatis 插件运行原理:
在 Mybatis 中,插件的运行基于 Java 的动态代理机制

Mybatis 支持针对四种核心接口创建插件,它们分别是 ExecutorParameterHandlerResultSetHandler StatementHandler。这些接口分别对应了 SQL 执行器(控制执行 SQL 的策略)、参数处理器(处理预编译语句中的参数设置)、结果集处理器(处理查询结果转换为对象)以及语句处理器(操作 JDBC Statement 对象)。
1. 插件加载:

  • 在 Mybatis 的配置文件(mybatis-config.xml)中通过 <plugins> 标签来配置插件。
  • 每个插件都通过 <plugin> 标签指定一个实现 Interceptor 接口的类全名,Mybatis 会根据配置信息加载并实例化这个类。

2. 插件执行:

  • 当 Mybatis 创建上述四个核心接口的对象时,实际上是创建了这四个接口对应的代理对象。
  • 这些代理对象在调用方法时,会触发拦截器链(Interceptor Chain),每个插件都是这个链上的一个节点。
  • 插件内部通过重写 Interceptor 接口的方法(如 intercept() 方法),在方法调用前后插入自定义逻辑。

3. 方法拦截与切面编程:

  • intercept() 方法接收一个 Invocation 对象作为参数,该对象封装了目标方法的信息和上下文。
  • 插件可以决定是否要执行目标方法(invocation.proceed()),并在方法调用前或后添加额外的操作,实现了面向切面编程(AOP)的功能。

编写一个 Mybatis 插件的基本步骤如下:

// 步骤一:实现 Interceptor 接口
public class MyPlugin implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 在目标方法执行前进行一些操作
        System.out.println("Before method execution");

        // 调用目标方法
        Object result = invocation.proceed();

        // 在目标方法执行后进行一些操作
        System.out.println("After method execution");

        return result;
    }

    @Override
    public Object plugin(Object target) {
        // 返回代理对象,这里使用的是默认的代理工厂 DefaultProxyFactory
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
        // 如果插件需要读取外部属性,可以通过此方法获取
        // ...
    }
}

// 步骤二:在 mybatis-config.xml 配置文件中注册插件
<plugins>
    <plugin interceptor="com.example.MyPlugin">
        <!-- 可以在这里配置插件的相关属性 -->
    </plugin>
</plugins>

在这个示例中,MyPlugin 类实现了 Interceptor 接口,并在其 intercept() 方法中添加了对目标方法前后执行的自定义逻辑。然后在 Mybatis 的配置文件中声明并启用该插件,这样每当 Mybatis 处理相关接口的方法时,就会自动触发插件的逻辑。 


9.ZK的ZAB 协议?

ZAB (ZooKeeper Atomic Broadcast) 协议是为分布式协调服务 ZooKeeper 设计的一种支持崩溃恢复和原子广播的一致性协议。

ZAB 是 ZooKeeper 实现数据一致性和高可用性的核心算法,确保在分布式环境中多个副本之间数据的一致性。
ZAB 协议的主要特点与功能包括:
1. 原子广播(Atomic Broadcast):

  • ZAB 确保一条消息要么被所有的服务器完整地接收,要么一个都没有接收到。这种特性确保了在整个集群中的所有服务器对事务操作的全局顺序达成一致。

2. 崩溃恢复(Crash Recovery):

  • 当 ZooKeeper 集群中的 Leader 节点发生崩溃时,ZAB 能够通过选举新的 Leader 并进行状态同步来实现系统的快速恢复。

3. Leader 选举(Leader Election):

  • 在 ZAB 中,集群首先会选举出一个 Leader 节点,Leader 负责处理客户端的所有事务请求,并将这些请求以 Proposal 的形式广播给其他 Follower 节点。

4. 事务序列化(Proposal Propagation and Ordering):

  • Leader 接收到客户端请求后,会对每个请求生成一个全局唯一的递增 ID,即zxid(ZooKeeper Transaction Id),并按照 zxid 的顺序向所有 Follower 发送 Proposal。
  • Follower 收到 Proposal 后,在本地执行事务操作并回复 Ack 给 Leader,当 Leader 收到过半数(quorum)Follower 的 Ack 后,该 Proposal 才被认为已提交。

5. 数据同步(Synchronization):

  • 新当选的 Leader 需要确保其状态与大多数 Follower 保持一致,因此在开始服务之前,Leader 会与 Follower 进行快照同步或基于 Proposal 日志的增量同步。

6. 持久化存储(Persistent Storage):

  • ZAB 要求所有事务都被持久化存储,即使节点崩溃也能从磁盘中恢复状态。

通过以上机制,ZAB 协议保证了即便在部分节点故障的情况下,整个 ZooKeeper 集群依然能够对外提供一致的数据视图和可靠的服务。


10.ZK客户端注册 Watcher 实现

在 ZooKeeper 中,客户端可以注册 Watcher 监听器来监控 ZooKeeper 节点的变化

以下是客户端如何注册 Watcher 的基本实现步骤:

import org.apache.zookeeper.*;

public class ZooKeeperWatcherExample {

    private static final String CONNECTION_STRING = "localhost:2181";
    private static final int SESSION_TIMEOUT = 3000;

    public static void main(String[] args) throws Exception {
        // 创建 ZooKeeper 客户端实例
        ZooKeeper zooKeeper = new ZooKeeper(CONNECTION_STRING, SESSION_TIMEOUT, new Watcher() {
            @Override
            public void process(WatchedEvent event) {
                // 这里是处理事件的回调方法
                System.out.println("Received event: " + event);
                // 根据 event.getType() 和 event.getState() 分别处理节点变化和会话状态变化
                if (event.getType() == Event.EventType.NodeCreated ||
                        event.getType() == Event.EventType.NodeDataChanged ||
                        event.getType() == Event.EventType.NodeChildrenChanged) {
                    // 对于数据或子节点发生变化的情况,重新设置 Watcher 或执行相关操作
                    try {
                        byte[] data = zooKeeper.getData(event.getPath(), true, null);
                        System.out.println("Node data changed: " + new String(data));
                    } catch (KeeperException | InterruptedException e) {
                        e.printStackTrace();
                    }
                } else if (event.getType() == Event.EventType.None && event.getState() == Event.KeeperState.SyncConnected) {
                    // 当与 ZooKeeper 服务器建立连接后,也可以在此时初始化或触发其他操作
                }
            }
        });

        // 注册 Watcher 示例:监视一个节点的数据变化
        try {
            Stat stat = zooKeeper.exists("/path/to/watch", true);  // 第二个参数为true表示设置Watcher
            if (stat != null) {
                // 如果节点存在,则获取数据并触发Watcher
                zooKeeper.getData("/path/to/watch", true, null);
            }
        } catch (KeeperException | InterruptedException e) {
            e.printStackTrace();
        }

        // 确保主线程不退出以维持ZooKeeper客户端连接
        synchronized (zooKeeper) {
            zooKeeper.wait();
        }

        // 最后,关闭 ZooKeeper 客户端连接
        zooKeeper.close();
    }
}

上述代码中,我们创建了一个 ZooKeeper 客户端实例,并在其构造函数中传入了 Watcher 实现。当 ZooKeeper 节点发生改变时,ZooKeeper 服务端会回调这个 Watcher 的 process() 方法。


在示例中,我们通过调用 exists() getData() 方法并传入 true 作为第二个参数来设置 Watcher。这样,在所关注的节点发生变化时,就会触发 Watcher 并执行相应的处理逻辑。

注意,Watchers 是一次性触发器,每次事件触发后需要重新注册才能继续监听后续事件。 


11.ZK的Chroot 特性

ZooKeeper 的 Chroot 特性允许客户端在与 ZooKeeper 服务器建立连接时指定一个路径作为其操作的根目录(chroot jail)

这意味着,当应用通过 chroot 连接到 ZooKeeper 时,所有后续的操作都将在该特定路径下进行相对寻址。

例如,如果客户端通过 zk://server:port/path/to/chroot 进行连接,那么对于客户端来说,“/”实际上是指向“/path/to/chroot”。
Chroot 特性的主要优点包括:

  • 命名空间隔离:不同的应用程序可以使用同一个 ZooKeeper 集群服务,但各自拥有独立的命名空间,这样可以在一定程度上避免不同应用之间的数据冲突。
  • 安全性提升:由于每个应用只能访问到自己的子树,因此可以限制应用对 ZooKeeper 树中其他部分的可见性和修改权限,从而提高系统的安全性。
  • 简化客户端逻辑:客户端不需要在其所有的路径操作前手动添加一个前缀,因为这个前缀已经在连接时定义好并自动应用于所有请求。
  • 可移植性增强:如果将来需要将某个应用的数据移动到另一台 ZooKeeper 服务器或集群,只需要重新配置 chroot 路径指向新的位置即可,无需更改应用内部对 ZooKeeper API 的调用。

然而,需要注意的是,一旦为客户端设置了 Chroot,它就无法看到 ZooKeeper 树中 chroot 路径之外的内容,也无法执行涉及这些外部节点的操作

此外,启动带有 chroot 的客户端时,必须确保其 chroot 路径在 ZooKeeper 中已存在,否则连接将失败。


12.ZK会话管理

在 ZooKeeper 中,会话管理是一个核心组件,它负责维护客户端与服务器之间的连接状态、会话超时以及会话的生命周期。以下是ZooKeeper中会话管理的主要方面:
1. 建立会话:

  • 当一个客户端首次连接到 ZooKeeper 集群时,会与集群中的某个节点(通常是 Leader)建立起一个会话。
  • 会话具有一个唯一的标识符(sessionID)和一个超时时间(sessionTimeout),这两个参数会在客户端连接时由服务器端分配。

2. 会话超时:

  • 客户端必须在会话超时时间内向 ZooKeeper 发送心跳请求(PING请求)以保持会话活跃。
  • 如果因为网络问题或其他原因导致客户端在会话超时时间内没有发送任何消息给 ZooKeeper,那么服务器将认为该会话已过期,并清理与该会话相关的临时节点和 Watcher 监听器。

3. 会话状态:

  • ZooKeeper 定义了多种会话状态,包括但不限于 CONNECTING(正在连接)、CONNECTED(已连接)、RECONNECTING(重新连接中)和 CLOSED(已关闭)。
  • 当客户端与服务器之间发生连接断开或恢复时,会话状态会发生相应的变化。

4. 会话重连:

  • 当客户端与 ZooKeeper 的连接中断后,客户端会自动尝试重新连接至集群并恢复会话。
  • 如果会话未过期,客户端可以在重新连接后继续之前的操作;如果会话已过期,则需要重新创建会话并可能丢失部分未持久化的临时数据。

5. 会话失效处理:

  • 对于临时节点,当其关联的会话过期时,ZooKeeper 服务端会自动删除这些临时节点。
  • 对于设置在节点上的 Watcher,会话过期后也会被清除,但客户端可以重新注册 Watcher 来继续接收事件通知。

通过这样的会话管理机制,ZooKeeper 能够有效地处理分布式环境下的网络不稳定问题,确保数据的一致性和可用性。


13.ZK服务器角色

在 Apache ZooKeeper 中,服务器角色指的是组成ZooKeeper集群的节点所承担的不同职责。ZooKeeper 集群中的服务器主要分为以下三种角色:
1. Leader:

  • Leader 是 ZooKeeper 集群的核心角色,负责处理客户端的所有事务请求(写操作)并确保这些事务按照全局有序的方式进行提交。
  • Leader 会为每一个事务生成一个全局唯一的事务ID(ZXID),并负责将事务提案广播给其他服务器。

Leader 还负责维护整个集群的一致性状态,并协调和管理集群中其他服务器的角色和行为。
2. Follower:

  • Follower 是集群中的从属节点,它接收客户端的读请求和来自 Leader 的事务提案。
  • 当收到 Leader 发送的事务提议时,Follower 将在本地执行该提议,并向 Leader 发送确认消息(ACK)。
  • 当大多数 Follower 对一个事务做出确认后,Leader 就认为该事务已提交并通过网络通知所有参与者的 Follower 更新其数据状态。

3. Observer(可选角色):

  • Observer 角色是从 ZooKeeper 3.3.0 版本开始引入的一个非必需角色,它的功能与 Follower 类似,可以接收客户端的读请求,并且能够接收到 Leader 发送的事务提议。
  • 不同之处在于 Observer 不参与投票过程,因此不会对事务提交的速度产生影响,主要用于扩展集群的读能力,减轻 Leader 和常规 Follower 节点的压力,尤其在读多写少的场景下能提升集群性能。

每个服务器在启动时都试图选举成为 Leader,但在选举过程中根据 ZooKeeper 自身的领导选举算法确定最终的 Leader、Follower 和 Observer 角色分配。在整个运行过程中,集群通过心跳检测机制保持各服务器之间的连接状态,并在 Leader 故障时重新发起选举以确保高可用性。


14.ZK数据同步

ZooKeeper 数据同步是通过 ZAB (ZooKeeper Atomic Broadcast) 协议实现的,确保在分布式系统中多个 ZooKeeper 节点之间保持数据一致性。以下是 ZooKeeper 数据同步的基本流程:
1. Leader 接收客户端请求:

  • 客户端将写操作请求发送给当前集群中的 Leader 节点。
  • Leader 为每个事务生成一个全局唯一的递增序列号(ZXID)。

2. Proposal 分发与投票:

  • Leader 将包含事务内容和 ZXID 的 Proposal 分发给所有 Follower 节点。
  • 每个 Follower 收到 Proposal 后,在本地执行事务并记录下这个 Proposal,但并不提交事务。

3. 过半数确认(Quorum Acknowledgement):

  • 当 Leader 收到超过半数(包括自己)的 Follower 对 Proposal 的确认(Ack)后,认为该 Proposal 已达成一致(即形成了 Quorum)。
  • 这种机制保证了即使部分节点故障,也能够确定某个事务已经被大多数节点接受,并且按照相同的顺序执行。

4. Commit 提交:

  • 一旦 Leader 收到足够的确认,它会向所有 Follower 发送 Commit 命令,指示它们可以安全地提交此事务到其内存数据库(内存中的数据结构)。
  • 所有 Follower 在接收到 Commit 命令后,正式将事务提交到本地状态机。

5. 持久化与同步:

  • 无论是 Leader 还是 Follower,在事务提交后都会将其持久化存储到磁盘上。
  • 若有新加入或重新启动的节点,Leader 会负责与其进行快照同步或者基于 Proposal 日志的增量同步,以达到数据的一致性。

6. Watcher 触发:

  • 在数据发生变化时,如果有客户端设置的 Watcher 监听器,则 Leader 会在事务被提交后触发相应的 Watcher 事件通知客户端。

通过以上步骤,ZooKeeper 确保了在一个高可用的分布式环境中,各个节点之间的数据始终保持一致。


15.如何理解 Spring 中的代理?

在Spring框架中,代理是一种实现AOP(面向切面编程)的关键技术,它允许开发者在不修改目标类源代码的基础上向方法调用前后添加额外的逻辑,比如事务管理、日志记录、权限检查等。
Spring提供了两种主要的代理方式:
1. JDK动态代理:

  • JDK动态代理基于Java反射和InvocationHandler接口实现。如果目标对象实现了至少一个接口,Spring会选择使用JDK动态代理来创建代理对象。
  • 当通过代理对象调用方法时,实际执行的是InvocationHandler中的invoke()方法,该方法会在调用真实对象的方法前后插入自定义的处理逻辑。

2. CGLIB代理:

  • CGLIB库是第三方库,Spring利用其动态生成字节码的功能为没有实现接口的目标类创建子类代理。
  • 对于未实现任何接口的类,Spring会采用CGLIB代理。CGLIB代理通过继承并覆盖目标类的方法来实现代理功能,在覆盖的方法内插入增强逻辑。

无论哪种代理方式,Spring AOP都是在运行时创建代理对象,并将切面逻辑织入到目标对象的方法调用过程中。这些切面通常定义在切面类中,通过注解或XML配置的方式与目标对象的方法关联起来。

总结来说,Spring中的代理机制使得业务逻辑代码和横切关注点(如事务管理、日志记录等)得以分离,提高了代码的可重用性和可维护性。


16.分布式集群中为什么会有 Master?

在分布式集群中引入Master节点的原因主要包括以下几个方面:
1. 协调管理:

  • Master节点负责整个集群的协调管理工作,比如资源分配、任务调度、元数据管理和维护等。例如,在Hadoop集群中,NameNode作为主节点管理文件系统的命名空间和块映射信息。

2. 一致性保障:

  • 在分布式系统中,为了保证数据的一致性,需要一个中心点来管理所有节点的状态,并执行相应的算法(如Paxos或Raft)达成共识。例如,ZooKeeper中的Leader节点用于处理客户端请求并确保事务的原子广播。

3. 负载均衡:

  • Master节点能够根据集群内各工作节点(Worker或Slave节点)的负载情况,动态地将任务分发给不同的节点,从而实现高效的负载均衡。

4. 故障恢复:

  • 当集群中的某个节点发生故障时,Master节点通常会检测到这种状态变化,并触发相应的故障恢复机制,比如重新分配失效节点上的任务,或者启动新的节点加入集群以替代失效节点。

5. 简化复杂性:

  • 将集群中的控制逻辑集中在Master节点上,可以简化整体架构设计,使得客户端只需与Master交互就能完成复杂的操作,而无需直接与每个工作节点通信。

6. 权限和安全控制:

  • 主节点可能还负责实施访问控制策略,对用户的操作请求进行认证和授权,确保集群的安全性。

7. 全局视图:

  • Master节点维护着整个集群的全局状态视图,这对于跨多个工作节点的全局决策至关重要,如全局锁服务、队列管理等。

总之,Master节点的存在是为了解决分布式环境下的一系列问题,提供一种集中式的管理和控制手段,确保集群的整体性能、可用性和安全性。


17.Zookeeper 对节点的 watch 监听通知是永久的吗?为什么不是永久的?

Zookeeper 对节点的 watch 监听通知不是永久的。

Watch事件是一次性的触发器,这意味着:

  • 当客户端对某个ZooKeeper节点设置了watch监听后,如果该节点的数据或状态发生变化(例如数据内容变更、子节点增删等),ZooKeeper服务器会立即向设置了该watch的客户端发送一个通知事件。
  • 一旦发生相应的事件并触发了watch,该watch就会被自动移除,不再对后续的相同事件进行监听。因此,如果客户端想要继续接收同一节点的变化通知,需要在接收到通知后重新设置watch。
  • 这种设计的主要原因是考虑到ZooKeeper作为一个高性能服务,避免每次变化都通知所有监听者可能会带来的性能开销和网络带宽压力。同时,也简化了watch机制的设计和实现。

总之,ZooKeeper中的watch机制是一种轻量级的通知服务,它只保证一次通知并不支持持久订阅式的监听。如果需要持续监控某个节点的状态变化,客户端应用需要在接收到通知后重新注册watch。


18.chubby 是什么,和 zookeeper 比你怎么看?

Chubby 是 Google 开发的一种分布式锁服务,它提供了一个高度可靠的粗粒度的分布式锁机制,用于构建和维护大型分布式系统的协调一致性。

Chubby 的设计目标是实现一个类似分布式文件系统的系统,其中包含一个全局唯一且持久化的命名空间,可以用来管理锁、领导选举以及其他形式的状态共享。
Chubby 与 ZooKeeper 在很多方面有相似之处,它们都是为了解决分布式环境中的数据一致性问题而设计的:
1. 功能相似性:

  • 都提供了分布式锁服务。
  • 都支持临时节点(在 Chubby 中称为“session lease”)来表示会话生命周期,并在客户端断开连接时自动清理资源。
  • 都能实现 leader 选举、配置管理、组成员管理等分布式系统中常见的任务。

2. 设计理念差异:

  • Chubby 更专注于提供强一致性的分布式锁服务,其设计之初就是为了服务于Google内部的大规模分布式存储系统如Bigtable等,对高可用性和强一致性有着非常严格的要求。
  • ZooKeeper 则是一个更通用的分布式协调框架,除了提供分布式锁之外,还广泛应用于服务发现、配置管理、集群管理、分布式队列等多个领域,它提供的API和服务更多样化。

3. 开源与应用范围:

  • Chubby 主要由 Google 内部使用,未对外开源,因此社区支持和第三方生态不如 ZooKeeper 强大。
  • ZooKeeper 是 Apache 基金会下的开源项目,拥有广泛的社区支持和大量的生产实践案例,在开源世界中被广泛应用到各种分布式系统场景。

总结来说,尽管两者的设计初衷和应用场景有所差异,但它们都在分布式系统中扮演着至关重要的角色,通过提供一种可信赖的协调服务,确保了复杂分布式环境下的一致性和正确性。如果从实用性和生态系统来看,ZooKeeper 由于其开源性质,得到了更为广泛的应用和发展。


19.说几个 zookeeper 常用的命令

ZooKeeper 提供了一系列命令行工具,用于与 ZooKeeper 集群进行交互。以下是一些常用的 ZooKeeper 命令:
1. 服务启动和停止:

  • zkServer.sh start:启动 ZooKeeper 服务。
  • zkServer.sh stop:停止 ZooKeeper 服务。
  • zkServer.sh status:查看 ZooKeeper 服务状态。

2. 连接到 ZooKeeper 服务器:

  • zkCli.sh -server <host:port>:通过命令行客户端连接到指定主机和端口的 ZooKeeper 服务器。

3. 节点操作(数据管理):

  • create /path data [flags]:在指定路径下创建一个新的 ZNode,并赋予其初始数据,可选参数 -s 或 -e 分别表示创建顺序节点或临时节点。
  • get /path [watch]:获取指定路径下的节点数据,并可选择性地设置 watch 监听事件。
  • set /path data [version]:更新指定路径下的节点数据,可以指定版本号来确保原子性的条件更新。
  • delete /path [version]:删除指定路径的节点,可选参数为版本号,仅当节点版本与提供的版本匹配时才会删除。

4. 节点列表及权限管理:

  • ls /path [watch]:列出指定路径下的子节点列表,也可以设置 watch 监听事件。
  • getAcl /path:获取指定节点的访问控制列表(ACL)信息。
  • setAcl /path acl:设置指定节点的 ACL 权限。

5. 监听器管理:

  • 由于 Watcher 是一次性的触发机制,在命令行中,每次执行 get、ls 等带监听功能的操作都会重新注册一个 Watcher。

6. 其他实用命令:

  • sync /path:强制同步某个节点的数据,保证客户端拥有最新版本的数据。
  • printwatches on|off:开启或关闭打印 Watcher 的详细输出。

请注意,上述命令是在 ZooKeeper 客户端命令行工具中执行的,并且不同的 ZooKeeper 版本可能有些微差异。实际使用时,请参考对应版本的官方文档以获取准确的命令格式和选项。


20.ZAB 和 Paxos 算法的联系与区别?

ZAB(ZooKeeper Atomic Broadcast)和Paxos都是分布式一致性算法,它们主要用于解决在分布式环境下的数据同步和状态协调问题。虽然两者的目标相似,但设计和实现上有所不同。
联系:
1. 一致性保证:

  • ZAB 和 Paxos 都是为了解决分布式系统中的一致性问题而设计的,都能确保在异步网络环境下,即使存在节点故障或消息丢失的情况下,集群内部仍能就某个值达成一致
  • 两者都采用了领导者选举机制来处理提议的提交与确认,并确保只有被多数节点接受的提案才能被最终确定。

2. 领导者-追随者模型:

  • 在ZAB协议中,有一个明确的Leader角色负责接收客户端请求、广播事务并维护全局有序的一致状态。
  • Paxos算法虽然没有严格的领导者概念,但在实际应用中(如Chubby, ZooKeeper),往往也会使用类似于领导者的角色,负责提案的发起和决定何时一个提案可以被接受。

区别:
1. 目标应用场景:

  • ZAB协议是专门为构建高可用的分布式数据主备系统(例如Apache ZooKeeper)而设计的,其关注点在于如何保证多个副本之间的数据强一致性以及在发生故障时能够快速恢复服务
  • Paxos算法则更通用,它可以应用于构建分布式一致性状态机系统,不仅限于数据存储服务,还可以用于分布式锁服务等场景

2. 协议流程:

  • ZAB协议包含了崩溃恢复阶段和消息广播阶段两个主要部分,崩溃恢复过程中会通过选举产生新的Leader并进行状态同步;消息广播阶段则是基于已有的Leader来进行原子广播操作。
  • Paxos算法的核心是解决多轮投票和决策的过程,包括提案提出、投票表决和最终决议确认三个基本步骤。Paxos本身并不包含像ZAB那样的恢复机制,但如果将其应用到类似ZooKeeper的服务中,通常会增加额外的组件来处理这些问题。

3. 消息传递复杂度:

  • ZAB协议在某些方面简化了Paxos的逻辑,比如它假设只有一个提议者(即Leader节点),这样可以减少协商过程中的复杂度,提高性能。
  • 相比之下,Paxos允许任意数量的提议者,这使得它的理论模型更加灵活但也更复杂,在工程实践中需要更多优化以适应大规模系统的高性能要求。

总之,ZAB和Paxos都在不同的层面提供了分布式系统一致性保障,但ZAB针对特定应用场景进行了优化设计,使其更适合构建诸如ZooKeeper这样的服务,而Paxos提供了一个更为通用的一致性框架。


21.Dubbo服务调用是阻塞的吗?

Dubbo服务调用既可以是阻塞的也可以是非阻塞的,这取决于调用方式和服务配置。

  • 默认同步阻塞调用: 默认情况下,Dubbo客户端发起一个服务调用时会等待服务端响应返回结果。当客户端发起请求后,线程会一直等待服务端处理完成并返回结果,期间该线程被阻塞。
  • 异步非阻塞调用: Dubbo也支持异步调用模式。在异步调用场景下,客户端发起调用后,不等待响应结果立即返回,并提供一个Future或Callback机制来获取最终结果。这种方式下,客户端线程不会被阻塞,可以继续执行其他任务。

在配置方面,可以通过设置async参数(对于消费者而言)来启用异步调用。开启异步调用后,Dubbo服务调用就不再是阻塞的。同时,服务提供者端也可以配置是否支持异步处理请求,以实现更高效的异步服务调用。


22.Dubbo 推荐用什么协议?

Dubbo推荐使用dubbo协议,这是因为它针对服务调用进行了高度优化,并且具有以下特点:

  • 高性能:Dubbo协议是基于TCP实现的二进制传输协议,它在设计上考虑了数据包小、序列化性能高等因素,因此在网络通信效率和系统性能上有很好的表现。
  • 功能丰富:Dubbo协议支持多种特性,如请求响应模式、单向通知模式、双向通信等,同时提供了服务治理相关的功能,比如负载均衡、容错策略、超时重试等。
  • 透明序列化:默认采用Hessian2进行序列化,提供高效快速的数据交换方式。用户也可以选择其他序列化方案(如Java原生序列化、JSON、Kryo等)。
  • 自定义扩展:虽然dubbo协议是推荐选项,但Dubbo也支持RMI、HTTP、Hessian、Webservice等多种协议,用户可以根据自身需求和环境灵活选择或扩展协议。

总结来说,对于Dubbo框架本身而言,dubbo协议在性能、功能以及与框架集成度方面都更胜一筹,因此是官方推荐使用的协议。当然,在实际项目中,需要根据具体业务场景和技术栈要求来综合评估和选择合适的通信协议。


Dubbo支持多种协议,每种协议都有其特点和适用场景。

以下是Dubbo支持的一些主要协议及其特点:
1. dubbo协议

  • 特点:基于TCP的二进制高效传输协议,是Dubbo默认推荐使用的协议。
  • 优点:数据包小、序列化性能高、适合大并发小数据量的服务调用,并且对消费者机器数远大于提供者机器数的情况进行了优化。
  • 缺点:不适合用于传输大文件或超大数据量的消息。

2. rmi协议

  • 特点:采用Java RMI(Remote Method Invocation)实现的远程方法调用协议。
  • 优点:由于RMI直接使用Java原生的序列化机制,对于Java对象的传输非常方便,无需额外定义接口和数据结构。
  • 缺点:只能在Java环境内部使用,跨语言兼容性差,序列化效率相对较低。

3. hessian2协议

  • 特点:基于HTTP协议,使用Hessian2进行序列化和反序列化,支持多语言。
  • 优点:跨平台性强,可与非Java环境交互。
  • 缺点:相比dubbo协议,其性能略低,但适用于异构系统的集成场景。

4. http协议

  • 特点:基于HTTP/HTTPS实现,易于穿越防火墙,可用于RESTful服务调用。
  • 优点:广泛兼容性和易用性,不受特定编程语言限制,能够轻松与其他系统对接。
  • 缺点:相对于二进制协议如dubbo,HTTP协议的数据包较大,网络传输效率稍低。

5. webservice协议

  • 特点:基于SOAP标准的XML格式消息传递,遵循Web服务标准。
  • 优点:具有良好的跨平台和跨语言互操作性,适用于企业级应用间的集成。
  • 缺点:相比其他轻量级协议,性能较低,数据冗余度较高,且开发复杂度相对增加。

在实际项目中,选择何种协议需要根据项目需求、性能要求、系统架构等因素综合考虑。通常情况下,如果是纯Java环境并且注重性能,推荐使用dubbo协议;若需与其他语言系统进行交互,则可能选用HTTP或Hessian2等支持跨语言的协议。


23.Dubbo Monitor 实现原理?

Dubbo Monitor 是 Dubbo 框架中的一个核心组件,主要用于收集、统计和展示服务调用的相关监控信息,以便于开发者和服务运维人员更好地了解分布式系统中各个服务的运行状态和性能指标。以下是 Dubbo Monitor 实现原理的详细说明:
1. 数据采集:

  • 在Dubbo框架中,服务调用的监控数据主要通过Filter机制进行收集。当客户端发起服务调用时,请求会经过一系列的Filter链。其中,MonitorFilter(或StatisticsFilter)作为其中一个重要的过滤器,在服务调用前后执行特定的操作。
  • 当请求到达服务提供者前,MonitorFilter会记录下开始时间戳以及其他可能的上下文信息。
  • 服务处理完成后,MonitorFilter会在响应返回到客户端之前,再次执行逻辑以记录结束时间戳,并计算出响应时间和结果状态等关键信息。

2. 统计数据封装与发送:

  • 收集到的服务调用数据会被封装成监控事件(如InvocationStat或RpcStat),这些事件包含了调用的接口名、方法名、耗时、是否成功以及任何自定义的元数据等。
  • 这些监控事件按照一定的频率(如每分钟一次)聚合后,通过RegistryProtocol或者自定义的MonitorServiceExporter将监控数据发送给注册中心,再由注册中心转发给监控中心。

3. 监控中心接收与处理:

  • 监控中心是一个独立的服务端应用,例如Dubbo官方提供的Dubbo Admin,或者是第三方集成的监控系统如Prometheus、Zipkin等。
  • 监控中心接收到监控数据后,将其持久化存储在数据库或其他支持的数据存储系统中(如MySQL、Redis、InfluxDB等)。
  • 根据不同的监控需求,监控中心会对这些数据进行实时分析、聚合以及可视化展现,比如生成服务调用次数、成功率、平均响应时间、最大并发数等统计报表。

4. 定制化与扩展:

  • Dubbo Monitor允许用户根据实际需求进行定制和扩展。用户可以自定义MonitorFilter实现额外的监控指标或行为,例如添加更详细的日志输出、设置阈值触发告警等。
  • 同时,Dubbo也提供了丰富的API供外部系统集成,以便接入各种监控工具和平台,进一步提升系统的可观测性和可管理性。

综上所述,Dubbo Monitor是通过拦截服务调用过程中的关键点,收集并汇总调用数据,然后发送给监控中心进行存储和分析,最终实现对分布式服务的全面监控和性能优化


24.Dubbo 用到哪些设计模式?

Dubbo 在多个关键组件和功能上运用了多种设计模式,以下是一些具体的设计模式应用示例:
1.工厂模式:

  • Dubbo 使用了 SPI(Service Provider Interface)机制来加载扩展实现。通过在 META-INF/services/ 目录下放置接口对应的配置文件,Dubbo 实现了服务提供者与接口的解耦,允许动态发现和实例化服务实现类,类似于工厂模式。

2.责任链模式:

  • 在 Filter 链的设计中,Dubbo 应用了责任链模式。Filter 接口定义了一系列预处理、后处理的方法,每个 Filter 类型代表了一个处理单元。当客户端发起调用时,请求会依次经过这些 Filter,每个 Filter 根据自身逻辑选择是否继续传递请求到下一个 Filter,这种设计使得系统可以灵活地添加新的过滤器以实现特定的功能如日志记录、权限校验、监控统计等。

3. 代理模式:

  • 为了隐藏远程调用细节,Dubbo 为服务提供者和服务消费者生成代理对象。客户端通过代理对象调用远程服务就像调用本地方法一样,这里使用的是代理模式。

4. 装饰器模式:

  • 虽然Dubbo没有明确标注为装饰器模式,但其 Filter 的设计具有装饰器模式的特点。Filter 可以看作是对原始 Invoker 对象的一种“装饰”,通过添加一层层的 Filter 来增强 Invoker 的功能。

5. 单例模式:

  • Dubbo 中的 Protocol 和 Registry 等组件通常采用单例模式进行设计,确保全局范围内只存在一个共享实例,例如ZookeeperRegistry只有一个实例管理注册中心连接。

6. 模板方法模式:

  • Dubbo 中的 AbstractInvoker 抽象类是一个典型的模板方法模式应用。它定义了执行远程服务调用的基本流程,包括初始化网络连接、发送请求、接收响应以及处理异常等步骤。具体的 Invoker 实现只需要关注那些需要变化的部分,而固定不变的流程则由抽象类统一完成。

7. 策略模式:

  • 在容错、负载均衡、路由策略等方面,Dubbo 使用了策略模式。比如 LoadBalance 接口提供了多种负载均衡策略的实现,用户可以根据实际需求配置不同的负载均衡算法。

8. 发布订阅模式:

  • Dubbo 的注册中心采用了发布订阅模式来实现服务地址的动态变更通知。当服务提供者的地址发生变化时,注册中心作为主题将变更信息发布出去,已订阅该服务的所有消费者都会收到通知并更新自己的服务列表信息。

以上是Dubbo框架中主要应用的设计模式,这些设计模式共同构建了Dubbo高性能、可扩展的分布式服务治理框架


25.Dubbo SPI 和 Java SPI 区别?

Dubbo SPI 和 Java SPI(Service Provider Interface)都是用于在运行时动态加载扩展实现的机制,但它们之间存在一些区别:
1. Java SPI:

  • 定义:Java SPI 是 JDK 自带的一种服务发现机制,主要体现在 java.util.ServiceLoader 类中。
  • 使用场景:Java SPI 通常用于应用程序和库之间的扩展点设计,允许第三方开发者为某个接口提供多个可选实现,并通过配置文件指定具体使用哪个实现。
  • 配置方式:Java SPI 要求在 META-INF/services/ 目录下创建一个与接口全限定名相同的文本文件,该文件中列出所有接口实现类的全限定名。
  • 特点:
  • 简单易用,只需遵循规范即可进行扩展。
  • 只能提供基本的服务发现功能,不支持更复杂的扩展点管理,例如生命周期管理、AOP(面向切面编程)、依赖注入等。
  • 当需要更新或切换扩展实现时,必须重新启动应用。

2. Dubbo SPI:

  • 定义:Dubbo SPI 是基于 Java SPI 的一种扩展和增强,是 Dubbo 框架内部用来实现组件扩展和服务治理的核心机制。
  • 使用场景:在 Dubbo 中,SPI 主要用于框架内部各组件如协议、注册中心、过滤器链等的扩展以及服务调用过程中的插件化处理。
  • 配置方式:Dubbo SPI 基于 Java SPI 的原理,同样需要在 META-INF/services/ 下创建配置文件,但是提供了更为丰富的特性。
  • 特点:
  • 功能更加强大,支持扩展点的生命周期管理,包括初始化、销毁等。
  • 支持 AOP 编程,可以对扩展点的方法执行前后添加额外操作。
  • 提供了更加灵活的扩展点加载策略,比如按需加载、延迟加载、条件激活等。
  • 内置缓存机制,提高扩展点实例化的性能。
  • 对 Java SPI 进行了优化,使得在大规模分布式环境下具有更好的性能和稳定性。

总结来说,Java SPI 是基础的服务发现机制,而 Dubbo SPI 在此基础上进行了大量的增强和扩展,使其更适合构建复杂的企业级分布式系统


26.简述 Http 请求 get 和 post 的区别以及数据包格式。

HTTP(Hypertext Transfer Protocol)中的GET和POST请求是两种最常见的HTTP方法,它们在数据提交方式、可见性、缓存策略以及用途上有所区别:
1. 数据提交方式:

  • GET:通过URL的查询字符串传递参数。所有发送的数据都会显示在URL中,直接附加在URL后面,以“?”分隔主体URL与查询字符串,并使用“&”分隔多个参数。
  • POST:将数据放在HTTP请求正文中发送。用户无法从浏览器地址栏看到这些数据。

2. 数据大小限制:

  • GET:由于浏览器和服务器对URL长度有限制(通常为2048字符),所以不适合传输大量数据。
  • POST:理论上没有特定的数据量限制,适合传输大容量的数据。

3. 可见性与安全性:

  • GET:因为参数暴露在URL中,所以不适用于敏感信息的传输,容易被记录或抓包。
  • POST:虽然在传输过程中也可能被捕获,但相较于GET更安全,因为它不在URL中直接显示数据。

4. 缓存策略:

  • GET:具有幂等性(多次执行结果相同,不会产生副作用),且可以被浏览器和代理服务器缓存。
  • POST:一般不被缓存,且不具备幂等性。

5. 用途:

  • GET:主要用于获取资源,不应该改变服务器状态。
  • POST:用于向服务器提交数据,通常用于创建或更新资源,可能引起服务器状态的变化。

6. 数据包格式:

  • GET 请求数据包示例:
      GET /path/to/resource?param1=value1&param2=value2 HTTP/1.1
      Host: example.com
      Connection: keep-alive
      Accept: */*
      
  • POST 请求数据包示例:

      POST /path/to/resource HTTP/1.1
      Host: example.com
      Connection: keep-alive
      Content-Type: application/x-www-form-urlencoded
      Content-Length: 18
    
      param1=value1&param2=value2
      

    对于POST请求,Content-Type可以根据需要设置为其他类型,如application/json、multipart/form-data等,并且请求体的内容会根据Content-Type的不同而变化。


27.HTTP 有哪些 method

HTTP(Hypertext Transfer Protocol)定义了多种方法(methods),用于客户端与服务器之间进行不同的交互操作。以下是一些常见的HTTP方法:
1. GET:

  • 从指定的资源请求数据,通常用来获取网页内容或API的数据。
  • GET请求会附加在URL之后作为查询字符串,并且请求的内容会被浏览器缓存和记录在浏览历史中。
  • GET请求是幂等的,多次执行相同请求应得到相同结果。

2. POST:

  • 向指定资源提交数据,请求服务器处理(如提交表单或者上传文件)。数据被包含在请求体中。
  • POST请求可能会导致新的资源创建或已有资源的修改,不具有幂等性。
  • 由于数据不在URL中显示,因此更适合传输敏感信息。

3. PUT:

  • 用于替换指定资源的所有当前表示。它跟POST类似,但具有幂等性,即多次执行同样的PUT请求会产生相同的结果。
  • 请求体必须包含完整的资源表示。

4. PATCH:

  • 用于对资源进行局部更新,只更新资源的部分内容而不是全部内容。
  • PATCH请求也具有幂等性,但其影响范围仅限于请求中所描述的更改。

5. DELETE:

  • 请求服务器删除指定的资源。
  • DELETE请求是幂等的,多次发出相同的DELETE请求将始终删除同一个资源。

6. HEAD:

  • 类似于GET,但是服务器只会返回响应头信息,而不会返回响应实体内容。

7. OPTIONS:

  • 用于询问服务器特定资源支持哪些HTTP方法,也可以用于获取服务器的其他通信选项。

8. CONNECT:

  • 用于建立一个到由目标资源标识的服务器的TCP连接通道,通常用于代理服务器场景。

9. TRACE:

  • 发送一个请求来查看服务器收到该请求后是如何对其进行处理的,主要用于诊断目的,服务器会将请求的路径、方法和头信息原样返回给客户端。

这些HTTP方法可以根据需要应用于不同的场景,以实现客户端与服务器之间的不同互动行为。


28.简述 HTTP 请求的报文格式。

HTTP 请求报文由起始行(Start Line)、头部(Header)、空行(Blank Line)以及可选的请求主体(Request Body)组成。
1. 起始行(Start Line):

  • 方法(Method):描述客户端想要执行的操作,如GET、POST、PUT、DELETE等。
  • 请求URI(Request-URI):标识要访问资源的位置,可以是绝对URI或相对URI。在浏览器中通常包含主机名、端口号和资源路径。
  • 协议版本(HTTP Version):例如"HTTP/1.1",表明使用的HTTP协议版本。

示例:GET /index.html HTTP/1.1
2. 头部(Headers):

  • 头部字段由键值对组成,每行一个字段,每个字段以冒号分隔键与值,键名称不区分大小写,值前后的空白字符会被忽略。
  • 常见的头部字段包括Host(主机名和端口信息)、Connection(连接管理)、User-Agent(客户端信息)、Accept(接受的内容类型)、Content-Type(请求体的数据类型)、Content-Length(请求体的长度)等。

示例:

   Host: www.example.com
   Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
   Content-Type: application/x-www-form-urlencoded
   Content-Length: 34
   

3. 空行(Blank Line):

  • 在所有头部字段之后必须有一个空行(CRLF),表示头部结束,接下来的部分(如果有)为请求主体。

4. 请求主体(Request Body):

  • 对于某些方法(如POST、PUT)而言,请求主体用于携带额外的数据,如表单数据、文件上传内容等。
  • 是否有请求主体及主体内容取决于所用的方法以及头部中的Content-Type和Content-Length字段。

示例请求报文:

POST /api/user/login HTTP/1.1
Host: api.example.com
Content-Type: application/json
Content-Length: 56

{
  "username": "user1",
  "password": "secret"
}

29.HTTP 的长连接是什么意思。

HTTP的长连接(HTTP Persistent Connection,也称为HTTP Keep-Alive或TCP Keep-Alive)是指在完成一次HTTP请求/响应后,客户端和服务端之间的TCP连接不立即关闭,而是保持一段时间的开放状态,允许多次HTTP请求复用同一个TCP连接

在早期HTTP/1.0版本中,默认每个HTTP请求都会创建一个新的TCP连接,并在请求完成后关闭该连接。这种方式在高并发场景下会带来较大的网络开销,如频繁的建立和断开TCP连接的三次握手与四次挥手过程。

从HTTP/1.1开始,引入了默认开启的长连接机制,通过在头部设置Connection: keep-alive指示客户端和服务端保持连接,使得多个HTTP请求可以在同一连接上连续发送而无需重新建立连接,从而显著提高了HTTP通信的效率并减少了服务器资源消耗。

然而,即使使用了长连接,也不能保证TCP连接始终存在,因为连接可能会由于各种原因(如超时、错误或者双方任意一方主动关闭)而关闭。此外,在实际应用中,通常还会设定一个连接的最大空闲时间或者最大请求数量限制超过这个限制也会关闭连接。


30.Thread 类的 sleep()方法和对象的 wait()方法都可以让线程暂停执行,它们有什么区别?

Java中的Thread.sleep()方法和对象的wait()方法都允许线程暂停执行,但它们在功能、使用场景和工作原理上有显著的区别:
1. Thread.sleep()

  • Thread.sleep(long millis) java.lang.Thread 类的方法,它让当前线程进入指定时间(毫秒)的阻塞状态。
  • 该方法不会释放任何锁资源。如果当前线程持有某个对象的锁,那么在 sleep 时间结束后,线程会继续执行,而仍然保持对锁的所有权。
  • sleep() 不是线程间的通信机制,它纯粹是为了让当前线程暂时停止执行一段时间。

2. Object.wait()

  • wait()java.lang.Object 类的方法,需要在同步代码块或同步方法中调用,并且必须先获取到相应的对象监视器(即锁)才能调用。
  • 当一个线程调用对象的 wait() 方法时,该线程会释放当前持有的对象锁,并进入等待队列等待被唤醒,直到其他线程调用同一对象的 notify() notifyAll() 方法。
  • wait() 是用于线程间协作的一种机制,主要用于线程间通信,比如生产者-消费者模式或者条件变量的控制。
  • 线程被唤醒后,要重新竞争获取对象锁,然后才能继续执行。

总结来说,Thread.sleep() 是为了给线程提供一种简单的延时机制,不涉及线程间的同步与通信;而 wait() 方法则是并发编程中实现线程同步和通信的关键手段之一,通过配合 notify()notifyAll() 方法,可以精确地控制线程之间的协调与同步。


31. Java 服务端问题排查(OOM,CPU 高,Load 高,类冲突)

在Java服务端进行问题排查时,针对不同的问题类型(如OOM、CPU使用率高、Load高以及类冲突)有不同的处理和分析方法。以下是一些基本的排查步骤:
1. OutOfMemoryError (OOM)

  • 查看错误日志:首先从堆栈跟踪中获取具体的错误信息,确定是哪个区域(堆空间、元空间或直接内存等)出现了溢出。
  • 检查JVM参数:
  • 确定是否分配了足够的内存给Java进程。例如通过-Xms设置初始堆大小,-Xmx设置最大堆大小,以及-XX:MaxMetaspaceSize配置元空间大小。
  • 检查其他可能导致内存泄漏的参数,比如持久代、代码缓存区等。
  • 监控与诊断工具:
  • 使用JDK自带的jmap命令行工具生成堆转储快照(heap dump),然后使用如MAT(Memory Analyzer Tool)、VisualVM等工具分析内存泄漏原因。
  • 可以使用jconsolejvisualvm实时监控内存使用情况,并观察是否存在长时间不被垃圾回收的对象。
  • 分析内存泄漏:
  • 通过分析堆转储文件找出占用内存最多的对象及其引用链,定位是否有循环引用导致无法释放的情况。
  • 查看是否存在一次性加载大量数据到内存或者持有大量静态变量导致内存持续增长的问题。

2. CPU使用率高

  • 系统监控:
  • 使用操作系统提供的监控工具(如Linux上的tophtopvmstatiostat等)查看哪些Java进程占据了较高的CPU资源。
  • Java线程分析:
  • 使用jstack命令获取Java进程的线程快照,分析哪些线程处于活动状态并消耗CPU资源。
  • 如果发现有线程长期执行某些计算密集型任务或死锁,需要针对性优化或修复代码逻辑。
  • 性能分析工具:
  • 使用jconsolejvisualvm中的CPU Profiler功能进行CPU热点分析,了解哪些方法调用占用了大部分CPU时间。

3. Load 高

  • 理解负载概念:
  • 在Unix/Linux系统中,Load Average表示单位时间内系统运行队列中平均有多少个进程等待调度。
  • 高Load可能是由于并发请求过多,或者是某个或多个线程阻塞造成上下文切换频繁导致。
  • 分析原因:
  • 同样可以通过tophtop等工具查看当前系统进程及线程状态,找到可能引起Load高的根源。
  • 结合日志和应用程序的业务逻辑来判断是否因为I/O操作、数据库查询瓶颈或者其他外部资源限制导致负载过高。

4. 类冲突

  • 确认版本一致性:
  • 当不同版本的类库同时存在于服务器上,可能会引发类加载冲突。确保所有依赖项版本统一,避免引入不同版本的jar包。
  • 分析错误日志:
  • 类冲突通常会在日志中抛出异常,如ClassCastExceptionNoClassDefFoundError等,通过这些异常信息定位具体冲突的类。
  • 类加载器机制:
  • 深入理解Java的类加载器机制,排查是否因自定义类加载器或双亲委派模型破坏而导致的类加载混乱。
  • 使用工具辅助:
  • 可以借助类似BTrace这样的动态追踪工具,在运行时检测类加载行为,帮助定位问题。

以上是对各种问题的基本排查流程,实际处理时可能需要结合具体情况进一步深入调查。


32. 如何查看 Java 应用的线程信息

在Java应用中,查看线程信息通常可以通过以下几种方式:
1. 使用JDK自带的jps和jstack命令

  • jps:列出正在运行的Java虚拟机进程。
   jps -lvm
   

        这个命令会显示Java进程ID(PID)以及主类全名,帮助定位到具体的Java应用。

  • jstack:打印指定Java进程的堆栈跟踪信息。
   jstack <pid>
   

        将 <pid> 替换为通过 jps 命令获取的Java进程ID。jstack 输出的信息包含了所有线程的状态、线程ID以及每个线程当前执行到的堆栈信息。


2. 使用JVisualVM或Mission Control等工具

  • JVisualVM是JDK自带的性能分析工具,可以监控多个Java应用程序,包括查看线程状态、CPU使用率、内存消耗等详细信息。
  • Oracle JDK中的Java Mission Control (JMC) 是一个更为强大的监控和故障排查工具,它可以实时查看线程详细信息,包括线程状态、线程调用栈、锁信息等,并且支持历史数据的收集和分析。

3. 在代码中实现线程信息打印

  • 可以通过 Thread.currentThread().getName() 获取当前线程名,在程序的关键位置输出日志来追踪线程执行情况。
  • 或者使用 Thread.getAllStackTraces() 方法获取所有线程及其堆栈信息,然后在控制台或日志文件中输出。

示例代码(获取并打印当前Java进程的所有线程信息)

import java.lang.management.ManagementFactory;
import java.lang.management.ThreadMXBean;
import java.util.Map;

public class ThreadInfoExample {
    public static void main(String[] args) {
        ThreadMXBean threadMxBean = ManagementFactory.getThreadMXBean();
        boolean isThreadContentionMonitoringSupported = threadMxBean.isThreadContentionMonitoringSupported();

        // 获取所有活动线程的详细信息
        Map<Thread, StackTraceElement[]> allStackTraces = Thread.getAllStackTraces();
        for (Map.Entry<Thread, StackTraceElement[]> entry : allStackTraces.entrySet()) {
            Thread thread = entry.getKey();
            System.out.println("Thread Name: " + thread.getName());
            System.out.println("Thread State: " + thread.getState());
            System.out.println("Thread ID: " + thread.getId());

            if (isThreadContentionMonitoringSupported) {
                long blockedTime = threadMxBean.getThreadInfo(thread.getId(), true, true).getBlockedTime();
                long waitedTime = threadMxBean.getThreadInfo(thread.getId(), true, true).getWaitedTime();
                System.out.println("Blocked Time: " + blockedTime);
                System.out.println("Waited Time: " + waitedTime);
            }

            StackTraceElement[] stackTraceElements = entry.getValue();
            for (StackTraceElement element : stackTraceElements) {
                System.out.println("\t" + element);
            }
            System.out.println("----------------------------------------");
        }
    }
}

上述代码片段将打印出当前Java进程中所有线程的基本信息以及它们的堆栈跟踪信息。


33.Redis hash 算法用的是什么?

 Redis的哈希(Hash)数据结构并未直接采用标准的一致性哈希算法进行内部节点映射,而是使用了一种简单的哈希槽(hash slot)分配方式


Redis Cluster将所有哈希键均匀地分布到16384个哈希槽中。当一个键被存储时,Redis会通过CRC16算法计算键的哈希值,并将这个哈希值对16384取模来决定该键应当存储在哪个槽中。每个Redis节点负责一部分槽位,客户端在存取数据时,根据键对应的哈希槽找到对应节点进行操作

        
这种哈希槽的设计简化了分布式环境下的数据管理和路由逻辑,使得集群可以动态地添加或删除节点,并且在调整节点时能够尽可能少地迁移键值对。

不过,需要注意的是,虽然哈希槽的分配策略与一致性哈希有所区别,但在大规模集群和高可用场景下,Redis Cluster通过主从复制、故障转移等机制保证了数据分布的相对均衡性和系统的稳定性。

  • 23
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

默语玄

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值