java高级篇.md 53 KB

面试专题-java高级篇

1. JVM

JVM由那些部分组成,运行流程是什么?

Java Virtual Machine Java程序的运行环境(java二进制字节码的运行环境)

好处:

  • 一次编写,到处运行(java跨平台语言,(上百种开发语言都能编译为字节码)JVM是跨语言平台)
  • 自动内存管理,垃圾回收机制

从下图中可以看出 JVM 的主要组成部分

  • ClassLoader(类加载器)
  • Runtime Data Area(运行时数据区,内存分区)
    • 线程私有
    • Java虚拟机栈(本地方法栈)(不存在GC,弹栈后变量基本引用被释放,存在OOM问题-栈溢出)
    • 程序计数器(唯一不存在OOM区域)
    • 线程共享
    • 方法区(元空间)
    • 堆(heap)
  • Execution Engine(执行引擎-垃圾回收器)
  • Native Method Library(本地库接口)

运行流程:

(1)类加载器(ClassLoader)把Java代码转换为字节码

(2)运行时数据区(Runtime Data Area)把字节码加载到内存中,而字节码文件只是JVM的一套指令集规范,并不能直接交给底层系统去执行,而是有执行引擎运行

(3)执行引擎(Execution Engine)将字节码翻译为底层系统指令,再交由CPU执行去执行,此时需要调用其他语言的本地库接口(Native Method Library)来实现整个程序的功能。

image-20230303154801760

什么是程序计数器?

程序计数器:线程私有的,内部保存的字节码的行号。用于记录正在执行的字节码指令的地址。

javap -verbose xx.class 打印堆栈大小,局部变量的数量和方法的参数。

image-20230506094602329

​ java虚拟机对于多线程是通过线程轮流切换并且分配线程执行时间。在任何的一个时间点上,一个处理器只会处理执行一个线程,如果当前被执行的这个线程它所分配的执行时间用完了【挂起】。处理器会切换到另外的一个线程上来进行执行。并且这个线程的执行时间用完了,接着处理器就会又来执行被挂起的这个线程。

​ 那么现在有一个问题就是,当前处理器如何能够知道,对于这个被挂起的线程,它上一次执行到了哪里?那么这时就需要从程序计数器中来回去到当前的这个线程他上一次执行的行号,然后接着继续向下执行。

​ 程序计数器是JVM规范中唯一一个没有规定出现OOM的区域,所以这个空间也不会进行GC。

你能给我详细的介绍Java堆吗?

线程共享的区域:主要用来保存对象实例,数组等,当堆中没有内存空间可分配给实例,也无法再扩展时,则抛出OutOfMemoryError异常。

线程共享的区域:主要用来保存对象实例,数组等,当堆中没有内存空间可分配给实例,也无法再扩展时,则抛出OutOfMemoryError异常。

image-20230506094803545

  • 年轻代被划分为三部分,Eden区和两个大小严格相同的Survivor区,根据JVM的策略,在经过几次垃圾收集后,任然存活于Survivor的对象将被移动到老年代区间。
  • 老年代主要保存生命周期长的对象,一般是一些老的对象
  • 元空间保存的类信息、静态变量、常量、编译后的代码

堆栈的区别是什么?

1、栈内存一般会用来存储局部变量和方法调用,但堆内存是用来存储Java对象和数组的的。堆会GC垃圾回收,而栈不会。

2、栈内存是线程私有的,而堆内存是线程共有的。

3,、两者异常错误不同,但如果栈内存或者堆内存不足都会抛出异常。

栈空间不足:java.lang.StackOverFlowError。

堆空间不足:java.lang.OutOfMemoryError。

什么是类加载器,类加载器有哪些?

要想理解类加载器的话,务必要先清楚对于一个Java文件,它从编译到执行的整个过程。

  • 类加载器:用于装载字节码文件(.class文件)
  • 运行时数据区:用于分配存储空间
  • 执行引擎:执行字节码文件或本地方法
  • 垃圾回收器:用于对JVM中的垃圾内容进行回收

类加载器

JVM只会运行二进制文件,而类加载器(ClassLoader)的主要作用就是将字节码文件加载到JVM中,从而让Java程序能够启动起来。现有的类加载器基本上都是java.lang.ClassLoader的子类,该类的只要职责就是用于将指定的类找到或生成对应的字节码文件,同时类加载器还会负责加载程序所需要的资源

类加载器种类

类加载器根据各自加载范围的不同,划分为四种类加载器:

  • 启动类加载器(BootStrap ClassLoader):

该类并不继承ClassLoader类,其是由C++编写实现。用于加载JAVA_HOME/jre/lib目录下的类库。

  • 扩展类加载器(ExtClassLoader):

该类是ClassLoader的子类,主要加载JAVA_HOME/jre/lib/ext目录中的类库。

  • 应用类加载器(AppClassLoader):

该类是ClassLoader的子类,主要用于加载classPath下的类,也就是加载开发者自己编写的Java类。

  • 自定义类加载器:

开发者自定义类继承ClassLoader,实现自定义类加载规则。

上述三种类加载器的层次结构如下如下:

image-20230506100746624

类加载器的体系并不是“继承”体系,而是委派体系,类加载器首先会到自己的parent中查找类或者资源,如果找不到才会到自己本地查找。类加载器的委托行为动机是为了避免相同的类被加载多次。

什么是双亲委派模型?

如果一个类加载器在接到加载类的请求时,它首先不会自己尝试去加载这个类,而是把这个请求任务委托给父类加载器去完成,依次递归,如果父类加载器可以完成类加载任务,就返回成功;只有父类加载器无法完成此加载任务时,才由下一级去加载。

image-20230506100920042

(1)通过双亲委派机制可以避免某一个类被重复加载,当父类已经加载后则无需重复加载,保证唯一性。

(2)为了安全,保证JDK自带类库API不会被修改

说一下类装载的执行过程?

类从加载到虚拟机中开始,直到卸载为止,它的整个生命周期包括了:加载、验证、准备、解析、初始化、使用和卸载这7个阶段。其中,验证、准备和解析这三个部分统称为连接(linking)。

image-20230506101032605

类加载过程详解

1.加载

image-20230506101115674

  • 通过类的全名,获取类的二进制数据流。

  • 解析类的二进制数据流为方法区内的数据结构(Java类模型)

  • 创建java.lang.Class类的实例,表示该类型。作为方法区这个类的各种数据的访问入口

image-20230506101213373

2.验证

image-20230506101420202

验证类是否符合JVM规范,安全性检查

(1)文件格式验证:是否符合Class文件的规范 (2)元数据验证

这个类是否有父类(除了Object这个类之外,其余的类都应该有父类)
这个类是否继承(extends)了被final修饰过的类(被final修饰过的类表示类不能被继承)
类中的字段、方法是否与父类产生矛盾。(被final修饰过的方法或字段是不能覆盖的)                       

(3)字节码验证

主要的目的是通过对数据流和控制流的分析,确定程序语义是合法的、符合逻辑的。

(4)符号引用验证:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量

比如:int i = 3; 字面量:3 符号引用:i

3.准备

image-20230506101445898

为类变量分配内存并设置类变量初始值

  • static变量,分配空间在准备阶段完成(设置默认值),赋值在初始化阶段完成

  • static变量是final的基本类型,以及字符串常量,值已确定,赋值在准备阶段完成(编译阶段完成赋值)

  • static变量是final的引用类型,那么赋值也会在初始化阶段完成

image-20230506101824622

4.解析

image-20230506101504632

把类中的符号引用转换为直接引用

比如:方法中调用了其他方法,方法名可以理解为符号引用,而直接引用就是使用指针直接指向方法。

image-20230506102311951

5.初始化

image-20230506101625087

对类的静态变量,静态代码块执行初始化(将静态变量+静态代码块自上而下合并到clinit方法)操作

  • 如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。

  • 如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。

6.使用

image-20230506101641837

JVM 开始从入口方法开始执行用户的程序代码

  • 调用静态类成员信息(比如:静态字段、静态方法)

  • 使用new关键字为其创建对象实例

7.卸载

当用户程序代码执行完毕后,JVM 便开始销毁创建的 Class 对象,最后负责运行的 JVM 也退出内存

小结:

  • 加载:
  • 链接
    • 验证
    • 准备,类中静态变量赋初值,常量在编译期完成赋值
    • 解析
  • 初始化
    • 将静态变量+静态代码块合并在clinit方法中进行初始化,完成静态变量赋值
  • 使用
  • 卸载

简述Java垃圾回收机制?(GC是什么?为什么要GC)

为了让程序员更专注于代码的实现,而不用过多的考虑内存释放的问题,所以,在Java语言中,有了自动的垃圾回收机制,也就是我们熟悉的GC(Garbage Collection)。

有了垃圾回收机制后,程序员只需要关心内存的申请即可,内存的释放由系统自动识别完成。

在进行垃圾回收时,不同的对象引用类型,GC会采用不同的回收时机

换句话说,自动的垃圾回收的算法就会变得非常重要了,如果因为算法的不合理,导致内存资源一直没有释放,同样也可能会导致内存溢出的。

当然,除了Java语言,C#、Python等语言也都有自动的垃圾回收机制。

对象什么时候可以被垃圾器回收

简单一句就是:如果一个或多个对象没有任何的引用指向它了,那么这个对象现在就是垃圾,如果定位了垃圾,则有可能会被垃圾回收器回收。

如果要定位什么是垃圾,有两种方式来确定,第一个是引用计数法,第二个是可达性分析算法

引用计数法

一个对象被引用了一次,在当前的对象头上递增一次引用次数,如果这个对象的引用次数为0,代表这个对象可回收

String demo = new String("123");

image-20230506111102825

String demo = null;

image-20230506111136231

当对象间出现了循环引用的话,则引用计数法就会失效

image-20230506111255401

先执行右侧代码的前4行代码

image-20230506111327590

目前上方的引用关系和计数都是没问题的,但是,如果代码继续往下执行,如下图

image-20230506111512450

虽然a和b都为null,但是由于a和b存在循环引用,这样a和b永远都不会被回收。

优点:

  • 实时性较高,无需等到内存不够的时候,才开始回收,运行时根据对象的计数器是否为0,就可以直接回收。
  • 在垃圾回收过程中,应用无需挂起。如果申请内存时,内存不足,则立刻报OOM错误。
  • 区域性,更新对象的计数器时,只是影响到该对象,不会扫描全部对象。

缺点:

  • 每次对象被引用时,都需要去更新计数器,有一点时间开销。
  • 浪费CPU资源,即使内存够用,仍然在运行时进行计数器的统计。
  • 无法解决循环引用问题,会引发内存泄露。(最大的缺点)

可达性分析算法

​ 现在的虚拟机采用的都是通过可达性分析算法来确定哪些内容是垃圾。

​ 会存在一个根节点【GC Roots】,引出它下面指向的下一个节点,再以下一个节点节点开始找出它下面的节点,依次往下类推。直到所有的节点全部遍历完毕。

根对象是那些肯定不能当做垃圾回收的对象,就可以当做根对象

局部变量,静态方法,静态变量,类信息

核心是:判断某对象是否与根对象有直接或间接的引用,如果没有被引用,则可以当做垃圾回收

image-20220904010634153

​ X,Y这两个节点是可回收的,但是并不会马上的被回收!! 对象中存在一个方法【finalize】。当对象被标记为可回收后,当发生GC时,首先会判断这个对象是否执行了finalize方法,如果这个方法还没有被执行的话,那么就会先来执行这个方法,接着在这个方法执行中,可以设置当前这个对象与GC ROOTS产生关联,那么这个方法执行完成之后,GC会再次判断对象是否可达,如果仍然不可达,则会进行回收,如果可达了,则不会进行回收。

​ finalize方法对于每一个对象来说,只会执行一次。如果第一次执行这个方法的时候,设置了当前对象与RC ROOTS关联,那么这一次不会进行回收。 那么等到这个对象第二次被标记为可回收时,那么该对象的finalize方法就不会再次执行了。

GC ROOTS:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象

    /**
    * demo是栈帧中的本地变量,当 demo = null 时,由于此时 demo 充当了 GC Root 的作用,demo与原来指向的实例 new Demo() 断开了连接,对象被回收。
    */
    public class Demo {
    public static  void main(String[] args) {
    	Demo demo = new Demo();
    	demo = null;
    }
    }
    
  • 方法区中类静态属性引用的对象

    /**
    * 当栈帧中的本地变量 b = null 时,由于 b 原来指向的对象与 GC Root (变量 b) 断开了连接,所以 b 原来指向的对象会被回收,而由于我们给 a 赋值了变量的引用,a在此时是类静态属性引用,充当了 GC Root 的作用,它指向的对象依然存活!
    */
    public class Demo {
    public static Demo a;
    public static  void main(String[] args) {
        Demo b = new Demo();
        b.a = new Demo();
        b = null;
    }
    }
    
  • 方法区中常量引用的对象

    /**
    * 常量 a 指向的对象并不会因为 demo 指向的对象被回收而回收
    */
    public class Demo {
        
    public static final Demo a = new Demo();
        
    public static  void main(String[] args) {
        Demo demo = new Demo();
        demo = null;
    }
    }
    
  • 本地方法栈中 JNI(即一般说的 Native 方法)引用的对象

JVM 垃圾回收算法有哪些?

标记清除算法

标记清除算法,是将垃圾回收分为2个阶段,分别是标记和清除

1.根据可达性分析算法得出的垃圾进行标记

2.对这些标记为可回收的内容进行垃圾回收

image-20230506112047190

可以看到,标记清除算法解决了引用计数算法中的循环引用的问题,没有从root节点引用的对象都会被回收。

同样,标记清除算法也是有缺点的:

  • 效率较低,标记和清除两个动作都需要遍历所有的对象,并且在GC时,需要停止应用程序,对于交互性要求比较高的应用而言这个体验是非常差的。
  • 重要)通过标记清除算法清理出来的内存,碎片化较为严重,因为被回收的对象可能存在于内存的各个角落,所以清理出来的内存是不连贯的。

复制算法

​ 复制算法的核心就是,将原有的内存空间一分为二,每次只用其中的一块,在垃圾回收时,将正在使用的对象复制到另一个内存空间中,然后将该内存空间清空,交换两个内存的角色,完成垃圾的回收。

​ 如果内存中的垃圾对象较多,需要复制的对象就较少,这种情况下适合使用该方式并且效率比较高,反之,则不适合。

image-20230506111919008

1)将内存区域分成两部分,每次操作其中一个。

2)当进行垃圾回收时,将正在使用的内存区域中的存活对象移动到未使用的内存区域。当移动完对这部分内存区域一次性清除。

3)周而复始。

优点:

  • 在垃圾对象多的情况下,效率较高
  • 清理后,内存无碎片

缺点:

  • 分配的2块内存空间,在同一个时刻,只能使用一半,内存使用率较低

标记整理算法

​ 标记压缩算法是在标记清除算法的基础之上,做了优化改进的算法。和标记清除算法一样,也是从根节点开始,对对象的引用进行标记,在清理阶段,并不是简单的直接清理可回收对象,而是将存活对象都向内存另一端移动,然后清理边界以外的垃圾,从而解决了碎片化的问题。

image-20230506111957793

1)标记垃圾。

2)需要清除向右边走,不需要清除的向左边走。

3)清除边界以外的垃圾。

优缺点同标记清除算法,解决了标记清除算法的碎片化的问题,同时,标记压缩算法多了一步,对象移动内存位置的步骤,其效率也有有一定的影响。

与复制算法对比:复制算法标记完就复制,但标记整理算法得等把所有存活对象都标记完毕,再进行整理

分代收集算法

在java8时,堆被分为了两份:新生代和老年代【1:2】,在java7时,还存在一个永久代。

image-20230506131229649

对于新生代,内部又被分为了三个区域。Eden区,S0区,S1区【8:1:1】

当对新生代产生GC:MinorGC【young GC】

当对老年代代产生GC:Major GC

当对新生代和老年代产生FullGC: 新生代 + 老年代完整垃圾回收,暂停时间长,应尽力避免

工作机制

image-20230506131308654

  • 新创建的对象,都会先分配到eden区

image-20230506131415418

  • 当伊甸园内存不足,标记伊甸园与 from(现阶段没有)的存活对象

  • 将存活对象采用复制算法复制到 to 中,复制完毕后,伊甸园和 from 内存都得到释放

image-20230506131442503

  • 经过一段时间后伊甸园的内存又出现不足,标记eden区域to区存活的对象,将存活的对象复制到from区

image-20230506131544447

image-20230506131607645

  • 当幸存区对象熬过几次回收(最多15次),晋升到老年代(幸存区内存不足或大对象会导致提前晋升)

MinorGC、 Mixed GC 、 FullGC的区别是什么

image-20230506131640893

  • MinorGC【young GC】发生在新生代的垃圾回收,暂停时间短(STW)

  • Mixed GC 新生代 + 老年代部分区域的垃圾回收,G1 收集器特有

  • FullGC: 新生代 + 老年代完整垃圾回收,暂停时间长(STW),应尽力避免

名词解释:

STW(Stop-The-World):暂停所有应用程序线程,等待垃圾回收的完成

java内存泄露的排查思路?

原因:

如果线程请求分配的栈容量超过java虚拟机栈允许的最大容量的时候,java虚拟机将抛出一个StackOverFlowError异常

如果java虚拟机栈可以动态拓展,并且扩展的动作已经尝试过,但是目前无法申请到足够的内存去完成拓展,或者在建立新线程的时候没有足够的内存去创建对应的虚拟机栈,那java虚拟机将会抛出一个OutOfMemoryError异常

如果一次加载的类太多,元空间内存不足,则会报OutOfMemoryError: Metaspace

image-20230506155704119

1、通过jmap指定打印他的内存快照 dump

有的情况是内存溢出之后程序则会直接中断,而jmap只能打印在运行中的程序,所以建议通过参数的方式的生成dump文件,配置如下:

-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/home/app/dumps/ 指定生成后文件的保存目录

2、通过工具, VisualVM(Ecplise MAT)去分析 dump文件

VisualVM可以加载离线的dump文件,如下图

文件-->装入--->选择dump文件即可查看堆快照信息

如果是linux系统中的程序,则需要把dump文件下载到本地(windows环境)下,打开VisualVM工具分析。VisualVM目前只支持在windows环境下运行可视化

image-20220904132925812

3、通过查看堆信息的情况,可以大概定位内存溢出是哪行代码出了问题

image-20220904133722905

4、找到对应的代码,通过阅读上下文的情况,进行修复即可

CPU飙高排查方案与思路?

1.使用top命令查看占用cpu的情况

image-20220904161818255

2.通过top命令查看后,可以查看是哪一个进程占用cpu较高,上图所示的进程为:30978

3.查看当前线程中的进程信息

ps H -eo pid,tid,%cpu | grep 40940

pid 进行id

tid 进程中的线程id

% cpu使用率

image-20220904162117022

4.通过上图分析,在进程30978中的线程30979占用cpu较高

注意:上述的线程id是一个十进制,我们需要把这个线程id转换为16进制才行,因为通常在日志中展示的都是16进制的线程id名称

转换方式:

在linux中执行命令

printf "%x\n" 30979

image-20220904162654928

5.可以根据线程 id 找到有问题的线程,进一步定位到问题代码的源码行号

执行命令

jstack 30978   此处是进程id

image-20220904162941977

经典面试问题

有做过jvm的调优吗?常用的jvm参数调优有哪些?

​ 回答话术: jvm参数之前在工作的时候也偶尔做过,但是也不能完全记得住,每次都是在需要的时候去查询文档 一般情况下都是使用默认值,只有在真正需要调优的时候会去重新设置值去覆盖默认值...停顿几秒思考 ​ 根据我的回忆,jvm参数分为三种 ​ 标准参数: 主要用于查看一些基本信息 比如jvm版本号 ​ X参数: 用于设置内存大小 基本都是传给 JVM 的,默认 JVM 实现这些参数的功能,但是并不保证所有 JVM 实 现都满足,且不保证向后兼容 稳定性好 ​ XX参数 用于设置内存大小 专门用于控制 JVM 的行为,跟具体的 JVM 实现有关,随时可能会在下个版本取消 ​ 稳定性差一些 ​ 接下来举例说明几个常用的参数 ​ -Xms 内存的初始值大小 以M(兆)为单位 默认为系统内存的1/64 ​ -Xmx 内存的最大值 也是以M(兆)为单位 最大值为系统内存的1/4 ​ 一般情况下,会将如下两个参数设置为相同的值,可以避免jvm内存的自动扩展,因为当堆内存大小发生扩 展的时候,就会发生内存的抖动,会影响到程序的稳定性 ​ -Xmn 用于设置新生代的内存大小,一般设置为堆空间的1/3或者1/4,因为新生代大的话,老年代就会变小 ​ -Xss 用于设置每个线程的虚拟机栈的大小也即堆栈的大小 ​ -XX -XX:+UseG1GC、-XX:+UseZGC 设置垃圾回收器 ​ 当然还有很多的参数,一般都是需要的时候会按照文档就行设置,具体的就记不住那么多了....

-Xmx1024M 最大堆内存
-Xms1024M 初始化堆内存,正常和最大堆内存相同,减少动态改变的内存损耗
-Xmn384M 年轻代内存
    
-XX:+PrintGCDetails 打印gc信息,可参考gc的比例进行调优
-XX:+UseConcMarkSweepGC 老年代使用cms,标记-清除算法会产生碎片
-XX:+UseParNewGC 年轻代使用并行收集器

如果jvm持续一段时间频繁的发生Young GC (轻GC) 可能原因有哪些?

​ 首先频繁的YGC 说明会频繁的创建对象并立马被回收了 我想造成这个问题的原因可能有两点 ​ 第一点: for循环内部 频繁的创建局部对象从而导致频繁的GC 当然也有可能是死循环导致的 ​ 第二点: 年轻代内存大小设置不足,无法满足程序逻辑的需要从而导致频繁GC ​ 为了解决这个问题,要么就是减少对象的创建,当然这个要修改程序,难度和周期都比较长一些,要么就是增大年轻代的内存大小,这样可以更快的解决这个问题.当然了,最可靠的还是修改代码,通过GC日志定位问题

2. Redis

Redis支持的数据类型有哪些?

string list hash zet zset

  • 缓存业务数据
    • 例如首页中读多写少业务数据:分类、专辑、声音、统计、主播信息,登录token等采用String
  • 排行榜
    • 首页中展示所有分类、不同排序方式下热门的TOP10专辑列表, 采用hash存放 Key:分类ID Field:排序字段 Value:专辑列表。(非实时排行榜)
    • 所有专辑TOP5的专辑列表,采用SortSet存放 Key:top排行榜 score:专辑热度 member :专辑信息 (实时排行榜)
  • 分布式锁/分布式读写锁
    • 自定义分布式锁 :采用String提供set k v EX NX 实现不可重入分布式锁
    • Redisson提供分布式锁:采用Lua+Hash
    • Lua脚本确保多个判断多个命令原子性
    • Hash作为锁存储选择 Key:锁名称 Field:线程标识 Value:重入次数
    • pub/sub:发布订阅,释放锁后通知客户端获取锁。 所有订阅话题客户端都可以收到消息
  • 延迟消息
    • 当订单选择支付方式为微信,超时未支付订单自动关单业务,选择Redisson提供延迟任务
    • List1:存放所有未投递消息 List2:到时需要被消费消息 Zset:存放需要被投递消息投递时间
  • 布隆过滤器
    • 解决缓存穿透 Redisson布隆过滤器
    • hash:存放布隆过滤器配置-包括数据规模、误判率、hash个数、bitmap位图长度
    • bitmap:存放存在数据,hash个数映射到bitmap为止将值改为1
  • Redis GEO 主要用于存储地理位置信息,并对存储的信息进行操作
    • geoadd:添加地理位置的坐标。
    • geodist:计算两个位置之间的距离。
    • georadius:根据用户给定的经纬度坐标来获取指定范围内的地理位置集合

单线程的redis为什么读写速度快?

  1. 纯内存操作
  2. 单线程操作,避免了频繁的上下文切换
  3. 采用了非阻塞I/O多路复用机制

redis服务器的的内存是多大?

Redis服务器的内存大小可以根据需求进行配置,可以配置为几十MB到几十GB不等。具体而言,可以在redis.conf配置文件中通过 maxmemory 参数来设置Redis实例的最大内存限制。例如,如果要将Redis实例的最大内存限制设置为1GB,则可以将该参数设置为maxmemory 1GB不设置该参数,则Redis将使用系统的所有可用内存。如果一台服务器中不会再安装除了redis之外的其他机器时,最佳内存值为服务器内存的3/4

对redis的持久化了解不?

持久化就是将内存的数据写入到磁盘当中,防止服务突然宕机,造成内存数据的丢失。

类型 介绍 具体配置 优点 缺点
RDB 默认持久化机制是按照一定的时间将内存中的数据以快照的形式保存到硬盘中 rdb.dump img 数据恢复速度快 可能丢失少量新数据持久化时存储数据效率低
AOF 是将Redis的每一次操作命令都写入到单独的日志文件中,当重启redis会重新从持久化的的日志中恢复数据 文件名为appendonly.aof img 持久化效率高 不会丢失数据 恢复数据时效率低aof中记录的所有命令进行重放,效率低
混合模式(RDB+AOF) 将RDB和AOF混合一起使用,在使用混合模式时,所有的数据操作也是保存在AOF当中,当进行恢复文件的时候,会将原有的AOF删除,并且将其中的数据全部以快照的形式保存至RDB文件当中 img 持久化效率高保证数据的安全性不会丢失数据且恢复的速度快

AOF无用命令过多存入aof文件解决办法:

执行 BGREWRITEAOF 命令, Redis 将生成一个新的 AOF 文件, 这个文件包含重建当前数据集所需的最少命令

同时开启RDB跟AOF持久化机制并非混合模式:RDB用来做备份

image-20240613214111687

RDB+AOF混合方式 1 开启混合方式设置 设置aof-use-rdb-preamble的值为 yes yes表示开启,设置为no表示禁用

2 RDB+AOF的混合方式--------->结论:RDB镜像做全量持久化,AOF做增量持久化(增量记录RDB期间正在执行写命令存入AOF),先使用RDB进行快照存储,然后使用AOF持久化记录所有的写操作,当重写策略满足或手动触发重写的时候,将最新的数据存储为新的RDB记录,这样的话,重启服务的时候会从RDB和AOF两部分恢复数据,既保证了数据完整性,又提高了恢复数据的性能。简单来说:混合持久化方式产生的文件一部分是RDB格式,一部分是AOF格式。----》AOF包括了RDB头部+AOF混写

redis的过期策略有了解过吗

Redis的过期策略是指在Redis中用来决定已过期的键(key)应该如何处理的机制。Redis中的过期键是指在一段时间(设置TTL)之后会被自动删除的键。Redis通过定期检查键是否过期来实现这种自动删除

Redis的过期策略主要有两种:

  • 惰性删除:惰性删除是指在读取键值对时,先判断该键是否过期,如果过期就删除,否则直接返回
  • 定期删除:定期删除是指Redis定期检查挑选部分键的过期时间,将其中已经过期的键删除。

定期清理有两种模式:

  • SLOW模式是定时任务,执行频率受server.hz影响,默认为10,即每秒执行10次,每次不超过25ms,以通过修改配置文件redis.conf 的hz 选项来调整这个次数
  • FAST模式执行频率不固定,但两次间隔不低于2ms,每次耗时不超过1ms

总结:

Redis的过期删除策略: 惰性删除(空间换时间) + 定期删除(时间换空间)两种策略进行配合使用

redis内存是有限的,内存被占满了怎么办?

其实就是想问redis的数据淘汰策略是什么?

内存淘汰策略是当Redis的内存使用达到上限,需要申请额外空间时触发的一种机制,它的目的是为了清除不再使用的缓存数据,从而释放更多的内存空间

Redis的内存淘汰机制有8种:

  1. noeviction:达到内存限制时,直接返回错误,不会删除任何键值对。默认就是这种策略

image-20240106105139894

  1. allkeys-lru(Least Recently Used):Redis会遍历所有键值对,选择最近使用的键值对进行删除。用当前时间减去最后一次访问时间,这个值越大则淘汰优先级越高 (推荐使用)

  2. allkeys-random:Redis会随机选择一些键值对进行删除。

  3. volatile-lru:Redis只会删除设置了过期时间的键值对中最近使用的那些。

  4. volatile-random:Redis只会随机删除设置了过期时间的键值对。

  5. volatile-ttl:Redis只会删除设置了过期时间的键值对中,剩余时间最短的那些。

  6. allkeys-lfu(Least Frequently Used): 对全体key,基于LFU算法进行淘汰 即最少使用频率 会统计每个key的访问频率,值越小淘汰优先级越高。

  7. volatile-lfu:对设置了TTL的key,基于LFU算法进行淘汰

数据淘汰策略-使用建议

  1. 优先使用 allkeys-lru 策略。充分利用 LRU 算法的优势,把最近最常访问的数据留在缓存中如果业务有明显的冷热数据区分,建议使用。

  2. 如果业务中数据访问频率差别不大,没有明显冷热数据区分,建议使用 allkeys-random,随机选择淘汰。

  3. 如果业务中有置顶的需求,可以使用 volatile-lru 策略,同时置顶数据不设置过期时间,这些数据就一直不删除会淘汰其他设置过期时间的数据

  4. 如果业务中有短时高频访问的数据,可以使用 allkeys-lfu 或 volatile-lfu 策略

关于数据淘汰策略其他的面试问题

数据库有1000万数据,Redis只能缓存20w数据如何保证Redis中的数据都是热点数据?

使用allkeys-lfu(挑选最近访问频率较低的数据淘汰)淘汰策略,留下来的都是经常访问的热点数据

Redis的内存用完了会发生什么?

主要看数据淘汰策略是什么? 如果是默认的配置(noeviction ),会直接报错

redis集群方式有哪些?

主从复制

单节点Redis的并发能力是有上限的,要进一步提高Redis的并发读能力,就需要搭建主从集群,实现读写分离。

image-20240115194807573

缺点: 无法保证redis高可用 一旦主节点挂掉,就会丧失写数据的能力 为了保证redis集群的高可用性,redis提供了哨兵模式

哨兵模式

redis提供了哨兵(sentinel)机制来实现主从集群的自动故障恢复,是保证redis集群高可用的一种手段.哨兵的结构和作用如下:

image-20240115203132612

哨兵的作用:

  • 监控: Sentinel会不断检查master和slave是否按预期工作 ping/pong 机制

  • 自动故障恢复:如果master故障,Sentinel会将一个slave提升为master。当故障实例恢复后也以新的master为主

  • 通知: Sentinel充当Redis客户端的服务发现来源,当集群发生故障转移时,会将最新信息推送给Redis的客户端

工作流程:

image-20240115222732479

Sentinel基于心跳机制监测服务状态,每隔1秒向集群的每个实例发送ping命令:

  • 主观下线:如果某sentinel节点发现某实例未在规定时间响应,则认为该实例主观下线。

  • 客观下线: 若超过指定数量(可在redis配置文件中设置)的sentinel都认为该实例主观下线,则该实例客观下线。quorum值最好超过Sentinel实例数的一半

Sentinel选举-目的选出完成故障转移Sentinel节点

  • 投票机制
    • Sentinel节点票数相同,再次进行一轮投票
    • 找出票数最多的Sentinel作为故障转移决裁者

哨兵选主规则

  • 首先判断主与从节点断开时间长短,如超过指定值就排除该从节点

  • 然后判断从节点的slave-priority值,越小优先级越高 (slave-priority是一个配置选项,用来指定一个从服务器(slave)在主服务器(master)选举过程中的优先级)

  • 如果slave-prority一样,则判断slave节点的offset值,越大优先级越高 一个更高的偏移量意味着该从服务器接收了更多的数据,因此它的数据更加完整,更接近于原主服务器的当前状态。

  • 最后是判断slave节点的运行id大小,越小优先级越高

关于脑裂问题的分析:

image-20240115223900296

集群脑裂是由于主节点和从节点和sentinel处于不同的网络分区,使得sentinel没有能够心跳感知到主节点,所以通过洗举的方式提升了一个从节点为主,这样就存在了两个master,就像大脑分裂了一样,这样会导致客户端还在老的主节点那里写入数据,新节点无法同步数据,当网络恢复后,sentinel会将老的主节点降为从节点,这时再从新master同步数据,就会导致数据丢失

解决:

可以修改redis的配置,可以设置最少的从节点数量以及缩短主从数据同步的延迟时间,达不到要求就拒绝请求就可以避免大量的数据丢失

话术:

你们使用redis是单点还是集群,哪种集群?

主从(1主1从)+哨兵就可以。单节点不超过10G内存,如果Redis内存不足则可以给不同服务分配独立的Redis主从节点 尽量不做分片集群。因为集群维护起来比较麻烦,并且集群之间的心跳检测和数据通信会消耗大量的网络带宽,也没有办法使用lua脚本和事务


redis哨兵模式 哨兵先启动,服务后启动 哨兵还会监视redis服务吗 后面主挂了,哨兵选举还生效吗?

即使哨兵先启动,在Redis服务(主节点或从节点)启动之前,哨兵也会尝试连接并监控配置中指定的Redis节点。如果在哨兵启动时Redis服务尚未运行,哨兵会定期尝试重新连接,直到服务启动并且可以被哨兵监控到。因此,即使哨兵先于Redis服务启动,它仍然能够在Redis服务启动后正常监控Redis服务。

分片集群

在哨兵模式的基础之上可以解决海量数据存储问题和高并发写问题

image-20240115225331053

特征和作用

集群中有多个master,每个master保存不同数据,每个master都可以有多个slave节点

master之间通过ping监测彼此健康状态(自动完成故障转移)

客户端请求可以访问集群任意节点,最终都会被转发到正确节点

redis存储和读取数据规则:

Redis 分片集群引入了哈希槽的概念,Redis 集群有 16384 个哈希,每个 key通过CRC16 校验后对16384 取模来决定放置哪个槽,集群的每个节点负责一部分 hash 槽。

image-20240115230143217

3. Kafka

Kafka的消息有哪些主要组件/Kafka架构?

kafka架构图

  • 【以下是概念性质的基础名词和专业术语,请先了解,后续慢慢展开】

| 核心组件 | 概念 | | ---------------------------------------- | ------------------------------------------------------------ | | Producer | 生产者,可以向Broker topic发布消息的客户端 | | Consumer | 消费者,从Broker topic订阅取消息的客户端 | | Broker | Broker英文翻译为经纪人 是一个kafka实例,简单说就是一台kafka服务器,kafkaCluster表示集群。一个Linux服务器上可以启动一个或多个 kafka服务器实例。 | | Topic | 主题,Kafka将消息分门别类,每一类的消息称之为一个主题。可以理解为一个队列,生产者和消费者面向的都是一个topic | | Partition | Topic的分区,每个 Topic 可以有多个分区,同一个Topic 在不同分区的数据是不重复的,每个Partition是一个有序的队列。分区作用是做负载,提高 kafka 的吞吐量。每个partition都由一系列有序的、不可变的消息组成,这些消息被连续的追加到partition中。partition中的每个消息都有一个连续递增的序列号叫做offset,偏移量offset在每个分区中是唯一的。 话题下分区只支持增加,不支持减少 | | Offset | 生产者Offset:消息写入的时候,每一个分区都有一个offset,这个offset就是生产者的offset,同时也是这个分区的最新最大的offset。 消费者Offset:某个分区的offset情况。例如:生产者写入的offset是最新值是10,当一个Consumer开始消费时,从0消费,一直消费到了5,消费者的offset为5。 | | Replication | Partition(分区)的副本。每个分区可以有多个Replication,由一个Leader和若干个Follower组成。Leader负责接收生产者push的消息和消费者poll消费消息。Follower会实时从自己的Leader中同步数据保持同步。Leader故障时,某个Follower会上位为新的Leader。保证高可用。分区副本数量(包含leader)不能大于>Broker数量 | | Message | kafka集群存储的消息是以topic为类别记录的,每个消息(也叫记录record)是由生产者进行发送(ProducerRecord),消费者进行消费(ConsumerRecords) | | ConsumerGroup(CG) | 消费者组,由多个Consumer组成,每个ConsumerGroup中可以有多个consumer,每个consumer属于一个ConsumerGroup。同一个Topic下的某一个分区只能被某个消费者组内的同一个消费者所消费,但可以被多个 consumer group 消费 | | In-sync Replicas(ISR) | (ISR)已同步副本:表示存活且副本都已和Leader同步的的broker集合,是Leader所有replicas副本的子集。如果某个副本节点宕机,该副本就会从ISR集合中剔除。 |

Kafka中的Broker、Topic、Consumer都会注册到zookeeper

要注意理解:

topic是一个逻辑概念,真正存储数据的是分区 分区中数据是分段存储的(segment)

zk在kafka集群中的作用是什么?

  1. 集群协调(Cluster Coordination): ZooKeeper用于管理和协调Kafka集群。Kafka集群中的多个broker(服务器)需要协调工作以保持稳定性和高可用性。
  2. Leader选举(Leader Election): Kafka中的每个Topic分区都有一个Leader,负责处理对该分区的所有读写操作。ZooKeeper用于管理这些分区的Leader选举过程。
  3. 配置管理(Configuration Management): ZooKeeper存储有关Kafka集群的关键配置信息,如Topic配置。这些信息由集群中的所有broker共享。
  4. 服务发现(Service Discovery): 当新的broker或者消费者加入到Kafka集群时,它们首先会在ZooKeeper中注册自己,以便其他系统组件可以发现它们。
  5. 集群状态(Cluster State): ZooKeeper维护了Kafka集群的状态信息,包括broker的上线和下线、Topic的创建和删除等。这有助于维护整个集群的健康状态。
  6. 故障检测(Fault Detection): ZooKeeper通过周期性的心跳检测来监控broker的健康状况。如果某个broker失去响应,ZooKeeper将通知集群中的其他成员。
  7. 消费者协调(Consumer Coordination): 对于Kafka的消费者群组来说,ZooKeeper帮助在消费者之间分配Topic分区,并且在消费者故障或者网络问题时重新分配分区。

由于ZooKeeper是一个为分布式系统设计的协调服务,它非常适合用于管理Kafka这样的分布式消息系统。然而,ZooKeeper自身也是一个复杂的系统,需要单独管理和维护,这也是为什么Kafka社区在最新版本中努力减少对ZooKeeper的依赖。在Kafka 2.8.0及以后的版本中,引入了KRaft模式(Kafka自己的Raft实现),旨在使Kafka不再依赖ZooKeeper。这种模式仍在不断发展和完善中。

kafka如何保证消息不丢失

RabbitMQ(双端(生产者,消费者)确认+持久化)

或者消息的可靠性

消息丢失的场景有如下三种

image-20240118234359249

生产者发送消息到Brocker丢失

1,设置异步发送

​ 案例代码如下

//同步发送 一般不采用这种方式
RecordMetadata recordMetadata = kafkaProducer.send(record).get();
//异步发送
kafkaProducer.send(record, new Callback() {
    @Override
    public void onCompletion(RecordMetadata recordMetadata, Exception e) {
         if (e != null) {
             System.out.println("消息发送失败 | 记录日志");
         }
        //获取消息信息
        long offset = recordMetadata.offset();
        int partition = recordMetadata.partition();
        String topic = recordMetadata.topic();
    }
});

2,消息重试

//设置重试次数
prop.put(ProducerConfig.RETRIES_CONFIG,10);

消息在Brocker中存储丢失

发送确认机制acks

image-20240118234624675

确认机制 说明
acks=0 生产者在成功写入消息之前不会等待任何来自服务器的响应,消息有丢失的风险,但是速度最快
acks=1(默认值) 只要集群首领节点收到消息,生产者就会收到一个来自服务器的成功响应
acks=-1/all 只有当所有参与赋值的节点全部收到消息时,生产者才会收到一个来自服务器的成功响应

消费者从Brocker接收消息丢失

image-20240119000916280

注意: kafka的每个分区都是按照偏移量进行存储数据的 如下图所示

image-20240119002159041

消费者组中的消费者假如按照如下的消费流程执行

P1消费到了偏移量4了, 那么下次消费就会从5开始,这样才不会导致重复消费,或者是丢失消息 也就是说每个消费者消费者消费完消息都会做一个标记 该标记称为消费者偏移量 那么下次消费找对应的偏移量即可

消费者默认是自动按期提交已经消费的偏移量,默认是每隔5S提交一次,如果出现重平衡的情况,可能会重复消费或者丢失消息数据

重平衡示意图: 消费者2宕机 那么它负责的分区3就需要其他消费者进行消费

image-20240119003041877

如果消费者2已经消费到了3位置,也提交了偏移量3 此时是没有问题的

如果消费者2已经消费到了6为止,但是只提交到了3,新接手的消费者1就会从3继续往后消费,此时3-6中间的消息就会被重复消费

如果消费者2在宕机之后将偏移量提交到了3,但它实际才消费了1,新接收的消费者1就会从3就行往后消费 此时1-2之间的消息就会丢失

导致以上可能存在问题的本质就是由于消息者提交消费偏移量的过程是自动的 如何解决呢?

禁止自动提交偏移量,改为在代码中手动进行提交

手动提交方式有如下三种

  • 同步提交(阻塞)
  • 异步提交(消费失败会导致偏移量不准)
  • 同步+异步组合提交

image-20240119003955909

总结:

需要从三个层面去解决这个问题:

  1. 生产者发送消息到Brocker丢失
    1. 设置异步发送,发送失败使用回调进行记录或重发
    2. 失败重试,参数配置,可以设置重试次数
  2. 消息在Brocker中存储丢失
    1. 发送确认acks,选择all,让所有的副本都参与保存数据后确认
  3. 消费者从Brocker接收消息丢失
    1. 关闭自动提交偏移量,开启手动提交偏移量
    2. 提交方式,最好是同步+异步提交
    3. 开启消费者重试,超过上限后将消息发送到死信话题
    4. 监听死信话题,将消息持久化“本地消费者异常消息表”
    5. 人工处理

消息吞吐量优先

  1. 生产者端,采用异步发送,采用批次提交(BatchSize:16K)批次提交时间间隔 两个批次条件满足其一发送一批、关闭重试
  2. Broker将生产者应答级别设置为0
  3. 消费者,采用自动提交(时间间隔提交)、消费者开启多线程、对分区数量进行扩容(消费者数量随之增加)

Kafka中消息的重复消费问题如何解决的?幂等性问题

  • 关闭自动提交偏移量,开启手动提交偏移量
  • 提交方式,最好是同步+异步提交
  • 幂等方案
    • Redis
    • 发送消息设置消息唯一标识/业务数据获取业务标识
    • 将业务标识作为Redis中KEy一部分,采用Redis命令:SET K V EX NX
      • 存入成功说明第一次处理,执行业务
      • 存入失败,说明非首次处理,忽略即可
    • 分布式锁 对于监听到同一个消息并发被监听到
    • 获取分布式锁进程执行业务处理,获取失败忽略即可

Kafka是如何保证消费的顺序性

应用场景:

  • 即时消息中的单对单聊天和群聊,保证发送方消息发送顺序与接收方的顺序一致
  • 充值转账两个渠道在同一个时间进行余额变更,短信通知必须要有顺序

image-20240119222054616

topic分区中消息只能由消费者组中的唯一一个消费者处理,所以消息肯定是按照先后顺序进行处理的。但是它也

仅仅是保证Topic的一个分区顺序处理,不能保证跨分区的消息先后处理顺序。 所以,如果你想要顺序的处理

Topic的所有消息,那就只提供一个分区。

做如下操作:

image-20240119222140274

总结:

  • 发送消息时指定分区号
  • 发送消息时按照相同的业务设置相同的key

Kafka的高可用机制有了解过嘛

集群模式

image-20240118234359249

Kafka 的服务器端由被称为 Broker 的服务进程构成,即一个 Kafka 集群由多个 Broker 组成

这样如果集群中某一台机器宕机,其他机器上的 Broker 也依然能够对外提供服务。这其实就是 Kafka 提供高可用

的手段之一

分区备份机制

一个topic有多个分区,每个分区有多个副本,其中有一个leader,其余的是follower 副本存储在不同的broker中

所有的分区副本的内容是都是相同的,如果leader发生故障时,会自动将其中一个follower提升为leader

如下图所示:

image-20240119224945683

关于follower有更详细的分类和重新选择主节点的关键因素 即分区备份机制

如图所示:

image-20240119225325987

ISR(in-sync replica)需要同步复制保存的follower 数据更加完整

普通 需要异步复制保存的follower

如果leader失效后,需要选出新的leader,选举的原则如下:

第一:选举时优先从ISR中选定,因为这个列表中follower的数据是与leader同步的

第二:如果ISR列表中的follower都不行了,就只能从其他follower中选取

可以通过如下配置进行设置:

image-20240119225544703

总结:

可以从两个层面回答,第一个是集群,第二个是复制机制

集群:

一个kafka集群由多个broker实例组成,即使某一台宕机,也不耽误其他broker继续对外提供服务

复制机制:

一个topic有多个分区,每个分区有多个副本,有一个leader,其余的是follower,副本存储在不同的broker中

所有的分区副本的内容是都是相同的,如果leader发生故障时,会自动将其中一个follower提升为leader,保证了

系统的容错性、高可用性

解释一下复制机制中的ISR?

ISR(in-sync replica)需要同步复制保存的follower

分区副本分为了两类,一个是ISR,与leader副本同步保存数据,另外一个普通的副本,是异步同步数据,当

leader挂掉之后,会优先从ISR副本列表中选取一个作为leader