2021面试题

1 Java相关

1.1 反射和动态代理

反射:在运行状态中,对于任意的一个类,都能够知道这个类的所有属性和方法,对任意一个对象都能够通过反射机制调用一个类的任意方法,这种动态获取类信息及动态调用类对象方法的功能称为java的反射机制。

动态代理:就是根据对象在内存中加载的Class类创建运行时类对象,从而调用代理类方法和属性。使用Proxy和InvocationHandler两个类,并实现invoke方法

1.2 Spring的两种动态代理:Jdk和Cglib 的区别和实现

java动态代理是利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理。

而cglib动态代理是利用asm开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。
1、如果目标对象实现了接口,默认情况下会采用JDK的动态代理实现AOP 
2、如果目标对象实现了接口,可以强制使用CGLIB实现AOP 
3、如果目标对象没有实现了接口,必须采用CGLIB库,spring会自动在JDK动态代理和CGLIB之间转换

JDK动态代理和CGLIB字节码生成的区别?
 (1)JDK动态代理只能对实现了接口的类生成代理,而不能针对类
 (2)CGLIB是针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法
   因为是继承,所以该类或方法最好不要声明成final

为什么JDK动态代理只能对实现了接口的类生成代理?

因为Java是单继承的,而代理类又必须继承自Proxy类,所以通过jdk代理的类必须实现接口

1.3 JVM内存模型

JVM内存模型.png-56.5kB

程序计算器:为了线程切换后能够恢复到正确的执行位置,每条线程都需要一个独立的程序计数器去记录其正在执行的字节码指令地址。
因此,程序计数器是线程私有的一块较小的内存空间,其可以看做是当前线程所执行的字节码的行号指示器。如果线程正在执行的是一个 Java 方法,计数器记录的是正在执行的字节码指令的地址;如果正在执行的是 Native 方法,则计数器的值为空。

虚拟机栈:   虚拟机栈描述的是Java方法执行的内存模型,是线程私有的。每个方法在执行的时候都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息,而且 每个方法从调用直至完成的过程,对应一个栈帧在虚拟机栈中入栈到出栈的过程。其中,局部变量表主要存放一些基本类型的变量(int, short, long, byte, float, double, boolean, char)和 对象句柄,它们可以是方法参数,也可以是方法的局部变量。


本地方法栈:本地方法栈与Java虚拟机栈非常相似,也是线程私有的,区别是虚拟机栈为虚拟机执行 Java 方法服务,而本地方法栈为虚拟机执行 Native 方法服务

堆: Java 堆的唯一目的就是存放对象实例,几乎所有的对象实例(和数组)都在这里分配内存。

         由于Java堆唯一目的就是用来存放对象实例,因此其也是垃圾收集器管理的主要区域,故也称为称为 GC堆。从内存回收的角度看,由于现在的垃圾收集器基本都采用分代收集算法,所以为了方便垃圾回收Java堆还可以分为 新生代 和 老年代 。新生代用于存放刚创建的对象以及年轻的对象,如果对象一直没有被回收,生存得足够长,对象就会被移入老年代。新生代又可进一步细分为 eden、survivorSpace0 和 survivorSpace1。刚创建的对象都放入 eden,s0 和 s1 都至少经过一次GC并幸存。如果幸存对象经过一定时间仍存在,则进入老年代。

TLAB (Thread Local Allocation Buffer,线程私有分配缓冲区)

  Sun Hotspot JVM 为了提升对象内存分配的效率,对于所创建的线程都会分配一块独立的空间 TLAB,其大小由JVM根据运行的情况计算而得。在TLAB上分配对象时不需要加锁(相对于CAS配上失败重试方式 ),因此JVM在给线程的对象分配内存时会尽量的在TLAB上分配,在这种情况下JVM中分配对象内存的性能和C基本是一样高效的,但如果对象过大的话则仍然是直接使用堆空间分配
 

方法区:方法区与Java堆一样,也是线程共享的并且不需要连续的内存,其用于存储已被虚拟机加载的 类信息常量静态变量即时编译器编译后的代码等数据

1.4 JVM调优经验

其实最有效的优化手段是架构和代码层面的优化,而JVM优化则是最后不得已的手段,也可以说是对服务器配置的最后一次“压榨”。

JVM调优步骤:

  • 分析GC日志及dump文件,判断是否需要优化,确定瓶颈问题点;
  • 确定JVM调优量化目标;
  • 确定JVM调优参数(根据历史JVM参数来调整);
  • 依次调优内存、延迟、吞吐量等指标;
  • 对比观察调优前后的差异;
  • 不断的分析和调整,直到找到合适的JVM参数配置;
  • 找到最合适的参数,将这些参数应用到所有服务器,并进行后续跟踪。

JVM参数优化:

  1. 针对JVM堆的设置,一般可以通过-Xms -Xmx限定其最小、最大值,为了防止垃圾收集器在最小、最大之间收缩堆而产生额外的时间,通常把最大、最小设置为相同的值。
  2. 通过NewRadio 来控制 年轻代与老年代的比率
  3. 年轻代和年老代设置多大才算合理
    1)更大的年轻代必然导致更小的年老代,大的年轻代会延长普通GC的周期,但会增加每次GC的时间;小的年老代会导致更频繁的Full GC。
    2)更小的年轻代必然导致更大年老代,小的年轻代会导致普通GC很频繁,但每次的GC时间会更短;大的年老代会减少Full GC的频率。
        如何选择应该依赖应用程序对象生命周期的分布情况: 如果应用存在大量的临时对象,应该选择更大的年轻代;如果存在相对较多的持久对象,年老代应该适当增大。但很多应用都没有这样明显的特性
  4. Xss:设置每个线程的堆栈大小。系统默认1M,设置256KB就行
  5. -XX:MaxTenuringThreshold=15:设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代
  6. -XX:SurvivorRatio=8:设置年轻代中Eden区与Survivor区的大小比值。设置为8,则两个Survivor区与一个Eden区的比值为2:8,一个Survivor区占整个年轻代的1/10

1.5 hashmap 1.8是如何扩容的

默认创建一个空数组,首次put的时候在创建一个长度为16的数组

1.6 hashmap 的负载因子为什么是0.75

 提高空间利用率和 减少查询成本的折中,主要是泊松分布,0.75的话碰撞最小,

1.7 HashMap的长度为什么要是2的n次方

HashMap为了存取高效,要尽量较少碰撞,就是要尽量把数据分配均匀,每个链表长度大致相同,这个实现就在把数据存到哪个链表中的算法;
这个算法实际就是取模,hash%length,计算机中直接求余效率不如位移运算,源码中做了优化hash&(length-1),
hash%length==hash&(length-1)的前提是length是2的n次方
 

1.8 如何破坏双亲委派模式

1 自定义类加载器 并重新loadclass方法。

2 使用线程上下文类加载器

1.9 Java对象是如何在内存中分配的

  1. 检查虚拟机是否加载了所要new的类,若没加载,则首先执行相应的类加载过程
  2. 在类加载检查通过后,对象所需内存的大小在类加载完成后便可完全确定,虚拟机就会为新生对象分配内存。指针碰撞:如果Java堆中内存绝对规整,空闲列表:如果Java堆中内存并不规整。 还需要考虑修改指针 (该指针用于划分内存使用空间和空闲空间)时的线程安全问题 1.对分配内存空间的动作进行同步处理 2.把内存分配的动作按照线程划分的不同的空间中(TLAB
  3. 内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值
  4. 在上面的工作完成之后,从虚拟机的角度来看,一个新的对象已经产生了,但从Java程序的视角来看,对象的创建才刚刚开始,此时会执行<init>方法把对象按照程序员的意愿进行初始化,从而产生一个真正可用的对象。

1.10 多线程的参数及处理过程

参数: ThreadPoolExecutor
1、corePoolSize:线程池中的常驻核心线程数
2、maximumPoolSize:线程池能容纳同时执行的最大线程数,此值必须>=1
3、keepAliveTime:多余的空闲线程的存活时间
    当前线程池数量超过corePoolSize时,当空闲的时间达到keepAliveTime值时,多余的空闲线程会被直接销毁直到只剩下corePoolSize个线程为止
4、timeUtile:keepAliveTime的单位
5、workQueue:任务队列,被提交但未被执行的任务
6、threadFactory:表示生成线程池中工作线程的线程工厂,用于创建线程一般用默认的即可。
7、handler:拒绝策略,表示当队列满了并且工作线程大于等于线程池的最大线程数 


执行过程:

当核心线程数全部在工作中,任务队列也占满,启用最大线程数的剩余线程。

当核心线程数全部在工作中,任务队列也占满,剩余线程也在工作中,触发拒绝策略,抛出异常。

当流量放缓,核心线程足够处理,剩余线程存活1s后销毁。

1.11 知道哪些队列

阻塞队列

  • LinkedList:继承Deque
  • PriorityQueue:优先队列,使用的时最小堆的数据结构,对元素进行了指定排序,不许插入空值;实现了AbstractQueue抽象类和Queue接口
  • ConcurrentLinkedQueue:是基于链接节点的、线程安全的队列;实现了AbstractQueue抽象类和Queue接口

非阻塞队列

  • ArrayBlockingQueue :一个由数组支持的有界队列。
  • LinkedBlockingQueue :一个由链接节点支持的可选有界队列。
  • PriorityBlockingQueue :一个由优先级堆支持的无界优先级队列。
  • DelayQueue :一个由优先级堆支持的、基于时间的调度队列。
  • SynchronousQueue :一个利用 BlockingQueue 接口的简单聚集(rendezvous)机制

1.12 信号量、countdownlatch的作用

CountDownLatch:利用它可以实现类似计数器的功能。比如有一个任务A,它要等待其他4个任务执行完毕之后才能执行,此时就可以利用CountDownLatch来实现这种功能了

CyclicBarrier:字面意思回环栅栏,通过它可以实现让一组线程等待至某个状态之后再全部同时执行。叫做回环是因为当所有等待线程都被释放以后,CyclicBarrier可以被重用

Semaphore:翻译成字面意思为信号量,Semaphore可以控制同时访问的线程个数,通过acquire()获取一个许可,如果没有就等待,而release()释放一个许可。

1) CountDownLatch和CyclicBarrier都能够实现线程之间的等待,只不过它们侧重点不同;

CountDownLatch一般用于某个线程A等待若干个其他线程执行完任务之后,它才执行;

而CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;

另外,CountDownLatch是不能够重用的,而CyclicBarrier是可以重用的。

2) Semaphore其实和锁有点类似,它一般用于控制对某组资源的访问权限
 

1.13 ThreadLocal 内存泄漏问题

ThreadLocal 的作用:是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。但是如果滥用ThreadLocal,就可能会导致内存泄漏

ThreadLocal原理是 :ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现keynullEntry

其实,ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施:在ThreadLocalget(),set(),remove()的时候都会清除线程ThreadLocalMap里所有keynullvalue

但是这些被动的预防措施并不能保证不会内存泄漏:

  • 使用staticThreadLocal,延长了ThreadLocal的生命周期,可能导致的内存泄漏,例如使用了线程池
  • 分配使用了ThreadLocal又不再调用get(),set(),remove()方法,那么就会导致内存泄漏

ThreadLocal 为什么不使用强引用:

  • key 使用强引用:引用的ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。
  • key 使用弱引用:引用的ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用set,getremove的时候会被清除

ThreadLocal泄漏的根源是: 由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。

ThreadLocal 如何避免: 每次使用完ThreadLocal,都调用它的remove()方法,清除数据。

深入分析 ThreadLocal 内存泄漏问题 - 简书

1.14 类加载过程

加载、验证、准备、解析、初始化、使用和卸载七个阶段

类加载阶段就是由类加载器负责根据一个类的全限定名来读取此类的二进制字节流到JVM内部,并存储在运行时内存区的方法区,然后将其转换为一个与目标类型对应的java.lang.Class对象实例

验证:验证类数据信息是否符合JVM规范,是否是一个有效的字节码文件,验证内容涵盖了类数据信息的格式验证、语义分析、操作验证等。

准备:准备阶段负责为类中static变量分配空间,并初始化(初始化的是基本类型的零值)。

解析:解析阶段负责将常亮池中所有符号引用转换为直接引用

初始化阶段负责将所有static域按照程序指定操作对应执行(赋值static变量,执行static块)。
如果执行的是static代码块,那么在初始化阶段,JVM就会执行static代码块中定义的所有操作。
 

1.15 垃圾回收器

Serial(串行GC)-复制
ParNew(并行GC)-复制
Parallel Scavenge(并行回收GC)-复制
Serial Old(MSC)(串行GC)-标记-整理
CMS(并发GC)-标记-清除
Parallel Old(并行GC)--标记-整理
G1(JDK1.7update14才可以正式商用)
说明:

1~3用于年轻代垃圾回收:年轻代的垃圾回收称为minor GC
4~6用于年老代垃圾当然也可以用于方法区的回收):年老代的垃圾回收称为full GC
G1独立完成"分代垃圾回收"
注意:并行与并发

并行:多条垃圾回收线程同时操作
并发:垃圾回收线程与用户线程一起操作
(2)常用五种组合:

Serial/Serial Old
ParNew/Serial Old:与上边相比,只是比年轻代多了多线程垃圾回收而已
ParNew/CMS:当下比较高效的组合
Parallel Scavenge/Parallel Old:自动管理的组合 jdk1.8默认的垃圾回收器
G1:最先进的收集器,但是需要JDK1.7update14以上
G1将堆按区域划分,分别进行回收,模糊了新生代和老年代的关系。从整体上看是标记-整理,局部看是复制算法

  1. 初始标记
  2. 并发标记
  3. 最终标记
  4. 筛选回收

CMS JDK1.9之后不在支持

  1. 初始标记
  2. 并发标记
  3. 重新标记
  4. 并发清除

1.16 JVM的GC执行时机在任何时候都可以吗,知道安全点吗

JVM是通过可达性分析来判断哪些对象需要被回收,即 通过GC ROOT来判断回收的对象,

有两种查找 GC Roots 的方法:

  1. 一种是遍历方法区和栈区查找(保守式 GC)。
  2. 一种是通过 OopMap 数据结构来记录 GC Roots 的位置(准确式 GC)。

很明显,保守式 GC 的成本太高。准确式 GC 的优点就是能够让虚拟机快速定位到 GC Roots。

对应 OopMap 的位置即可作为一个安全点(Safe Point)。

在执行 GC 操作时,所有的工作线程必须停顿,这就是所谓的”Stop-The-World”。

为什么呢?

因为可达性分析算法必须是在一个确保一致性的内存快照中进行。如果在分析的过程中对象引用关系还在不断变化,分析结果的准确性就不能保证。

安全点意味着在这个点时,所有工作线程的状态是确定的,JVM 就可以安全地执行 GC 。

如何选定安全点?

安全点太多,GC 过于频繁,增大运行时负荷;安全点太少,GC 等待时间太长。

一般会在如下几个位置选择安全点:

1、循环的末尾

2、方法临返回前

3、调用方法之后

4、抛异常的位置

为什么选定这些位置作为安全点:

主要的目的就是避免程序长时间无法进入 Safe Point。比如 JVM 在做 GC 之前要等所有的应用线程进入安全点,如果有一个线程一直没有进入安全点,就会导致 GC 时 JVM 停顿时间延长。比如这里,超大的循环导致执行 GC 等待时间过长。

如何在 GC 发生时,所有线程都跑到最近的 Safe Point 上再停下来?

主要有两种方式:

抢断式中断:在 GC 发生时,首先中断所有线程,如果发现线程未执行到 Safe Point,就恢复线程让其运行到 Safe Point 上。

主动式中断:在 GC 发生时,不直接操作线程中断,而是简单地设置一个标志,让各个线程执行时主动轮询这个标志,发现中断标志为真时就自己中断挂起。

JVM 采取的就是主动式中断。轮询标志的地方和安全点是重合的。
安全区域又是什么?

Safe Point 是对正在执行的线程设定的。

如果一个线程处于 Sleep 或中断状态,它就不能响应 JVM 的中断请求,再运行到 Safe Point 上。

因此 JVM 引入了 Safe Region。

Safe Region 是指在一段代码片段中,引用关系不会发生变化。在这个区域内的任意地方开始 GC 都是安全的。

线程在进入 Safe Region 的时候先标记自己已进入了 Safe Region,等到被唤醒时准备离开 Safe Region 时,先检查能否离开,如果 GC 完成了,那么线程可以离开,否则它必须等待直到收到安全离开的信号为止。

什么是GC root

1、 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
2、在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。
3、在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。
4、本地方法栈中 JNI(Native方法)引用的对象
5、Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
 

1.17 volatile底层的内存屏障是如何实现的

内存屏障是一个CPU指令

编译器和CPU可以在保证输出结果一样的情况下对指令重排序,使性能得到优化。插入一个内存屏障,相当于告诉CPU和编译器先于这个命令的必须先执行,后于这个命令的必须后执行

Java内存模型将在写操作后插入一个写屏障指令,在读操作前插入一个读屏障指令

https://www.cnblogs.com/monkeysayhi/p/7654460.html

1.18 线程池拒绝策略

  1. 第一种拒绝策略是 AbortPolicy,这种拒绝策略在拒绝任务时,会直接抛出异常 RejectedExecutionException (属于RuntimeException),让你感知到任务被拒绝了,于是你便可以根据业务逻辑选择重试或者放弃提交等策略。
  2. 第二种拒绝策略是 DiscardPolicy,这种拒绝策略正如它的名字所描述的一样,当新任务被提交后直接被丢弃掉,也不会给你任何的通知,相对而言存在一定的风险,因为我们提交的时候根本不知道这个任务会被丢弃,可能造成数据丢失。
  3. 第三种拒绝策略是 DiscardOldestPolicy,如果线程池没被关闭且没有能力执行,则会丢弃任务队列中的头结点,通常是存活时间最长的任务,这种策略与第二种不同之处在于它丢弃的不是最新提交的,而是队列中存活时间最长的,这样就可以腾出空间给新提交的任务,但同理它也存在一定的数据丢失风险。
  4. 第四种拒绝策略是 CallerRunsPolicy,相对而言它就比较完善了,当有新任务提交后,如果线程池没被关闭且没有能力执行,则把这个任务交于提交任务的线程执行,也就是谁提交任务,谁就负责执行任务。这样做主要有两点好处。
    • 第一点新提交的任务不会被丢弃,这样也就不会造成业务损失。
    • 第二点好处是,由于谁提交任务谁就要负责执行任务,这样提交任务的线程就得负责执行任务,而执行任务又是比较耗时的,在这段期间,提交任务的线程被占用,也就不会再提交新的任务,减缓了任务提交的速度,相当于是一个负反馈。在此期间,线程池中的线程也可以充分利用这段时间来执行掉一部分任务,腾出一定的空间,相当于是给了线程池一定的缓冲期。

2  数据库方面

2.1 数据库发送死锁了怎么办

2.2 MySQL查询原理

  • 客户端发送一条查询给服务器;
  • 服务器先检查查询缓存,如果命中了缓存,则立刻返回存储在缓存中的结果。否则进入下一阶段。
  • 服务器段进行SQL解析、预处理,在优化器生成对应的执行计划;
  • mysql根据优化器生成的执行计划,调用存储引擎的API来执行查询。
  • 将结果返回给客户端。

2.3 水平分表之后查询问题

分库分表的中间件:cobar、TDDL,atlas,sharding-jdbc,mycat

sharding-jdbc
当当开源的,属于 client 层方案。确实之前用的还比较多一些,因为 SQL 语法支持也比较多,没有太多限制,而且目前推出到了 2.0 版本,支持分库分表、读写分离、分布式 id 生成、柔性事务(最大努力送达型事务、TCC 事务)

2.4 缓存与数据库一致性问题

2.5 为什么innodb支持事务,其他搜索引擎不行

MVCC ( Multi-Version Concurrency Control )多版本并发控制

InnoDB :通过为每一行记录添加两个额外的隐藏的值来实现MVCC,这两个值一个记录这行数据何时被创建,另外一个记录这行数据何时过期(或者被删除)。但是InnoDB并不存储这些事件发生时的实际时间,相反它只存储这些事件发生时的系统版本号。这是一个随着事务的创建而不断增长的数字。每个事务在事务开始时会记录它自己的系统版本号。每个查询必须去检查每行数据的版本号与事务的版本号是否相同。

myisam读写效率高于innodb

2.6 建立多列索引和建立多个单列索引的区别

多列索引是一个索引树,多个字段在一起,查询效率较高

多个单列索引是多个索引树,查询原理是单个查询,并对结果取交集

2.7 如果估算索引树的高度

数据是N,每个索引有B个目录项

h= logN/logB

现在,我们假设表有1600w条记录(因为2^24≈1600w,便于接下来的计算),如果每个节点保存64个索引KEY,那么索引高度就是 (log10^7)/log64 ≈ 24/6 = 4。

计算每个节点可以存多少个索引:

以BIGINT为例,存储大小为8个字节。INT存储大小为4个字节(32位)。索引树上每个节点除了存储KEY,还需要存储指针。所以每个节点保存的KEY的数量为pagesize/(keysize+pointsize)(如果是B-TREE索引结构,则是pagesize/(keysize+datasize+pointsize))。

假设平均指针大小是4个字节,那么索引树的每个节点可以存储16k/((8+4)*8)≈171。那么:一个拥有1600w数据,且主键是BIGINT类型的表的主键索引树的高度就是(log10^7)/log171 ≈ 24/7.4 ≈ 3.2。

3 框架

3.1 hsf和dubbo的区别

3.2 spring事务失效的情况

  1. 未启用spring事务管理功能
  2. 方法不是public类型的
  3. 数据源未配置事务管理器
  4. 自身调用问题
  5. 异常类型错误
  6. 异常被吞了
  7. 业务和spring事务代码必须在一个线程中

spring是通过aop的方式,对需要spring管理事务的bean生成了代理对象,然后通过代理对象拦截了目标方法的执行,在方法前后添加了事务的功能,所以必须通过代理对象调用目标方法的时候,事务才会起效。

看下面代码,大家思考一个问题:当外部直接调用m1的时候,m2方法的事务会生效么?

@Component
public class UserService {
    public void m1(){
        this.m2();
    }
    
    @Transactional
    public void m2(){
        //执行db操作
    }
}

显然不会生效,因为m1中通过this的方式调用了m2方法,而this并不是代理对象,this.m2()不会被事务拦截器,所以事务是无效的,如果外部直接调用通过UserService这个bean来调用m2方法,事务是有效的,上面代码可以做一下调整,如下,@1在UserService中注入了自己,此时m1中的m2事务是生效的

3.3 spring创建bean的哪几种方式

普通构造方法创建、静态工厂创建、实例工厂创建

静态工厂就是直接可以通过静态方法来实例化一个对象

实例工厂 是先创建工厂类对象,如何通过对象来调用创建实例对象的方法

3.4 springcloud核心组件

  • Eureka:各个服务启动时,Eureka Client都会将服务注册到Eureka Server,并且Eureka Client还可以反过来从Eureka Server拉取注册表,从而知道其他服务在哪里
  • Ribbon:服务间发起请求的时候,基于Ribbon做负载均衡,从一个服务的多台机器中选择一台
  • Feign:基于Feign的动态代理机制,根据注解和选择的机器,拼接请求URL地址,发起请求
  • Hystrix:发起请求是通过Hystrix的线程池来走的,不同的服务走不同的线程池,实现了不同服务调用的隔离,避免了服务雪崩的问题
  • Zuul:如果前端、移动端要调用后端系统,统一从Zuul网关进入,由Zuul网关转发请求给对应的服务

4 分布式系统问题

4.1 高并发处理经验

  1. 消息队列,进行流量削峰
  2. 缓存
  3. 数据库读写分离
  4. 分库分表
  5. 系统拆分
  6. 负载均衡

4.2 如何高效的设计主键自增

雪花算法

4.3 手写RPC框架

RPC实现核心——动态代理+网络通讯增强socket

RPC中的远程调用是给目标对象提供一个代理, 并且由代理对象控制目标对象的引用。
这样做的目的是因为:屏蔽调服务处理的细节,通过网络通讯远程调用做前置和后置的增强。

首先server 会将自己暴露出的服务注册到register-center中,client 通过代理对象获取服务的实现类并调用。

4.3.1 客户端

  1. 远程服务的代理对象,参数为客户端要调用的的服务
  2. 服务接口
  3. 调用远端接口

4.3.2 服务端

  1. 写服务接口,和实现类
  2. 向注册中心添加服务

4.3.3 注册中心

用到了多线程+反射+socket技术+IO

1、定义一个线程池和存放服务注册的容器

2、注册方法,加入到容器

3、启动注册中心,监听绑定的接口

4、启动现场监听客户端的TCP连接

5、请求到达服务端,执行并反馈到客户端

4.4 Redis分布式锁的原理

4.5 分布式事务解决方案

分布式理论 CAP(一致性,可用性,分区容错性);base理论(基本可用、软状态、最终一致性)

  1. 两阶段提交 2PC
  2. 三阶段提交 3PC
  3. 补偿事务 TCC
  4. 消息中间件
  5. 最大努力通知
  6. sagas事务 将长事务拆分为多个本地短事务」,由Saga事务协调器协调,如果正常结束那就正常完成,如果「某个步骤失败,则根据相反顺序一次调用补偿操作

RocketMQ实现可靠消息最终一致性

MQ有本地事务和消息发送原子性的功能。当A服务向消息中间件发送消息时,会实现MQ的事务消息监听,监听到事务发送成功,则执行本地事务,否则rollback,消息不投递。

B服务订阅消息,如果接受失败,会一直重试直到成功。

4.6 缓存与数据库一致性问题

缓存和数据库数据一致性问题中,推荐 先更新数据库,再删除缓存。如果缓存删除失败可以用MQ消息队列的方式进行重试删除。

怎么解决DB读写分离,导致数据不一致问题?

数据不一致问题是因为读请求走的是从库,把从库的旧值又设置到缓存中了

延迟消息:

1)在订阅到binlog更新日志时,先不删除缓存,而是投递一个延迟消息(如:延迟10秒的消息,就是过10秒此消息才会被消费者监听到,从而被消费)
2)延迟消息的延迟时间,设置为主库与从库的数据同步延迟的时间,可自行预估
3)监听到延迟消息,在删除缓存。

如果要保证本用户(更新数据的用户)一定读到的是新值,这边可以采用本地缓存标记方案,直接从主数据库读取

其他用户的读请求会有暂时间读取到的是旧值,如何缩短时间?其实是有一个方案,就是让更新用户再次发起读请求

4.7 常见的限流算法

  1. 通过控制最大并发数来进行限流。通过Semaphore(信号量)
  2. 使用漏桶算法来进行限流 通过队列和 LockSupport
  3. 使用令牌桶算法来进行限流 ,通过rateLimiter。

从原理上看,令牌桶算法和漏桶算法是相反的,一个“进水”,一个是“漏水”。这种算法可以应对突发程度的请求,因此比漏桶算法好

4.8 Java秒杀系统的设计与实现

特点:

  • 时间短、瞬间访问量大
  • 读多写少的场景

难点:

  • 库存只有一份,但大量用户在集中时间对该数据进行读写
  • 秒杀系统之所以挂,是因为请求没有经过上游的过滤与拦截,直接压倒在下游的数据层

优化思路:

核心思想:尽量将请求拦截在系统上游;读多写少的场景使用缓存

1)浏览器端的拦截

  • 点击 “购买” 后,按钮置灰,禁止用户重复提交请求。
  • 通过 JS 代码,限制用户在 x 秒之内只能提交一次请求。

2)站点层的拦截

  • 秒杀时间开始前,不暴露秒杀链接并对URL加密。这样部分用户无法使用程序代替人工进行秒杀。
  • 页面缓存。x 秒内到达的请求,直接返回页面缓存。

3)服务层的拦截

  • 将秒杀商品的数据缓存到 Redis 等内存数据库。这样读请求就不需要达到最下层的数据库。
  • 将库存数据缓存到 Redis 中,加上 Redis 事务和 lua 脚本进行查询库存并减库存的操作,放到 Redis 完成。
  • 将写请求放入到写请求队列中,每次让有限的写请求达到数据层,当库存不足就直接让队列里的写请求直接返回 “已售完”
  • 秒杀接口 限流、熔断、降级。

4)其他

  • 启动多个应用实例,并使用 Nginx 进行负载均衡。
  • 分库分表,增加并发度。
  • CDN 缓存

超卖问题:

  •  一种是使用数据库自带的行锁机制,这种方式我在工作中用过,完全可以解决超卖问题,对于流量不大的秒杀场景实际上完全够用,性能消耗也不明显;
  • 另一种是现在比较流行的version版本号实现的乐观锁机制,就是在数据库中加一个version字段来表示版本号,修改库存时先获取当前版本号,然后修改时就传入该版本号并且对当前版本号+1,这种相较于第一种就更科学,在性能上更优越

5 中间件

5.1 Redis的数据结构

5.1.1 基本数据结构

  • string 类似于 Java 中的 ArrayList,它可以存储任意二进制数据,可以通过预分配冗余空间的方式来减少内存的频繁分配
  • Hash 类似Map的一种结构。 用处:可以存入结构化数据,例如往缓存存一个对象,可以方便的操作其中的某个字段。
  • List 使用LinkedList(有序链表)的数据结构,可以做简单的消息队列的功能。另外还有一个就是,可以利用lrange命令(返回列表指定区间的元素),做基于redis的分页功能,性能极佳。
  • Set 是无序集合,很明显的一个作用就是去重。
  • Sorted Set(zset) 排好序的Set,底层实现跳表,多了一个权重参数score分数,写进去自动根据分数排序。
    用处:排行榜应用之类的,用Sorted Sets来做带权重的队列

5.1.2 高级数据结构

  • Bitmap
    位图,字符串一个字符是8个比特, bitmap 底层就是 string, 用处:例如计算一个用户在指定时间内签到的次数,8位,1位表示一天,0表示未签到,1表示签
  • HyperLogLog
    提供不精确的去重计数,小规模使用set去重,那要是访问量巨大呢?HyperLogLog就出现了,优势就是只占用 12KB 的内存。
  • GeoHash
    可以用来保存地理位置,并作位置距离计算或者根据半径计算位置等。常用来计算 附近的人,附近的商店
  • 布隆过滤器:可以去重,可以防恶意攻击。对见过的数据一定可以识别出来,没见过的不一定可以识别。原理是一个数组和多个hash函数。提供两个方法,一个是添加,一个是判断是否存在,当添加的时候,多个hash函数分别对key进行计算hash值,然后对数组取余。计算出数组的位置置为1,当判断是否存在时,只要有一个为0,那么肯定不存在,都为1的话可能不存在,因为这个1可能是其他key值计算的。数组越大,key越少 则计算越准确。

5.2 解决redis分布式锁过期时间到了业务没执行完问

实Redis分布式锁比较正确的姿势是采用redisson这个客户端工具

只要客户端一旦加锁成功,就会启动一个watch dog看门狗,他是一个后台线程,会每隔10秒检查一下,如果客户端还持有锁key,那么就会不断的延长锁key的生存时间

5.3 sharding-jdbc分片策略

1 标准分片策略

标准分片策略用于处理单一建(分表字段)作为分表建的场景,包含两种分片算法:

精确分片算法,对应实现接口PreciseShardingAlgorithm。sql在分表键上执行 = 与 IN 时触发分表算逻辑,否则不走分表,全表执行。
范围分片算法,对应实现接口RangeShardingAlgorithm。sql在分表键上执行 BETWEEN AND、>、<、>=、<= 时触发分表算逻辑,否则不走分表,全表执行。

2  复合分片策略

于处理使用多键(多字段)作为分片键进行分片的场景

Hint分片策略

通过Hint指定分片值而非从SQL中提取分片值的方式进行分片的策略。

用于查询固定的库固定的表

行表达式分片策略

最简单的策略

5.不分片策略

5.4 RabbitMq 组件​​​​​​​

  1. Broker:消息队列服务器实体。
  2. Virtual Host (vhost):虚拟主机,一个broker里可以开设多个vhost,用作权限分离,把不同的系统使用的rabbitmq区分开,共用一个消息队列服务器,但看上去就像各自在用不用的rabbitmq服务器一样。
  3. Connection:生产者/消费者与broker之间的TCP连接。
  4. Channel:信道,仅仅创建了客户端到Broker之间的连接后,客户端还是不能发送消息的。
  5. Exchange:交换机,用来接收生产者发送的消息并将这些消息路由给服务器中的队列。
  6. Queue:消息队列,用来保存消息直到发送给消费者。它是消息的容器。一个消息可投入一个或多个队列。
  7. Binding:绑定关系,用于消息队列和交换机之间的关联。通过路由键(Routing Key)将交换机和消息队列关联起来。
  8. Message:消息,它是由消息头和消息体组成。

5.5 rabbitmq和kafka的区别及应用场景

  1. 吞吐量 Kafka 是每秒几十万条消息吞吐,而 RabbitMQ 的吞吐量是每秒几万条消息
  2. 消息的匹配 rabbitMQ 是允许在消息中添加 routing_key 或者自定义消息头,然后通过一些特殊的 Exchange,很简单的就实现了消息匹配分发。kafka不支持
  3. ​​​​​​​消息的保持. kafka会将消费过后的消息持久化到日志文件中,rabbitmq消费之后就被删除
  4. rabbitmq支持多种消费模式及对消息进行更细粒度的控制。
  5. kafka擅长对流式数据处理

6 其他

6.1 设计模式在项目中的应用

6.2 链表如何确定是否闭环,闭环的长度及入环点

创建两个指针,一个走一步,另一个走2步。当两个指针相等时,说明该链表存在闭环。相等的那个点就是首次相遇点。

求闭环的长度:两个指针在相遇点开始移动,保持原来的速度,当第二次相遇时,走一步的指针走多少步闭环的长度就是多少。

入环点:一个指针在开始节点,一个指针在首次相遇点。两个指针速度相同,每次都移动一步,当相遇时该节点就是入环点

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: 面试题整理是为了帮助准备面试的候选人更好地了解面试的内容和要求。对于Java2021面试题整理,可以从各个方面进行组织和分类,以便更好地帮助面试者准备。下面是我对Java2021面试题整理的一些建议。 1.基础知识:面试题可以包括Java语言的基本语法、关键字、数据类型、流程控制语句、异常处理等方面的问题。这些问题可以帮助面试者检验自己对Java语言基础知识的掌握程度。 2.面向对象:面试题可以涉及Java面向对象的概念、封装、继承、多态以及接口、抽象类等方面的问题。这些问题可以帮助面试者了解Java面向对象编程的特点和应用。 3.集合框架:面试题可以包括关于Java集合框架的知识,如ArrayList、LinkedList、HashSet、HashMap等的特性、用法和区别。这些问题可以帮助面试者检验自己对Java集合框架的理解和应用能力。 4.多线程:面试题可以涉及Java多线程编程的基本概念、线程的创建与启动、线程同步与互斥、线程池等方面的问题。这些问题可以帮助面试者了解多线程编程的原理和实践。 5.IO流:面试题可以包括关于Java IO流的知识,如输入输出流的分类、字符流和字节流的区别、文件读写操作等方面的问题。这些问题可以帮助面试者检验自己对IO流的理解和应用。 6.异常处理:面试题可以涉及Java异常处理的机制、try-catch语句的使用、自定义异常等方面的问题。这些问题可以帮助面试者了解异常处理的原理和常见应用。 7.Java虚拟机:面试题可以包括Java虚拟机(JVM)的基本概念、内存模型、垃圾回收算法等方面的问题。这些问题可以帮助面试者了解JVM的工作原理和性能优化。 8.框架和工具:面试题可以涉及Java常用的开发框架和工具,如Spring、Hibernate、MyBatis、Maven等方面的问题。这些问题可以帮助面试者了解开发框架的应用和工具的使用。 通过对这些方面的面试题整理,可以帮助面试者全面了解Java2021面试的内容和要求,并有针对性地准备和复习相关知识。面试者应该注重理论的学习,同时结合实践经验进行练习,以便在面试时能够更好地展示自己的能力和潜力。同时,面试者还应注意自己的沟通能力、问题分析能力和解决问题的能力,这些都是面试过程中重要的评估指标。 ### 回答2: Java2021面试题整理主要集中在以下几个方面: 1. 基础知识:Java中的基本数据类型、变量和常量、运算符、控制语句等内容是面试中常见的考点。面试官会通过这些问题判断候选人对Java基础知识的熟悉程度和掌握能力。 2. 面向对象编程:Java是一门面向对象的编程语言,所以面试中对面向对象的理解和应用也是重要的考点。常见的问题包括类和对象、继承和多态、封装和抽象等。 3. 异常处理:Java中的异常处理是编程中的重要内容,面试中会涉及到异常的概念、异常的分类、如何捕获和处理异常、自定义异常等。 4. 集合框架:Java集合框架是Java开发中常用的工具,常见的面试题会涉及到ArrayList、LinkedList、HashMap等集合的特点和应用场景,以及集合的遍历和使用方法。 5. 多线程:Java是一门支持多线程的语言,所以多线程的知识也是面试中的热点考点。常见的问题包括线程的生命周期、线程同步与互斥、线程间的通信、线程池等。 6. JVM相关知识:Java虚拟机(JVM)是Java运行的基础,所以对JVM的了解也是面试中的重要考点。常见问题包括JVM的结构、内存模型、垃圾回收机制等。 此外,面试中还可能涉及到数据库、网络编程、设计模式等其他相关知识。因此,面试前需要对Java的相关知识有全面的掌握,并且要能够灵活运用这些知识进行问题的解答。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值