• JAVA面试题



    视频地址


    文章目录

    javaSE

    基础+集合+JVM

    JDK、JRE、JVM之间的区别

    JDK是java标准开发包,提供了编译、运行所需要的各种工具和资源,包括java编译器、java运行时环境,以及常用的java类库等

    JRE是java运行环境,用于运行编译后的字节码class文件。包括了JVM和JVM工作时所需要的类库。

    JVM是java虚拟机,是负责运行字节码文件的

    三者之间是一个包含关系,JDK中包括了JRE,而JRE中又包括了JVM。

    我们开发时编写的.java文件,需要先使用JDK中的编译器javac来进行编译生成字节码文件。JVM就可以直接运行编译后的字节码文件了。

    另外,JVM在运行字节码文件时,需要把字节码解释为机器指令,而不同的操作系统的机器指令是不同的,所以不同的操作系统上的JVM也不一样,最终就导致了我们在安装JDK的时候需要选择操作系统

    hashCode()与equals()

    判断一个对象是否相等,首先是调用该对象的hashCode()方法获取到hash值进行比较,如果不相等就表示这两个对象不相等,如果hash值相等再调用equals()方法判断是否相等。

    • 如果两个对象的hashCode不相同,那么这两个对象肯定不同
    • 如果两个对象的hashCode相同,不代表两个对象一定是同一个对象
    • 如果两个对象相等,那么他们的hashCode一定相同

    如下图所示,一个User类中有一个name属性,如果要根据name属性的值来比较User对象是否相等的话就需要如下图所示重写hashCode()和equals()方法

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-F34QPm4U-1659605378832)(E:\Java笔记\picture\image-20220725131347619.png)]

    通过equals()方法比较比较重,逻辑比较多,而hashCode()得到的实际上就是一个数字,相对轻量级,所以比较两个对象是否相同时通常会根据hashCode相比较一下。

    String、StringBuilder、StringBuffer

    • String是一个常量,是不可变的,如果尝试去修改是会生成一个新的字符串常量
    • StringBuilder是可变的,线程不安全
    • StringBuffer是可变的,线程安全

    泛型中extends和super的区别

    • < ? extends T > 表示包括T在内的任何T的子类
    • < ? super T>表示包括T在内的任何T的父类

    == 和 equals区别

    • == 两边如果是基本数据类型则比较是值,如果是引用类型,则比较的是引用地址
    • equals:具体要看各个类重写equals()方法之后的比较逻辑,比如String,虽然是引用类型,但是String类中重写了equals()方法,方法内部比较的是字符串中的各个字符是否全部相等。如果进行equals比较的类内部没有重写equals()方法,那么其实就是调用的Objec类中的equals()方法,方法体中使用的是== 也就是说比较的是两个对象的引用地址。

    重载和重写的区别

    • 重载:发生在同一个类中,方法名相同,方法的参数个数不同、顺序不同、类型不同;重载的时候返回值可以不同
    • 重写:发生在子父类中,子类重写父类的方法,方法名和参数必须相同,返回值类型要<=父类方法的返回值,异常要<=父类方法异常,访问权限修饰符>=父类访问权限修饰符,父类中的private修饰的方法不能重写。

    List和Set的区别

    • List存储的元素有序,按照元素插入的顺序进行保存;存储元素可以重复;可以存储多个Null对象;获取元素的时候除了可以使用Iterate迭代器取出所有元素之外,还可以使用get(int index)方法获取指定下标的元素
    • Set存储的元素无序;不可以存储重复元素;只能存储一个Null元素;只能通过Iterate迭代器获取所有元素

    ArrayList和LinkedList的区别

    ArrayList和LinkedList都实现了List接口,区别是底层的实现方式不同:

    • ArrayList的的底层实现是数组,LinkedList是链表
    • 在顺序遍历数据的时候,其实都差不多,都是从头遍历到尾
    • 在随机访问某一个数据的时候,ArrayList要快,可以直接通过索引查询,但是链表就需要一个元素一个元素的去遍历
    • 在尾巴处添加或删除元素,效率都差不多
    • 如果在中间位置添加/删除元素时,链表要快,因为数组还需要在插入位置之后的所有数据都要进行后移的操作,也就是数组还需要重新计算大小和更新索引
    • LinkedList比ArrayList更占用内存,因为它每一个节点存储了两个引用指针,一个指向前,一个指向后
    • LinkedList除了实现List接口之外,还实现了Deque接口,所以它还可以当做队列来使用。

    ConcurrentHashMap的扩容机制

    JDK1.7版本:

    ConcurentHashMap默认有16个Segment,每一个Segment可以看成一个HashMap,如果要进行扩容的话Segment是不会进行扩容的,只会是Segment内部的HashMap进行扩容,会创建一个容量*2的新数组,然后将扩容前旧HashMap中的元素一个一个的拷贝到新HashMap中去,然后在把Segment中的一个指针指向新HashMap

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WQGg3wQT-1659605378835)(E:\Java笔记\picture\image-20220725180057875.png)]

    JDK1.8版本:

    它只有一个数组了,扩容的时候就直接把当前这个数组进行扩容2两倍。那么现在就需要把旧数组中的key-value对象拷贝到新数组中来

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Wou7cSvD-1659605378837)(E:\Java笔记\picture\image-20220725181228810.png)]

    那么是怎么拷贝的嘞?JDK8有一个多线程扩容的特性,有多个线程,每个线程负责拷贝一块区域的数据,这些多个线程同时对旧数组中的元素进行拷贝到新数组中

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AdEfleRF-1659605378838)(E:\Java笔记\picture\image-20220725181353215.png)]

    总结:

    1.7版本

    • 1.7版本的ConcurrentHashMap是基于Segment分段实现的
    • 每个Segment相当于一个小型的HashMap
    • 每个Segment内部会进行扩容,和HashMap的扩容逻辑类似
    • 先生成新的数组,然后转移元素到新的数组中
    • 扩容的判断也是每个Segment内部单独判断的,判断每个Segment内部的HashMap是否超过阀值

    1.8版本

    • 1.8版本的ConcurrentHashMap不再基于Segment实现
    • 当某个线程进行put时,首先会判断当前ConcurrentHashMap是否正在进行扩容,如果是那么当前线程一起进行扩容操作,扩容完成后在进行put
    • 如果put时没有进行扩容,则将key-value添加到ConcurrentHashMap中,判断判断是否超过阀值,是否进行扩容
    • ConcurrentHashMap是支持多个线程同时进行扩容的
    • 扩容之前也是先生成一个新的数组
    • 在转移元素时,先将原数组分组,将每个分组分给不同的线程来进行元素的转移,每个线程负责一组或多组元素的转移工作

    1.7到1.8HashMap的变化

    • 1.7中底层是数组+链表,1.8中底层是数组+链表+红黑树;加红黑树的目的的提高HashMap中查询和插入的效率,因为插入事先也会进行查询操作判断元素是否存在,所以这里假如红黑树也是会提高插入的效率的
    • 1.7中链表使用的头插法,1.8中链表使用的是尾插法;因为1.8中需要统计链表的个数,所以需要遍历链表统计个数,所以就正好使用尾插发,默认达到8个就将链表转换为红黑树,减少为6个就将红黑树转换为链表。
    • 1.7中哈希算法更加复杂,存在各种右移与异或运算,1.8中进行简化,因为复杂的哈希算法是为了提高散列性,而1.8中引入了红黑树来提高效率,所以可以适当的简化哈希算法,节省cpu资源。

    HashMap的put方法

    • 首先根据key通过哈希算法和逻辑与运算得出数组的下标
    • 判断这个数组的下标位置是否为空,如果为空,则将key-value封装为entry对象(jdk1.7是entry对象,1.8是node对象)并放入该位置
    • 如果数值下标为不为null,则分情况讨论
      • 如果是JDK1.7,首先判断数组是否需要进行扩容,如果要扩容就先扩容,如果不用扩容就直接生成Entry对象使用头插法添加到当前位置的链表中
      • 如果是JDK1.8,首先判断当前位置上的Node类型是链表还是红黑树
        • 如果是红黑树,则进行遍历,查看当前要插入的元素是否已经存在,如果存在就进行替换,如果不存在就进行新增
        • 如果是链表,则使用尾插发插入到最后,在遍历的过程中判断如果当前对象已经存在就进行替换操作,当前Node节点插入到链表尾部后,会判断当前链表的个数是否大于等于8,如果是则进行链表转红黑树操作
        • 当插入操作完成后就判断是否需要进行扩容,如果不用根据结束put()操作

    jdk1.7是先判断是否扩容再插入,JDK1.8是先插入再判断是否要扩容

    HashMap的扩容机制

    JDK1.7版本

    • 先创建一个容量*2的新数组
    • 遍历老数组中每一个位置上链表的每一个元素
    • 取每一个元素的Key,根据新数组的长度计算出每个元素在新数组中的位置
    • 将元素添加到新数组中去
    • 将新数组赋值给HashMap对象的table属性

    JDK1.8

    • 先创建一个当前容量*2的新数组

    • 遍历老数组中每个位置上链表或红黑树的元素

    • 如果是链表就可JDK1.7的处理方式一样,直接计算出各个元素在新数组中的位置,并添加到新数组中去

    • 如果是红黑树,则先遍历红黑树,计算每个元素在新数组中的位置

      • (红黑树肯定是有多个元素,多个元素不一定在新数组的一个位置,假如红黑树中有20个元素,可能5个元素在下标1的位置,15个元素在下标6的位置)
      • 统计每个下标位置的元素个数,其实这里最多也就两个下标位置
      • 如果该位置下的元素个数>=8,则生成一个新的红黑树,并将根节点添加到新数组的对应位置
      • 如果该位置下的元素个数<8,那么则生成一个链表,并将链表的头节点添加到新数组的对应位置
    • 所有的元素转移完后,将新数组赋值给HashMap的table属性。

    深拷贝与浅拷贝

    • 浅拷贝是将对象中的基本类型的值、引用类型的引用地址拷贝到新增对象中,新老对象中的引用属性都是指向的同一个对象
    • 深拷贝,既会拷贝基本类型的值,也会对引用类型所指向的对象进行复制一份,深拷贝出来的对象内部属性指向的不是同一个对象。

    CopyOnWriteArrayList底层原理

    因为ArrayList不是线程安全的,所以我们需要一个线程安全的CopyOnWriteArrayList。它适用于读多写少的场景

    • 首先CopyOnWriteArrayList内部也是用数组来是实现的。在想CopyOnWriteArrayList添加元素时,会复制一个新数组,写操作在新数组上进行,读操作在原数组上进行
    • 并且,写操作会加锁,避免多个写线程并发写入丢失数据问题
    • 写操作结束后会把原数组指向新数组
    • CopyOnWriteArrayList允许在写操作时来读取数据,大大提高了读的性能,因此适合读多写少的场景,但是CopyOnWriteArrayList会比较占用内存,同时可能读取到的数据不是实时最新的数据,所以不适合实时性要求很高的场景

    什么是字节码?采用字节码的好处

    我们通过JDK中的编译器javac将java源文件编译成字节码文件后,可以做到一次编译到处运行,windows编译好的.class文件,linux上可以直接运行,通过这种方式做到跨平台。

    虽然字节码文件是通用的,但是需要把字节码文件解释成各个操作系统的机器码是需要不同的解释器的,所以我们需要针对不同的操作系统安装不同的JDK或JRE

    采用个字节码的好处是,一方面实现了跨平台,另一方面也提高了代码执行的性能,编译器在编译源代码时可以做一些编译器的优化,比如锁消除、标量替换、方法内联等。

    java的异常体系

    java中的所有异常都来自顶级父类Throwable,Throwable下有两个子类Exception和Error

    • Error表示非常严重的错误,比如OutOfMemoryError内存溢出,仅仅靠程序自己是解决不了的

    • Exception表示异常,当程序出现异常时可以靠程序自己来解决,Exception又分为运行期异常和编译期异常

      • 运行期异常表示这个异常是在程序运行过程中抛出来的,比如空指针异常和数组下标越界异常
      • 5编译期异常,也称为检查异常,是必须要进行处理的异常,如果不进行处理程序就不能编译。比如IOException、SQLException等

    异常什么时候该抛出/处理

    在本方法中,是否能够合理的处理这个异常,如果能处理就进行捕获处理,如果不能处理就向上抛出

    java中有哪些类加载器

    JDK自带三个类加载器:Bootstrap ClassLoader、ExtClassLoader、AppClassLoader

    • Bootstrap ClassLoaderExtClassLoader的父类加载器,默认负责加载%JAVA_HOME%lib下的jar包和class文件
    • ExtClassLoaderAppClassloader的父类加载器,复制加载%JAVA_HOME%/lib/ext文件夹下的jar包和class文件
    • AppClassloader是自定义类加载器的父类,复制加载classpath下的类文件。

    类加载器的双亲委派模型

    JVM中存在三个类加载器:BootStrapClassLoader、ExtClassLoader、AppClassLoader

    AppClassLoader的父类是ExtClassLoader,ExtClassLoader的父类是BootStrapClassLoader

    JVM在加载一个类的时候,会通过AppClassLoader的loadClass()方法来加载这个类,在这个方法中会先调用ExtClassLoader类中的loadClass()方法来加载,同样,在ExtClassLoader类中的loadClass()方法中会先调用BootstrapClassLoader类的loadClass()方法来加载。如果BootstrapClassLoader类加载到了就直接成功,如果没有加载到就通过ExtClassLoader来加载,如果还是没有加载成功再通过AppClassLoader来加载。

    所以,双亲委派指的是,JVM在加载类时,会委派给Ext和Bootstrap进行加载,如果没有加载到才由自己来进行加载

    JVM中哪些是线程共享区

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8wZwJMbV-1659605378840)(E:\Java笔记\picture\image-20220726184903904.png)]

    堆和方法区是线程共享的,栈、本地方法栈、程序计数器是每个线程独享的。

    栈:

    • 描述的是方法执行的内存模型,每个方法调用都会创建一个栈帧
    • JVM为每一个线程都创建一个栈,用于存储该线程执行方法的信息(参数、局部变量等)
    • 它的存储特性的先进后出
    • 栈由系统自动分配,速度快,是连续的内存空间

    • 用于存储创建好的对象和数组
    • JVM只要一个堆,被所有线程共享
    • 堆是一个不连续的内存空间,分配灵活,速度慢

    方法区:

    • JVM只有一个方法区,被所有线程共享
    • 方法区实际也是堆
    • 用于存放程序中永远不变或唯一的内容。比如类信息Class对象、静态变量、常量等

    比如下方的以为数组内存图,栈中存储方法相关的信息包括局部变量等,堆中存放创建好了的对象,然后局部变量在指向堆中的对象地址。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CkQTrALL-1659605378845)(E:\Java笔记\picture\image-20220726185914312.png)]

    项目中如何排查JVM问题

    对于还在正常运行的系统:

    • 可以使用jmap来查看JVM中各个区域的使用情况
    • 可以使用jstack来查看线程的运行情况,比如哪些线程阻塞、是否出现了死锁
    • 可以通过jstat命令来查看垃圾回收的情况,特别是full GC ,如果发现full GC比较频繁,那么就得进行调优了
    • 通过各个命令的结果,或者jvisualvm等工具来进行分析
    • 首先,初步猜测频繁发生full GC的原因,如果频繁发生Full GC但又没有出现内存溢出OOM,那么表示Full GC实际上是回收了很多对象了,所以这些对象最好在youngGC过程中就直接回收掉,避免这些对象进入到老年代,对于这种情况,就要考虑这些存活时间不长的对象是不是比较大,导致年轻代放不下,直接进入老年代了,尝试加大年轻代的大小,如果改完之后,Full GC减少则证明修改有效
    • 同时,还可以找到占用CPU最多的线程,定位到具体的方法,优化这个方法的执行,看看是否能避免某些对象的创建,从而节省内存

    对于已经发生OOM的系统:

    1. 一般生产系统中都会设置当系统发生了OOM时,生成当时的dump文件(设置如下2个参数)

      -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof
      -XX:+HeapDumpOnOutOfMemoryError 设置当首次遭遇内存溢出时导出此时堆中相关信息
      -XX:HeapDumpPath=/tmp/heapdump.hprof 指定导出堆信息时的路径或文件名

    2. 我们可以利用jsisualvm等工具来分析dump文件

    3. 根据dump文件找到异常的实例对象和异常的线程(占用cpu高),定位到具体的代码

    4. 然后再进行详细的分析和调试。

    类加载到GC清除经历的过程

    1. 首先把字节码文件内容加载到方法区
    2. 然后在根据类信息在堆区中创建对象
    3. 对象首先会分配在堆区中年轻代的Eden区,经过一次Minor GC后,对象如果存活就会进入到Suvivor区。在后续的每次Minor GC中,如果对象一直存活,就会在Suvivor区来回拷贝,每移动一次,年龄+1
    4. 当年龄超过15后,对象依然存活就会进入到老年代
    5. 如果经过Full GC,被标记为垃圾对象,那么就会被GC线程清理掉

    怎么确定对象是不是垃圾对象

    1. 引用计数法:为堆中的每一个对象记录一个引用的个数,如果个数为0就表示该对象是垃圾对象。这是早期的jDK使用的方式,它无法解决循环引用的问题
    2. 可达性算法:在内存中,从根对象向下一直找引用,找到的对象不是垃圾对象,没有找到的对象就是垃圾对象

    JVM有哪些垃圾回收算法

    • 标记清除算法:该算法分为两个阶段,标记阶段将垃圾内存标记出来,清除阶段直接清除将垃圾内存回收;这种算法比较简单,缺点是会找出内存碎片化
    • 复制算法:为了解决标记清除算法产生的内存碎片化问题,就产生了复制算法。复制算法将内存分为大小相同的两半,每次使用其中一半。垃圾回收时将存活的对象拷贝到另一半,然后清除当前这一半。这种算法没有内存碎片化问题,缺点是比较浪费空间。
    • 标记压缩算法:为了解决复制算法的缺陷就产生了标记压缩算法。该算法和标记清除算法一样,在完成标记阶段后,不是直接清理,而是将存活对象往一端移动,然后见边界以外的内存直接清理。

    什么是STW

    STW:stop the world,停止整个世界。是在垃圾回收算法执行过程中,将JVM的内存内存冻结的一种状态。在STW状态下,java除了GC线程之外的线程都停止执行。所以JVM调优的重点就是减少STW。

    常用的JVM启动参数

    JVM的启动参数非常多,常用的JVM配置参数也就10来个,如下所示

    # 设置堆内存
    -Xmx4g -Xms4g
    
    # 指定GC算法
    -XX:+UseG1GC -XX:MaxGCPauseMillis=50
    
    # 指定GC并行线程数
    -XX:ParallelGCThreads=4
    
    # 打印GC日志
    -XX:+PrintGCDetails -XX:+PrintGCDateStamps
    
    # 指定GC日志文件
    -Xloggc:gc.log
    
    # 指定Mete区的最大值
    -XX:MaxMetaspaceSize=2g
    
    # 设置单个线程栈的大小
    -Xss1m
    
    # 指定对内存溢出时自动进行Dump
    -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    线程

    对线程安全的理解

    线程安全指的是某一段代码,在多线程同时执行的时候不会产生混乱,能够得到正常的结果。就比如i++ ,i的初始组为0,如果两个线程都执行这行代码,那么i的结果应该是2。如果出现了两个线程的结果都是1,则表示这段代码不是线程安全的。

    所以线程安全这段就是多个线程同时执行一段代码能否得到正确的结果。

    守护线程

    在java中线程分为用户线程和守护线程。用户线程就是普通的线程,而守护线程是JVM的后台线程,垃圾回收就是守护线程,守护线程和普通该线程的一个主要区别就是,普通线程执行完后就停止了,而守护线程是会一直运行的,当所有普通线程都停止运行之后守护线程才会自动关闭。

    我们可以通过thread.setDaemon(true)来把一个线程设置为守护线程。

    ThreadLocal的底层原理

    1. ThreadLocal是java中提供的线程本地缓存机制,可以利用该机制将数据缓存到某个线程内部。该线程再任何时刻任何方法中都可以获取到缓存的数据
    2. ThreadLocal底层是通过ThreadLocalMap来实现的,每个Thread线程对象中都存在一个ThreadLocalMap,该map的key是ThreadLocal,value是需要缓存的值
    3. 如果在线程池中使用ThreadLocal会造成内存泄漏,因为当ThreadLocal对象使用完之后,应该把key-value 也就是Entry对象进行回收,但线程池中的线程不会回收,我们需要在使用了ThreadLocal对象之后,手动调用remove()方法进行清除Entry对象
    4. ThreadLocal经典的应用场景就是连接管理(一个线程持有一个连接,该连接对象可以在不同的方法之间进行传递,线程之间不共享一个连接)
    public class Test {
    
        private ThreadLocal<String> threadLocal = new ThreadLocal<String>();
    
        public void setName(){
            threadLocal.set("胡尚");
        }
        
        public void getName(){
            String name = threadLocal.get();
            threadLocal.remove();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    源码如下:

    // 首先获取当前线程对象,然后获取当前线程对象中的属性ThreadLocalMap,再往这里面存值,key是我们创建的ThreadLoca对象,value是我们要存的值
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
    
    // 首先获取当前线程对象,然后获取当前线程对象中的属性ThreadLocalMap,再通过key获取到Entry对象再获取到value返回
    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }
    
    // 我们可以创建多个ThreadLocal对象,往当前线程中的ThreadLocalMap中存储多个值,特别的当前线程还是线程池中的线程时,这个线程就不会轻易被回收,那么线程中的ThreadLocalMap就会越来越大,就有可能造成内存泄漏,所以当我们确定某个值使用完成后就及时的调用remove()方法删除
    public void remove() {
        ThreadLocalMap m = getMap(Thread.currentThread());
        if (m != null)
            m.remove(this);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    并发、并行、串行

    • 串行:同一时刻只有一个任务在执行,一个任务执行完后才会去执行下一个
    • 并行:多个任务可以同时进行
    • 并发:从整体上来看两个任务是同时进行的,但底层两个任务被拆分成了很多份,然后一个一个执行。也就是整体来看是并行的但是底层实际上是串行。

    如何避免死锁

    首先我们需要知道造成死锁的原因:

    1. 一个资源每次只能被一个线程使用
    2. 线程再阻塞等待某个资源时,不释放已占有的资源
    3. 线程在资源使用完成之前不能被强行剥夺
    4. 若干个线程形成头尾相接的循环等待资源关系

    这是造成死锁的四个必要条件,如果要打破死锁只要不满足其中任意一个条件即可。而其中前三个条件是锁的必要条件,所以要避免死锁就需要打破第四个条件,不出现循环等待锁的关系。

    • 注意加锁的顺序,保证每个线程按同样的顺序进行加锁
    • 注意加锁实时限,可以针对锁设置一个超时时间
    • 注意死锁检查,这是一种预防机制,确保在第一时间发现死锁并进行解决。

    线程池的底层工作原理

    线程池内部是通过队列+线程实现的。当我们利用线程池执行任务时:

    1. 先要保证线程池中的线程数量达到核心线程数corePoolSize。

      如果此时线程池中的线程数小于corePoolSize,即使线程池中的线程处于空间状态也要先创建新的线程达到corePoolSize后再来处理被添加的任务

    2. 线程池数量等于corePoolSize,并且都在执行任务没有空闲,但是缓存队列workQueue未满,那么任务被放入到缓存队列中

    3. 线程池数量>=corePoolSize,并且workQueue也满了,但是线程池的数量

    4. 如果线程次数量>=corePoolSize,并且workQueue也满了,线程池的数量等于maxmumPoolSize,那么就通过handler指定的拒绝策略来处理该任务

    5. 当线程池中的数量>=corePoolSize,如果某些线程空闲时间超过了keepAliveTime,该线程将被终止。

    总结:先创建核心线程数的线程–>再用缓存队列–>再创建线程达到最大线程数–>如果还不行就触发拒绝策略 --> 如果线程数大于了核心线程数,线程的空闲时间超过了keepAliveTime线程就被终止。

    线程池为什么先添加队列而不是先创建最大线程

    拿生活的例子来说,本来公司10个程序员能够正常进行业务的开发,随着公司的发展业务需求越来越多,需求就先写在需求列表中,程序员加班加点还是勉勉强强能完成,当需求列表都写满了,就不得不再招人了。

    创建线程和销毁线程需要cpu资源的,就比如公司只是某一段时间需求很多,这个时候招人与辞退需要成本的。

    ReentrantLock中的公平锁和非公平锁

    首先不管是公平锁还是非公平锁,都是基于AQS排队机制的。区别是 线程在使用lock()加锁时,如果是公平锁,线程首先会检查AQS队列中是否有线程再进行排队,如果有就跟着排队;而非公平锁是不管AQS队列中是否有线程再排队,都去尝试直接竞争锁

    不管是公平锁还是非公平锁,一旦没有竞争到锁都会进入到AQS队列中排队,当锁释放时,都是唤醒排在最前面的线程,所以非公平锁只是体现在了线程加锁阶段,而没有体现在线程被唤醒阶段

    另外ReentrantLock是可重入锁,不管是公平锁或非公平锁都是可重入的。

    synchronize和ReentrantLock都是可重入锁,可重入锁是线程得到了当前对象的锁后,可以在锁中再次进入带有锁的方法。

    ReentranLock中tryLock()和lock()方法的区别

    • tryLock()表示尝试加锁,可能会加锁,也可能不会加锁,该方法不会阻塞线程,如果加到锁就返回true,没有加到锁就返回false,我们需要进行判断来执行相应的业务逻辑
    • lock()方法表示阻塞加锁,线程会阻塞直到加到锁。方法也没有返回值

    CountDownLatch和Semaphore

    CountDownLatch表示计数器,可以给CountDownLatch设置一个数字,线程调用await()方法将会阻塞,其他线程调用countDown()方法将会对数字减一,当数字被减成0后所有await的线程都将被唤醒。

    对应的底层原理就是,调用await()方法的线程会利用AQS排队,一旦数字被减为0,则会将AQS中排队的线程依次唤醒

    Semaphore表示信号量,可以设置许可的个数,表示同时允许多少个线程使用该信号量,通过调用acquire()来获取许可,如果没有许可则阻塞,并通过AQS队列排队,可以通过release()方法释放许可,当某个线程释放许可后,会从AQS中正在排队的第一个线程依次唤醒,直到没有空闲许可。

    Synchronized的偏向锁、轻量级锁、重量级锁

    • 偏向锁:在锁对象的对象头中记录一下当前按获取到该锁的线程id,该线程下次如果又来获取该锁就可以直接获取到了
    • 轻量级锁:由偏向锁升级而来,当一个线程获取到锁后,此时这把锁是偏向锁,此时有来了一个线程来竞争锁,偏向锁就会升级为轻量级锁。轻量级锁底层是通过自旋锁来实现的,并不会阻塞线程。
    • 自旋锁:自旋锁就是线程在获取锁的过程中,不会去阻塞线程,也就没有唤醒线程了,阻塞和唤醒这两个操作都是操作系统去进行的,比较消耗时间。自旋锁是线程通过CAS算法获取预期的一个标记,如果没有获取到,则继续循环获取,如果获取到了则表示获取到了锁,这个过程线程一直在运行中,相对而言没有使用太多操作系统资源,比较轻量
    • 重量级锁:如果自旋次数过多就会升级为重量级锁,重量级锁会导致线程阻塞。

    Synchronized和ReentrantLock的区别

    • Synchronized是关键字,ReentrantLock是类
    • Synchronized会自动加锁与释放锁,ReentrantLock需要程序手动加锁与释放锁
    • Synchronized是JVM层面的锁,ReentrantLock是API层面的锁
    • Synchronized是非公平锁,ReentrantLock可以选择,默认也是非公平锁
    • Synchronized锁的是对象,锁信息保存在对象头中,ReentrantLock通过代码中int类型的state标识来标识锁的状态
    • Synchronized底层有一个锁升级的过程

    AQS理解、AQS如何实现可重入锁

    AQS同步队列,在AQS中主要是维护了一个state变量和一个双向链表的队列,这个队列是给线程去排队的,里面存储的就是一个一个的线程对象,state是一个标记,用来控制线程去排队或者获取锁放行。

    在可重入锁的场景下,state表示加锁的次数。0表示没有加锁,没加一次锁,state就加1,释放锁就减1。

    TCP的三次握手四次挥手

    TCP协议是传输层中的协议,负责数据的可靠传输。

    在建立TCP连接时需要通三次握手来建立,过程是:

    1. 客户端向服务器发送一个SYN
    2. 服务器接收到到SYN后,向客户端发送给一个SYN_ACK
    3. 客户端接收到了SYN_ACK 后,再给服务器端发送一个ACK

    在断开连接时,TCP需要通过四次挥手来断开,过程是:

    1. 客户端想服务器端发送FIN
    2. 服务端接收到FIN后,向客户端发送ACK,表示我接收到了断开连接的请求,客户端你可以不发数据了,不过服务端这边可能还有数据正在处理
    3. 服务端处理完所有数据后,向客户端发送FIN,表示服务端现在可以断开连接了
    4. 客户端收到服务器的FIN后,向服务端发送ACK,表示客户端也会断开连接了

    浏览器请求到响应经历的过程

    1. 浏览器解析用户输入的URL,生成一个HTTP格式的请求
    2. 先根据url域名从本地hosts文件查找是否有映射ip,如果没有则将域名发送给电脑所配置的DNS进行域名解析,得到ip
    3. 浏览器通过操作系统将请求通过四层网络协议发送出去
    4. 途中可能会经过各种路由器、交换机,最终达到服务器
    5. 服务器收到请求后,根据请求所指定的端口,将请求传递给绑定了该端口的应用程序,比如8080被tomcat占用了
    6. tomcat接收到请求数据后,安装http协议的格式进行解析,解析得到所要访问的servlet
    7. 然后servlet来处理这个请求,如果是SpringMVC中的DispatcherServlet,那么则会找到对应的Controller方法执行得到结果
    8. tomcat得到响应结果后封装成http响应的格式,并再次通过网络发送给浏览器所在的服务器
    9. 浏览器所在的服务器拿到响应结果后再传递给浏览器,浏览器复制解析并渲染。

    跨域请求

    框架

    Spring

    谈谈你对IOC的理解

    spring的两大特性:IOC和APO;IOC就是控制反转

    我们可以从以下几个点来分析:

    1. 什么是控制?控制了什么?
    2. 什么是反转?反转之前是谁控制的?反转之后是谁控制的?如何控制的?
    3. 为什么要反转?反转之前有什么问题?反转之后有什么好处?

    我们在使用Spring的时候,我们会定义类,并使用@Autowrite注解,这个时候获取到的对象是已经创建好了的并且属性中也是有值的,这就是控制,将对象的创建和对象属性的赋值交给了spring来控制,如果不使用spring,那么对象的控制权就需要我们自己来进行,而使用spring就将对象的控制权转移给了Spring,这就是反转

    如果不使用反转,那么类的创建与属性的赋值就需要我们自己来进行,就比如现在有三个类:

    • A类中有一个属性 C c
    • B类中又给属性C c
    • C类

    我们需要编写的代码如下

    A a = new A();
    B b = new B();
    C c = new C();
    a.c = c;
    b.c = c;
    
    • 1
    • 2
    • 3
    • 4
    • 5

    如果类多一点,属性多一点,我们就需要写非常多的这些代码。所以我们可以将控制权交由spring来控制。

    总结:IOC是控制反转,如果用Spring,那么对象的创建以及对象属性的赋值将由spring来进行,对象的控制权将交由spring。

    单例Bean和单例模式

    单例模式表示JVM中某个类的对象只会存在一个

    而单例Bean并不表示JVM中只能存在唯一的某个类的Bean对象,单例Bean是表示在spring容器中,通过一个bean的名字获取到的是同一个对象。就比如User类型的对象可以在Spring容器中存在多个,但是他们的名字可能叫user1、user2、user3, 我们通过bean的名字获取到的是同一个对象。

    Spring事务传播机制

    事务的传播机制是:在使用spring事务的过程中,我们调用一个方法会开启一个事务,当这个方法再去调用其他方法的时候要不要开启一个新的事务,还是说共用一个事务,还是不以事务的方式运行这些都是可以去选择的。

    如下图所示有七种事务传播行为

    在这里插入图片描述

    Spring事务什么时候会失效

    Spring事务的原理是AOP,要查看事务是否会失效主要就是看是否是Spring事务所产生的代理对象在调用这个方法。

    • 发生了自调用,类里面使用this调用类的方法(this通常省略),此时这个this对象不是代理对象而是当前类对象本身。解决方法就是主动注入当前类对象,通过注入的对象去调用。
    • 方法不是public修饰符
    • 数据库不支持事务
    • 当前类是否被Spring容器管理
    • 有没有捕获异常没有进行抛出。

    Spring中的Bean创建的生命周期有哪些步骤

    1. 推断构造方法;要创建一个对象就需要判断到底该使用哪一个构造方法
    2. 实例化;利用构造方法实例化得到一个对象
    3. 填充属性;得到对象后就会去给对象中的属性赋值,也就是依赖注入
    4. 处理Aware回调接口;依赖注入完成后就会去处理一些回调的接口
    5. 初始化前,处理@PostConstruct注解
    6. 初始化,处理InitializingBean接口;主要就是看当前创建的这个Bean上是否实现了InitializingBean接口,如果实现了就回去调用重写接口中的afterPropertiesSet()方法
    7. 初始化后,进行AOP

    Spring中的Bean是线程安全的吗

    首先Spring并没有对Bean做线程安全的处理,Bean其实就是一个对象,这个对象是不是线程安全的还是要看这个Bean本身

    • 如果这个Bean是无状态的,那么就是线程安全的
    • 如果这个Bean是有状态的,那么就是线程不安全的

    有状态指的就是如果有方法对Bean内部属性的值有修改操作,那么这个bean 就是有状态的。

    ApplicationContext和BeanFactory区别

    首先ApplicationContext和BeanFactory都是spring的Bean工厂,可以生成Bean,维护Bean。

    从源码中可以知道,ApplicationContext接口继承了BeanFactory接口。区别是ApplicationContext除了继承BeanFactory接口之外还继承了很多其他的接口,也就还有很多其他的功能,比如:获取系统环境变量、国际化、事件发布等功能

    // 其中的ListableBeanFactory, HierarchicalBeanFactory都继承了BeanFactory接口
    // 所以ApplicationContext也继承了BeanFactory接口
    public interface ApplicationContext extends EnvironmentCapable, ListableBeanFactory, HierarchicalBeanFactory, MessageSource, ApplicationEventPublisher, ResourcePatternResolver {
        String getId();
    
        String getApplicationName();
    
        String getDisplayName();
    
        long getStartupDate();
    
        ApplicationContext getParent();
    
        AutowireCapableBeanFactory getAutowireCapableBeanFactory() throws IllegalStateException;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    Spring中的事务是如何实现的

    首先我们知道Spring的事务是基于AOP来实现的,那么具体大致的实现步骤如下所示:

    • 对于使用了@Transaction注解的Bean,spring就会创建一个代理对象

    • 当代理对象去调用方法时会判断方法上面是否加了@Transaction注解

    • 如果加了,那么就会利用事务管理器创建一个数据库连接对象

    • 将自动提交autocommit改为false

    • 执行当前方法,方法中 执行sql

    • 执行完成后如果没有出现异常就提交事务

    • 如果出现才异常,并且这个异常是需要回滚的就回滚事务,否则仍然提交

    spring的事务隔离级别对应的是数据库的隔离级别

    spring事务的传播机制是Spring事务自己实现的,也是Spring事务最复杂的的,它是基于数据库连接来做的,一个数据库连接对应一个事务,如果事务传播机制需要重新开一个事务实际上就是重新创建一个数据库连接,在此新数据库连接上执行sql。

    Spring容器启动流程

    Spring要使用的话肯定是需要先启动Spring容器的,启动Spring容器的主要目的是为了创建Bean对象做一些准备。

    1. 在启动Spring时,首先会进行扫描,扫描得到所有的BeanDefinition对象,并存入Map中

    2. 扫描筛选出非懒加载的单例BeanDefinition进行创建Bean。多例Bean不需要在启动的时候进行创建,它会在每次获取Bean时利用BeanDefinition去创建。懒加载的也是一样的。

    3. 利用BeanDefinition创建Bean,也就是Bean的创建生命周期,包括合并BeanDefinition、推断构造函数、实例化、依赖注入进行属性赋值、初始化前@PostConstruct注解、初始化、初始化后的步骤 AOP

    4. Bean创建完成后,会发布一个容器启动事件

    5. Spring容器启动结束

    在源码中会更加复杂,比如源码中会提供一些模板方法让子类实现、源码中还涉及到BeanFactoryPostProcessor和BeanPostProcessor的注册,Spring的扫描就是在BeanFactoryPostProcessor来实现的,依赖注入就是通过BeanPostProcessor来实现的、还有@Import等注解的处理

    Spring中的设计模式

    • 工厂模式:Spring通过ApplicationContext和BeanFactory来创建对象
    • 原型模式:多例Bean其实就是使用的原型模式
    • 单例模式:Bean默认为单例模式
    • 构建器模式:比如BeanDefinition构造器BeanDefinitionBuilder、StringBuilder等等
    • 策略模式:例如Resource的实现类,针对不同的资源文件实现了不同的资源获取策略
    • 代理模式:AOP功能
    • 模板方法模式:可以将相同的代码放在父类中,将不同的代码放入不同的子类总,用来解决代码重复问题。比如RestTemplate。
    • 适配器模式:Spring AOP的增强Advice使用了适配器模式
    • 观察者模式:Spring事件驱动模型就是观察者模式的一个经典应用

    SpringBoot

    SpringBoot常用注解

    1. @SpringApplication注解,这个注解其实是由下面三个注解组成的复合注解
      • @SpringBootConfiguration,它实际上就是一个@Configuration注解,标记主启动类也是一个配置类
      • @EnableAutoConfiguration,向sprig容器中导入一个Selector,用来加载classpath下的spring.factories中定义的自动配置类,将这些自动配置类加载为配置Bean
      • @ComponentScan:标识扫描路径,因为默认是没有配置实际扫描路径,所以SpringBoot扫描的路径是启动类所在的当前目录
    2. @Bean注解,往Spring容器中添加一个Bean,在Spring启动的时候会对加了@Bean注解的方法进行解析,将方法名作为Bean的beanName,并通过执行方法得到Bean对象。
    3. @Controller、@service、@ResponseBody、@Autowired

    SpringBoot是如何启动Tomcat的

    1. SpringBoot在启动时首先会创建一个Spring容器
    2. 在创建Spring容器的过程中,会通过@ConditionOnClass注解来判断当前classpath中是否存在tomcat的依赖
    3. 如果存在则会生成一个启动Tomcat的Bean
    4. Spring容器创建完之后,就会获取启动Tomcat的Bean,并创建Tomcat对象、绑定端口等,然后启动Tomcat。

    SpringBoot中配置文件的加载顺序是怎样的

    以jar包发布springboot项目时,默认会先使用jar包同级目录下的application.properties来作为项目配置文件。但使用–spring.config.location指定了配置文件,则读取指定的配置文件。

    如果在不同的目录中存在多个配置文件,它的读取顺序是:

    1. config/application.properties(项目同级目录中config目录下)
      config/application.yml
    2. application.properties(项目同级目录下)
      application.yml
    3. resources/config/application.properties(项目resources目录中config目录下)
      resources/config/application.yml
    4. resources/application.properties(项目的resources目录下)
      resources/application.yml

    mybatis

    Mybatis的优缺点

    优点

    • SQL语句可以写在xml文件中,与java代码解耦合。
    • 消除了大量的JDBC冗余的代码
    • 支持对象与数据库ORM字段关系映射

    缺点:

    • SQL语句依赖于数据库,导致数据库的移植性差,不能随意更换数据库。

    #{}与${}的区别

    #{}是预编译处理、是占位符;可以防止SQL注入,提高系统安全性

    ${}是字符串替换、是拼接符

    mybatis在处理#{}时,会将SQL中的#{}替换为? 然后调用PreparedStatement来赋值

    mybatis在处理 时,会将 S Q L 中的 {}时,会将SQL中的 时,会将SQL中的{}替换成变量的值,调用Stetement来赋值

    分布式

    CAP理论,BASE理论

    Consistency一致性:更新操作发生后,所有的节点在同一时间的数据要保证完全一致

    Availability可用性:即服务一直是可用的状态

    Partition Tolerance分区容错性:即分布式系统在遇到某个节点或网络分区故障的时候,仍然能够对外提供满足一致性和可用性的服务。比如分布式系统中某几个服务器宕机了,剩下的服务器还能够正常运转满足系统的需求,对于用户而言并没有什么体验上的影响。

    CP和AP:分区容错性是必须保证的,当发生网络分区的时候,如果要继续提供服务,那么强一致性和可用性只能二选一。在分布式集群中,某一个节点与其他节点网络断开了,这就是网络分区,这个时候有一些更新操作,这时数据是不一致的,那么就需要选择了,一是继续对外提供服务,但是暂时数据不一致;二是先保证数据一致性,但是在处理一致性的过程中暂时不能对外提供服务,所以只能二选一

    BASE理论

    • Basically Available(基本可用)
    • Soft state(软状态)
    • Eventually consistent(最终一致性)

    BASE理论是对CAP中一致性和可用性权衡的结果,是基于CAP理论逐步演化而来的。BASE理论的核心思想是:即使无法做到强一致性,但每个应用都可以根据自身业务特点采用适当的方式来使系统达到最终一致性

    基本可用:

    • 响应时间上的损失:正常情况下,处理用户请求需要0.5s,但是由于系统出现故障处理用户请求的时间变为3s
    • 系统功能上的损失:正常情况下,用户可以使用系统的全部功能,但是由于系统访问量忽然暴增,系统的部分非核心功能无法使用

    软状态:数据同步允许一定的延迟

    最终一致性:系统中所有的数据副本在经过一段时间的同步后,最终能够达到一个一致的状态,不要求实时。

    数据一致性模型有哪些

    • 强一致性:整个分布式系统中所有的数据都要保持一致性
    • 弱一致性:可以忍受数据不一致性
    • 最终一致性:在分布式系统中所有的数据,不用一开始就一致,可以用一点时间去进行同步,最终能够达到一个一致的状态。本质是需要系统保证最终数据能够达到一致而不需要实时保证系统数据的强一致性。

    什么是RPC

    RPC,远程过程调用,对于java这种面向对象语言来说我们可以翻译为远程方法调用。

    PRC调用和HTTP调用是有区别的,RPC表示的是一种调用远程方法的方式,我们只要保证A进程中的方法能够调用B进程中的方法就行了,底层可以使用HTTP协议或者直接基于TCP协议来实现RPC。

    在java中,我们可以通过直接使用某个服务器接口的代理对象来执行方法,而底层则通过构造HTTP请求来调用远端的方法,所以有一种说法是RPC协议是HTTP协议之上的一种协议。

    分布式id的解决方案

    • uuid:最简单,复杂度低。缺点是影响存储空间和性能,可能泄漏mac地址
    • 利用单机数据库的自增主键,作为分布式id的生成器。这就需要有一个数据库专门用来生成全局id。在高并发的情况下这种方式比较影响性能,因为需要多操作一次数据库
    • 利用redis、zookeeper的特性来生成id,比如redis自增命令、zookeeper的顺序节点。性能上面肯定要比第二种方式数据库去生成高。
    • 雪花算法:比较常用,底层原理就是通过某台机器在某一毫秒内对某一个数字自增。

    分布式锁实现方案

    在单体架构中,加锁是为了解决多个线程去访问同一个资源的并发安全。ReentrantLock、Synchronized都是一个进程中去控制多个线程的锁机制。

    而在分布式架构中,有多个进程分别运行在不同的服务器上面,这种情况下我们也需要去控制一些共享的资源就需要使用分布式锁

    现在主流的分布式锁的实现方案有两种:

    • zookeeper:利用得到是zookeeper的临时节点、顺序节点、watch机制来实现的,zookeeper分布式锁的特点是高一致性,因为zookeeper是CP原则,所以它实现的分布式锁更可靠,不会出现混乱
    • redis:利用redis的setnx、lua脚本、消息订阅等机制来实现的,redis分布式锁的特点是高可用,因为redis保证的是AP,所以由它实现的分布式锁可能不可靠,不稳定,一旦redis中的数据出现了不一致,可能会出现多个客户端同时加到锁的情况

    什么是ZAB协议

    ZAB协议是zookeeper用来实现一致性的原子广播协议,该协议描述了zookeeper是如何实现一致性的,分为三个阶段:

    1. 领导者选举阶段:从zookeeper集群中选出一个节点做为leader,所有的写请求都会由leader节点来处理
    2. 数据同步阶段:集群中所有节点中的数据都要和leader保存一致,如果不一致则要进行同步
    3. 请求广播阶段:当leader节点接收到写请求时,会利用两阶段提交来广播该写请求,使写请求像事务一样在其他节点上执行,达到节点上的数据实时一致

    但值得注意的是,zookeeper只是尽量的在达到强一致性,实际上仍然只是最终一致性

    负载均衡算法有哪些

    • 轮询
    • 权重
    • 随机
    • 源地址哈希
    • 加权轮询
    • 加权随机
    • 最小连接数

    Session共享方案

    1. 采用无状态服务,抛弃session。比如采用JWT
    2. 存入cookie中,但是有安全风险,因为cookie是存储在客户端的,有一定的安全风险
    3. 服务器之间进行session同步,缺点是比较占用资源,每一个服务都要保存全量的session信息,其次同步还会涉及到同步延迟甚至是同步失败的问题。
    4. IP绑定策略, 使用Nginx中的ip绑定策略,让同一个ip只能在指定的同一台服务器访问,缺点是失去了负载均衡的意义,还有就是一台服务器挂掉之后会影响一批用户的使用,风险较大。
    5. 使用redis存储,把session存储在redis中,虽然需要多访问一次redis,但是这种方案带来的好处是很大的
      • 实现了session共享
      • 可以水平扩展,增加redis服务器
      • 服务器重启session也不会丢失
      • 不仅仅可以跨服务器session共享,甚至可以跨平台,例如网页端和app端。

    如何实现接口的幂等性问题

    因为网路等其他问题,可能客户会在短时间内点击多次按钮,那么后台会就接收到多次请求,这就是幂等性问题,当前前端可以使用防抖解决。

    幂等性:在高并发场景下,相同的请求参数,重复点击按钮发送请求,能不能保证数据是正确的

    实现方法如下:

    • 唯一id。每次操作前都根据操作和内容生成唯一id,保存在数据库或者redis中,在执行之前先判断id是否存在,如果不存在则执行后续操作
    • 服务器提供发送token接口,前端先调用接口获取token,然后在调用业务方法,把token携带过去,首先判断token是否存在redis中,如果存在则表示第一次请求,可以继续执行业务,执行业务完成后需要删除redis中的token
    • 建去重表。将业务中有唯一标识的字段保存到去重表中,如果表中存在则表示已经处理过了
    • 版本控制。增加版本号,当版本号符合时才能更新数据,也就是乐观锁
    • 状态控制。例如订单的状态不能从已支付改为未支付,当处于未支付的时候才允许修改为已支付。

    存储拆分后如何解决唯一主键

    也就是分布式id问题,分库分表之后的主键如何生成

    • uuid:简单,缺点是没有顺序,存在泄漏mac地址的风险
    • 数据库主键:可以修改起始值不一样,步长保持一致;缺点是扩展性差
    • redis、mongodb、zookeeper等中间件:缺点是增加了系统的复杂性
    • 雪花算法

    什么是服务雪崩、服务限流

    服务雪崩:在微服务调用链路中,由于某个服务不可用导致上游服务不停的挂掉,解决方法就是服务降级和服务熔断

    服务限流:对访问服务的请求进行数量上的限制

    什么是服务熔断、服务降级

    服务熔断:服务A调用服务B不可用时,为了保证本身服务不受影响,从而不再调用服务B,直接返回一个结果,直到服务B恢复

    服务降级:当发现系统压力过载时,可以通过关闭某个服务或者限流某个服务来减轻系统压力

    服务熔断是下游服务故障触发的,服务降低是为了降低系统的负载

    DDD领域驱动设计

    DDD是一个思想,一个方法论 。它不是一个技术框架。会利用它在微服务拆分中作为一个指导的思想

    什么是中台

    中台就是将业务线上可以复用的一些功能抽取出来,剥离个性,抽取共性,形成一些可复用的组件。

    大体上,中台可以分为三类:业务中台、数据中台和技术中台

    中台和DDD结合,DDD会通过界限上下文将系统拆分成一个一个的领域,而这种界限上下文,天生就成了中台之间的逻辑屏障

    DDD在技术与资源调度方面都能够给中台建设提供不错的指导

    DDD分为战略设计和战术设计。上层的战略设计能够很好的指导中台划分,下层的战术设计能够很好的指导微服务搭建。

    MySql

    索引的基本原理

    索引其实就是对某些字段的值进行排序的数据结构。

    1. 把创建索引的列的内容进行排序
    2. 对排序结果生成倒排表
    3. 在倒排表内容上拼上数据地址链
    4. 在查询的时候,先拿到倒排表内容,再取出数据地址链,从而拿到数据

    索引设计的原则

    就是我们在为表创建索引时,需要遵守的一些原则,主要目的就是使查询更快、占用空间更小

    • 适合创建索引的列是出现在where字句后面,或者是连接字句中指定的列
    • 数据量较小的表,不适合建索引
    • 使用短索引,如果对较长的varchar类型的列创建索引,可以指定一个前缀长度用来节省空间
    • 不要创建太多索引。影响更新数据效率
    • 外键的列一定要建索引
    • 频繁更新的字段列不适合创建索引
    • 区分度不高的列不适合创建索引,比如性别字段
    • 尽量的扩展索引,不要新建索引。根据实际情况来处理
    • 对应查询中select后面很少涉及到的列、重复值较多的列不适合建索引
    • 对于定义为test、image、bit的数据类型列不适合创建索引

    事务的基本特性和隔离级别

    事务的基本特性

    • 原子性:一个事务中的操作要么全部成功、要么全部失败。靠Undo Log来实现的

    • 一致性:指的是数据库从一个一致性的状态转换为另一个一致性的状态。靠其他三个特性来保证的

    • 持久性:事务进行提交后的操作是会永久保存到数据库的。靠Redo Log来实现的

    • 隔离性:一个事务的的修改操作在提交前对其他事务是不可见的。靠锁来实现的

    隔离级别:

    • 读未提交:能够读取到其他事务还没有提交的数据,也加脏读
    • 读已提交:解决了脏读问题,但是一次事务中两次读取的数据不一致,也交不可重复读
    • 可重复读:解决了脏读和不可重复读问题,但是在一次事务中的两次进行范围性的查询得到的数据量不一样,也叫幻读
    • 串行化:解决了所有并发问题,但是一般不会使用,它会给每一行数据加锁,会导致大量的超时和锁竞争问题。

    MVCC

    MVCC,多版本并发控制。

    指的是在读已提交和可重复读这两种隔离级别下,事务在执行select操作时并不是去读取数据库中真正存储的数据而是会去访问记录的版本链的过程。多版本的意思就是一条数据的多个版本会形成一个链表,如果是多个事务同时来读取的话,会根据事务的id和版本链上面的事务id进行一个比对,从而返回当前这个事务应该看到的一个数据。

    这两种隔离级别不同点就是生成ReadView的时机不同,READ COMMITTD在每次进行普通的select操作前都会生成一个ReadView,而REPEATABLE READ只在第一次进行select操作前生成一个ReadView,之后的查询操作都重复使用这个ReadView。

    MyISAM和InnoDB区别

    MyISAM

    • 不支持事务
    • 支持表级锁
    • 会存储表的总行数
    • 一个MyISAM数据表有三个文件:索引文件、表结构文件、数据文件
    • 采用非聚集索引

    InnoDB

    • 支持事务、外键
    • 支持行级锁
    • 不会存储表的总行数
    • InnoDB数据表就一个idb文件
    • 采用聚集索引

    索引覆盖是什么

    索引覆盖指的是,一个SQL在执行时,可以使用索引来快速查找,并且这个SQL所需要查询的所有字段该索引对应的字段中都包含了,所需要查询的字段在当前索引的叶子节点中都包含,不需要进行回表查询了。

    最左前缀原则是什么

    当一个SQL想要利用索引时,那么就一定要在查询条件中有创建索引时最左边的字段,至于放在什么位置不重要,查询优化器在底层会进行优化。

    之所以要满足最左前缀原则是因为B+树在底层对字段进行排序时,首先就是按照创建索引时指定的第一个字段的大小进行排序的,第一个字段相同的情况下再去使用第二个字段,以此类推。最终创建好一个B+树。

    InnoDB是如何实现事务的

    1. 在执行一条Update语句时,首先会查询这条要更新的数据是否在Buffer Pool中,如果不在则根据条件在磁盘中找到这条数据所在的页,将页缓存在Buffer Pool中
    2. 执行Update语句,修改Buffer Pool中的数据
    3. 针对update语句生成一个Redo Log对象,并存入Log Buffer中
    4. 针对update语句生成一个Undo Log对象,用于事务回滚
    5. 如果事务提交,则持久化redolog,后续checkPoint机制进行刷新脏页操作
    6. 如果事务回滚,则利用Undolog进行事务回滚

    B树和B+树,为什么用B+树

    Mysql中的B+树其实是B树的一个升级,主要增加了两个特征:

    1. 叶子节点之间也有指针,方便范围查询
    2. 非叶子结点上的元素在叶子节点上都冗余,叶子节点存储了所有的元素

    Mysql之所以使用B+树,原因是使用B+树,树的高度不会太高,B+树的一个节点对应的InnoDB的一页,默认16KB。在非叶子结点中,它如果不存储数据仅仅存储索引那么一个节点中就能够存储更多的元素,进而降低树的高度。

    MySql锁有哪些

    按锁的粒度分为:

    • 行锁:锁的粒度小,并发度搞
    • 表锁:锁的粒度大,并发度低
    • 间隙锁:锁的是一个区间

    还可以分为:

    • 排他锁:写锁,其他事务就不能加读锁,也不能加写锁,但是能够读取数据
    • 共享锁:读锁,其他事务可以加读锁,但是不加写锁

    还可以分为:

    • 乐观锁:并不会真正的去锁某一行的数据,而是通过一个版本号来实现的
    • 悲观锁:上面说的行锁与表锁都是悲观锁,会真正的锁住数据。

    MySql的慢查询如何优化

    1. 检查是否走了索引,如果没有则优化sql语句
    2. 检查使用选择了最优的索引
    3. 检查查询的字段是否都是必须的,删除多余的字段
    4. 检查数据表中的数据是否过多,是否需要分库分表
    5. 检查数据库实例所在的服务器性能配置,是否太低,是否需要增加资源

    Redis

    RDB和AOF

    RDB(Redis DataBase),在指定的时间间隔内将内存中的数据集快照写入磁盘,实际操作过是fork一个子进程,先将数据集写入临时文件,写入成功后在替换之前的dump.rdb文件,用二进制压缩存储

    优点:

    • 整个Redis数据库将只包含一个dump.rdb文件,方便持久化
    • 容灾性好,方便备份
    • 性能最大化。fork子进程来完成写操作,让主进程继续处理命令,所以IO最大化。
    • 相对于数据集大时,比AOF的启动效率更高

    缺点:

    • 数据安全性低。RDB是间隔一段时间进行持久化,如果在这个期间内redis宕机了就会出现数据丢失。
    • 由于RDB是通过fork子进程来协助完成数据持久化工作的,当数据集较大时,可能会导致整个服务器停止服务几百毫秒,甚至是1秒

    AOF(Append Only File),以日志的形式记录服务器所处理的每一个更新操作,查询不会被记录,以文本的方式记录

    优点:

    • 数据安全,Redis中提供了3中同步策略,即每秒同步、每修改同步和不同步。事实上,每秒同步也是异步完成的,效率很高,所差的是一旦系统出现宕机,那么一秒钟之内的数据会丢失。而每修改同步,我们可以将其视为同步持久化,每次数据变化都会立即记录到磁盘中。
    • 通过append模式写文件,服务器宕机也不会影响已经存在的内容,可以通过redis-check-aof工具解决数据一致性问题
    • AOF机制的rewrite模式。定期对AOF文件进行重写,以达到压缩的目的

    缺点

    • AOF文件比RDB文件大,恢复数据速度慢
    • 数据集大时,比RDB启动效率低
    • 运行效率没有RDB高

    总结AOF文件比RDB文件更新频率高,有限使用AOF还原数据,AOF比RDB更安全,RDB比AOF性能好,如果两个都配置了优先加载AOF。

    Redis过期键的删除策略

    • 惰性过期:当访问key时才会去判断key是否过期,过期则清除。这种策略对CPU很友好,但对内存不太友好,因为过期了的数据也还是会存在。
    • 定期过期:每个一定时间就回去扫描一定数量的key,并清除其中已经过期了的key。这中策略可以在不同情况下使得CPU和内存资源达到一个平衡效果。

    redis中同时使用了惰性过期和定期过期两种过期策略。

    还有一种定时过期,但是redis没有采用这种策略。它是为每个设置过期时间的key都创建一个定时器,到期就立刻清理,缺点是会占用大量cpu资源

    Redis的事务实现

    redis的事务是把多条redis命令放在一个队列中一起执行,如果出现了命令语法有问题那么所有的命令都不会执行,如果是set key value时 key冲突这种问题,那么其他命名正常执行。

    1. 事务开始

      MULTI命令的执行,标识着一个事务的开始。MULTI命令会将客户端状态的flags属性中打开REDIS_MULTI标识来完成的

    2. 命令入队

      当一个客户端切换到事务状态之后,服务器会根据这个客户端发送的命令来执行不同的操作。

      如果客户端发送的命令是MULTI EXEC WATCH DISCARD中的一个,立即执行这个命令

      其他命令,首先检查此命令的语法格式是否正确,如果不正确服务器会在客户端状态(redisClient)的flags属性关闭REDIS_MULTI标识,并返回错误信息给客户端,如果正确,将命令放入一个事务队列里面,然后向客户端返回QUEUED回复

    3. 事务执行

      客户端发送EXEC命令,服务器执行EXEC命令逻辑。

      • 如果客户端装的flags属性不包含REDIS_MULTI标识,或者包含REDIS_DIRTY_CAS或者REDIS_DIRTY_EXEC表示,那么就直接取消事务的执行
      • 否则客户端处理事务状态(flags有REDIS_MULTI标识),服务器会遍历客户端的事务队列,然后执行事务队列中所有的命令,最后将返回结果全部返回给客户端

    redis不支持事务回滚机制,但是它会检查每一个事务中的命令是否有错误。

    • watch命令是一个乐观锁,可以监控一个或多个key,一旦其中有一个key被修改或删除,之后的事务就不会执行了,监控一直持续到exec命令
    • multi命令用于开启一个事务
    • exec :执行所有事务块内的命令。
    • discard:放弃执行事务并退出事务状态
    • unwatch命令,可以取消watch对所有可以的监控

    Redis主从复制核心原理

    通过slaveof命令让一个服务器去复制另一个服务器中的数据。主库可以读写操作,当写操作导致数据改变时会自动将数据同步给从数据库。从数据库只读,接收主数据库同步过来的数据。

    我们需要先明白两个概念

    全量复制:

    • 主节点通过bgsave命令fork子进程进行rdb持久化,该过程非常消耗cpu、内存、硬盘io的。
    • 主节点通过网路将rdb文件发送给从节点,对主从节点的带宽都会带来很大的消耗。
    • 从节点清空老数据、载入新的rdb文件的过程是阻塞的,无法响应客户端的命令;如果从节点执行bgrewriteaof,也会带来额外的消耗

    增量复制:

    • 复制偏移量:执行复制的双方会分别维护一个复制偏移量offset,如果主从两边的offset不相等了是以主节点的为准。
    • 复制积压缓冲区:主节点内部维护了一个固定长度的、先进先出队列作为复制积压缓冲区,当主从节点offset的差距过大超过缓存区长度时,将无法执行增量复制,只能执行全量复制。
    • 服务器运行id(runid): 每个Redis节点都有一个runid,是在启动时自动生成的,主节点会将自己的runid发送给从节点,从节点会将主节点的 runid存起来。从节点与主节点断开连接后重连的时候,就是根据runid来判断同步的进度:
      • 从节点保存的runid与主节点的runid相同,说明主从节点之前同步过,主节点会继续尝试使用setoff进行增量复制,到底能不能增量复制还是要看offset和积压缓冲区的情况;
      • 如果从节点保存的runid与主节点的runid不相同,说明从节点在断开连接前同步的Redis节点并不是当前主节点,只能进行全量复制。

    过程原理:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mqdSsaFp-1659605378846)(E:\Java笔记\picture\image-20220801101016989.png)]

    Redis分布式锁底层是如何实现的

    1. 首先利用setnx命令来保证:如果key不存在才能获取到锁,如果key存在则获取不到锁
    2. 然后还要利用lua脚本来保证多个redis操作的原子性
    3. 同时还要考虑锁过期,所以需要额外的一个看门狗定时任务来监听锁是否需要续约
    4. 同时还要考虑到redis节点挂掉后的情况,所以需要采用红锁的方式来同时向N/2+1个节点申请锁,都申请到了才证明获取锁成功,这样就算其中某个redis节点挂掉了,锁也不能被其他客户端获取到

    Redis主从复制的核心原理

    主从复制的流程如下:

    1. 集群启动时,主从库之间会先建立连接,为全量复制做准备
    2. 主库将所有数据同步给从库。从库收到数据后,在本地完成数据加载,这个过程依赖于内存快照rdb
    3. 在主库同步数据给从库过程中,主库不会阻塞,仍然可以正常接收请求。但是这些请求中的写操作并没有记录到刚刚生成的rdb文件中,为了保证主从库的数据一致性,主库会在内存中用专门的replication buffer,记录rdb文件生成收到的所有写操作。
    4. 最后也就是第三个阶段,主库会把第二阶段执行过程中新收到的写命令,再发送给从库。具体的操作:当主库完成了rdb文件发送后,就会把此时replication buffer中修改操作发送给从库,从库在执行这些操作。这样一来主从库数据就一致了
    5. 后续主从库都可以处理客户端的操作,主库接收到写操作后还会将写操作发送给从库,实现增量同步。

    Redis集群策略

    redis提供了三种集群策略

    • 主从模式:主库进行读写,从库进行读操作。主库宕机后客户端需要手动修改ip、也比较难进行扩容,主库的内存容量决定了整个集群所能存储的数据
    • 哨兵模式:在主从模式的基础上新增了哨兵节点,但是仍然不是很好的解决redis的容量上限问题。
    • cluster模式:是比较常用的,它支持多主多从,这种模式会按照key进行槽位的分配,可以使不同的key分散到不同的主节点上,利用这种模式可以使得整个集群支持更大的数据容量,同时每个主节点可以拥有自己的多个从节点,如果主节点宕机会从它的从节点中选举一个新的主节点。

    如果Redis中数据量不大可以选择哨兵模式,如果要存的数据量大并且需要持续的库容就选择cluster模式

    缓存穿透/击穿/雪崩

    缓存的目的是存放热点数据,请求可以直接从缓冲中获取而不用访问mysql

    • 缓冲雪崩:某一时刻,大量的热点数据同时过期,那么就有可能导致大量请求直接访问mysql了;

      解决方法:

      • 在过期时间上加一点随机值不要同一时刻过期

      • 缓存预热,主要针对系统刚启动时,缓存中是没有数据的,所以大量请求就直接访问mysql

      • 互斥锁,可以在方法上加锁,不让大量的请求直接落在mysql中,只处理一些请求,就比如sentinel限流策略

    • 缓存击穿:和缓存雪崩类似,缓存雪崩是大量特点数据过期,缓存击穿是某一个热点数据过期,也导致了大量请求直接访问mysql数据库,解决方法:

      • 这个热点数据不设置过期时间
      • 互斥锁
    • 缓存穿透:缓存和数据库中都没有数据,导致所有的请求都落到数据库上,造成数据库短时间内承受大量请求而宕机

      解决方法:

      • 如果数据库中没有查询到,可以存一个key-null进缓存,缓存时间可以设置短一点30s。

      • 使用布隆过滤器,它的作用是如果它认为一个key不存在那么这个key就肯定不存在,所以可以在缓存之前加一层布隆过滤器来拦截不存在的key。

    Redis和Mysql如何保证数据一致

    延时双删:先删除redis中的数据,更新mysql,延迟几百毫秒之后再删除redis中的数据。

    redis的持久化机制

    Redis的数据是保存在内存中的,那么宕机就会造成数据丢失,所以就还需要持久化的机制将数据保存在文件中,redis有两种持久化机制:RDB和AOF

    RDB:Redis DataBase 将某一时刻的内存快照,以二进制的方式写入磁盘

    手动触发:

    • save命令:使redis处于阻塞状态,知道rdb持久化完成后才会响应其他客户端发送来的命令。

    • bgsave命令:fork出一个子进程执行持久化,主进程只在fork过程中有短暂的阻塞,子进程创建完成后主进程就可以响应客户端请求了。

      这里有一个问题,两个进程都在进行操作,主进程在执行写操作,那么如何保证子进程生成的rdb快照文件的数据是正确的嘞?

      redis的主进程和子进程都是在操作一块共享内存空间,数据都是存储在这里,Redis是利用COW(copy on write)机制来解决生成的快照文件就是这个时刻的数据。当子进程在使用共享内存空间生成rdb快照文件时,假如这个时候主进程触发了写操作,主进程会把共享内存中要写的这条数据拷贝出来,copy出一个副本,在副本中进行修改操作,此时子进程读取的还是原来共享内存空间的数据,最后再把副本中的数据写回去。

    自动触发:

    • save m n : 在m秒内,如果有n个key发生了改变,则自动触发持久化,通过bgsave命令执行,如果设置多个,只要满足其中之一就会触发,配置文件有默认的配置
    • flushall:用于清空redis所有的数据库,flushdb清空当前redis所在库数据 ,会清空RDB文件,同时也会生成dump.rdb文件,内容为空
    • 主从同步:全量同步时会自动触发bgsave命令,生成新的rdb文件发送给从节点

    优点:

    • 整个redis数据库只有一个dump.rdb文件,方便持久化
    • 容灾性好,方便备份
    • 性能最大化,fork子进程来完成写操作,让主进程继续处理命令,所以io最大化。
    • 数据量较大时,启动效率去AOF更高

    缺点:

    • 数据安全性低。RDB是每隔一段时间持久化一次,那么就有可能丢失这一段时间的数据。更适合数据要求不严谨的时候
    • 由于RDB是fork子进程来协助主进程完成持久化工作的,因为当数据集较大时,可能会导致整个redis服务停止服务几百毫秒,甚至是1秒

    AOF :append only file 以日志的形式记录服务器所处理的每一个写、删除操作以文件的方式记录,可以打开文件看到详细的操作记录

    1. 所有的写命令会追加到AOF缓冲中
    2. AOF缓冲区根据对应的策略向磁盘进行同步操作
    3. 随着AOF文件越来越大,需要定期会AOF文件进行重写,达到压缩的目的
    4. 当Redis重启时,可以加载AOF文件进行数据恢复

    同步策略:

    • 每秒同步:异步完成,效率高,出现系统宕机这一秒的数据会丢失
    • 每修改同步:同步持久化,每次发生数据更改都会立即记录到磁盘中,数据丢失最多只丢失一条
    • 不同步:由操作系统控制,它可以自己控制将AOF缓冲区中的数据进行持久化。可能丢失较多数据

    优点:

    • 数据安全
    • 通过append模式写文件,中途服务器宕机也不会影响之前已经存在的内容
    • AOF的rewrite模式。定期对AOF文件进行重写,以达到压缩的目的

    缺点:

    • AOF文件比RDB文件要大,恢复速度要慢
    • 数据集较大时,服务启动效率要低
    • 运行效率没有RDB高

    AOF文件比RDB文件更新效率高,优先使用AOF还原数据

    AOF比RDB更安全也更大

    RDB新能比AOF好

    如果两个都配置了有限加载AOF

    redis单线程为什么这么快

    • 纯内存操作,每一个操作都很快,单线程反而避免了多线程频繁上下文切换带来的性能问题
    • 核心是基于非阻塞的io多路复用机制

    常用的缓存淘汰算法

    • FIFO(First in First Out):先进先出,根据缓存被存储的时间,离当前最远的数据有限被淘汰
    • LRU(LeastRecentlyUsed):最近最少使用,根据最近被使用的时间,离当前时间最远的数据有限被淘汰
    • LFU(LeastFrequentlyUsed):最不经常使用,在一段时间内,缓存数据被使用次数最少的会被淘汰

    布隆过滤器原理、优缺点

    布隆过滤器就是一个int数组,就比如长度为10的数组int[10],每个int类型的整数占4*8=32bit,则int[10]共有320bit,每个比特位是二进制非0即1,初始化时都是0 。

    添加数据时,首先将key进行哈希运算,得到一个哈希值,这个哈希值对应一个bit位,将对应的bit位改为1。哈希函数可以定义多个,假如定义3个哈希函数,key经过三个哈希运算后会得到三个哈希值,将这三个哈希对应的bit位上的值都改为1。多个哈希函数的目的是减少哈希冲突

    查询数据时,首先对key经过哈希运算,对应的bit位上,如果有一个0,则表示数据不在bit中,如果都为1则表示数据可能在bit中。

    优点:

    • 占用内存小
    • 添加和查询元素时,时间复杂度都是O(K) k为哈希函数的个数
    • 哈希函数相互之间没有关系,多个哈希函数之间是可以并行运算
    • 布隆过滤器不需要存储元素本身,只是判断这个元素是否存在,保密比较高
    • 使用同一个哈希函数的多个布隆过滤器可以进行交、并、差集运算

    缺点

    • 误判率,不能准确判断元素是否在集合中,有一定的误判
    • 不能获取元素本身
    • 一般情况下不能从布隆过滤器删除元素

    分布式缓存寻址算法

    在数据量很大时,一台redis能存储是数据量是有限的,那么就要使用cluster模式的集群,一个redis存储一部分的key。在查询的时候就需要寻址算法了。

    • hash算法:根据key进行hash运算,结果和分片数取模,确定分片

      优点实现简单,适用于固定分片的场景

      缺点是扩展分片或减少分片时,所有的数据都需要重新计算分片,重新存储

    • 一致性hash:将整个hash值的区间组织成一个闭合的圆环,计算每台服务器的hash值、映射到圆环中。使用相同的的hash算法计算数据的hash值,映射到圆环中,顺时针寻址,找到的第一个服务器就是数据存储的服务器。

      新增或减少节点时只会影响到它逆时针最近的一个服务器之间的值

      存在hash倾斜的问题,即服务器分布不均匀,可以通过虚拟节点解决

    • hash slot:hash槽,将数据与服务器隔离开,数据与slot映射,slot与服务器映射,数据进行hash决定存放的slot,新增即删除节点时,将slot进行迁移即可。

    MQ

    RocketMQ的事务消息是如何实现的

    1. 生产者订单系统发送一条half消息到Broker,half消息对于消费者而言是不可见的
    2. 再创建订单,订单创建成功与否,决定想Broker发送commit或rollback
    3. 生产者订单系统还可以提供Broker回调接口,当Broker一段时间后half消息没有收到任何操作命令则主动调用此接口来查询订单是否创建成功
    4. 一旦half消息commit了,half消息就会变成一条完整的消息,消费者库存系统就会来消费,如果消费成功则消息销毁,分布式事务成功结束。如果消费失败则根据重试策略进行重试,最后还是失败则进入死信队列等待进一步处理
    5. 一旦half消息rollback了,Broker就会删除掉half消息

    RocketMQ的底层实现原理

    RocketMQ由NameServer集群、producer集群、consumer集群、broker集群组成。

    消息生产和消费的大致原理如下:

    1. Broker在启动的时候向所有的NameServer注册,并保持长连接,每30s发送一次心跳
    2. Producer在发送消息的时候先从NameServer获取Broker服务器地址,根据负载均衡选择一台服务器来发送消息
    3. Consumer消费消息的时候同样从NameServer获取Broker地址,然后主动拉取消息进行消费

    消息队列如何保证消息可靠传输

    消息可靠传输代表了两层意思,既不能多也不能少。

    • 消息不能多,就是消息不能重复,生成者不能重复生成消息,消费者也不能重复消费消息

      确保生产者消息不多发,这个不常出现,也比较难控制,因为如果出现了多次,很大的原因是生产者自己的原因,如果要避免出现问题,就需要在消费端做控制,最保险的机制就是消费者实现幂等性,保证就算重复消费也不会有问题。

    • 消息不能少,就是消息不能丢失,要保证生产者可靠生产消息,消费者可靠消费消息

      生产者发送消息时,要确认Broker确实收到了并持久化了这条消息,比如RabbitMQ的confirm机制,Kafka的ack机制都可以保证生产者能正确的将消息发送给Broker

      Broker要等待消费者真正确认消费到了消息才会删除掉消息,通常就是消费端的ack机制,消费者接收到一条消息后,如果确认没问题了就向Broker发送一个ack,Broker接收到ack后才会删除消息

    消息队列有哪些作用

    • 解耦:使用消息队列来作为两个系统之间的通讯方式,两个系统不需要相互依赖了
    • 异步:给消息队列发送完消息之后就可以做其他事情了
    • 流量削峰:消息在消息队列中排队,由消费者自己控制消费速度

    死信队列和延时队列

    • 死信队列也是一个消息队列,它是用来存放那些没有消费成功的消息的,通常可以用来作为消息重试
    • 延时队列就是用来存放需要在指定时间被处理的元素的队列,通过可以用来处理一些具有过期性操作的业务。比如十分钟未支付就取消订单

    如何保证消息的高速读写

    零拷贝:kafka和RocketMQ都是通过零拷贝技术来优化文件读写

    传统方式中,程序要执行一个复制文件操作需要进行四次拷贝,程序是运行在用户空间的,用户空间不能直接访问硬件,所以需要先拷贝到内核空间,然后拷贝到用户空间,程序操作完成后再拷贝回去。所需要经历的过程如下图所示:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zkh3y2Dx-1659605378847)(E:\Java笔记\picture\image-20220803144013807.png)]

    零拷贝有两种方式,mmap和transfile。

    mmap就是省略掉内核空间到用户空间的拷贝,用户空间就不拿内核空间的完整内容了,只是拿文件的一个映射,映射主要包括文件的内存地址、长度等信息,用户空间的操作就不操作文件了,只是对映射做一些修改,实际上整个文件是在内核空间中直接完成读写,这样就少了两次文件拷贝

    还有一种方式就是在底层的时候使用DMA技术, 它允许不同的设置共同访问一个内存空间,这样就不需要cpu进行大量的中断负载,减少cpu的消耗

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wubDQ4jZ-1659605378849)(E:\Java笔记\picture\image-20220803150647378.png)]

    所以零拷贝并不是真正的不进行文件拷贝了,其实还是有两次拷贝,只是内核空间不会往用户空间进行拷贝了。

    java中对零拷贝进行了API封装,mmap通过MapedByteBuffer对象操作,trannsfile通过FileChannel来进行操作

    mmap适合比较小的文件,通常文件大小不要超过1.5G~2G

    transfile没有文件大小限制

    RocketMQ采用Mmap方式来对它的文件进行读写。

    kafka当中,它的index日志文件也是通过mmap方式来读写的,其他的文件当中并没有使用零拷贝的方式,它使用transfile方式将硬盘数据加载到网卡。

  • 相关阅读:
    scale自适应分辨率 实现缩放自适应(vue3)
    简单封装一个易拓展的Dialog
    【lwip】08-ARP协议一图笔记及源码实现
    SpringBoot+Lombok+Builder实现任意个数属性的对象构造
    IDEA中创建编写JSP
    格式化之 %d,%2d, %02d
    MyBatis-Plus演绎:数据权限控制,优雅至极!
    Python实现秒杀抢购某宝商品,不再害怕双十一抢不到了
    UE5 中 LiveLink 的开发全流程教程
    postgresql 数据库巡检
  • 原文地址:https://blog.csdn.net/qq_44027353/article/details/126163635