Java面试题

Java基础面试题

1、==和equals的区别

对于==比较的是值是否相等,如果作用于基本数据类型的变量,则直接比较其存储的 值是否相等,

如果作用于引用类型的变量,则比较的是所指向的对象的地址是否相等。

对于equals方法,比较的是是否是同一个对象

首先,equals()方法不能作用于基本数据类型的变量,

另外,equals()方法存在于Object类中,而Object类是所有类的直接或间接父类,所以说所有类中的equals()方法都继承自Object类,在没有重写equals()方法的类中,调用equals()方法其实和使用==的效果一样,也是比较的是引用类型的变量所指向的对象的地址,不过,Java提供的类中,有些类都重写了equals()方法,重写后的equals()方法一般都是比较两个对象的值,比如String类。

2、为什么局部/匿名内部类在使用外部局部变量时, 只能使用被final修饰的变量?

首先要清楚内部类和外部类的级别是一样的,所以内部类不会因为定义在方法中就会随着方法的执行结束完毕就被销毁

这里就会产生问题:当外部类的方法结束时,局部变量就会被销毁,但是内部类的对象可能还存在。这里就出现了一个矛盾:内部类对象访问了一个不存在的变量,为了解决这个问题,就将局部变量复制了一份作为内部类的成员变量,这样当局部变量死亡时,内部类仍然可以访问它,就好像延长了局部变量的生命周期

但是内部类的变量和外部局部变量是两个完全不同的变量, 那么如果在执行方法的过程中, 内部类中修改了变量所指向的值, 就会产生数据不一致问题.

正因为我们的原意是内部类和外部类访问的是同一个变量, 所以当在内部类中使用外部局部变量的时候应该用final修饰局部变量, 这样局部变量的值就永远不会改变, 也避免了数据不一致问题的发生.

3、final修饰变量

当final修饰的是一个基本数据类型数据时, 这个数据的值在初始化后将不能被改变; 当final修饰的是一个引用类型数据时, 也就是修饰一个对象时, 引用在初始化后将永远指向一个内存地址, 不可修改. 但是该内存地址中保存的对象信息, 是可以进行修改的.

4、泛型中extends和super的区别

1.<? extends T> 表示T在内的任何T的子类

2.<? super T>表示包括T在内的任何T的父类

5、String、StringBuffer、StringBuilder的区别

1、String是不可变的,如果尝试去修改,会新生成一个字符串对象,StringBuffer和StringBuilder是可变的

2、StringBuffer是线程安全的,StringBuilder是线程不安全的,所以在单线程的情况下用StringBuilder效率更高

6、深拷贝和浅拷贝的区别

深拷贝和浅拷贝是指对象的拷贝,一个对象中存在两种类型的属性,一种是基本数据类型,一种是实例对象的引用

1、浅拷贝是指,只会拷贝基本数据类型的值,以及实例对象的引用地址,并不会复制一份引用地址所指向的对象,也就是浅拷贝出来的对象,内部的类属性指向的是同一个对象

2、深拷贝是指,既会拷贝基本数据类型的值,也会针对实例对象的引用地址所指向的对象进行复制,深拷贝出来的对象,内部的类执行指向的不是同一个对象

集合

list种类

线程安全:Vector、SynchronizedList、CopyOnWriteArrayList

线程不安全:ArrayList、LinkedList

Vector:采用了同步关键词synchronized修饰方法保证线程安全,效率低下

SynchronizedList:和Vector的区别在于它采用了同步代码块实现线程间的同步,比 Vector 有更好的扩展性和兼容性。所有方法都是带同步对象锁的,和 Vector 一样,它不是性能最优的

CopyOnWriteArrayList:在写操作的时候复制数组。为了将读取的性能发挥到极致,在该类的使用过程中,读读操作和读写操作都不互斥,在面临写操作的时候,CopyOnWriteArrayList会先复制原来的数组并且在新数组上进行修改,最后再将原数组覆盖。如果写操作的过程中发生了线程切换,并且切换到读线程,因为此时数组并未发生覆盖,读操作读取的还是原数组。换句话说,就是读操作和写操作位于不同的数组上

读多写少的情况下,推荐使用CopyOnWriteArrayList方式,因为copyOnWriteArrayList每进行一次写操作都会复制一次数组,这是非常耗时的操作
读少写多的情况下,推荐使用Collections.synchronizedList()的方式

ArrayList和LinkedList的区别

ArratyList:基于动态数组,连续内存存储,适合下标访问(随机访问),扩容机制:因为数组长度固定,超出长度存数据时需要新建数组,然后将老数组拷贝到新数组,如果不是尾部插入数据还会涉及到元素的移动(往后复制一份,插入新元素),使用尾插法并指定初始容量可以极大的提升性能,甚至超过linkedList(需要创建大量的node对象)

LinkedList:基于链表,可以存储在分散的内存中,适合做数据插入及删除操作 ,不适合查询,需要逐一遍历

但是LinkedList内部维护了一个node类,每插入一个元素都会创建一个node对象,所以如果是大量插入,就需要创建大量的node对象,这就会损耗性能,损耗在创建对象这里

遍历LinkedList最好使用迭代器不要使用for循环,因为每次for循环体内通过get(i)取得某一元素时都需要对list重新进行遍历,性能消耗极大。

另外不要试图使用indexOf等返回元素索引,并利用其进行遍历,使用indexOf对list进行了遍历,结果为空时会遍历整个列表

相同点和区别:同样都实现了List接口,但是LinkedList还实现了Deque接口,所以LinkedList其实可以当作双端队列,可以在队列的头部和尾部添加元素

ArrayList底层要考虑扩容,LinkedList不需要考虑,指定下标位置插入元素,LinkedList要快

ConcurrentHashmap的扩容机制

1.7版本

1、1.7的ConcurrentHashmap是基于Segment分段实现的

2、每个Segment相对于一个小型的HashMap

3、每个Segment内部会进行扩容,和HashMap扩容逻辑类似

4、先生成新的数组,然后转移元素到新数组中

5、扩容的判断也是每个Segment内部单独判断的,判断是否超过阈值

1.8版本

1、1.8版本的ConcurrentHashmap不再基于Segment分段实现的

2、当某个线程进行put时,如果发现ConcurrentHashmap正在进行扩容那么该线程一起进行扩容

3、如果某个线程put时,发现没有正在进行扩容,则将key-value添加到ConcurrentHashmap中,然后判断是否超过了阈值,超过了则进行扩容

4、ConcurrentHashmap是支持多个线程同时扩容的

5、扩容之前也先生成一个新的数组

6、在转移元素时,先将原数组分组,将每组分给不同的线程来进行元素的转移,每个线程负责一组或多组的元素转移工作

ConcurrentHashmap原理简述

jdk7:数据结构:ReentrantLock+Segment+HashEntry,一个Segment中包含一个HashEntry数组,每个HashEntry又是一个链表结构.
元素查询:二次hash,第一次Hash定位到Segment,第二次Hash定位到元素所在的链表的头部锁:Segment分段锁 Segment继承了ReentrantLock,锁定操作的Segment,其他的Segment不受影响,并发度为segment个数,可以通过构造函数指定,数组扩容不会影响其他的segmentget方法无需加锁,volatile保证
jdk8:数据结构:synchronized+CAS+Node+红黑树,Node的val和next都用volatile修饰,保证可见性查找,替换,赋值操作都使用CAS
锁:锁链表的head节点,不影响其他元素的读写,锁粒度更细,效率更高,扩容时,阻塞所有的读写操作、并发扩容。
读操作无锁:
Node的val和next使用volatile修饰,读写线程对该变量互相可见
数组用volatile修饰,保证扩容时被读线程感知。

CopyOnWriteArrayList的底层原理是怎样的

1、首先CopyOnWriteArrayList内部也是用过数组来实现的,在向CopyOnWriteArrayList添加元素时,会复制一个新的数组,写操作在新数组上进行,读操作在原数组上进行

2、并且,写操作会加锁,防止出现并发写入丢失数据的问题

3、写操作结束之后会把原数组指向新数组

4、CopyOnWriteArrayList允许在写操作时来读取数据,大大提高了读的性能,因此适合读多写少的应用场景,但是CopyOnWriteArrayList会比较占内存,同时可能读到的数据不是实时最新的数据,所以不适合实时性要求很高的场景

HashMap的底层原理是怎样的

1.7:数组加链表

1.8:数组加链表加红黑树

HashMap的实现原理中,先采用一个数组表示位桶,每个位桶的实现在1.8之前都是使用链表,但当每个位桶的数据较多的时候,链表查询的效率就会不高,因此在1.8之后,当位桶的数据超过阈值(8)的时候,就会采用红黑树来存储该位桶的数据(在阈值之前还是使用链表来进行存储),所以,HashMap的实现包括数组+链表+红黑树。

为了提升整个HashMap的读取效率,当HashMap中存储的元素大小等于桶数组大小乘以负载因子(0.75)的时候整个HashMap就要扩容,以减小哈希碰撞,所以为了HashMap的效率考虑,最好是根据实际业务在初始化时设置合适的容量,避免频繁扩容

HashMap内部的bucket数组长度为什么一直都是2的整数次幂答:这样做有两个好处,第一,可以通过(table.length - 1) & key.hash()这样的位运算快速寻址,第二,在HashMap扩容的时候可以保证同一个桶中的元素均匀地散列到新的桶中,具体一点就是同一个桶中的元素在扩容后一般留在原先的桶中,一般放到了新的桶中。

HashMap什么时候开辟bucket数组占用内存答:在第一次put的时候调用resize方法

set种类

线程不安全:HashSet、TreeSet、LinkedHashSet

线程安全:CopyOnWriteArraySet、Collections.synchronizedSet()、Collections.newSetFromMap()、ConcurrentHashMap.newKeySet()

CopyOnWriteArraySet:和CopyOnWriteArrayList实现原理一样,采用读写分离的并发策略,读操作的时候不加锁,写操作时创建底层数据的新副本,在新副本上执行写操作,写操作结束后将原引用指向新的容器。适合读多写少的场景。

特点

  • 读操作性能高
  • 当使用迭代器遍历时,在遍历的间隔中如果修改了集合,不会抛出ConcurrentModificationException异常。
  • 由于要复制新副本,会占用较大内存
  • 写操作时在新的副本上操作,此时的读操作还是在旧副本上,所以无法保证实时性
  • 大量写操作性能很差

Collections.synchronizedSet():在原先Set的每一个方法上都加上synchronized关键字

特点

  • 性能较差
  • 遍历间隔中如果修改了集合,仍会抛出异常ConcurrentModificationExceptions

Collections.newSetFromMap():封装了ConcurrentHashMap,利用ConcurrentHashMap的性质确保生成的Set是线程安全的。

特点
该方法必须传入一个空的map
性能较好,ConcurrentHashMap是分区的,写操作时对应的分区时是synchronized的,读操作与其他读操作、 写 操作是完全并发的(但可能没法看到当前正在写入的更改的结果)
迭代器创建后可能会看到变化,也可能看不到变化,而且批量操作不是原子操作
reSize操作较慢,因此在初始化时最好能够指定大小(3/4满时就会reSize)
需要hashCode()方法有较好的性能
不允许再和传入的map做交互

ConcurrentHashMap.newKeySet():jdk1.8以后引入的特性,是ConcurrentHashMap的特性的一部分

特点

  • 与上面提到的Collections.newSetFromMap(new ConcurrentHashMap<>())相似

JVM

1、JDK1.7到 JDK1.8中Java虚拟机发生了什么变化

1.7中存在永久代,1.8中没有永久代,替换它的是元空间,元空间所占的内存不是在虚拟机内部,而是本地内存空间,这么做的原因是,不管是永久代还是元空间,他们都是方法区的具体实现,之所以元空间所占的内存改成本地内存,官方的说法是为了和JRockit统一,不过额外还有一些原因,比如方法区所存储的类信息通常是比较难确定的,所以对于方法区的大小是很难指定的,太小了容易出现方法区溢出,太大了又会占用太多虚拟机的内存空间,而转移到本地内存后则不会影响虚拟机所占用的内存

2、GC如何判断对象可以被回收
  • 引用计数法:每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时减1;任何时候计数器为0的对象就是不可能再被使用的。这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题

  • 可达性分析算法:将**“GC Roots”** 对象作为起点,从这些节点开始向下搜索引用的对象,找到的对象都标记为非垃圾对象,其余未标记的对象都是垃圾对象

    GC Roots根节点:线程栈的本地变量、方法区中的静态变量、本地方法栈的变量、正在运行的线程等等

    即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程。

    标记的前提是对象在进行可达性分析后发现没有与GC Roots相连接的引用链

    1. 第一次标记并进行一次筛选。

    筛选的条件是此对象是否有必要执行finalize()方法。

    当对象没有覆盖finalize方法,对象将直接被回收。

    2. 第二次标记

    如果这个对象覆盖了finalize方法,finalize方法是对象脱逃死亡命运的最后一次机会,如果对象要在finalize()中成功拯救自己,只要重新与引用链上的任何的一个对象建立关联即可,譬如把自己赋值给某个类变量或对象的成员变量,那在第二次标记时它将移除出“即将回收”的集合。如果对象这时候还没逃脱,那基本上它就真的被回收了。

3、Java类加载

分为五步:加载>>验证>>准备>>解析>>初始化

加载:将字节码文件从各个来源通过类加载器加载进内存中

验证:验证字节码文件是否符合jvm规范,确保不会造成安全错误

准备:将类中的静态变量分配内存,并赋予初值也就是默认值

解析:将常量池内的符号引用替换为直接引用

举个例子,现在调用一个方法hello(),其内存地址是123456,其中方法名就是符号引号,内存地址是直接引用

虚拟机会把所有的类名,方法名,字段名这些符号引用替换为直接引用

该阶段会把一些静态方法替换为直接引用,这就是所谓的静态链接,动态链接就是指在程序运行期间将符号引用替换为直接引用

初始化:对类的静态变量初始化为指定的值,执行静态代码块

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

如果有多个静态代码块或者静态变量,则按照自上而下的顺序依次执行

4、Java类加载器有哪些

BootStrapClassloader:负责加载支撑JVM运行的位于JRE的lib目录下的核心类库

ExtClassloader:负责加载支撑JVM运行的位于JRE的lib目录下的ext扩展目录中的JAR类包

AppClassloader:负责加载ClassPath路径下的类包,主要就是加载你自己写的那些类

自定义类加载器:负责加载用户自定义路径下的类包

5、JVM有哪些垃圾回收器

Serial收集器:最基本、历史最悠久的垃圾收集器了。它的 “单线程” 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( “Stop The World” ),直到它收集结束。

新生代采用复制算法,老年代采用标记-整理算法。

由于没有线程交互的开销,可以获得很高的单线程收集效率,所以与其他收集器的单线程相比,它简单而高效

Parallel Scavenge收集器Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和Serial收集器类似。默认的收集线程数跟cpu核数相同,当然也可以用参数(-XX:ParallelGCThreads)指定收集线程数,但是一般不推荐修改。

新生代采用复制算法,老年代采用标记-整理算法。

使用多线程和“标记-整理”算法。在注重吞吐量以及CPU资源的场合,都可以优先考虑 Parallel Scavenge收集器和Parallel Old收集器(JDK8默认的新生代和老年代收集器)。

ParNew收集器跟Parallel收集器很类似,区别主要在于它可以和CMS收集器配合使用。新生代采用复制算法,老年代采用标记-整理算法。

CMS收集器一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用,它是HotSpot虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。

核心思想,就是将STW打散,让一部分GC线程与用户线程并发执行

是一种 “标记-清除”算法实现的,它的运作过程相比于前面几种垃圾收集器来说更加复杂一些。整个过程分为四个步骤:

  • 初始标记: 暂停所有的其他线程(STW),并记录下gc roots直接能引用的对象速度很快
  • 并发标记: 并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程, 这个过程耗时较长但是不需要停顿用户线程, 可以与垃圾收集线程一起并发运行。因为用户程序继续运行,可能会有导致已经标记过的对象状态发生改变。
  • 重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录(主要是处理漏标问题),这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短。主要用到三色标记里的****增量更新算法(见下面详解)做重新标记。
  • 并发清理: 开启用户线程,同时GC线程开始对未标记的区域做清扫。这个阶段如果有新增对象会被标记为黑色不做任何处理(见下面三色标记算法详解)。
  • **并发重置:**重置本次GC过程中的标记数据

主要优点:并发收集、低停顿

它有下面几个明显的缺点:

  • 对CPU资源敏感(会和服务抢资源);
  • 无法处理浮动垃圾(在并发标记和并发清理阶段又产生垃圾,这种浮动垃圾只能等到下一次gc再清理了);
  • 它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生,当然通过参数-XX:+UseCMSCompactAtFullCollection可以让jvm在执行完标记清除后再做整理
  • 执行过程中的不确定性,会存在上一次垃圾回收还没执行完,然后垃圾回收又被触发的情况,特别是在并发标记和并发清理阶段会出现,一边回收,系统一边运行,也许没回收完就再次触发full gc,也就是"concurrent mode failure",此时会进入stop the world,用serial old垃圾收集器来回收

G1收集器:他的内存模型是实际不分代,但是逻辑上是分代的。在内存模型中,对于堆内存就不再分老年代和新生代,而是划分成一个一个的小内存块,叫做Region。每个Region可以隶属于不同的年代。

GC分为四个阶段:

第一:初始标记 标记出GCRoot直接引用的对象。STW

第二:标记Region,通过RSet标记出上一个阶段标记的Region引用到的Old区Region。

第三:并发标记阶段:跟CMS的步骤是差不多的。只是遍历的范围不再是整个Old区,而只需要遍历第二步标记出来的Region。

第四:重新标记: 跟CMS中的重新标记过程是差不多的。

第五:垃圾清理:与CMS不同的是,G1可以采用拷贝算法,直接将整个Region中的对象拷贝到另一个Region。而这个阶段,G1只选择垃圾较多的Region来清理,并不是完全清理。

6、JVM中有哪些垃圾回收算法

标记-清除算法:算法分为“标记”和“清除”阶段:标记存活的对象, 统一回收所有未被标记的对象(一般选择这种);也可以反过来,标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象 。它是最基础的收集算法,比较简单,但是会带来两个明显的问题:

  1. 效率问题 (如果需要标记的对象太多,效率不高)
  2. 空间问题(标记清除后会产生大量不连续的碎片)

标记-复制算法:为了解决标记清除算法的内存碎片问题,就产生了标记-复制算法。它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。这种算法没有内存碎片,但是他的问题就在于浪费空间。而且,他的效率跟存货对象的个数有关

标记-整理算法:根据老年代的特点特出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。

7、你们项目如何排查JVM问题

对于还在正常使用的系统:

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

对于已经发生了OOM的系统:

  1. 一般生产系统都会设置当系统发生了OOM时,生成当时的dump文件(-XX:+HeapDumpOnOutOfMemoryError - XX:HeapDumpPath=/usr/local/bae)
  2. 我们可以利用jvisualvm等工具来分析dump文件
  3. 根据dump文件找到异常的示例对象,和异常的线程(占用cpu过高),定位到具体的代码
  4. 然后再进行详细的分析和调试
8、JVM中有哪些是线程共享区

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

9、说说类加载器的双亲委派模型

加载某个类时会先委托父加载器寻找目标类,找不到再委托上层父加载器加载,如果所有父加载器在自己的加载类路径下都找不到目标类,则在自己的类加载路径中查找并载入目标类

为什么要设计双亲委派机制?

  • 沙箱安全机制:自己写的java.lang.String.class类不会被加载,这样便可以防止核心API库被随意篡改
  • 避免类的重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次,保证被加载类的唯一性
10、如何进行JVM调优

优化思路其实简单来说就是尽量让每次Young GC后的存活对象小于Survivor区域的50%,都留存在年轻代里。尽量别让对象进入老年代。尽量减少Full GC的频率,避免频繁Full GC对JVM性能的影响

所以在系统运行时就可以针对几个特定参数进行设置比如**-Xms**:设置堆的初始可用大小,默认物理内存的1/64;-Xmx:设置堆的最大可用大小,默认物理内存的1/4;-Xmn:新生代大小;-XX:MetaspaceSize: 指定元空间触发Fullgc的初始阈值(元空间无固定初始大小), 以字节为单位,默认是21M左右,达到该值就会触发full gc进行类型卸载, 同时收集器会对该值进行调整: 如果释放了大量的空间, 就适当降低该值; 如果释放了很少的空间, 那么在不超过-XX:MaxMetaspaceSize(如果设置了的话) 的情况下, 适当提高该值。这个跟早期jdk版本的**-XX:PermSize**参数意思不一样,-XX:PermSize代表永久代的初始容量。

11、怎么确定一个对象是不是垃圾

类需要同时满足下面3个条件才能算是 “垃圾”

  • 该类所有的对象实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
  • 加载该类的 ClassLoader 已经被回收。
  • 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
12、对象在JVM中经历的过程

1、用户创建一个对象,JVM首先需要到方法区去找对象的类型信息。然后再创建对象。

2、JVM要实例化一个对象,首先要在堆当中先创建一个对象。-> 半初始化状态

3、对象首先会分配在堆内存中新生代的Eden。然后经过一次Minor GC,对象如果存活,就会进入S区。在后续的每次GC中,如果对象一直存活,就会在S区来回拷贝,每移动一次,年龄加1。-> 多大年龄才会移入老年代? 年龄最大15, 超过一定年龄后,对象转入老年代。

4、当方法执行结束后,栈中的指针会先移除掉。

5、堆中的对象,经过Full GC,就会被标记为垃圾,然后被GC线程清理掉。

13、说一说GC的流程

小白是怎么搞懂GC全过程? - 腾讯云开发者社区-腾讯云 (tencent.com)

1、大对象直接进入到老年代。

2、小对象先在eden区分配内存,当eden满了后,触发一次Minor GC,清理eden区域。

3、存活下来的对象进入到survivor区域,年龄+1。

4、当年龄>15(默认)时进入到老年代,当老年代满了后触发一次Full GC。

正式 Minor GC 前的检查

在正式 Minor GC 前,JVM 会先检查新生代中对象,是比老年代中剩余空间大还是小。**为什么要做这样的检查呢?**原因很简单,假如 Minor GC 之后 Survivor 区放不下剩余对象,这些对象就要进入到老年代,所以要提前检查老年代是不是够用。这样就有两种情况:

  1. 老年代剩余空间大于新生代中的对象大小,那就直接 Minor GC,GC 完 survivor 不够放,老年代也绝对够放
  2. 老年代剩余空间小于新生代中的对象大小,这个时候就要查看是否启用了「老年代空间分配担保规则」,具体来说就是看

​ -XX:-HandlePromotionFailure参数是否设置了(一般都会设置)

老年代空间分配担保规则是这样的。如果老年代中剩余空间大小,大于历次 Minor GC 之后剩余对象的大小,那就允许进行 Minor GC。因为从概率上来说,以前的放的下,这次的也应该放的下。那就有两种情况:

  1. 老年代中剩余空间大小,大于历次 Minor GC 之后剩余对象的大小,进行 Minor GC
  2. 老年代中剩余空间大小,小于历次 Minor GC 之后剩余对象的大小,进行 Full GC,把老年代空出来再检查

Minor GC 后的处境

前面说了,开启老年代空间分配担保规则只能说是大概率上来说,Minor GC 剩余后的对象够放到老年代,所以当然也会有万一,Minor GC 后会有这样三种情况:

  1. Minor GC 之后的对象足够放到 Survivor 区,皆大欢喜,GC 结束
  2. Minor GC 之后的对象不够放到 Survivor 区,接着进入到老年代,老年代能放下,那也可以,GC 结束
  3. Minor GC 之后的对象不够放到 Survivor 区,老年代也放不下,那就只能 Full GC

实在不行只能 OOM

前面都是成功 GC 的例子,还有 3 中情况,会导致 GC 失败,报 OOM:

  1. 紧接上一节 Full GC 之后,老年代任然放不下剩余对象,就只能 OOM
  2. 未开启老年代分配担保机制,且一次 Full GC 后,老年代任然放不下剩余对象,也只能 OOM
  3. 开启老年代分配担保机制,但是担保不通过,一次 Full GC 后,老年代任然放不下剩余对象,也是能 OOM
14、Java的内存结构,堆分为哪几部分,默认年龄多大进入老年代

1、年轻代

​ Eden区(8)

​ From Survivor区(1)

​ To Survivor区(1)

2、老年代

默认对象的年龄达到15以后进入老年代

15、说一说JVM的内存模型

JVM的内存模型是一种规范,定义了Java程序在执行过程中的内存使用情况。它定义了Java虚拟机中所使用的内存区域,这些内存区域主要包括以下几个部分:

1.程序计数器(Program Counter Register):是一块较小的内存区域,用于保存下一条指令的地址,它不会产生OutOfMemoryError。

2.栈(JVM Stack):每个线程都有一个私有栈,用于存储局部变量、操作数栈和函数调用和返回等信息。它是线程私有的,随线程的创建和销毁而创建和销毁,因此,它可能会导致StackOverflowError或OutOfMemoryError。

3.堆(Heap):它是Java虚拟机中最大的一块内存区域,用于存储Java对象。所有线程共享堆,因此,可能会导致OutOfMemoryError。

4.方法区(Method Area):它存储类的结构信息,如类的成员变量、方法信息等。它是所有线程共享的区域,因此,也可能会导致OutOfMemoryError。

5.运行时常量池(Runtime Constant Pool):它是方法区的一部分,用于存储各种常量信息。

6.本地方法栈(Native Method Stack):它主要与Native方法相关,与Java虚拟机实现无关。

16、什么是STW?为什么需要STW?

stop the world指的是GC事件发生过程中,会产生应用程序的停顿。停顿产生时整个应用程序线程都会被暂停,没有任何响应, 有点像卡死的感觉,这个停顿称为STW。Java中一种全局暂停现象,全局停顿,所有Java代码停止,native代码可以执行,但不能与JVM交互;这些现象多半是由于gc引起。

垃圾回收是根据可达性分析算法,搜索GC Root根的引用链,将不在引用链上的对象当做垃圾回收,设想我们执行某个方法的时候,此时产生了很多局部变量,刚好老年代满了需要进行Full gc,如果不停止线程,垃圾回收正在根据这些局部变量也就是GC Root根搜索引用链,此时这个方法结束了,那么这些局部变量就都会被销毁,这些引用链的GC Root根都销毁了,这些引用当然也成了垃圾对象,这样就会导致在垃圾回收的过程中还会不断的产生新的垃圾。

框架面试题

1、什么是spring

https://blog.csdn.net/zhaohuodian/article/details/126364919

Spring: 是一个企业级java应用框架,他的作用主要是简化软件的开发以及配置过程,简化项目部署环境。

Spring的优点:

1、Spring低侵入设计,对业务代码的污染非常低。

2、Spring的DI机制将对象之间的关系交由框架处理,减少组件的耦合。

3、Spring提供了AOP技术,支持将一些通用的功能进行集中式管理,从而提供更好的复用。

4、Spring对于主流框架提供了非常好的支持。

2、什么是bean的自动装配,有哪些方式?

自动装配就是会通过Spring的上下文为你找出相应依赖项的类,通俗的说就是Spring会在上下文中自动查找,并自动给Bean装配与其相关的属性!

开启自动装配,只需要在xml文件中定义autowire属性

autowire属性有五种装配方式:

​ no:默认值,表示不使用自动装配,Bean 依赖必须通过 ref 元素定义

​ byName:根据bean的属性名称进行自动装配

​ byType:根据bean的类型进行自动装配

constructor:类似于byType,不过是应用于构造器的函数,如果一个bean与构造器参数的类型相同,则进行自 动装配,否则导致异常

autodetect:如果Bean中有默认的构造方法,则用constructor模式,否则用byType模式

优点

  • 自动装配只需要较少的代码就可以实现依赖注入

缺点

  • 不能自动装配简单数据类型,比如 int、boolean、String 等。
  • 相比较显示装配,自动装配不受程序员控制。
3、如何理解springboot的starter

starter就是定义一个starter的jar包,写一个@Configuration配置类,将这些bean定义在里面,然后在starter包的META-INF/spring.factoreis中写入该配置类,springboot会按照约定来加载该配置类

开发人员只需要将相应的starter包依赖进应用,进行相应的属性配置(使用默认配置时,不需要配置),就可以直接进行代码开发,使用对应的功能了,比如mybatis-spring-boot-starter

4、如何实现一个IOC容器

1、配置文件配置包扫描路径

2、递归包扫描.class文件

3、反射、确定需要交给IOC管理的类

4、对需要注入的类进行依赖注入

  • 配置文件中指定需要扫描的包路径
  • 定义一些注解,分别表示访问控制层、业务服务层、数据持久层、依赖注入注解、获取配置文件注解
  • 从配置文件中获取需要扫描的包路径,获取到当前路径下的文件信息及文件夹信息,我们将当前路径下所有的以.class结尾的文件添加到一个set集合中进行存储
  • 遍历这个set集合,获取在类上有指定注解的类,并将其交给IOC容器,定义一个安全的Map用来存储这些对象
  • 遍历这个IOC容器,获取到每一个类的实例,判断里面是否有依赖其他的类的实例,然后进行递归注入
5、spring中Bean是线程安全的吗?

Spring本身并没有针对Bean做线程安全的处理,所以:

1、如果Bean是无状态的,那么Bean则是线程安全的

2、如果Bean是有状态的,那么Bean则不是线程安全的

另外Bean是不是线程安全,跟Bean的作用域没有关系,Bean的作用域只是表示Bean的生命周期范围,对于任何生命周期的Bean都是一个对象,这个对象是不是线程安全的,还是看这个Bean对象本身

6、对IOC的理解

IOC 就是控制反转,控制反转是一种设计思想,而不是一种具体的技术实现

在 Spring 中,控制反转指的是将对象的控制权转移给 Spring 框架进行管理,由 Spring 帮我们创建对象,管理对象之间的依赖关系

以前创建对象的主动权和时机都是由自己把控的,现在由 IOC 容器来做,在很大程度上简化了应用的开发

IOC 容器实际上就是一个 Map 的键值对,Map 里面存放的是各种对象。IOC 容量就像一个工厂一样,当我们需要创建对象的时候,只需要通过 xml 配置文件或者注解,把对象注册到组件中,而我们完全不用考虑对象是如何被创建出来的

其中,IOC 的最常见以及最合理的实现方式叫做依赖注入(DI)
IOC的优点:

  • 降低对象之间的依赖程度和耦合度
  • 便于资源的管理。在 IOC 容器中,所有对象默认都是单例的
7、简述spring bean的生命周期
  1. 解析类得到BeanDefinition
  2. 如果有多个构造方法,则要推断构造方法
  3. 确定好构造方法好,进行实例化得到一个对象
  4. 对对象中的加了@Autowired注解的属性进行属性填充
  5. 回调Aware方法,比如BeanNameAware、BeanFactoryAware
  6. 调用BeanPostProcessor的初始化前的方法
  7. 调用初始化方法
  8. 调用BeanPostProcessor的初始化后的方法,在这里会进行AOP
  9. 如果当前创建的bean是单例的则会把bean放入单例池
  10. 使用bean
  11. Spring容器关闭时调用DisposableBean中的destory()方法
8、spring用到了哪些设计模式

工厂模式:BeanFactory、FactoryBean

适配器模式:AdvisorAdapter接口,对AdvisorAdapter进行了适配

访问者模式:PropertyAccessor接口,属性访问器,用来访问和设置某个对象的某个属性

装饰器模式:BeanWarpper

代理模式:AOP

观察者模式:事件监听机制

策略模式:InstantiationStrategy --根据不同的情况进行实例化

模板模式:JdbcTemplate

委派模式:BeanDefinitionParserDelegate

责任链模式:BeanPostProcessor

9、对AOP的理解

系统是由许多不同的组件所组成的,每一个组件各负责一块儿特定功能,除了实现自身核心功能以外,这些组件还经常承担着额外的职责。例如日志、事务管理和安全这样的核心服务经常融入到自身具有核心业务逻辑的组件中去。这些系统服务经常被称为横切关注点,因为它们会跨越系统的多个组件。

当我们需要为分散的对象引入公共行为的时候,OOP则显得无能为力。也就是说,OOP允许你定义从上到下的关系,但并不适合定义从左到右的关系,例如日志功能。

日志代码往往水平的散布在所有对象层次中,而与它所散布到的对象的核心功能毫无关系

在OOP的设计中,它导致了大量代码的重复,而不利于各个模块的重用

AOP:将程序中的业务交叉逻辑(比如安全,日志,事务等),封装成一个切面,然后注入到目标对象(具体业务逻辑)中去。AOP可以对某个对象或某些对象的功能进行增强,比如对象中的方法进行增强,可以在执行某个方法之前额外的做一些事情,在某个方法执行之后额外做一些事情

10、说一下Spring的事务机制
  1. Spring的事务底层是基于数据库事务和AOP机制的
  2. 首先对于使用了@Transactional注解的bean,spring会创建一个代理对象作为bean
  3. 当调用代理对象的方法时,会先判断该方法上是否加了@Transactional注解
  4. 如果加了,那么则利用事务管理器创建一个数据库连接
  5. 并且修改数据库连接的autocommit属性为false,禁止此连接的自动提交,这是实现spring事务非常重要的一步
  6. 然后执行当前方法,方法中会执行sql
  7. 执行完当前方法后,如果没有出现异常就直接提条事务
  8. 如果出现了异常,并且这个异常是需要回滚的就会回滚事务,否则仍然提交事务
  9. Spring事务的隔离级别对应的就是数据库的隔离级别
  10. Spring事务的传播机制是Spring事务自己实现的,也是Spring事务中最复杂的
  11. Spring事务的传播机制是基于数据库连接来做的,一个数据库连接一个事务,如果传播机制配置为需要新开一个事务,那么实际上就是先建立一个数据库连接,在此新数据库上连接上执行sql
11、说说常用的springboot注解,及其实现
  1. @SpringbootApplication注解:这个注解标识了一个Springboot工程,它实际上是另外三个注解的组合,这三个注解是:

    a.@SpringBootConfiguration注解:这个注解实际就是一个@Configuration,表示启动类也是一个配置类

    b.@EnableAutoConfiguration注解:向Spring容器中导入一个Selector,用来加载Classpath下SpringFactoreis中所定义的自动配置类,将这些自动加载为配置bean

    c.@ComponentScan:标识扫描路径,因为默认是没有配置实际扫描路径,所以SpringBoot扫描的路径是启动类所在的当前目录

  2. @Bean注解:用来定义Bean,类似于xml中的bena标签,Spring在启动时,会对加了@Bean注解的方法进行解析,将方法的名字作为BeanName,并通过执行方法得到bean对象

  3. @Controller注解:用来标识一个类为SpringMVC的控制器,Spring在启动时,会对加了@Controller注解的类实例化,并将每个对象的URL和函数的映射关系缓存到了全局

  4. @Service注解:用来表示一个类是一个业务逻辑层的服务 Bean,当 Spring 应用启动时,该 Bean 会被自动创建并加入到 Spring 应用上下文中。

  5. @Autowired注解:它可以对类成员变量、方法及构造函数进行标注,完成自动装配的工作。默认的注入方式为 byType

12、什么时候@Transactional失效

因为Spring的事务时基于代理来实现的,所以某个加了@Transactional的方法只有是被代理对象调用时,那么这个注解才会生效,所以如果是被代理对象来调用这个方法,那么@Transactional是不会生效的

同时如果某个方法是private的,那么@Transactional也会失效,因为底层cglib是基于父子类来实现的,子类是不能重载父类的private方法的,所以无法很好的利用代理,也会导致@Transactional失效

13、如何实现AOP,哪些地方用到了AOP

利用动态代理来实现AOP,比如JDK动态代理或者cglib动态代理,利用动态代理技术,可以针对某个类生成代理对象,当调用代理对象的某个方法时,可以任意控制该方法的执行,比如可以先打印执行时间,再执行该方法,并且该方法执行完成后,再次打印执行时间

项目中,比如事务、权限控制、方法执行时长日志都是通过AOP技术来实现的,凡是需要对某些方法做统一处理的都可以用AOP来实现,利用AOP可以做到业务无侵入

14、spring如何处理循环依赖问题

通过三级缓存来处理

  1. singletonObjects:缓存经过了完整生命周期的bean
  2. earlySingletonObjects:缓存未经过完整生命周期的bean,如果某个bean出现了循环依赖,就会提前把这个暂时未经过完整生命周期的bean放入earlySingletonObjects中,这个bean如果要经过AOP,那么就会把代理对象放入earlySingletonObjects中,否则就是把原始对象放入earlySingletonObjects,但是不管怎么样,就是是代理对象,代理对象所代理的原始对象也是没有经过完整生命周期的,所以放入earlySingletonObjects我们就可以统一认为是未经过完整生命周期的bean。
  3. singletonFactories:缓存的是一个ObjectFactory,也就是一个Lambda表达式。在每个Bean的生成过程中,经过实例化得到一个原始对象后,都会提前基于原始对象暴露一个Lambda表达式,并保存到三级缓存中,这个Lambda表达式可能用到,也可能用不到,如果当前Bean没有出现循环依赖,那么这个Lambda表达式没用,当前bean按照自己的生命周期正常执行,执行完后直接把当前bean放入singletonObjects中,如果当前bean在依赖注入时发现出现了循环依赖(当前正在创建的bean被其他bean依赖了),则从三级缓存中拿到Lambda表达式,并执行Lambda表达式得到一个对象,并把得到的对象放入二级缓存((如果当前Bean需要AOP,那么执行lambda表达式,得到就是对应的代理对象,如果无需AOP,则直接得到一个原始对象))。
  4. 其实还要一个缓存,就是earlyProxyReferences,它用来记录某个原始对象是否进行过AOP了。

对于对象之间的普通引用,二级缓存会保存new出来的不完整对象,这样当单例池中找到不依赖的属性时,就可以先从二级缓存中获取到不完整对象,完成对象创建,在后续的依赖注入过程中,将单例池中对象的引用关系调整完成。

三级缓存:如果引用的对象配置了AOP,那在单例池中最终就会需要注入动态代理对象,而不是原对象。而生成动态代理是要在对象初始化完成之后才开始的。于是Spring增加三级缓存,保存所有对象的动态代理配置信息。在发现有循环依赖时,将这个对象的动态代理信息获取出来,提前进行AOP,生成动态代理。

核心代码就在DefaultSingletonBeanRegistry的getSingleton方法当中

15、spring中后置处理器的作用

Spring中的后置处理器分为BeanFactory后置处理器和Bean后置处理器,它们是Spring底层源码设计架构设计中非常重要的一种机制,同时开发者也可以利用这两种后置处理器来进行扩展,BeanFactory后置处理器表示针对BeanFactory的处理器,Spring启动过程中,会先创建出BeanFactory实例,然后利用BeanFactory处理器来加工BeanFactory,比如Spring的扫描就是基于BeanFactory后置处理器来实现的,而Bean后置处理器也类似,Spring在创建一个Bean的过程中,首先会实例化得到一个对象,然后再利用Bean后置处理器来对该实例对象进行加工,比如我们常说的依赖注入就是基于一个Bean后置处理器来实现的,通过该Bean后置处理器来给实例对象中加了@Autowired注解的属性自动赋值,还比如我们常说的AOP,也是利用一个Bean后置处理器来实现的,基于原实例对象,判断是否需要进行AOP,如果需要,那么就基于原实例对象进行动态代理,生成一个代理对象

17、spring如何处理事务

Spring当中支持编程式事务管理和声明式事务管理两种方式:

1、编程式事务可以使用TransactionTemplate。

2、声明式事务: 是Spring在AOP基础上提供的事务实现机制。他的最大优点就是不需要在业务代码中添加事务管理的代码,只需要在配置文件中做相关的事务规则声明就可以了。但是声明式事务只能针对方法级别,无法控制代码级别的事务管理。

18、spring中Bean创建的生命周期步骤有哪些

首先,简单来说,Spring框架中的Bean经过四个阶段: 实例化 -》 属性赋值 -》 初始化 -》 销毁

然后: 具体来说,Spring中Bean 经过了以下几个步骤:

​ 1、实例化: new xxx(); 两个时机: 1、当客户端向容器申请一个Bean时,2、当容器在初始化一个Bean时发现还需要依赖另一个Bean。 BeanDefinition 对象保存。-到底是new一个对象还是创建一个动态代理?

​ 2、设置对象属性(依赖注入):Spring通过BeanDefinition找到对象依赖的其他对象,并将这些对象赋予当前对象。

​ 3、处理Aware接口:Spring会检测对象是否实现了xxxAware接口,如果实现了,就会调用对应的方法。

​ BeanNameAware、BeanClassLoaderAware、BeanFactoryAware、ApplicationContextAware

​ 4、BeanPostProcessor前置处理: 调用BeanPostProcessor的postProcessBeforeInitialization方法

​ 5、InitializingBean: Spring检测对象如果实现了这个接口,就会执行他的afterPropertiesSet()方法,定制初始化逻辑。

​ 6、init-method: 如果Spring发现Bean配置了这个属性,就会调用他的配置方法,执行初始化逻辑。@PostConstruct

​ 7、BeanPostProcessor后置处理: 调用BeanPostProcessor的postProcessAfterInitialization方法

到这里,这个Bean的创建过程就完成了, Bean就可以正常使用了。

​ 8、DisposableBean: 当Bean实现了这个接口,在对象销毁前就会调用destory()方法。

​ 9、destroy-method: @PreDestroy

19、spring支持的bean作用域

1.singleton:默认作用域Spring IOC容器仅存在一个Bean实例,Bean以单例方式存在,在创建容器时就同时自动创建了一个Bean对象。作用域范围是ApplicationContext中。

2.prototype:每次从容器中调用Bean时,都会返回一个新的实例,即每次调用getBean时。作用域返回是getBean方法调用直至方法结束。

相当于执行newXxxBean().Prototype是原型类型,再我们创建容器的时候并没有实例化,而是当我们获取Bean的时候才会去创建一个对象,而且我们每次获取到的对象都不是同一个对象。

3.request:每次HTTP请求都会创建一个新的Bean,作用域范围是每次发起http请求直至拿到相应结果。该作用域仅适用于WebApplicationContext环境。

4.session:首次http请求创建一个实例,作用域是浏览器首次访问直至浏览器关闭。

同一个HTTP Session共享一个Bean,不同Session使用不通的Bean,仅适用于WebApplicationContext环境。

5.global-session:作用域范围是WebApplicationContext中。一般用于Portlet应用环境,该运用域仅适用于WebApplicationContext环境。

作用域范围比较:

prototype < request < session < global-session < singleton

为什么要定义作用域:

可以通过Spring配置的方式限定Spring Bean的作用范围,可以起到对Bean使用安全的保护作用

20、spring容器的启动流程是怎样的?

使用AnnotationConfigApplicationContext 来跟踪一下启动流程:

this(); 初始化reader和scanner

scan(basePackages); 使用scanner组件扫描basePackage下的所有对象,将配置类的BeanDefinition注册到容器中。

refresh(); 刷新容器。

​ prepareRefresh 刷新前的预处理

​ obtainFreshBeanFactory: 获取在容器初始化时创建的BeanFactory

​ prepareBeanFactory: BeanFactory的预处理工作,会向容器中添加一些组件。

​ postProcessBeanFactory: 子类重写该方法,可以实现在BeanFactory创建并预处理完成后做进一步的设置。

​ invokeBeanFactoryPostProcessors: 在BeanFactory初始化之后执行BeanFactory的后处理器。

​ registerBeanPostProcessors: 向容器中注册Bean的后处理器,他的主要作用就是干预Spring初始化Bean的流程,完成代理、自动注入、循环依赖等这些功能。

​ initMessageSource: 初始化messagesource组件,主要用于国际化。

​ initApplicationEventMulticaster: 初始化事件分发器

​ onRefresh: 留给子容器,子类重写的方法,在容器刷新的时候可以自定义一些逻辑。

​ registerListeners: 注册监听器。

​ finishBeanFactoryInitialization: 完成BeanFactory的初始化,主要作用是初始化所有剩下的单例Bean。

​ finishRefresh: 完成整个容器的初始化,发布BeanFactory容器刷新完成的事件。

21、springmvc的九大组件

1、MultipartResolver(文件处理器),对应的初始化方法是initMultipartResolver(context),用于处理上传请求。处理方法是将普通的request包装成MultipartHttpServletRequest,后者可以直接调用getFile方法获取File。

2、LocaleResolver(当前环境处理器),对应的初始化方法是initLocaleResolver(context),这就相当于配置数据库的方言一样,有了这个就可以对不同区域的用户显示不同的结果。SpringMVC主要有两个地方用到了Locale:一是ViewResolver视图解析的时候;二是用到国际化资源或者主题的时候。

3、ThemeResolver(主题处理器),对应的初始化方法是initThemeResolver(context),用于解析主题。SpringMVC中一个主题对应一个properties文件,里面存放着跟当前主题相关的所有资源,如图片、css样式等。SpringMVC的主题也支持国际化。

4、HandlerMappings(处理器映射器),对应的初始化方法是initHandlerMappings(context),这就是根据用户请求的资源uri来查找Handler的。在SpringMVC中会有很多请求,每个请求都需要一个Handler处理,具体接收到一个请求之后使用哪个Handler进行处理呢

5、HandlerAdapters(处理器适配器),对应的初始化方法是initHandlerAdapters(context),从名字上看,它就是一个适配器。Servlet需要的处理方法的结构却是固定的,都是以request和response为参数的方法。如何让固定的Servlet处理方法调用灵活的Handler来进行处理呢?这就是HandlerAdapters要做的事情。

6、HandlerExceptionResolvers(异常处理器),对应的初始化方法是initHandlerExceptionResolvers(context),其它组件都是用来干活的。在干活的过程中难免会出现问题,出问题后怎么办呢?这就要有一个专门的角色对异常情况进行处理,在SpringMVC中就是HandlerExceptionResolver。

7、RequestToViewNameTranslator(视图名称翻译器),对应的初始化方法是initRequestToViewNameTranslator(context),有的Handler处理完后并没有设置View也没有设置ViewName,这时就需要从request获取ViewName了,如何从request中获取ViewName就是RequestToViewNameTranslator要做的事情了。

8、ViewResolvers(页面渲染处理器),对应的初始化方法是initViewResolvers(context),ViewResolver用来将String类型的视图名和Locale解析为View类型的视图。View是用来渲染页面的,也就是将程序返回的参数填入模板里,生成html(也可能是其它类型)文件。

9、FlashMapManager(参数传递管理器),对应的初始化方法是initFlashMapManager(context),用来管理FlashMap的,FlashMap主要用在redirect重定向中传递参数。

22、spring的事务传播机制

Spring中对事务定义了不同的传播级别: Propagation

1、 PROPAGATION_REQUIRED:默认传播行为。 如果当前没有事务,就创建一个新事务,如果当前存在事务,就加入到事务中。

2、PROPAGATION_SUPPORTS: 如果当前存在事务,就加入到该事务。如果当前不存在事务,就以非事务方式运行。

3、PROPAGATION_MANDATORY: 如果当前存在事务,就加入该事务。如果当前不存在事务,就抛出异常。

4、PROPAGATION_REQUIRES_NEW: 无论当前存不存在事务,都创建新事务进行执行。

5、PROPAGATION_NOT_SUPPORTED: 以非事务方式运行。如果当前存在事务,就将当前事务挂起。

6、PROPAGATION_NEVER : 以非事务方式运行。如果当前存在事务,就抛出异常。

7、PROPAGATION_NESTED: 如果当前存在事务,则在嵌套事务内执行;如果当前没有事务,则按REQUEIRED属性执行。

23、springmvc工作流程

1.客户端浏览器向前端控制器(DispatcherServlet)发出请求。

2.DispatcherServlet接收到请求后,调用处理器映射器(HandlerMapping)。

3.HandlerMapping根据请求url查找相应的处理器(Handler,也称后端控制器),返回处理器对象(Handler),并且如果有处理器拦截器(HandlerInterceptor)的话,会将处理器对象(Handler)和处理器拦截器对象(HandlerInterceptor)一并返回给DispatcherServlet。

4.DispatcherServlet拿到这些信息后,会调用处理器适配器(HandlerAdapter),HandlerAdapter会执行Handler,Handler执行处理DispatcherServlet发来的请求,生成ModelAndView对象返回给HandlerAdapter。

5.HandlerAdapter将ModelAndView对象返回给DispatcherServlet。

6.DispatcherServlet在拿到ModelAndView对象之后,将ModelAndView对象发给视图解析器(ViewResolver)。

7.ViewResolver将ModelAndView对象进行解析,生成View对象,将View对象返回给DispatcherServlet。

8.DispatcherServlet拿到View对象,对jsp页面进行渲染(将模型数据填充到视图中),将渲染后的页面呈现给用户。

24、springmvc中的控制器是不是单例模式

控制器是单例模式。

单例模式下就会有线程安全问题。

Spring中保证线程安全的方法

1、将scop设置成非singleton。 prototype, request。

2、最好的方式是将控制器设计成无状态模式。在控制器中,不要携带数据。但是可以引用无状态的service和dao。

25、springboot的自动配置原理

@Import + @Configuration + Spring spi

自动配置类由各个starter提供,使用@Configuration+@Bean定义配置类,放到META-INF/spring.factories下

使用Spring spi扫描META-INF/spring.factories下的配置类

使用@Import导入配置类

26、mybatis和hibernate的对比

https://cloud.tencent.com/developer/article/2131425

mybatis:

  1. 入门简单,即学即用,提供了数据库查询的自动对象绑定功能,而且延续了很好的SQL使用经验,对于没有那么高的对象模型要求的项目来说,相当完美。

  2. 可以进行更为细致的SQL优化,可以减少查询字段。

  3. 缺点就是框架还是比较简陋,功能尚有缺失,虽然简化了数据绑定代码,但是整个底层数据库查询实际还是要自己写的,工作量也比较大,而且不太容易适应快速数据库修改。

  4. 二级缓存机制不佳。

hibernate:

  1. 功能强大,数据库无关性好,O/R映射能力强,如果你对Hibernate相当精通,而且对Hibernate进行了适当的封装,那么你的项目整个持久层代码会相当简单,需要写的代码很少,开发速度很快,非常爽。

  2. 有更好的二级缓存机制,可以使用第三方缓存。

  3. 缺点就是学习门槛不低,要精通门槛更高,而且怎么设计O/R映射,在性能和对象模型之间如何权衡取得平衡,以及怎样用好Hibernate方面需要你的经验和能力都很强才行。

28、springboot中配置文件的加载顺序是怎样的?

优先级从高到低,高优先级的配置覆盖低优先级的配置,所有配置会形成互补配置

1、命令行参数,所有的配置都可以在命令行上进行指定

2、Java系统属性(System.getProperties())

3、操作系统环境变量

4、jar包外部的application-{profile}.properties或application.yml(带spring.profile)配置文件

5、jar包外部的application-{profile}.properties或application.yml(带spring.profile)配置文件,再来加载不带profile

6、jar包外部的application.properties或application.yml(不带spring.profile)配置文件

7、@Configuration注解类上的@PropertySource

29、spring、springmvc、springboot的区别

spring是一个IOC容器,用来管理Bean,使用依赖注入来实现控制反转,可以很方便的整合各种框架,提供AOP机制弥补OOP的代码重复问题,更方便将不同类不同方法中的共同处理抽取成切面、自动注入给方法执行,比如日志、异常等

SpringMVC是spring对web框架的一个解决方案,提供了一个总的前端控制器servlet,用来接收请求,然后定义了一套路由策略(url到handle的映射)及适配执行handle,将handle结果使用试图解析技术生成视图展现给前端

Springboot是spring提供的一个快速开发工具包,让程序员能更方便、更快速的开发spring+springmvc应用,简化了配置(约定了默认配置),整合了一系列的解决方案(starter机制)、redis、mongodb、es,可以开箱即用

31、springboot是如何启动tomact的

1、首先,Springboot在启动时会先创建一个Spring容器

2、在创建Spring容器过程中,会利用@ConditionalOnclass技术来判断当前classpath中是否存在Tomact依赖,如果存在则会生成一个启动Tomact的Bean

3、Spring容器创建完之后,就会获取启动Tomact的Bean,并创建Tomact对象,并绑定端口等,然后启动Tomcat

32、mybatis中#{}和${}的区别是什么

1、#{}是预编译处理、是占位符,${}是字符串替换、是拼接符

2、Mybatis在处理#{}时,会将sql中的#{}替换为?号,调用PreparedStatement来赋值

3、Mybatis在处理 时,就是把 {}时,就是把 时,就是把{}替换成变量的值,调用Statement来赋值

4、使用#{}可以有效的防止SQL注入,提高系统安全性

33、ApplicationContext和BeanFactory的区别

BeanFactory是Spring中非常核心的组件,表示Bean工厂,可以生成Bean,维护Bean,而ApplicaitonContext继承了BeanFactory,所以ApplicationContext拥有BeanFactory所有的特点,也是一个Bean工厂,但是ApplicationContext除开继承了BeanFactory之外,还集成了诸如MessageSource、ApplicationEventPublisher等接口,从而ApplicationContext还有获取系统环境变量、国际化、事件发布等功能,这是BeanFactory所不具备的

34、mybatis插件运行原理及开发流程、

Mybatis只支持针对ParameterHandler、ResultSetHandler、StatementHandler、Executor这4种接口的插件,Mybatis使用JDK的动态代理,为需要拦截的接口生成代理对象以实现接口方法拦截功能,每当执行这4种接口对象的方法时,就会进入拦截方法,具体就是InvocationHandler的invoke()方法,拦截那些你指定需要拦截的方法

编写插件:实现Mybatis的Interceptor接口并复用intercept()方法,然后在给插件编写注解,指定要拦截哪一个接口的哪些方法即可,在配置文件中配置编写的插件

35、mybatis的优缺点

优点:

1、基于SQL语句编程,相当灵活,不会对应用程序或者数据库的现有设计造成任何影响,SQL写在XML里,解除SQL与程序代码的耦合,便于统一管理;提供XML标签,支持编写动态SQL语句,并可重用

2、与JDBC相比,减少了50%以上的代码量,消除了JDBC大量冗余的代码,不需要手动开关连接

3、很好的与各种数据库兼容(因为Mybatis使用JDBC来连接shu’ju’ku,所以只需要JDBC支持的数据库Mybatis都支持)

4、能够与Spring很好的集成

5、提供映射标签,支持对象与数据库的ORM字段关系映射;提供对象关系映射标签,支持对象关系组件维护

缺点:

1、SQL语句的编写工作量较大,尤其当字段多、关联表多时,对开发人员编写SQL语句的功底有一定要求

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

36、mybatis的三级缓存

一级缓存也就做本地缓存,一级缓存是在会话也就是SqlSession层面实现的,一级缓存的作用范围是在同一个SqlSession中,不同的SqlSession及时查询相同的数据也不会走缓存。

  • 当执行增删改方法时会清除缓存。
  • MyBatis全局配置属性localCacheScope配置为Statement时,那么完成一次查询就会清除缓存。
  • 是否配置了flushCache=true属性,如果配置了则会清除一级缓存
  • SqlSession 需要开启事务才会生效。否则每次查询都坏创建一个新的SqlSession
  • 不同的SqlSession 对应不同的缓存
  • 同一个 SqlSession 查询条件不同 也会不走缓存。
  • 手动清空缓存,调用SqlSession clearCache()方法。

在同一个SqlSession中 两次相同条件查询中,第一次查询后 然后手动修改表数据或者另一个SqlSession对象修改了数据库或者分布式情况下数据发生了修改 那么第二次查询是直接走缓存,查询结果依旧相同,会存在数据不一致问题。

二级缓存是mapper级别的 需要手动开启,他的作用范围更广也就是mapper文件的一个命名空间(namespace)中

  • 当执行增删改方法时会清除缓存。

二级缓存也存在同一级缓存一样的问题,只不过二级缓存的作用域比一级缓存大,

在同一个命名空间(namespace)中 两次相同条件查询中,第一次查询后 然后手动修改表数据或者分布式情况下数据发生了修改 那么再次查询是直接走缓存,查询结果依旧相同,会存在数据不一致问题

三级缓存:第三方自定义缓存

37、介绍一下Spring,读过源码介绍一下大致流程

1、Spring是一个快速开发框架,Spring帮助程序员来管理对象

2、Spring的源码实现是非常优秀的,设计模式的应用、并发安全的实现、面试接口的设计等

3、在创建spring容器,也就是启动spring时:

​ a、首先会进行扫描,扫描得到所有的BeanDefintion对象,并存在一个map中

​ b、然后筛选出非懒加载的单例BeanDefintion进行创建Bean,对于多例Bean会在每次获取Bean时利用BeanDefintion去创建

​ c、利用BeanDefintion创建Bean的创建生命周期,这期间包括了合并BeanDefintion、推断构造方法、实例化、属性填充、初始化前、初始化、初始化后等步骤,其中AOP就是发生在初始化后这一步骤中

4、单例Bean创建完了以后,Spring会发布一个容器启动事件、

5、Spring启动结束

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

7、在spring启动过程中还会处理@Import等注解

38、Spring中事务的隔离级别

Spring中事务的隔离级别:

1、ISOLATION_DEFAULT: 使用数据库默认的事务隔离级别。

2、ISOLATION_READ_UNCOMMITTED: 读未提交。允许事务在执行过程中,读取其他事务未提交的数据。

3、ISOLATION_READ_COMMITTED: 读已提交。允许事务在执行过程中,读取其他事务已经提交的数据。

4、ISOLATION_REPEATABLE_READ: 可重复读。 在同一个事务内,任意时刻的查询结果是一致的。

5、ISOLATION_SERIALIZABLE: 所有事务依次执行。

redis

1、说一下你知道的redis高可用方案

主从

哨兵模式:

sentinel,哨兵是redis集群中非常重要的一个组件,主要有以下功能:

  • 集群监控:负责监控
  • 消息通知:如果某个redis实例有故障,那么哨兵负责发送消息作为报警通知给管理员
  • 故障转移:如果master node挂掉了,会自动转移到slave node上
  • 配置中心:如果故障转移发生了,通知client客户端新的master地址

哨兵用于实现redis集群的高可用,本身也是分布式的,作为一个哨兵集群去运行,互相协同工作

  • 故障转移时,判断一个master node是否宕机了,需要大部分的哨兵都同意才行,涉及到了分布式选举
  • 即使部分哨兵节点挂掉了,哨兵集群还是能正常工作的
  • 哨兵通常需要3个实例,来保证自己的健壮性
  • 哨兵+redis主从的部署架构,是不保证数据零丢失的,只能保证redis集群的高可用性
  • 对于哨兵+redis主从这种复杂的部署架构,尽量在测试环境和正式环境,都进行充足的测试和演练

Redis Cluster是一种服务端Sharding技术,3.0版本开始正式提供,采用slot(槽)的概念,一共分成1638个槽,将请求发送到任意节点,接收到请求的节点会将查询请求发送到正确的节点上执行

方案说明

  • 通过哈希的方式,将数据分片,每个节点均分存储一定哈希槽(哈希值)区间的数据,默认分配了16384个槽位
  • 每份数据分片会存储在多个互为主从的多节点上
  • 数据写入先写主节点,再同步到从节点(支持配置为阻塞同步)
  • 同一分片多个节点间的数据不保持强一致性
  • 读取数据时,当客户端操作的key没有分配在该节点上时,redis会返回转向指令,指向正确的节点
  • 不支持批量操作(piepeline管道操作)
  • 分布式逻辑和存储模块耦合等

Redis Sharding是Redis Cluster出来之前,业界普遍使用的多Redis实例集群方法。其主要思想是采用哈希算法将Redis数据的key进行散列,通过hash函数,特定的key会映射到特定的Redis节点上。Java redis客户端驱动jedis,支持Redis Sharding功能,即Sharded jedis以及结合缓存池的Sharded jedis pool

优点

优势在于非常简单,服务端的Redis实例彼此独立,相互无关联,每个Redis实例像单服务器一样运行,非常容易线性扩展,系统的灵活性很强

缺点

由于Sharding处理放到客户端,规模进一步扩大时给运维带来挑战

客户端Sharding不支持动态增删节点。服务端Redis实例群拓扑结构有变化时,每个客户端需要更新调整。连接不能共享,当应用规模增大时,资源浪费制约优化

2、如何避免缓存穿透、缓存击穿、缓存雪崩

缓存雪崩是缓存同一时间大面积的失效,所以,后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉

解决方案

  • 缓存数据设置随机过期时间,防止同一时间大量数据过期现象发生
  • 给每一个缓存数据增加相应的缓存标记,记录缓存是否失效,如果缓存标记失效,则更新数据缓存
  • 缓存预热
  • 互斥锁

缓存穿透是指缓存和数据库中都没有的数据,导致所有的请求都落到数据库上,造成数据库短时间内承受大量请求而崩掉

解决方案

  • 接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截;
  • 从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击
  • 采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据库会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力

缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。和缓存雪崩不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库

解决方案

  • 设置热点数据永不过期
  • 加互斥锁
3、如何保存数据库与缓存一致性

由于缓存和数据库是分开的,无法做到原子性的同时进行数据修改,可能出现缓存更新失败,或者数据库更新失败的情况,这时候会出现数据不一致,影响前端业务

  • 先更新数据,再更新缓存,缓存可能更新失败,读到老数据

  • 先删缓存,再更新数据库。并发时,读操作可能还是会将旧数据读回缓存

  • 先更新数据库,再删缓存。也存在缓存删除失败的可能

    最经典的缓存+数据库读写的模式,读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。更新的时候,先更新数据库,然后再删除缓存

为什么是删除而不是更新?

删除更加轻量,延迟加载的一种实现,更新可能涉及到多个表,比较耗时

延时双删: 先删除缓存,然后再写数据库,休眠一小会,再次删除缓存。 如果数据写操作很频繁,同样还是会有脏数据的问题。

4、Redis主从同步机制

1、从节点执行slaveof masterlp port,保存主节点信息

2、从节点中的定时任务发现主节点信息,建立和主节点的socket连接

3、从节点发送信号,主节点返回,两边能互相通信

4、连接建立后,主节点将所有的数发给从节点(数据同步)

5、主节点把当前的数据同步给从节点后,便完成了复制过程。接下来,主节点就会持续的把写命令发送给从节点,保证主从数据一致性

runid:每个redis节点启动都会生成唯一的runid,每次redis重启后,runid都会发生变化

offset:主从节点各自维护自己的复制偏移量offset,当主节点有写入命令时,offset=offset+命令的字节长度。

从节点在收到主节点发送的命令后,也会增加自己的offset,并把自己的offset发送给主节点。主节点同时保存自己的offset和从节点的offset,通过对比offset来判断主从节点数据是否一致

repl_backlog_size:保存在主节点上的一个固定长度的先进先出队列,默认大小是1MB

全量复制:

  • 从节点发出psync命令,psync runid offset(由于是第一次,runid为?,offset为-1)
  • 主节点返回FULLRESYNC runid offset,runid是主节点的runid,offset是主节点目前的offset。从节点保存信息
  • 主节点启动bgsave命令fork子进程进行RDB持久化
  • 主节点将RDB文件发送给从节点,到从节点加载数据完成之前,写命令到缓冲区、
  • 从节点清理本地数据并加载RDB,如果开启了AOF会重写AOF

部分复制:

1、复制偏移量:psync runid offset

2、复制积压缓冲区:当主从节点offset的差距过大超过缓冲区长度时,将无法执行部分复制,只能执行全量复制。

  • 如果从节点保存的runid与主节点现在的runid相同,说明主从节点之前同步过,主节点会继续尝试使用部分复制(到底能不能部分复制还要看offset和复制积压缓冲区的情况)
  • 如果从节点保存的runid与主节点现在的runid不同,说明从节点在断线前同步的redis节点并不是当前的主节点,只能进行全量复制
5、简述redis事务实现

(28条消息) Redis 事务的实现_简述redis事务实现_vawterchen的博客-CSDN博客

1、事务开始:MULTI 命令的执行标志着事务的开始:MULTI 命令可以将执行该命令的客户端从非事务状态切换成事务状态,这个切换是通过在客户端状态的 flags 属性中打开 REDIS_MULTI 标识来完成的。

2、命令入队:当一个客户端处于非事务状态时,这个客户端发送的命令会立即被服务器执行,但是当一个客户端切换到事务状态之后,服务器会根据这个客户端发来的不同命令执行不同的操作

如果客户端发送的命令是 EXEC、DISCARD、WATCH、MULTI 四个命令的其中一个,那么服务器立即执这个命令;

如果客户端发送的命令是 EXEC、DISCARD、WATCH、MULTI 四个命令以外的其他命令,那么服务器并不立即执行这个命令,而是将这个命令放入一个事务队列里,然后向客户端返回 QUEUED 回复。

事务队列已先进先出(FIFO)的方式保存入队命令,较先入队的命令会被放到数组的前面,而较后入队的命令则会被放到数组的后面。

3、执行事务:当一个处于事务状态的客户端向服务器发送 EXEC 命令时,这个 EXEC 命令将立即被服务器执行。服务器会遍历这个客户端的事务队列,执行队列中保存的所有命令,最后执行命令所得的结果全部返回给客户端

  • 如果客户端的 REDIS_DIRTY_CAS 标识已经被打开,那么说明客户端所监视的键当中,至少有一个键已经被修改过了,在这种情况下,客户端提交的事务已经不再安全,所以服务器会拒绝执行客户端提交的事务;

  • 如果 客户端的 REDIS_DIRTY_CAS 标识没有被打开,那么说明客户端监视的所有键都没有被修改过(或者客户端没有监视任何键),事务仍然是安全的,服务器将执行客户端提交的事务。

Redis 的事务和传统的关系型数据库事务的最大区别在于,Redis 不支持事务回滚机制(rollback),即使事务队列中的某个命令在执行期间出现了错误,整个事务也会继续执行下去,直到将事务队列中的所有命令都执行完毕为止。但redis会检查每一个事务中命令是否正确

Redis事务不支持检查那些程序员自己的逻辑错误。例如对String类型的数据键执行对HashMap类型的操作

  • WATCH 命令是一个乐观锁(optimistic locking),它可以在 EXEC 命令执行之前,监视任意数量的数据库键,并在 EXEC 命令执行时,检查被监视的键是否至少有一个已经被修改过了,如果是的话,服务器将拒绝执行事务,并向客户端返回代表事务执行失败的空回复
  • MULTl命令用于开启一个事务,它总是返回OK。MULTI执行以后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被放到一个队列中,当EXEC命令被调用时,所有队列中的命令才会被执行
  • EXEC:执行所有事务块内的命令。返回事务块内的所有命令的返回值,按命令执行的先后顺序排列。当操作被打断时,返回空值null
  • 通过调用DISCARD,客户端可以清空事务队列,并放弃执行事务,并且客户端会从事务状态中退出
  • UNWATCH命令可以取消watch对所有key的监控
6、redis分布式锁实现

面试官:你真的了解Redis分布式锁吗? - 技术进阶之路 - SegmentFault 思否

7、简述redis九大数据结构

String(字符串)、Hash(哈希)、List(列表)、Set(集合)、Zset(有序集合),和三种特殊类型 GeoHash(地理位置,借助Sorted Set实现,通过zset的score进行排序就可以得到坐标附近的其它元素,通过将score还原成坐标值就可以得到元素的原始坐标)、HyperLogLog(基数统计,统计不重复数据)、Bitmaps(布隆过滤器)、streams(内存版的kafka)

8、缓存过期都有哪些策略

定时过期

这是最常见也是应用最多的策略,为每个设置过期时间的 key 都需要创建一个定时器,到过期时间就会立即清除。这种方式可以立即删除过期数据,避免浪费内存,但是需要耗费大量的 CPU 资源去处理过期的数据,可能影响缓存服务的性能。

惰性过期

可以类比懒加载的策略,这个就是懒过期,只有当访问一个 key 时,才会判断该 key 是否已过期,并且进行删除操作。这种方式可以节省 CPU 资源,但是可能会出现很多无效数据占用内存,极端情况下,缓存中出现大量的过期 key 无法被删除。

定期过期

这种方式是上面方案的整合,添加一个即将过期的缓存字典,每隔一定的时间,会扫描一定数量的 key,并清除其中已过期的 key。

合理的缓存配置,需要协调内存淘汰策略和过期策略,避免内存浪费,同时最大化缓存集群的吞吐量。另外,Redis 的缓存失效有一点特别关键,那就是如何避免大量主键在同一时间同时失效造成数据库压力过大的情况。

10、布隆过滤器原理、优缺点

bitmap:int[10],每个int类型的整数是4*8=32个bit,则int[10]一共有320bit,每个bit非0即1,初始化时都是0

添加数据时,将数据进行hash得到hash值,对应到bit位,将该bit改为1,hash函数可以定义多个,则一个数据添加会将多个(hash函数个数)bit改为1,多个hash函数的目的是减少hash碰撞的概率

查询数据:hash函数计算得到hash值,对应到bit中,如果有一个为0,则说明数据不在bit中,如果都为1,则该数据可能在bit中

优点:

增加和查询元素的时间复杂度为:O(K), (K为哈希函数的个数,一般比较小),与数据量大小无关
哈希函数相互之间没有关系,方便硬件并行运算
布隆过滤器不需要存储元素本身,在某些对保密要求比较严格的场合有很大优势
在能够承受一定的误判时,布隆过滤器比其他数据结构有这很大的空间优势
数据量很大时,布隆过滤器可以表示全集,其他数据结构不能
使用同一组散列函数的布隆过滤器可以进行交、并、差运算

缺点:

  1. 有误判率,即存在假阳性(False Position),即不能准确判断元素是否在集合中(补救方法:再建立一个白
    名单,存储可能会误判的数据)
  2. 不能获取元素本身
  3. 一般情况下不能从布隆过滤器中删除元素
  4. 如果采用计数方式删除,可能会存在计数回绕问题
11、常见的缓存淘汰算法

FIFO(First In First Out,先进先出),根据缓存被存储的时间,离当前最远的数据优先被淘汰;

LRU(Least Recently Used,最近最少使用),根据最近被使用的时间,离当前最远的数据优先被淘汰;

LFU(Least Frequently Used,最不经常使用),在一段时间内,缓存数据被使用次数最少的会被淘汰。

12、redis的持久化机制

(28条消息) Redis(一):Redis的持久化的原理和操作_redis持久化_CodingALife的博客-CSDN博客

RDB:对redis中的数据执行周期性的持久化,例如每隔几分钟、几小时、几天生成redis内存中的数据的一份完整快照

优点:

容灾性好,方便备份

整个redis将只包含一个dump.db,方便持久化

读写性能高。RDB是对redis对外提供的读写服务,影响非常小,是保证redis性能最高的:因为redis主进程只需要fork一个子进程,让子进程执行磁盘IO操作来进行RDB持久化即可。

数据恢复快速。相对于AOF持久化机制来说,直接基于RDB数据文件来重启和恢复redis进程,更加快速。

缺点:

RDB丢失数据。如果想要在redis故障时,尽可能少的丢失数据,那么RDB没有AOF好。一般来说,RDB数据快照文件,都是每隔5分钟,或者更长时间生成一次,这个时候就得接受一旦redis进程宕机,那么会丢失最近5分钟的数据。

RDB影响性能。RDB每次在fork子进程来执行RDB快照数据文件生成的时候,如果数据文件特别大,可能会导致对客户端提供的服务暂停数毫秒,或者甚至数秒

AOF:将写入的一条条数据写入os cache中,然后每隔一定时间调用操作系统的 fsync 操作强制将os cache 中的数据刷入磁盘文件AOF中。对每条写入命令作为日志,以append-only的模式写入一个日志文件中,在redis重启的时候,可以通过回放AOF日志中的写入指令来重新构建整个数据集。

优点:

AOF保护数据。AOF可以更好的保护数据不丢失,一般AOF会每隔1秒,通过一个后台线程执行一次fsync操作,最多丢失1秒钟的数据。每隔1秒,就执行一次fsync操作,保证os cache中的数据写入磁盘中,redis进程挂了,最多丢掉1秒钟的数据。

AOF写入性能高。AOF日志文件以append-only模式写入,所以没有任何磁盘寻址的开销,写入性能非常高,而且文件不容易破损,即使文件尾部破损,也很容易修复。

AOF日志文件即使过大的时候,出现后台重写操作,也不会影响客户端的读写。因为在rewrite log的时候,会对其中的指导进行压缩,创建出一份需要恢复数据的最小日志出来。再创建新日志文件的时候,老的日志文件还是照常写入。当新的merge后的日志文件ready的时候,再交换新老日志文件即可。

灾难性误删除紧急恢复。AOF日志文件的命令通过非常可读的方式进行记录,这个特性非常适合做灾难性的误删除的紧急恢复。比如某人不小心用flushall命令清空了所有数据,只要这个时候后台rewrite还没有发生,那么就可以立即拷贝AOF文件,将最后一条flushall命令给删了,然后再将该AOF文件放回去,就可以通过恢复机制,自动恢复所有数据。

缺点:

数据快照文件大:对于同一份数据来说,AOF日志文件通常比RDB数据快照文件更大。

运行效率没有RDB高

数据恢复效率不如RDB

推荐两者一起使用

13、redis的线程模型,单线程为什么快?

Redis基于Reactor模式开发了网络事件处理器,这个处理器被称为文件事件处理器。

redis 内部使用文件事件处理器 file event handler,这个文件事件处理器是单线程的,所以 redis 才叫做单线程的模型。 它采用 IO 多路复用机制同时监听多个 socket,根据 socket 上的事件来选择对应的事件处理器进行处理。 多个 socket 可能会并发产生不同的操作,每个操作对应不同的文件事件,但是 IO 多路复用 程序会监听多个 socket,会将 socket 产生的事件放入队列中排队,事件分派器每次从队列中取出一个事件,把该事件交给对应的事件处理器进行处理。

它的组成结构为4部分:多个套接字、IO多路复用程序、文件事件分派器、事件处理器

单线程操作很快,主要依赖于以下几个原因:

(一)纯内存操作,避免大量访问数据库,减少直接读取磁盘数据,redis将数据储存在内存里面,读写数据的时候都不会受到硬盘 I/O 速度的限制,所以速度快;

(二)单线程操作,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;

(三)采用了非阻塞I/O多路复用机制

14、Redis的淘汰机制

在Redis内存已经满的时候,添加了一个新的数据,执行淘汰机制。

  • volatile-lru:在内存不足时,Redis会再设置过了生存时间的key中干掉一个最近最少使用的key。
  • allkeys-lru:在内存不足时,Redis会再全部的key中干掉一个最近最少使用的key。
  • volatile-lfu:在内存不足时,Redis会再设置过了生存时间的key中干掉一个最近最少频次使用的key。
  • allkeys-lfu:在内存不足时,Redis会再全部的key中干掉一个最近最少频次使用的key。
  • volatile-random:在内存不足时,Redis会再设置过了生存时间的key中随机干掉一个。
  • allkeys-random:在内存不足时,Redis会再全部的key中随机干掉一个。
  • volatile-ttl:在内存不足时,Redis会再设置过了生存时间的key中干掉一个剩余生存时间最少的key。
  • noeviction:(默认)在内存不足时,直接报错。

指定淘汰机制的方式:maxmemory-policy 具体策略,设置Redis的最大内存:maxmemory 字节大小

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

1、首先利用setnx来保证:如果key不存在才能获取到锁,如果key存在,则获取不到锁

2、然后还要利用lua脚本来保证多个redis操作的原子性

3、同时还要考虑到锁过期,所以需要额外的一个看门狗定时任务来监听锁是否需要续约

4、同时还要考虑redis节点挂掉后的情况,所以需要采用红锁的方式来同时向N/2+1个节点申请锁,都申请到了才证明获取锁成功,这样就算其中某个redis节点挂掉了,锁也不能被其他客户端获取到

mysql

1、ACID靠什么保证的

A 原子性由undo log日志保证,它记录了需要回滚的日志信息,事务回滚时撤销已经执行成功的sql

C一致性由其他三大特性保证、程序代码要保证业务上的一致性

I 隔离性由MVCC来保证

D持久性由内存+redo log来保证,mysql修改数据同时在内存和redo log记录这次操作,宕机的时候可以从redo log恢复。

InnoDB redo log 写盘,InnoDB 事务进入 prepare 状态。
如果前面 prepare 成功,binlog 写盘,再继续将事务日志持久化到 binlog,如果持久化成功,那么InnoDB 事务则进入 commit 状态(在 redo log 里面写一个 commit 记录)

redolog的刷盘会在系统空闲时进行

2、B树和B+树的区别,为什么Mysql使用B+树

B树的特点:

  1. 所有键值分布在整颗树中(索引值和具体data都在每个节点里);
  2. 任何一个关键字出现且只出现在一个结点中;
  3. 搜索有可能在非叶子结点结束(最好情况O(1)就能找到数据);
  4. 在关键字全集内做一次查找,性能逼近二分查找;

B+树的特点:

1、拥有B树的特点

2、叶子节点之间有指针

3、所有关键字存储在叶子节点出现,内部节点(非叶子节点并不存储真正的 data)

一般来说,索引本身也很大,不可能全部存储在内存中,因此索引往往以索引文件的形式存储的磁盘上。这样的话,索引查找过程中就要产生磁盘I/O消耗,相对于内存存取,I/O存取的消耗要高几个数量级,所以评价一个数据结构作为索引的优劣最重要的指标就是在查找过程中磁盘I/O操作次数的渐进复杂度。换句话说,索引的结构组织要尽量减少查找过程中磁盘I/O的存取次数。

B树不管叶子节点还是非叶子节点,都会保存数据,这样导致在非叶子节点中能保存的指针数量变少(有些资料也称为扇出)

指针少的情况下要保存大量数据,只能增加树的高度,导致IO操作变多,查询性能变低

而B+树通过对数据进行排序所以可以提高查询速度的,然后通过一个节点中可以存储多个元素,从而可以使得B+树的高度不会太高,在MySQL中一个innodb页就是一个B+树节点,一个innodb页默认16kb。所以一般情况下一颗两层的B+树可以存2000万行左右的数据,然后通过利用B+树叶子节点存储了所有数据并且进行了排序,并且叶子节点之间有指针,可以很好的支持全表扫描,范围查找等sql语句

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

事务: 表示多个数据操作组成一个完整的事务单元,这个事务内的所有数据操作要么同时成功,要么同时失败。

事务的特性:ACID

1、原子性:事务是不可分割的,要么完全成功,要么完全失败。

2、一致性:事务无论是完成还是失败,都必须保持事务内操作的一致性。当失败时,都要对前面的操作进行回滚,不管中途是否成功。

3、隔离性:当多个事务操作一个数据的时候,为防止数据损坏,需要将每个事务进行隔离,互相不干扰。

4、持久性: 事务开始就不会终止。他的结果不受其他外在因素的影响。

事务的隔离级别:SHOW VARIABLES like ‘transaction%’

设置隔离级别: set transaction level xxx 设置下次事务的隔离级别。

set session transaction level xxx 设置当前会话的事务隔离级别

set global transaction level xxx 设置全局事务隔离级别

MySQL当中有五种隔离级别

NONE : 不使用事务。

读未提交(READ UNCOMMITED): 允许脏读

读已提交(READ COMMITED): 防止脏读,最常用的隔离级别

可重复读(REPEATABLE READ):防止脏读和不可重复读。MYSQL默认

串行化(SERIALIZABLE): 事务串行,可以防止脏读、幻读,不可重复度。

五种隔离级别,级别越高,事务的安全性是更高的,但是,事务的并性能也就会越低。

3、Explain语句结果中各个字段分表表示什么?
列名描述
select_typeselect关键字对应的那个查询的类型
idd列的编号是 select 的序列号,有几个 select 就有几个id,并且id的顺序是按 select 出现的顺序增长的。 id列越大执行优先级越高,id相同则从上往下执行,id为NULL最后执行。
table表明
type针对单表的查询方式(全表扫描、索引)
partitions匹配的分区信息
possible_keys这一列显示查询可能使用哪些索引来查找
key实际上使用的索引
key_len实际上使用的索引长度
ref当使用索引列等值查询时,与索引列进行等值匹配的对象信息
rows预估需要读取的记录条数
filtered某个表经过搜索条件过滤后剩余记录条数的百分比
Extra一些额外的信息,比如排序等
4、Innodb是如何实现事务的

利用回滚日志(undo log) 和 重做日志(redo log) 两种表实现事务,并实现 MVCC (多版本并发控制);

在执行事务的每条SQL时,会先将数据原值写入undo log 中, 然后执行SQL对数据进行修改,最后将修改后的值写入redo log中。

redo log 重做日志包括两部分:1 是内存中的重做日志缓冲 ;2 是重做日志文件。在事务提交时,必须先将该事务的所有日志写入到重做日志文件进行持久化,待事务commit操作完成才算完成。

当一个事务中的所有SQL都执行成功后,会将redo log 缓存中的数据刷入磁盘,然后提交。

如果发生回滚,会根据undo log 恢复数据。

5、什么是MVCC

什么是MySQL的MVCC机制? - 知乎 (zhihu.com)

MVCC全称是多版本并发控制 (Multi-Version Concurrency Control),只有在InnoDB引擎下存在。MVCC机制的作用其实就是避免同一个数据在不同事务之间的竞争,提高系统的并发性能。

它的特点如下:

  • 允许多个版本同时存在,并发执行。
  • 不依赖锁机制,性能高。
  • 只在读已提交和可重复读的事务隔离级别下工作。

RC和RR隔离级别的实现就是通过版本控制来完成,核心处理逻辑就是判断所有版本中哪个版本是当前事务可见的处理,通过什么判断呢?就是上文讲到的ReadView,ReadView包含了当前系统活跃的读写事务的信息,判断的逻辑如下:

  • 如果被访问版本的trx_id属性值小于ReadView的最小事务Id,表示该版本的事务在生成 ReadView 前已经提交,所以该版本可以被当前事务访问。
  • 如果被访问版本的trx_id属性值大于ReadView的最大事务Id,表示该版本的事务在生成 ReadView 后才生成,所以该版本不可以被当前事务访问。
  • 如果被访问版本的trx_id属性值在m_ids列表最小事务Id和最大事务Id之间,那就需要判断一下 trx_id 属性值是不是包含在 m_ids 列表中,如果包含的话,说明创建 ReadView 时生成该版本的事务还是活跃的,所以该版本不可以访问;如果不包含的话,说明创建 ReadView 时生成该版本的事务已经被提交,该版本可以被访问。
6、mysql的索引结构是什么样的?

二叉树 -》 AVL树 -》 红黑树 -》 B-树 -》 B+树

二叉树: 每个节点最多只有两个子节点, 左边的子节点都比当前节点小,右边的子节点都比当前节点大。

AVL树: 树中任意节点的两个子树的高度差最大为1

红黑树:1、每个节点都是红色或者黑色。2 根节点是黑色。3 每个叶子节点都是黑色的空节点。4 红色节点的父子节点都必须是褐色。5 从任一节点到其每个叶子节点的所有路径都包含相同的黑色节点。

B-树: 1、B-树的每个非叶子节点的子节点个数都不会超过D(这个D就是B-树的阶)2、所有的叶子节点都在同一层。3.所有节点关键字都是按照递增顺序排列。

B+树: 1、非叶子节点不存储数据,只进行数据索引。2、所有数据都存储在叶子节点当中。3、每个叶子节点都存有相邻叶子节点的指针。4、叶子节点按照本身关键字从小到大排序。

7、mysql的锁有哪些?

从锁的粒度来区分

1、行锁:加锁粒度小,但是加锁资源开销比较大。 InnDB支持。

​ 共享锁: 读锁。多个事务可以对同一个数据共享同一把锁。持有锁的事务都可以访问数据,但是只能读不能修改。select xxx LOCK IN SHARE MODE。

​ 排他锁: 写锁。只有一个事务能够获得排他锁,其他事务都不能获取该行的锁。InnoDB会对update\delete\insert语句自动添加排他锁。SELECT xxx FOR UPDATE。

​ 自增锁: 通常是针对MySQL当中的自增字段。如果有事务回滚这种情况,数据会回滚,但是自增序列不会回滚。

2、表锁:加锁粒度大,加锁资源开销比较小。MyISAM和InnoDB都支持。

​ 表共享读锁

​ 表排他写锁

​ 意向锁:是InnoDB自动添加的一种锁,不需要用户干预。

3、全局锁: Flush tables with read lock 。 加锁之后整个数据库实例都处于只读状态。所有的数据变更操作都会被挂起。一般用于全库备份的时候。

常见的锁算法: user: userid ( 1,4,9) update user set xxx where userid=5; REPEATABLE READ 间隙锁锁住(5,9)

1、记录锁:锁一条具体的数据。

2、间隙锁:RR隔离级别下,会加间隙锁。锁一定的范围,而不锁具体的记录。是为了防止产生幻读。(-xx,1)(1,4)(4,9)(9,xxx)

3、Next-key : 间隙锁+右记录锁。(-xx,1](1,4](4,9](9,xxx)

8、mysql集群如何搭建

MySQL通过将主节点的Binlog同步给从节点完成主从之间的数据同步。

MySQL的主从集群只会将binlog从主节点同步到从节点,而不会反过来同步。由此也就引申出了读写分离的问题。

因为要保证主从之间的数据一致,写数据的操作只能在主节点完成, 而读数据的操作,可以在主节点或者从节点上完成。

9、mysql聚簇索引和非聚簇索引的区别

MyISAM使用的是非聚簇索引,树的子节点上的data不是数据本身,而是数据存放的地址。InnoDB采用的是聚簇索引,树的叶子节点上的data就是数据本身。

聚簇索引的数据物理存放顺序和索引顺序是一致的,所以一个表当中只能有一个聚簇索引,而非聚簇索引可以有多个。

​ InnoDB中,如果表定义了PK,那PK就是聚簇索引。 如果没有PK,就会找第一个非空的unique列作为聚簇索引。否则,InnoDB会创建一个隐藏的row-id作为聚簇索引。

10、mysql慢查询该如何优化

1、检查是否走了索引,如果没有则优化sql利用索引

2、检查所利用的索引是否是最优索引

3、检查所有的字段是否都是必须的,是否查询了过多的字段,查出了多余的数据

4、检查表中的数据是否过多,是否应该进行分库分表了

5、检查数据库实例所在机器的性能配置,是否太低,是否可以适当增加资源

11、MySQL索引的数据结构,各自优劣

InnoDB引擎默认索引实现:B+树索引。对于hash索引来说,底层的数据结构就是哈希表,因此绝大数需求为单条记录查询的时候,可以使用hash索引,查询性能最快;其余大部分场景,建议选择B+Tree

12、MySQL有哪几种存储引擎

MySQL中通过show ENGINES指令可以看到所有支持的数据库存储引擎。 最为常用的就是MyISAM 和InnoDB 两种。

MyISAM和InnDB的区别:

1、存储文件。 MyISAM每个表有两个文件。 MYD和MYISAM文件。 MYD是数据文件。 MYI是索引文件。 而InnDB每个表只有一个文件,idb。

2、InnoDB支持事务,支持行级锁,支持外键。

3、InnoDB支持XA事务

4、InnoDB支持savePoints

13、mysql执行计划怎么看

通过explain关键字

14、MySQL主从同步原理

mysql自身实现主从同步,主要是利用到binlog 日志。

当sql操作写入binlog,就已经算作sql执行成功了,而不是写入到对应磁盘中(刷盘)。所以binlog中对应的值,我们可以理解为就是mysql的一个映射,同步mysql数据不需要捞取磁盘中的数据进行同步,而只需要同步binlog日志就行。
具体的同步原理如下:

(1)主从同步设置好之后(进行相关的诸如ip,端口,服务id,等操作设置后)
(2)相关变动会写入到binlog中
(3)从机会发送获取binlog的请求到主机中
(4)maser会启动一个线程:binlog dumplog 线程,这个线程会通知从机,当前存在SQL变更,并将binlog的变动发送到从机上
(5)从机收到请求后,会启动线程:i/o线程 ,该线程会将收到的binlog日志加载到中继日志delay log中
(6)从机中的另外一个线程:SQL 线程会读取relay日志中的信息,刷新到从机中

注:主从节点使用binlog文件+postion偏移量来定位主从同步的位置,从节点会保存其已接收到的偏移量,如果从节点发生宕机重启,则会自动从postion的位置发生同步

由于mysql默认的复制方式是异步的,主库把日志发送给从库后从不关心从库是否已经处理,这样会产生一个问题就是假设主库挂了,从库处理失败了,这时候从库升为主库后,日志就丢失了,由此产生两个概念

全同步复制

指当主库执行完一个事务,所有的从库都执行了该事务才返回给客户端。因为需要等待所有从库执行完该事务才能返回,所以全同步复制的性能必然会收到严重的影响。

半同步复制

是介于全同步复制与全异步复制之间的一种,主库只需要等待至少一个从库节点收到并且 Flush Binlog 到 Relay Log 文件即可,主库不需要等待所有从库给主库反馈。同时,这里只是一个收到的反馈,而不是已经完全完成并且提交的反馈,如此,节省了很多时间。

15、MySQL数据库,什么情况下设置了索引但无法使用?

1、没有符合最左前缀原则

2、字段进行了隐私数据类型转化

3、走索引没有全表扫描效率高

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

UUID:简单、性能好,没有顺序,没有业务含义,存在泄露mac地址的风险

雪花算法

数据库主键:实现简单,单调递增,具有一定的业务可读性,强依赖db、存在性能瓶颈,存在暴露业务信息的风险

redis、mongodb、zk等中间件:增加了系统的复杂度和稳定性

16、在海量数据下如何快速查找一条记录?

1、使用布隆过滤器,快速过滤不存在的记录。

​ 使用Redis的bitmap结构来实现布隆过滤器。

2、在Redis中建立数据缓存。 - 将我们对Redis使用场景的理解尽量表达出来。

​ 以普通字符串的形式来存储,(userId -> user.json)。 以一个hash来存储一条记录 (userId key-> username field-> , userAge->)。 以一个整的hash来存储所有的数据,UserInfo-> field就用userId , value就用user.json。一个hash最多能支持2^32-1(40多个亿)个键值对。

​ 缓存击穿:对不存在的数据也建立key。这些key都是经过布隆过滤器过滤的,所以一般不会太多。

​ 缓存过期:将热点数据设置成永不过期,定期重建缓存。 使用分布式锁重建缓存。

3、查询优化。

​ 按槽位分配数据,

​ 自己实现槽位计算,找到记录应该分配在哪台机器上,然后直接去目标机器上找。

17、简述MyISAM和InnoDB的区别
  1. InnoDB支持事务,MyISAM不支持,对于InnoDB每一条SQL语言都默认封装成事务,自动提交,这样会影响速度,所以最好把多条SQL语言放在begin和commit之间,组成一个事务;

  2. InnoDB支持外键,而MyISAM不支持。对一个包含外键的InnoDB表转为MYISAM会失败;

  3. InnoDB是聚集索引,使用B+Tree作为索引结构,数据文件是和(主键)索引绑在一起的(表数据文件本身就是按B+Tree组织的一个索引结构),必须要有主键,通过主键索引效率很高。但是辅助索引需要两次查询,先查询到主键,然后再通过主键查询到数据。因此,主键不应该过大,因为主键太大,其他索引也都会很大。

  4. InnoDB不保存表的具体行数,执行select count(*) from table时需要全表扫描。而MyISAM用一个变量保存了整个表的行数,执行上述语句时只需要读出该变量即可,速度很快(注意不能加有任何WHERE条件);

  5. Innodb不支持全文索引,而MyISAM支持全文索引,在涉及全文索引领域的查询效率上MyISAM速度更快高;PS:5.7以后的InnoDB支持全文索引了

  6. MyISAM表格可以被压缩后进行查询操作

  7. InnoDB支持表、行(默认)级锁,而MyISAM支持表级锁

  8. InnoDB表必须有唯一索引(如主键)(用户没有指定的话会自己找/生产一个隐藏列Row_id来充当默认主键),而Myisam可以没有

  9. Innodb存储文件有frm、ibd,而Myisam是frm、MYD、MYI

Innodb:frm是表定义文件,ibd是数据文件

Myisam:frm是表定义文件,myd是数据文件,myi是索引文件

如何选择:

  1. 是否要支持事务,如果要请选择innodb,如果不需要可以考虑MyISAM;
  2. 如果表中绝大多数都只是读查询,可以考虑MyISAM,如果既有读也有写,请使用InnoDB。
  
  3. 系统奔溃后,MyISAM恢复起来更困难,能否接受;
  
  4. MySQL5.5版本开始Innodb已经成为Mysql的默认引擎(之前是MyISAM),说明其优势是有目共睹的,如果你不知道用什么,那就用InnoDB,至少不会差。
18、简述MySQL中索引类型及对数据库的性能的影响

普通索引: 允许被索引的数据列包含重复的值
唯一索引: 可以保证数据记录的唯一性
主键索引: 是一种特殊的唯一索引,在一张表中只能定义一个主键索引,主键用于唯一标识一条记录,使用关键字primary key来创建
联合索引: 索引可以覆盖多个数据列
全文索引: 通过建立倒排索引,可以极大的提升检索效率,解决判断字段是否包含的问题,是目前搜索引擎使用的一种关键技术
索引可以极大地提高数据的查询速度
通过使用索引,可以在查询的过程中,使用优化隐藏器,提高系统的性能
但是会降低插入、删除、更新表的速度,因为在执行这些写操作的时候,还要操作索引文件
索引需要占物理空间,除了数据表占数据空间之外,每一个索引还要占一定的物理空间,如果要建立聚簇索引,那么需要的空间就会更大,如果非聚簇索引很多,一旦聚簇索引改变,那么所有非聚簇索引都会跟着变

19、MySQL的锁你了解哪些

按锁粒度分类:

1.行锁:锁某行数据,锁粒度最小,并发度高。

2.表锁:锁整张表,锁粒度最大,并发度低。

3.间隙锁:锁的是一个区间

还可以分为:

1.共享锁:也就是读锁,一个事务给某行数据加了读锁,其他事务也可以读,但是不能写。

2.排他锁:也就是写锁,一个事务给某行数据加了写锁,其他事务不能读,也不能写。

还可以分为:

1.乐观锁:并不会真正的去锁某行记录,而是通过一个版本号来实现的。

2.悲观锁:上面说的行锁、表锁等都是悲观锁。

在事务的隔离级别实现中,就需要利用锁来解决幻读

21、如何实现分库分表

将原本存储于单个数据库上的数据拆分到多个数据库,把原来存储在单张数据表的数据拆分到多张数据表中,实现数据切分,从而提升数据库操作性能。

分库分表的实现可以分为两种方式:垂直切分和水平切分。

水平:将数据分散到多张表,涉及分区键,

分库:每个库结构一样,数据不一样,没有交集。库多了可以缓解io和cpu压力

分表:每个表结构一样,数据不一样,没有交集。表数量减少可以提高sql执行效率、减轻cpu压力

垂直:将字段拆分为多张表,需要一定的重构

分库:每个库结构、数据都不一样,所有库的并集为全量数据

分表:每个表结构、数据不一样,至少有一列交集,用于关联数据,所有表的并集为全量数据存储拆分后如何解决唯一主键问题

UUID:简单、性能好,没有顺序,没有业务含义,存在泄漏mac地址的风险

数据库主键:实现简单,单调递增,具有一定的业务可读性,强依赖db、存在性能瓶颈,存在暴露业务信息的风险

redis,mongodb,zk等中间件:增加了系统的复杂度和稳定性

雪花算法

22、什么是脏读、幻读、不可重复读

脏读

1、在事务A执行过程中,事务A对数据资源进行了修改,事务B读取了事务A修改后的数据。

2、由于某些原因,事务A并没有完成提交,发生了RollBack操作,则事务B读取的数据就是脏数据。

这种读取到另一个事务未提交的数据的现象就是脏读(Dirty Read)。

不可重复读

事务B读取了两次数据资源,在这两次读取的过程中事务A修改了数据,导致事务B在这两次读取出来的数据不一致。

这种在同一个事务中,前后两次读取的数据不一致的现象就是不可重复读(Nonrepeatable Read)。

幻读

事务B前后两次读取同一个范围的数据,在事务B两次读取的过程中事务A新增了数据,导致事务B后一次读取到前一次查询没有看到的行。

幻读和不可重复读有些类似,但是幻读强调的是集合的增减,而不是单条数据的更新。

23、索引的基本原理

就是把无序的数据变成有序地查询

    1.创建了索引的列的内容进行排序

    2.对排序结果生成倒排表(对于hash来说倒排表里面的数据是hashcode?)

    3.在倒排表内容上拼上数据地址链 (类似于对hash中的数据中保存实际地址的索引)

    4.在查询的时候,先拿到倒排表内容,再取出数据地址链,从而拿到数据
24、索引的设计原则

查询更快、占用空间更小

  1. 适合索引的列是出现在where子句中的列,或者连接子句中指定的列
  2. 基数较小的表,索引效果较差,没有必要在此列建立索引
  3. 使用短索引,如果对长字符串列进行索引,应该指定一个前缀长度,这样能够节省大量索引空间,如果搜索词超过索引前缀长度,则使用索引排除不匹配的行,然后检查其余行是否可能匹配。
  4. 不要过度索引。索引需要额外的磁盘空间,并降低写操作的性能。在修改表内容的时候,索引会进行更新甚至重构,索引列越多,这个时间就会越长。所以只保持需要的索引有利于查询即可。
  5. 定义有外键的数据列一定要建立索引。
  6. 更新频繁字段不适合创建索引
  7. 若是不能有效区分数据的列不适合做索引列(如性别,男女未知,最多也就三种,区分度实在太低)
  8. 尽量的扩展索引,不要新建索引。比如表中已经有a的索引,现在要加(a,b)的索引,那么只需要修改原来的索引即可
  9. 对于那些查询中很少涉及的列,重复值比较多的列不要建立索引
  10. 对于定义为text、image和bit的数据类型的列不要建立索引。、
25、索引覆盖是什么?

指一个查询语句的执行只用从索引中就能够取得,那么就表示此SQL走完索引后不用回表从数据表中读取。也可以称之为覆盖索引

26、谈谈如何对MySQL进行分库分表

什么是分库分表? 就是当表中的数据量过大时,整个查询效率就会降低得非常明显。这时为了提升查询效率,就要将一个表中的数据分散到多个数据库的多个表当中。

分库分表最常用的组件: Mycat\ ShardingSphere

数据分片的方式有垂直分片和水平分片。垂直分片就是从业务角度将不同的表拆分到不同的库中,能够解决数据库数据文件过大的问题,但是不能从根本上解决查询问题。水平分片就是从数据角度将一个表中的数据拆分到不同的库或表中,这样可以从根本上解决数据量过大造成的查询效率低的问题。

有非常多的分片策略,比如 取模、按时间、按枚举值。。。。

阿里提供的开发手册当中,建议:一个表的数据量超过500W或者数据文件超过2G,就要考虑分库分表了。

一个user表,按照userid进行了分片,然后我需要按照sex字段去查,这要怎么查?强制指定只查一个数据库,要怎么做?查询结果按照userid来排序,要怎么排?

分库分表的问题: 跨库查询、跨库排序、分布式事务、公共表、主键重复。。。。。

27、怎么处理慢查询

发现了慢查询之后,关于如何定位问题发生原因,最常用的方法就是利用EXPLAIN关键字模拟查询优化器执行查询SQL,从而知道MySQL是如何处理你的查询SQL,通过执行计划来分析性能瓶颈。

三个方向:

1、分析语句,看看是否加载了额外的数据

2、分析语句的执行计划,使得语句尽量命中索引

3、如果语句已经无法优化,考虑分库分表

  • 优化数据结构
  • 应用索引策略
  • 查询缓存
28、最左前缀匹配原则

https://blog.csdn.net/yuanchangliang/article/details/107798724

最左前缀匹配原则,非常重要的原则,建立一个索引,对于索引中的字段,mysql会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配,比如a = 1 and b = 2 and c > 3 and d = 4 如果建立(a,b,c,d)顺序的索引,d是用不到索引的,如果建立(a,b,d,c)的索引则都可以用到,a,b,d的顺序可以任意调整。
=和in可以乱序,比如a = 1 and b = 2 and c = 3 建立(a,b,c)索引可以任意顺序,mysql的查询优化器会帮你优化成索引可以识别的形式

微服务

1、谈谈你对微服务的理解

就目前而言,对于微服务业界并没有一个统一的,标准的定义。

但通常而言,微服务架构是一种架构模式或者说是一种架构风格,它提倡将单一应用程序划分一组小的服务,每个服务运行在其独立的自己的进程中,服务之间相互协调、互相配合,为用户提供最总价值。服务之间采用轻量级的通信机制互相沟通(通常是基于HTTP的RESTful API),每个服务都围绕着具体的业务进行构建,并且能够被独立的构建在生产环境、类生产环境等。另外,应避免统一的、集中式的服务管理机制,对具体的一个服务而言,应根据业务上下文,选择合适的语言、工具对其进行构建,可以有一个非常轻量级的集中式管理来协调这些服务,可以使用不同的语言来编写服务,也可以使用不同的数据存储。

2、怎么拆分微服务

拆分微服务的时候,为了尽量保证微服务的稳定,会有一些基本的准则:

1、微服务之间尽量不要有业务交叉。

2、微服务之前只能通过接口进行服务调用,而不能绕过接口直接访问对方的数据。

3、高内聚,低耦合。

高内聚低耦合,是一种从上而下指导微服务设计的方法。实现高内聚低耦合的工具主要有 同步的接口调用 和 异步的事件驱动 两种方式。

3、项目中怎么保证微服务敏捷开发

开发运维一体化。

敏捷开发: 目的就是为了提高团队的交付效率,快速迭代,快速试错。

每个月固定发布新版本,以分支的形式保存到代码仓库中。快速入职。任务面板、站立会议。团队人员灵活流动,同时形成各个专家代表。测试环境- 生产环境 -》 开发测试环境SIT、集成测试环境、压测环境STR、预投产环境、生产环境PRD。文档优先。晨会、周会、需求拆分会。

链路追踪:1、基于日志。 形成全局事务ID,落地到日志文件。 filebeat- logstash-Elasticsearch 形成大型报表。2、基于MQ,往往需要架构支持。经过流式计算形成一些可视化的结果。

持续集成:SpringBoot maven pom ->build -> shell ; Jenkins。

AB发布:1、蓝绿发布、红黑发布。 老版本和新版本是同时存在的。2、灰度发布、金丝雀发布。

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

服务雪崩是指,由于服务提供者不可用导致服务调用者不可用,并且在生产过程中,这种不可用逐渐扩大的现象。

服务限流其实是指就是防止雪崩的一种方式之一,当系统资源不够,不足以应对大量请求,即系统资源与访问量出现矛盾的时候,我们为了保证有限的资源能够正常服务,因此对系统按照预设的规则进行流量限制或功能限制的一种方法。

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

服务熔断的作用类似于我们家用的保险丝,当某服务出现不可用或响应超时的情况时,为了防止整个系统出现雪崩,暂时停止对该服务的调用。

服务降级是从整个系统的负荷情况出发和考虑的,对某些负荷会比较高的情况,为了预防某些功能(业务场景)出现负荷过载或者响应慢的情况,在其内部暂时舍弃对一些非核心的接口和数据的请求,而直接返回一个提前准备好的fallback(退路)错误处理信息。这样,虽然提供的是一个有损的服务,但却保证了整个系统的稳定性和可用性。

相同点:

  • 目标一致 都是从可用性和可靠性出发,为了防止系统崩溃;
  • 用户体验类似 最终都让用户体验到的是某些功能暂时不可用;

不同点:

  • 触发原因不同 服务熔断一般是某个服务(下游服务)故障引起,而服务降级一般是从整体负荷考虑;
6、Spring cloud核心组件及其作用

Eureka(注册中心)
每个微服务都有一个 Eureka Client 组件,专门负责将这个服务的信息注册到 Eureka Server 中,也就是告诉 Eureka Server,自己在
哪台机器上,监听着哪个端口。而 Eureka Server 是一个注册中心,里面有一个注册表,保存了各服务所在的机器和端口号。

Feign(REST 客户端)
Feign 是一个声明式 REST 客户端,主要是为了简便服务调用,更快捷、优雅地调用 HTTP API。主要是实现原理是用动态代理,你要是调用
哪个接口,本质就是调用 Feign 创建的动态代理。

Ribbon(负载均衡)
Ribbon 的作用是负载均衡,会帮你在每次请求时选择一台机器,均匀的把请求分发到各个机器上,默认使用的最经典的 Round Robin 轮询
算法(如果发起 10 次请求,那就先让你请求第 1 台机器、然后是第 2 台机器、第 3 台机器,接着再来—个循环,第 1 台机器、第 2 台
机器。。。以此类推)

Hystrix(熔断器)
微服务框架是许多服务互相调用的,要是不做任何保护的话,某一个服务挂了,就会引起连锁反应,导致别的服务也挂。Hystrix 是隔离、熔
断以及降级的一个框架。如果调用某服务报错(或者挂了),就对该服务熔断,在 5 分钟内请求此服务直接就返回一个默认值,不需要每次都
卡几秒,这个过程,就是所谓的熔断。但是熔断了之后就会少调用一个服务,此时需要做下标记,标记本来需要做什么业务,但是因为服务挂
了,暂时没有做,等该服务恢复了,就可以手工处理这些业务。这个过程,就是所谓的降级。

Zuul(服务网关)
Zuul,也就是微服务网关。这个组件是负责网络路由的。假设你后台部署了几百个服务,现在有个前端兄弟要来调用这些服务,难不成你让他把
所有服务的名称和地址全部记住,这是不现实的,所以一般微服务架构中都必然会设计一个网关,所有请求都往网关走,网关会根据请求中的一
些特征,将请求转发给后端的各个服务。而且有一个网关之后,还有很多好处,比如可以做统一的降级、限流、认证授权、安全,等等。

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

7、什么是Hystrix?简述实现机制

分布式容错框架

阻止故障的连锁反应,实现熔断
快速失败,实现优雅降级
提供实时的监控和告警
资源隔离:线程隔离、信号量隔离

线程隔离:Hystrix 会给每一个Command分配一个单独的线程池,这样在进行单个服务调用的时候,就可以在独立的线程池里面进行,而不会对其它线程池造成影响
信号量隔离:客户端向依赖服务发起请求时,首先要获取一个信号量才能真正发起调用,由于信号量的数量优先,当发生请求量超过信号量个数时,后续的请求都会直接拒绝,进入 fallback流程。信号量隔离主要是通过控制并发请求量,防止请求线程大面积阻塞,从而达到限流和防止雪崩的目的。
熔断和降级:调用服务失败后快速失败

熔断:是为了防止异常不扩散,保证系统的稳定性

降级:编写好调用失败的补救逻辑,然后对服务直接停止运行,这样这些接口就无法正常调用,但又不至于直接报错,只是服务水平下降

通过HystrixCommand 或者 HystrixObservableCommand 将所有的外部系统(或者称为依赖)包装起来,整个包装对象是单独运行在一个线程之中(这是典型的命令模式)。

超时请求应该超过你定义的阈值

为每个依赖关系维护一个小的线程池(或信号量);如果它变满了,那么依赖关系的请求将立即被拒绝,而不是排队等待

统计成功,失败(由客户端抛出的异常),超时和线程拒绝

打开断路器可以在一段时间内停止对特性服务的所有请求,如果服务的错误百分比通过阈值,手动或自动的关闭断路器。

当请求被拒绝、连接超时或者断路器打开,直接执行 fallback 逻辑。

近乎实时监控指标和配置变化。

8、Spring cloud和Spring cloud Alibaba都有哪些组件?

Springcloud Alibaba

Sentinel:把流量作为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。
Nacos:一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。
RocketMQ:一款开源的分布式消息系统,基于高可用分布式集群技术,提供低延时的、高可靠的消息发布与订阅服务。
Dubbo:Apache Dubbo™ 是一款高性能 Java RPC 框架。
Seata:阿里巴巴开源产品,一个易于使用的高性能微服务分布式事务解决方案。
Alibaba Cloud OSS: 阿里云对象存储服务(Object Storage Service,简称 OSS),是阿里云提供的海量、安全、低成本、高可靠的云存储服务。您可以在任何应用、任何时间、任何地点存储和访问任意类型的数据。
Alibaba Cloud SchedulerX: 阿里中间件团队开发的一款分布式任务调度产品,提供秒级、精准、高可靠、高可用的定时(基于 Cron 表达式)任务调度服务。
Alibaba Cloud SMS: 覆盖全球的短信服务,友好、高效、智能的互联化通讯能力,帮助企业迅速搭建客户触达通道。

9、高并发场景下如何实现系统限流

限流方案中有一点非常关键,那就是如何判断当前的流量已经达到我们设置的最大值,具体有不同的实现策略,下面进行简单分析。

  1. 计数器法

一般来说,我们进行限流时使用的是单位时间内的请求数,也就是平常说的 QPS,统计 QPS 最直接的想法就是实现一个计数器。

计数器法是限流算法里最简单的一种算法,我们假设一个接口限制 100 秒内的访问次数不能超过 10000 次,维护一个计数器,每次有新的请求过来,计数器加 1。

这时候判断,

  • 如果计数器的值小于限流值,并且与上一次请求的时间间隔还在 100 秒内,允许请求通过,否则拒绝请求
  • 如果超出了时间间隔,要将计数器清零
  1. 漏桶和令牌桶算法

漏桶算法和令牌桶算法,在实际应用中更加广泛,也经常被拿来对比。

漏桶算法可以用漏桶来对比,假设现在有一个固定容量的桶,底部钻一个小孔可以漏水,我们通过控制漏水的速度,来控制请求的处理,实现限流功能。

漏桶算法的拒绝策略很简单:如果外部请求超出当前阈值,则会在水桶里积蓄,一直到溢出,系统并不关心溢出的流量。

漏桶算法是从出口处限制请求速率,并不存在上面计数器法的临界问题,请求曲线始终是平滑的。

它的一个核心问题是对请求的过滤太精准了,我们常说“水至清则无鱼”,其实在限流里也是一样的,我们限制每秒下单 10 万次,那 10 万零 1 次请求呢?是不是必须拒绝掉呢?

大部分业务场景下这个答案是否定的,虽然限流了,但还是希望系统允许一定的突发流量,这时候就需要令牌桶算法。

在令牌桶算法中,假设我们有一个大小恒定的桶,这个桶的容量和设定的阈值有关,桶里放着很多令牌,通过一个固定的速率,往里边放入令牌,如果桶满了,就把令牌丢掉,最后桶中可以保存的最大令牌数永远不会超过桶的大小。当有请求进入时,就尝试从桶里取走一个令牌,如果桶里是空的,那么这个请求就会被拒绝。

不知道你有没有使用过 Google 的 Guava 开源工具包?在 Guava 中有限流策略的工具类 RateLimiter,RateLimiter 基于令牌桶算法实现流量限制,使用非常方便。

RateLimiter 会按照一定的频率往桶里扔令牌,线程拿到令牌才能执行,RateLimter 的 API 可以直接应用,主要方法是 acquiretryAcquire

acquire 会阻塞,tryAcquire 方法则是非阻塞的。

3、滑动窗口算法

滑动窗口算法是计数器算法的一种改进,将原来的一个时间窗口划分成多个时间窗口,并且不断向右滑动该窗口。流量经过滑动时间窗口算法整形之后,可以保证任意时间窗口内,都不会超过最大允许的限流值,从流量曲线上来看会更加平滑,可以部分解决上面提到的临界突发流量问题。对比固定时间窗口限流算法,滑动时间窗口限流算法的时间窗口是持续滑动的,并且除了需要一个计数器来记录时间窗口内接口请求次数之外,还需要记录在时间窗口内每个接口请求到达的时间点,对内存的占用会比较多。 在临界位置的突发请求都会被算到时间窗口内,因此可以解决计数器算法的临界问题。

10、SOA、分布式、微服务之间有什么关系和区别?

1.分布式架构是指将单体架构中的各个部分拆分,然后部署不同的机器或进程中去, SOA 和微服务基本上都是分布式架构师
2.SOA是一种面向服务的架构,系统的所有服务都注册在总线上,当调用服务时,从总线上查找服务信息,然后调用
3.微服务是一种更彻底的面向服务的架构,将系统中各个功能个体抽成一个个小的应用程序,基本保持一个应用对应的一个服务的架构

消息中间件

1、kafka为什么比rocketMQ的吞吐量要高

kafka性吞吐量更高主要是由于Producer端将多个小消息合并,批量发向Broker。. kafka采用异步发送的机制,当发送一条消息时,消息并没有发送到broker而是缓存起来,然后直接向业务返回成功,当缓存的消息达到一定数量时再批量发送。. 此时减少了网络io,从而提高了消息发送的性能,但是如果消息发送者宕机,会导致消息丢失,业务出错,所以理论上kafka利用此机制提高了io性能却降低了可靠性。

2、kafka的pull与push分别有什么优缺点?

1、pull表示 消费者主动拉取 ,可以 批量拉取 ,也可以 单条拉取 ,所以pull可以由消费者⾃⼰控制,根据⾃⼰的消息处理能⼒来进⾏控制,但是消费者不能及时知道是否有消息, 可能会拉到的消息为空

2、push表示Broker主动给消费者推送消息,所以肯定是有消息时才会推送,但是消费者不能按⾃⼰的能⼒来消费消息,推过来多少消息,消费者就得消费多少消息,所以可能会造成网络堵塞消费者压⼒⼤等问题

3、kafka、ActiveMQ、RabbitMQ、RocketMQ对比
特性ActiveMQRabbitMQRocketMQKafka
开发语言javaerlangjavascala
单机吞吐量万级万级10万级10万级
时效性ms级us级ms级ms级以内
可用性高(主从架构)高(主从架构)非常高(分布式架构)非常高(分布式架构)
功能特性成熟的产品,在很多公司得到应用;有较多的文档;各种协议支持较好基于erlang开发,所以并发能力很强,性能极其好,延时很低;管理界面较丰富MQ功能比较完备,扩展性佳只支持主要的MQ功能,像一些消息查询,消息回溯等功能没有提供,毕竟是为大数据准备的,在大数据领域应用广。

综合上面的材料得出以下两点:
(1)中小型软件公司,建议选RabbitMQ.一方面,erlang语言天生具备高并发的特性,而且他的管理界面用起来十分方便。正所谓,成也萧何,败也萧何!他的弊端也在这里,虽然RabbitMQ是开源的,然而国内有几个能定制化开发erlang的程序员呢?所幸,RabbitMQ的社区十分活跃,可以解决开发过程中遇到的bug,这点对于中小型公司来说十分重要。不考虑rocketmq和kafka的原因是,一方面中小型软件公司不如互联网公司,数据量没那么大,选消息中间件,应首选功能比较完备的,所以kafka排除。不考虑rocketmq的原因是,rocketmq是阿里出品,如果阿里放弃维护rocketmq,中小型公司一般抽不出人来进行rocketmq的定制化开发,因此不推荐。
(2)大型软件公司,根据具体使用在rocketMq和kafka之间二选一。一方面,大型软件公司,具备足够的资金搭建分布式环境,也具备足够大的数据量。针对rocketMQ,大型软件公司也可以抽出人手对rocketMQ进行定制化开发,毕竟国内有能力改JAVA源码的人,还是相当多的。至于kafka,根据业务场景选择,如果有日志采集功能,肯定是首选kafka了。具体该选哪个,看使用场景。

4、kafka消息高可靠解决方案

(29条消息) Kafka如何保证消息的可靠性?_红丶的博客-CSDN博客

5、MQ保证消息幂等性

其实就是要方式消费者重复消费消息的问题。

所有MQ产品并没有提供主动解决幂等性的机制,需要由消费者自行控制。

这一块应该还是要结合业务来选择合适的方法,有以下几个方案:

消费数据为了单纯的写入数据库,可以先根据主键查询数据是否已经存在,如果已经存在了就没必要插入了。或者直接插入也没问题,因为可以利用主键的唯一性来保证数据不会重复插入,重复插入只会报错,但不会出现脏数据。

消费数据只是为了缓存到redis当中,这种情况就是直接往redis中set value了,天然的幂等性。

针对复杂的业务情况,可以在生产消息的时候给每个消息加一个全局唯一ID,消费者消费消息时根据这个ID去redis当中查询之前是否消费过。如果没有消费过,就进行消费并将这个消息的ID写入到redis当中。如果已经消费过了,就无需再次消费了。

6、MQ如何保证分布式事务的最终一致性

分布式事务:业务相关的多个操作,保证他们同时成功或者同时失败。

最终一致性: 与之对应的就是强一致性

MQ中要保护事务的最终一致性,就需要做到两点

1、生产者要保证100%的消息投递。 事务消息机制

2、消费者这一端需要保证幂等消费。 唯一ID+ 业务自己实现幂等

分布式MQ的三种语义:

at least once

at most once

exactly once:

​ RocketMQ 并不能保证exactly once。商业版本当中提供了exactly once的实现机制。

​ kafka: 在最新版本的源码当中,提供了exactly once的demo。

​ RabbitMQ: erlang天生就成为了一种屏障。

7、MQ如何保证消息的高效读写

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

传统文件复制方式: 需要对文件在内存中进行四次拷贝。

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

Java当中对零拷贝进行了封装, Mmap方式通过MappedByteBuffer对象进行操作,而transfile通过FileChannel来进行操作。

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

Transfile没有文件大小限制。

RocketMQ当中使用Mmap方式来对他的文件进行读写。commitlog。 1G

在kafka当中,他的index日志文件也是通过mmap的方式来读写的。在其他日志文件当中,并没有使用零拷贝的方式。

​ kafka使用transfile方式将硬盘数据加载到网卡。

8、MQ如何保证消息的顺序

全局有序和局部有序: MQ只需要保证局部有序,不需要保证全局有序。

生产者把一组有序的消息放到同一个队列当中,而消费者一次消费整个队列当中的消息。

RocketMQ中有完整的设计,但是在RabbitMQ和Kafka当中,并没有完整的设计,需要自己进行设计。

RabbitMQ:要保证目标exchange只对应一个队列。并且一个队列只对应一个消费者。

Kafka: 生产者通过定制partition分配规则,将消息分配到同一个partition。 Topic下只对应一个消费者。

9、MQ如何保证消息不丢失

2、怎么去防止消息丢失。

2.1 生产者发送消息不丢失

kafka: 消息发送+回调

RocketMQ: 1、消息发送+回调。2、事务消息。

RabbitMQ: 1、消息发送+回调

​ 2、 手动事务: channel.txSelect()开启事务, channel.txCommit()提交事务, channel.txRollback()回滚事务。这种方式对channel是会产生阻塞的,造成吞吐量下降。

​ 3、Publisher Confirm。整个处理流程跟RocketMQ的事务消息,基本是一样的。

2.2 MQ主从消息同步不丢失

RocketMQ: 1、普通集群中,同步同步、异步同步。异步同步效率更高,但是有丢消息的风险。同步同步就不会丢消息。

​ 2、Dledger集群-两阶段提交:

RabbitMQ: 普通集群:消息是分散存储的,节点之间不会主动进行消息同步,是有可能丢失消息的。

​ 镜像集群:镜像集群会在节点之间主动进行数据同步,这样数据安全性得到提高。

Kafka: 通常都是用在允许消息少量丢失的场景。acks。0,1,all

2.3 MQ消息存盘不丢失

RocketMQ: 同步刷盘 异步刷盘:异步刷盘效率更高,但是有可能丢消息。同步刷盘消息安全性更高,但是效率会降低。

RabbitMQ: 将队列配置成持久化队列。新增的Quorum类型的队列,会采用Raft协议来进行消息同步。

2.4 MQ消费者消费消息不丢失

RocketMQ: 使用默认的方式消费就行, 不要采用异步方式。

RabbitMQ: autoCommit -> 手动提交offset

Kafka: 手动提交offset

10、RabbitMQ镜像队列原理

镜像队列同样由这两部分组成,amqqueue_process 仍旧进行协议相关的消息处理,backing_queue 则是由 master 节点和 slave 节点组成的一个特殊的 backing_queue。Leader 节点和 Follower 节点都由一组进程组成,一个负责消息广播的 GM,一个负责对 GM 收到的广播消息进行回调处理。

在 Leader 节点上回调处理是 coordinator,在slave节点上则是 mirror_queue_slave。mirror_queue_slave 中包含了普通的 backing_queue 进行消息的存储,Leader 节点中 backing_queue 包含在 mirror_queue_master 中由 amqqueue_process 进行调用。

GM 模块实现的是一种可靠的组播通信协议,该协议能够保证组播消息的原子性,即保证组中活着的节点要么都收到消息要么都收不到。

它的实现大致为:将所有的节点形成一个循环链表,每个节点都会监控位于自己左右两边的节点,当有节点新增时,相邻的节点保证当前广播的消息会复制到新的节点上 : 当有节点失效时,相邻的节点会接管以保证本次广播的消息会复制到所有的节点。在 Leader 和 Follower 上的这些 GM 形成一个组 (gm_group) ,这个组的信息会记录在 Mnesia 中。不同的镜像队列形成不同的组。操作命令从 Leader 对应的 GM 发出后,顺着链表传送到所有的节点。由于所有节点组成了一个循环链表, Leader 对应的 GM 最终会收到自己发送的操作命令,这个时候 Leader 就知道该操作命令都同步到了所有的 slave 上。

消息从 Leader 节点发出,顺着节点链表发送。在这期间,所有的 Follower 节点都会对消息进行缓存,当 Leader 节点收到自己发送的消息后,会再次广播 ack 消息,同样 ack 消息会顺着节点链表经过所有的 Follower 节点,其作用是通知 Follower 节点可以清除缓存的消息,当 ack 消息回到 Leader 节点时对应广播消息的生命周期结束。

master发布的消息是依次经过所有slave节点,在这期间的任何时刻,有可能有节点失效,那么相邻的节点可能需要重新发送给新的节点。例如,A->B->C->D->A形成的循环链表,A为master节点,广播消息发送给节点B,B再发送给C,如果节点C收到B发送的消息还未发送给D时异常结束了,那么节点B感知后节点C失效后需要重新将消息发送给D。同样,如果B节点将消息发送给C后,B,C节点中新增了E节点,那么B节点需要再将消息发送给新增的E节点。

11、RabbitMQ的死信队列、延迟队列原理

死信队列:

DLX,全称为Dead-Letter-Exchange,可以称之为死信交换器,也有人称之为死信邮箱。当消息在一个队列中变成死信(dead message)之后,它能被重新被发送到另一个交换器中,这个交换器就是DLX,绑定DLX的队列就称之为死信队列。

以下几种情况会导致消息变成死信:

消息被拒绝(Basic.Reject/Basic.Nack),并且设置requeue参数为false;
消息过期;
队列达到最大长度。
DLX是一个正常的交换器,和一般的交换器没有区别,它能在任何的队列上被指定,实际上就是设置某个队列的属性。当这个队列中存在死信时,RabbitMQ就会自动地将这个消息重新发布到设置的DLX上去,进而被路由到另一个队列,即死信队列。可以监听这个队列中的消息以进行相应的处理,这个特性与将消息的TTL设置为0配合使用可以弥补immediate参数的功能。

延时队列:

在AMQP协议中,或者RabbitMQ本身没有直接支持延迟队列的功能,但是有两种方案来实现:

方案1:采用rabbitmq-delayed-message-exchange 插件实现。(RabbitMQ 3.6.x开始支持)
推荐。原因:它解决了死信队列的消息投递问题:在第一条消息成为死信之前,后面的消息即使过期也不会投递为死信。
方案2:通过前面所介绍的DLX和TTL模拟出延迟队列的功能。
不推荐。原因:死信队列的设计目的是为了存储没有被正常消费的消息,便于排查和重新投递。死信队列没有对投递时间做出保证,在第一条消息成为死信之前,后面的消息即使过期也不会投递为死信。

RabbitMQ可以针对队列设置x-expires(则队列中所有的消息都有相同的过期时间)或者针对Message设置x-message-ttl(对消息进行单独设置,每条消息TTL可以不同),来控制消息的生存时间,如果超时(两者同时设置以最先到期的时间为准),则消息变为dead letter(死信)

Dead Letter Exchanges(DLX)RabbitMQ的Queue可以配置x-dead-letter-exchange和x-dead-letter-routing-key(可选)两个参数,如果队列内出现了dead letter,则按照这两个参数重新路由转发到指定的队列。x-dead-letter-exchange:出现dead letter之后将dead letter重新发送到指定exchange

x-dead-letter-routing-key:出现dead letter之后将dead letter重新按照指定的routing-key发送

12、RabbitMQ如何保证消息的可靠性传输?

(29条消息) 【RabbitMQ】RabbitMQ 如何保证消息的可靠传输_rabbitmq可靠性传输_zhaopeng.chau的博客-CSDN博客

生产者投递消息失败:

  1. 事务机制
    事务机制可以基本确保生产者投递消息成功,但是这种方式有比较大的缺点,基本上 RabbitMQ 事务机制(同步)一搞,基本上吞吐量会下来,因为太耗性能了。

  2. confirm机制
    针对上述问题,如果还要确保 RabbitMQ 生产者的消息正确投递,可以开启 confirm 模式,在生产者端设置开启 confirm 模式之后,你每次写的消息都会分配一个唯一的 id,然后如果写入了 RabbitMQ 中,RabbitMQ 会给你回传一个 ACK 消息,告诉你说这个消息 ok 了。

事务机制和 confirm 机制最大的不同在于,事务机制是同步的,你提交一个事务之后会阻塞在那儿,但是 confirm 机制是异步的,你发送个消息之后就可以发送下一个消息,然后那个消息 RabbitMQ 接收之后会异步回调你一个接口通知你这个消息接收到了。

所以一般生产者到 RabbitMQ 这块避免数据丢失,会采用 confirm 机制更多些。

消息队列自身丢失:

就是 RabbitMQ 自己弄丢了数据,这个你必须开启 RabbitMQ 的持久化,就是消息写入之后会持久化到磁盘

消费者宕机:

对于消费者端所产生的情况就是:消费者成功接收到消息,但是还未将消息处理完毕就宕机了。针对这种情况,可以利用 RabbitMQ 提供的 消息确认机制

13、RabbitMQ事务消息

事务的实现主要是对信道(Channel)的设置,主要方法如下:

1.channel.txSelect() 声明启动事务模式

2.channel.basicPublish:发送消息,可以是多条,可以是消费消息提交ack

2.channel.txCommit() 提交事务

3.channel.txRollback()回滚事务

消费者使用事务:

1、autoAck=false。手动提交ack,以事务提交或回滚为准

2、autoAck=true,不支持事务的,也就是说你即使在收到消息之后在回滚事务也是无事于补的。队列已经把消息移除了

事务消息很影响性能

14、RabbitMQ可以直连队列吗?

可以

消费者和生产者使用相同的参数声明队列就可以,重复声明不会改变队列,谁先生效就用谁。

15、RabbitMQ的交换机类型?

RabbitMQ一共四种交换机,如下所示:

  1. Direct Exchange:直连交换机,根据Routing Key(路由键)进行投递到不同队列。
  2. Fanout Exchange:扇形交换机,采用广播模式,根据绑定的交换机,路由到与之对应的所有队列。
  3. Topic Exchange:主题交换机,对路由键进行模式匹配后进行投递,符号#表示一个或多个词,*表示一个词。
  4. Header Exchange:头交换机,不处理路由键。而是根据发送的消息内容中的headers属性进行匹配。
16、简述RabbitMQ的普通集群模式

就是在多台机器上启动多个rabbitmq实例每个机器启动一个。
但是你创建的queue,只会放在一个rabbtimq实例上,但是每个实例都同步queue的元数据(存放含queue数据的真正实例位置)。消费的时候,实际上如果连接到了另外一个实例,那么那个实例会从queue所在实例上拉取数据过来。

这种方式确实很麻烦,也不怎么好,没做到所谓的分布式,就是个普通集群。
因为这导致你要么消费者每次随机连接一个实例然后拉取数据,要么固定连接那个queue所在实例消费数据,前者有数据拉取的开销,后者导致单实例性能瓶颈。

而且如果那个放queue的实例宕机了,会导致接下来其他实例就无法从那个实例拉取,如果你开启了消息持久化,让rabbitmq落地存储消息的话,消息不一定会丢,得等这个实例恢复了,然后才可以继续从这个queue拉取数据。

所以这个普通集群比较尴尬了,这就没有什么所谓的高可用性可言了,这方案主要是提高吞吐量的,就是说让集群中多个节点来服务某个queue的读写操作。

17、简述RabbitMQ的架构设计

RabbitMQ 整体上是一个生产者与消费者模型,主要负责接收、存储和转发消息。从计算机术语层面来说, RabbitMQ 模型更像是一种交换机模型。

Producer∶ 生产者-投递消息的一方

生产者创建消息,然后发布到 RabbitMQ 中。消息一般可以包含 2 个部分:

  1. 消息体,消息体也可以称之为 payload,在实际应用中,消息体一般是一个带有业务逻辑结构的数据,比如一个 JSON 字符串。当然可以进一步对这个消息体进行序列化操作
  2. 标签(Label),用来表述这条消息,比如一个交换器的名称和一个路由键。生产者把消息交由RabbitMO, RabbitMQ 之后会根据标签把消息发送给感兴趣的消费者(Consumer)。

Consumer∶ 消费者-接收消息的一方

消费者连接到 RabbitMQ 服务器,订阅到队列。消费者消费一条消息时,只消费消息的消息体(payload)。在消息路由的过程中,消息的标签会丢弃,存入到队列中的消息只有消息体,消费者只会消费到消息体,也就不知道消息的生产者是谁,当然消费者也不需要知道。

Broker∶ 消息中间件的服务节点

一个RabbitMQ Broker 可以看作一个 RabbitMQ 服务节点,或RabbitMQ服务实例

Queue:队列

RabbitMQ 的内部对象,用于存储消息

Exchange∶ 交换器

生产者将消息发送到 Exchange(交换器,通常也可以用大写的"X"来表示),由交换器将消息路由到一个或者多个队列中。如果路由不到,或许会返回给生产者,或许直接丢弃。

RoutingKey∶ 路由键

生产者将消息发给交换器的时候,一般会指定一个 RoutingKey,用来指定这个消息的路由规则,而这个 Routing Key 需要与交换器类型和绑定键(BindingKey)联合使用才能最终生效。 在交换器类型和绑定键(BindingKey)固定的情况下,生产者可以在发送消息给交换器时,通过指定 RoutingKey 来决定消息流向哪里。

Binding∶ 绑定

RabbitMQ 中通过绑定将交换器与队列关联起来,在绑定的时候一般会指定一个绑定键(BindingKey),这样 RabbitMQ 就知道如何正确地将消息路由到队列了

生产者发送消息:

生产者连接到 RabbitMO Broker,建立一个连接(Connection),开启一个信道(Channel)

生产者声明一个交换器,并设置相关属性,比如交换机类型、是否持久化等

生产者声明一个队列并设置相关属性,比如是否排他、是否持久化、是否自动删除等

生产者通过路由键将交换器和队列绑定起来

生产者发送消息至 RabbitMO Broker,其中包含路由键、交换器等信息

相应的交换器根据接收到的路由键查找相匹配的队列。

如果找到,则将从生产者发送过来的消息存入相应的队列中。

如果没有找到,则根据生产者配置的属性选择丢弃还是回退给生产者

关闭信道。

关闭连接。

消费者消费消息:

消费者连接到 RabbitMQ Broker,建立一个连接(Connection),开启一个信道(Channel)。

消费者向 RabbitMQ Broker 请求消费相应队列中的消息,可能会设置相应的回调函数,以及做一些准备工作

等待 RabbitMQ Broker 回应并投递相应队列中的消息,消费者接收消息。

消费者确认(ack)接收到的消息。

RabbitMQ 从队列中删除相应已经被确认的消息。

关闭信道。

并发

1、CountDownLatch和Semaphore的区别和底层原理

https://blog.csdn.net/m0_53611007/article/details/120585577

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

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

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

2、ReentrantLock中tryLock()和lock()方法的区别

(5条消息) ReentrantLock中tryLock()和Lock()方法的区别_reentrantlock lock trylock_C位出道_2022的博客-CSDN博客

ReentrantLock:

    Lock():阻塞加锁,无返回值,如果该线程没有获得锁就阻塞,直到获得锁放开

    tryLock():尝试枷锁,可能加到也可能加不到,该方法不会阻塞线程,加到锁返回true否则返回false

利用tryLock()可以有自旋锁,
自旋锁相对灵活,但对cpu消耗较大,但是性能要优于lock()方法

3、ReentrantLock中的公平锁和非公平锁的底层实现

首先不管是公平锁还是非公平锁,它们的底层都是使用 AQS进行排队;
区别: 线程使用 lock() 方法加锁时,如果是公平锁,会先检查 AQS队列中是否存在线程排队,如果有线程在排队,那么当前线程也进行排队;如果是非公平锁,则不会去检查 AQS队列中是否有线程排队,而是直接去竞争锁;

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

另外 Reentrantlock 是可重入锁,不管公平锁还是非公平锁; 注意:重入几次锁,后面也得释放几次锁;
如下:
ReentrantLock lock = new ReentrantLock();
lock.lock();
lock.lock();
lock.unlock();
lock.unlock();

4、sleep、wait、join、yield

sleep

线程类(Thread)的方法,导致此线程暂停执行指定时间,给执行机会给其 他线程,但是监控状态依然保持,到时后会自动恢复。调用 sleep 不会释放对象锁。 sleep() 使当前线程进入阻塞状态,在指定时间内不会执行

wait

Object 类的方法,对此对象调用 wait 方法导致本线程放弃对象锁,进入等待 此对象的等待锁定池,只有针对此对象发出 notify 方法(或 notifyAll)后本线程才进入对象 锁定池准备获得对象锁进入运行状态。

区别比较

1、这两个方法来自不同的类分别是 Thread 和 Object 2、最主要是 sleep 方法没有释放锁,而 wait 方法释放了锁,使得其他线程可以使用同 步控制块或者方法。 3、wait,notify 和 notifyAll 只能在同步控制方法或者同步控制块里面使用,而 sleep 可 以在任何地方使用(使用范围) 4、sleep 必须捕获异常,而 wait,notify 和 notifyAll 不需要捕获异常

(1) sleep 方法属于 Thread 类中方法,表示让一个线程进入睡眠状态,等待一定的时 间之后,自动醒来进入到可运行状态,不会马上进入运行状态,因为线程调度机制恢复线程 的运行也需要时间,一个线程对象调用了 sleep 方法之后,并不会释放他所持有的所有对象 锁,所以也就不会影响其他进程对象的运行。但在 sleep 的过程中过程中有可能被其他对象 调用它的 interrupt(),产生 InterruptedException 异常,如果你的程序不捕获这个异常,线程 就会异常终止,进入 TERMINATED 状态,如果你的程序捕获了这个异常,那么程序就会继 续执行 catch 语句块(可能还有 finally 语句块)以及以后的代码。

sleep()方法是静态方法,也就是说他只对当前对象有效,通过 t.sleep()让 t 对象进入 sleep,这样的做法是错误的,它只会是使当前线程被 sleep 而不是 t 线程 (2) wait 属于 Object 的成员方法,一旦一个对象调用了 wait 方法,必须要采用 notify() 和 notifyAll()方法唤醒该进程;如果线程拥有某个或某些对象的同步锁,那么在调用了 wait() 后,这个线程就会释放它持有的所有同步资源,而不限于这个被调用了 wait()方法的对象。 wait()方法也同样会在 wait 的过程中有可能被其他对象调用 interrupt()方法而产生

yield()

停止当前线程,让同等优先权的线程或更高优先级的线程有执行的机会。 如果没有的话,那么 yield()方法将不会起作用,并且由可执行状态后马上又被执行。

join

用于在某一个线程的执行过程中调用另一个线程执行,等到被调用的线程执 行结束后,再继续执行当前线程。如:t.join();//主要用于等待 t 线程运行结束,若无此句, main 则会执行完毕,导致结果不可预测

notify

只唤醒一个等待(对象的)线程并使该线程开始执行。所以如果有多个线程 等待一个对象,这个方法只会唤醒其中一个线程,选择哪个线程取决于操作系统对多线程管理的实现。

notifyAll

唤醒所有等待(对象的)线程,尽管哪一个线程将会第一个处理取决于操作系统的实现。

5、Sychronized的偏向锁、轻量级锁、重量级锁

多线程-synchronized解析-偏向锁、轻量级锁、重量级锁 - 知乎 (zhihu.com)

偏向级锁

​ 顾名思义,偏向某一个线程,当线程数目不多的时候,由于反复获取锁会使得我们的运行效率下降,于是出现了偏向级锁。JVM使用CAS操作把线程ID记录到对象的Mark Word当中,并修改标识位,name当前线程就拥有了这把锁。

偏向级锁不需要操作系统的介入,JVM使用CAS操作将线程ID放入对象的Mark Word字段中,于是线程获得了锁,可以执行synchronized代码块的内容,当线程再次执行到这个synchronized的时候,JVM通过锁对象的Mark Word判断 :当前线程ID还存在,还持有这个对象的锁,于是就可以继续进入临界区执行,而不需要再次获得锁

    偏向锁,在没有别的线程竞争的时候,一直偏向当前线程,当前线程就可以一直进入synchronized修饰的代码块一直运行。

    如果在运行中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁升级到轻量级锁。

轻量级锁

​ 轻量级锁是由偏向级锁升级来的,当一个线程运行同步代码块时,另一个线程也加入想要运行这个同步代码块时,偏向锁就会升级为轻量级锁。

轻量级锁是由偏向级锁升级来的,当一个线程运行同步代码块时,另一个线程也加入想要运行这个同步代码块时,偏向锁就会升级为轻量级锁。

    首先,JVM会将锁对象的Mark Word恢复成为无锁状态,在当前两线程的栈桢中各自分配一个空间,叫做Lock Record,把锁对象account的Mark Word在两线程的栈桢中各自复制了一份,官方称为:Displaced Mark Word

然后一个线程尝试使用CAS将对象头中的Mak Word替换为指向锁记录的指针,如果替换成功,则当前线程获得锁,如果失败,则当前线程自旋重新尝试获取锁。当自旋获取锁仍然失败时,表示存在其他线程竞争锁(两个或两个以上线程竞争同一个锁),则轻量级锁会膨胀成重量级锁

    举个例子: 线程A、线程B同时想要执行一个同步代码块,假设线程A抢到了锁,则线程A的Lock Record的地址 会被CAS操作放到了锁对象Mark Word中,并且将锁标志位改为00,这意味着线程A就获取到了该锁,可以执行同步代码块。

    而线程B没有抢到锁,但是线程B不会阻塞,而是通过自旋的方式,等待获取锁。(一般默认自旋10次),如果线程A释放掉锁,则将线程A中的Displaced mark word使用CAS复制回锁对象的Mark Word字段,此时线程B就可以获取锁对象,如果线程B还没有获取成功,则说明同时存在两个或两个以上的线程同时竞争这一把锁,轻量级锁会升级成为重量级锁。

重量级锁
我们上面提到,当多个线程竞争同一个锁时,会导致除锁的拥有者外,其余线程都会自旋,这将导致自旋次数过多,cpu效率下降,所以会将锁升级为重量级锁。

    重量级锁需要操作系统的介入,依赖操作系统底层的Muptex Lock。JVM会创建一个monitor对象,把这个对象的地址更新到Mark Word中。

当一个线程获取了该锁后,其余线程想要获取锁,必须等到这个线程释放锁后才可能获取到,没有获取到锁的线程,就进入了阻塞状态。

自旋锁的定义:当一个线程尝试去获取某一把锁的时候,如果这个锁此时已经被别人获取(占用),那么此线程就无法获取到这把锁,该线程将会等待,间隔一段时间后会再次尝试获取。这种采用循环加锁 -> 等待的机制被称为自旋锁(spinlock)。这个过程中线程一直在运行中,相对而言没有使用太多的操作系统资源,比较轻量

6、Sychronized和ReentrantLock的区别

相似点:
这两种同步方式有很多相似之处,它们都是加锁方式同步,而且都是阻塞式的同步,也就是说当如果一个线程获得了对象锁,进入了同步块,其他访问该同步块的线程都必须阻塞在同步块外面等待,而进行线程阻塞和唤醒的代价是比较高的(操作系统需要在用户态与内核态之间来回切换,代价很高,不过可以通过对锁优化进行改善)。

功能区别:
这两种方式最大区别就是对于Synchronized来说,它是java语言的关键字,是原生语法层面的互斥,需要jvm实现。而ReentrantLock它是JDK 1.5之后提供的API层面的互斥锁,需要lock()和unlock()方法配合try/finally语句块来完成

便利性:很明显Synchronized的使用比较方便简洁,并且由编译器去保证锁的加锁和释放,而ReenTrantLock需要手工声明来加锁和释放锁,为了避免忘记手工释放锁造成死锁,所以最好在finally中声明释放锁。

锁的细粒度和灵活度:很明显ReenTrantLock优于Synchronized

性能的区别:
在Synchronized优化以前,synchronized的性能是比ReenTrantLock差很多的,但是自从Synchronized引入了偏向锁,轻量级锁(自旋锁)后,两者的性能就差不多了,在两种方法都可用的情况下,官方甚至建议使用synchronized,其实synchronized的优化我感觉就借鉴了ReenTrantLock中的CAS技术。都是试图在用户态就把加锁问题解决,避免进入内核态的线程阻塞。

总结:

( 1) ReentrantLock 显示获得、 释放锁, synchronized 隐式获得释放锁
( 2) ReentrantLock 可响应中断、 可轮回, synchronized 是不可以响应中断的, 为处理
锁的不可用性提供了更高的灵活性
( 3) ReentrantLock 是 API 级别的, synchronized 是 JVM 级别的
( 4) ReentrantLock 可以实现公平锁
( 5) ReentrantLock 通过 Condition 可以绑定多个条件

7、ThreadLocal的底层原理

(5条消息) 深入解析ThreadLocal底层实现原理_threadlocal底层原理_蜜蜂采蜜的博客-CSDN博客

ThreadLocal ,也叫线程本地变量,可能很多朋友都知道ThreadLocal为变量在每个线程中都创建了所使用的的变量副本。使用起来都是在线程的本地工作内存中操作,并且提供了set和get方法来访问拷贝过来的变量副本。底层也是封装了ThreadLocalMap集合类来绑定当前线程和变量副本的关系,各个线程独立并且访问安全!

(1) ThreadLocal仅仅是个变量访问的入口;

(2) 每一个Thread对象都有一个ThreadLocalMap对象,这个ThreadLocalMap持有对象的引用;

(3) ThreadLocalMap以当前的threadLocal对象为key,以真正的存储对象为value。get()方法时通过threadLocal实例就可以找到绑定在当前线程上的副本对象。

ThreadLocal这样设计有两个目的:

​ 第一:可以保证当前线程结束时,相关对象可以立即被回收;第二:ThreadLocalMap元素会大大减少,因为Map过大容易造成哈希冲突而导致性能降低。

8、ThreadLocal的使用场景

数据库事务:通过AOP的方式,对执行数据库事务的函数进行拦截。函数开始前,获取connection开启事务并存储在ThreadLocal中,任何用到connection的地方,从ThreadLocal中获取。函数执行完毕,提交事务释放connection。

解决线程安全的问题。比如Java7中的SimpleDateFormat不是线程安全的,可以用ThreadLocal来解决这个问题。

用户登录信息等:用户登录信息好多个方法上都要用到,给每个方法都添加一个User非常麻烦,而且有些时候,如果调用链有无法修改源码的第三方库,User对象就传不进去了。而ThreadLocal可以在一个线程中传递同一个对象。

9、ThreadLocal的内存泄露问题如何避免?

ThreadLocal的内存泄露?什么原因?如何避免? - 知乎 (zhihu.com)

由于Thread中包含变量ThreadLocalMap,因此ThreadLocalMap与Thread的生命周期是一样长,如果都没有手动删除对应key,都会导致内存泄漏。

但是使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set(),get(),remove()的时候会被清除。

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

  • 每次使用完ThreadLocal都调用它的remove()方法清除数据
  • 将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉 。
10、Thread和Runnable

Runnable的实现方式是实现其接口即可
Thread的实现方式是继承其类
Runnable接口支持多继承,但基本上用不到
Thread实现了Runnable接口并进行了扩展,而Thread和Runnable的实质是实现的关系,不是同类东西,所以Runnable或Thread本身没有可比性。

Thread和Runnable的实质是继承关系,没有可比性。无论使用Runnable还是Thread,都会new Thread,然后执行run方法。用法上,如果有复杂的线程操作需求,那就选择继承Thread,如果只是简单的执行一个任务,那就实现runnable。

11、如何查看线程死锁

jstack命令,排查线程死锁问题。

12、线程之间如何通讯的

拜托,线程间的通信真的很简单。 - 知乎 (zhihu.com)

线程通信主要可以分为三种方式,分别为共享内存消息传递管道流。每种方式有不同的方法来实现

  • 共享内存:线程之间共享程序的公共状态,线程之间通过读-写内存中的公共状态来隐式通信。

volatile共享内存

  • 消息传递:线程之间没有公共的状态,线程之间必须通过明确的发送信息来显示的进行通信。

wait/notify等待通知方式
join方式

  • 管道流

管道输入/输出流的形式

13、并发、并行、串行

并行(parallel):指在同一时刻,有多条指令在多个处理器上同时执行。所以无论从微观还是从宏观来看,二者都是一起执行的。

并发(concurrency):指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。

并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔发生。
并发的关键是你有处理多个任务的能力,不一定要同时。
并行的关键是你有同时处理多个任务的能力

并发就是同步的串行,一个任务执行完执行下一个任务;
并行,在用同一个时刻执行多个线程;

14、并发的三大特性

(5条消息) 并发中的三大特性详解_并发的三大特性_撸智深的博客-CSDN博客

原子性、可见性、有序性。要保证并发代码的安全性则必须满足这三大特性

**原子性的定义:**一个或者多个操作,要么全部执行(执行的过程是不会被打断的)、要么全部不执行。

原子性的解决:内置锁(同步关键字):synchronized;显示锁:Lock;自旋锁:CAS;

有序性:

处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。

**可见性的定义:**一个线程对共享变量的写入时,能对另一个线程可见

可见性的解决:最轻量的就是:Volatile

15、Java如何开启线程?怎么保证线程安全?

线程和进程的区别:进程是操作系统进行资源分配的最小单元。线程是操作系统进行任务分配的最小单元,线程隶属于进程。

如何开启线程? 1、继承Thread类,重写run方法。 2、实现Runnable接口,实现run方法。3、实现Callable接口,实现call方法。通过FutureTask创建一个线程,获取到线程执行的返回值。4、通过线程池来开启线程。

怎么保证线程安全? 加锁: 1、 JVM提供的锁, 也就是Synchronized关键字。 2、 JDK提供的各种锁 Lock。

16、Volatile和Synchronized有什么区别?Volatile能不能保证线程安全?DCL(Double Check Lock)单例为什么要加Volatile?

1、Synchronized关键字,用来加锁。 Volatile只是保持变量的线程可见性。通常适用于一个线程写,多个线程读的场景。

2、不能。Volatile关键字只能保证线程可见性, 不能保证原子性。

3、Volatile防止指令重排。在DCL中,防止高并发情况下,指令重排造成的线程安全问题。、

17、Java的锁机制是怎样的?锁机制如何升级?

1、JAVA的锁就是在对象的Markword中记录一个锁状态。无锁,偏向锁,轻量级锁,重量级锁对应不同的锁状态。

2、JAVA的锁机制就是根据资源竞争的激烈程度不断进行锁升级的过程。

18、谈谈你对AQS的理解。AQS如何实现可重入锁?

1、AQS是一个JAVA线程同步的框架。是JDK中很多锁工具的核心实现框架。

2、 在AQS中,维护了一个信号量state和一个线程组成的双向链表队列。其中,这个线程队列,就是用来给线程排队的,而state就像是一个红绿灯,用来控制线程排队或者放行的。 在不同的场景下,有不用的意义。

3、在可重入锁这个场景下,state就用来表示加锁的次数。0标识无锁,每加一次锁,state就加1。释放锁state就减1。

19、对线程安全的理解
 线程安全,其实是内存安全,对事共享内存,可以被所有线程访问。
- 当多个线程访问一个对象,如果不需要额外的控制,调用的找个对象行为都是正确的,通常可以认为是线程安全的;
- 如果多个线程访问一个对象,每个线程所修改的内容都会覆盖其他线程产生的数据,且对象行为不正确,通常可以认为是非线程安全的;

不是线程安全、应该是内存安全,堆是共享内存,可以被所有线程访问

  • 当多个线程访问一个对象时,如果不进行额外的同步控制或者其他的协调操作,调用这个对象的行为都可以获得正确的结果,我们就说这个对象是线程安全的
  • 堆是线程和进程共有的空间,分去全局堆和局部堆,全局堆就是所有没有分配的空间,局部堆就是用户分配的空间。堆在操作系统堆进程初始化的时候分配,运行过程中也可以向系统要额外的堆,但是用完了要还给操作系统,要不然就是内存泄漏
  • 在java中,堆就是Java虚拟机所管理的内存中最大的一块,是所有线程共享的一块内存区域,
    在虚拟机启动时创建。堆所在的内存区域的唯一目的就是存放对象实例,几乎所有的对象实例
    以及数组都在这里分配内存。
  • 栈是每个线程独有的,保存其运行状态和局部自动变量的。栈在线程开始的时候初始化,每个线程的栈互相独立,因此,线程是安全的。操作系统在切换线程的时候会自动切换栈。栈空间不需要在高级语言里面显示的分配和释放

目前主流操作系统都是多任务的,即多个进程同时运行。为了保证安全,每个进程只能访问分配给自己的内存空间,而不能访问别的进程的。这是由操作系统保障的。

在每个进程的内存空间中都会有一块特殊的公共区域,通常称为堆(内存)。进程内的所有线程都可以访问到该区域,这就是造成问题的潜在原因。

20、Java死锁如何避免

死锁产生的4个必要条件
1、互斥: 某种资源一次只允许一个进程访问,即该资源一旦分配给某个进程,其他进程就不能再访问,直到该进程访问结束。
2、占有且等待: 一个进程本身占有资源(一种或多种),同时还有资源未得到满足,正在等待其他进程释放该资源。
3、不可抢占: 别人已经占有了某项资源,你不能因为自己也需要该资源,就去把别人的资源抢过来。
4、循环等待: 存在一个进程链,使得每个进程都占有下一个进程所需的至少一种资源。
当以上四个条件均满足,必然会造成死锁,发生死锁的进程无法进行下去,它们所持有的资源也无法释放。这样会导致CPU的吞吐量下降。所以死锁情况是会浪费系统资源和影响计算机的使用性能的。那么,解决死锁问题就是相当有必要的了。

1、死锁预防 ----- 确保系统永远不会进入死锁状态
产生死锁需要四个条件,那么,只要这四个条件中至少有一个条件得不到满足,就不可能发生死锁了。由于互斥条件是非共享资源所必须的,不仅不能改变,还应加以保证,所以,主要是破坏产生死锁的其他三个条件。
a、破坏“占有且等待”条件
方法1:所有的进程在开始运行之前,必须一次性地申请其在整个运行过程中所需要的全部资源。
优点:简单易实施且安全。
缺点:因为某项资源不满足,进程无法启动,而其他已经满足了的资源也不会得到利用,严重降低了资源的利用率,造成资源浪费。使进程经常发生饥饿现象。

方法2:该方法是对第一种方法的改进,允许进程只获得运行初期需要的资源,便开始运行,在运行过程中逐步释放掉分配到的已经使用完毕的资源,然后再去请求新的资源。这样的话,资源的利用率会得到提高,也会减少进程的饥饿问题。

b、破坏“不可抢占”条件
当一个已经持有了一些资源的进程在提出新的资源请求没有得到满足时,它必须释放已经保持的所有资源,待以后需要使用的时候再重新申请。这就意味着进程已占有的资源会被短暂地释放或者说是被抢占了。
该种方法实现起来比较复杂,且代价也比较大。释放已经保持的资源很有可能会导致进程之前的工作实效等,反复的申请和释放资源会导致进程的执行被无限的推迟,这不仅会延长进程的周转周期,还会影响系统的吞吐量。

c、破坏“循环等待”条件
可以通过定义资源类型的线性顺序来预防,可将每个资源编号,当一个进程占有编号为i的资源时,那么它下一次申请资源只能申请编号大于i的资源。
这样虽然避免了循环等待,但是这种方法是比较低效的,资源的执行速度回变慢,并且可能在没有必要的情况下拒绝资源的访问,比如说,进程c想要申请资源1,如果资源1并没有被其他进程占有,此时将它分配个进程c是没有问题的,但是为了避免产生循环等待,该申请会被拒绝,这样就降低了资源的利用率

2、避免死锁 ----- 在使用前进行判断,只允许不会产生死锁的进程申请资源
的死锁避免是利用额外的检验信息,在分配资源时判断是否会出现死锁,只在不会出现死锁的情况下才分配资源。
两种避免办法:
1、如果一个进程的请求会导致死锁,则不启动该进程
2、如果一个进程的增加资源请求会导致死锁 ,则拒绝该申请。
避免死锁的具体实现通常利用银行家算法

在开发过程中:

1.要注意加锁顺序,保证每个线程按同样的顺序进行

2.要注意加锁时限,可以针对锁设置一个超时时间

3.要注意死锁检查,这是一种预防机制,确保在第一时间发现死锁并进行解决

21、如果你提交任务时,线程池队列已满,这时会发生什么?

有俩种可能:
1、如果使用的是无界队列 LinkedBlockingQueue,也就是无界队列的话,没关系,继续添加任务到阻塞队列中等待执行,因为 LinkedBlockingQueue 可以近乎认为是一个无穷大的队列,可以无限存放任务
2、如果使用的是有界队列比如 ArrayBlockingQueue,任务首先会被添加到ArrayBlockingQueue中,ArrayBlockingQueue 满了,会根据maximumPoolSize 的值增加线程数量,如果增加了线程数量还是处理不过来,ArrayBlockingQueue 继续满,那么则会使用拒绝策略RejectedExecutionHandler处理满了的任务,默认是 AbortPolicy

22、volatile关键字,它是如何保证可见性、有序性

1.对于加了关键字的成员变量,在对这个变量进行修改时,会直接将cpu高级缓存中的数据写回到主内存,对这个变量的读取也会直接从主内存中读取,从而保证了可见性

为了性能优化,JVM会在不改变数据依赖性的情况下,允许编译器和处理器对指令序列进行重排序,而有序性问题指的就是程序代码执行的顺序与程序员编写程序的顺序不一致,导致程序结果不正确的问题。而加了volatile修饰的共享变量,则通过内存屏障解决了多线程下有序性问题。

23、简述线程池原理?FixedThreadPool用的阻塞队列是什么?

Java 线程池的实现原理其实就是一个线程集合 workerSet 和一个阻塞队列 workQueue。
当用户向线程池提交一个任务(也就是线程)时, 线程池会先将任务放入 workQueue 中。
workerSet 中的线程会不断的从 workQueue 中获取线程然后执行。 当 workQueue 中没有
任务的时候, worker 就会阻塞, 直到队列中有任务了就取出来继续执行。

FixedThreadPool 使用的是“无界队列”LinkedBlockingQueue

24、如何理解volatile关键字

volatile是Java中的关键字,用来修饰会被不同线程访问和修改的变量。volatile可以说是java虚拟机提供的最轻量级的同步机制。

当一个变量被定义成volatile之后,它具备2个特性

第一项是保证此变量对所有线程的可见性

这里的可见性是指当一个线程修改了这个变量的值,新值对于其它线程来说是可以立即得知的,二普通变量并不能做到这一点,普通变量的值在线程间传递是均需要通过主内存来完成。比如,线程A修改一个普通变量的值,然后向主内存进行回写,另外一条线程B在线程A回写完成了之后再对主内存进行读取,新变量值才会对线程B可见。

但是这并不能保证volatile变量是线程安全的。

第二项是使用volatile变量禁止指令重排序优化

重排序是指编译器和处理器为了优化程序性能而对指令序列进行排序的一种手段。

重排序在单线程下一定能保证结果的正确性,但是在多线程环境下,可能发生重排序,影响结果

使用volatile关键字修饰共享变量便可以禁止这种重排序。若用volatile修饰共享变量,在编译时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序,volatile禁止指令重排序也有一些规则:

 a.当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;

 b.在进行指令优化时,不能将对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。

 即执行到volatile变量时,其前面的所有语句都执行完,后面所有语句都未执行。且前面语句的结果对volatile变量及其后面语句可见
25、说说你对守护线程的理解

守护线程:为所有非守护线程提供服务的线程;任何一个守护线程都是整个JVM中所有非守护线程的保姆

守护线程类似于整个进程的一个默默无闻的小喽喽;它的生死无关重要,它却依赖整个进程而运行;只要其他线程结束,没有执行了,程序就结束了,守护线程也就中断了

注意:守护线程的终止是自身无法控制的,因此千万不要把IO、File等重要操作逻辑分配给它;

守护线程的作用?

举例,GC垃圾回收线程就是一个经典的守护线程,当我们的程序中不再有任何运行的Thread,程序就不会再产生垃圾,也就不需要垃圾回收器,所以当垃圾回收线程是JVM上仅剩的线程时,垃圾回收线程会自动离开。它始终在低级别的状态中运行,用于实时监控和管理系统中的可回收资源

应用场景:

为其它线程提供服务支持
在任何情况下,程序结束时,这个线程必须正常且立刻关闭;反之如果一个正在执行某个操作的线程必须要正确的关闭否则就会出现异常的情况,就不能使用守护线程,而是使用用户线程(如数据库录入或者更新,这些操作都是不能中断的)。
设置守护线程:thread.setDaemon(true)必须在thread.start()之前设置,否者会报异常(IllegalThreadStateException),不能把正在运行的常规线程设置为守护线程

在守护线程中产生的新线程也是守护线程。守护线程不能用于访问固有资源,比如读写操作或者计算逻辑。因为它会在任何时候甚至在一个操作的中间出现中断

java中自带的多线程框架,比如ExecutorService,会将守护线程转换为用户线程,所以如果要使用后台线程就不能用java的线程池

26、为什么使用线程池,参数解释

一、为什么用线程池

1、降低资源消耗:提高线程利用率,降低创建和销毁线程的消耗。

2、提高响应速度:任务来了,直接有线程可用可执行,而不是先创建线程,再执行。

3、提高线程的可管理性:线程是稀缺资源

二、线程池参数解释
1、corePoolSize:代表核心线程数,也就是正常情况下创建工作的线程数,这些线程数创建后并不会消除,而是一种常驻线程。

2、maxinumPoolSize:代表的最大线程数,它与核心线程数相对应,表示最大允许被创建的线程数,比如当前任务较多,将核心线程数都用过了,还无法满足需求时,此时就会创建新的线程,但是线程池内线程总数不会超过最大线程数。

3、keepAliveTime、unit:表示超出核心线程数之外的线程的空闲存活时间,也就是核心线程不会消除,但是超出核心线程数的部分线程如果空闲一定的时间则会被消除。我们可以通过setKeepAliveTime来设置空闲时间。

4、workQueue:用来存放待执行的任务,假设我们现在核心线程都已被使用,还有任务进来则会全部放入队列,直到整个队列被放满但任务还在持续进入则会开始创建新的线程。

5、ThreadFactory:实际上是一个线程工厂,用来生产线程执行任务。我们可以选择使用默认的创建工厂,产生的线程都在同一个组内,拥有相同的优先级,且都不是守护线程。当然我们也可以选择自定义线程工厂,一般我们会根据业务来制定不同的线程工厂。

6、Handle:任务拒绝策略,有两种情况,第一种是当我们调用shutdown等方法关闭线程池后,这时候即使线程池内部还有没执行完的任务正在执行,但是由于线程池已经关闭,我们再继续向线程池提交任务就会遭到拒绝。另外一种情况就是当达到最大线程数,线程池已经没有能力处理新提交的任务时,也会拒绝。

27、线程池处理流程

提交任务后,线程池先判断线程数是否达到了核心线程数(corePoolSize)。如果未达到线程数,则创建核心线程处理任务;否则,就执行下一步;

接着线程池判断任务队列是否满了。如果没满,则将任务添加到任务队列中;否则,执行下一步;

接着因为任务队列满了,线程池就判断线程数是否达到了最大线程数。如果未达到,则创建非核心线程处理任务;否则,就执行饱和策略,默认会抛出RejectedExecutionException异常。

饱和策略:RejectedExecutionHandler

当任务队列和线程池都满了时所采取的应对策略,默认是AbordPolicy,表示无法处理新任务,并抛出RejectedExecutionException异常。此外还有3种策略:

  • CallerRunsPolicy:用调用者所在的线程处理任务。此策略提供简单的反馈机制,能够减缓新任务的提交速度。
  • DiscardPolicy:不能执行任务,并将任务删除。
  • DiscardOldestPolicy:丢弃队列最近的任务,并执行当前的任务
28、线程池复用的原理

线程池将线程和任务进行解耦,线程是线程,任务是任务,摆脱了之前通过 Thread 创建线程时的一个线程必须对应一个任务的限制。

在线程池中,同一个线程可以从阻塞队列中不断获取新任务来执行,其核心原理在于线程池对 Thread 进行了封装,并不是每次执行任务都会调用 Thread.start() 来创建新线程,而是让每个线程去执行一个“循环任务”,在这个“循环任务”中不停的检查是否有任务需要被执行,如果有则直接执行,也就是调用任务中的 run 方法,将 run 方法当成一个普通的方法执行,通过这种方式将只使用固定的线程就将所有任务的 run 方法串联起来。

29、线程池中阻塞队列的作用?为什么是先添加队列而不是先创建最大线程

阻塞队列的作用
一般的队列只能是有限长度的缓冲区,一旦超出缓冲长度,就无法保留了。阻塞队列通过阻塞可以保留住当前想要继续入队的任务。

阻塞队列可以在队列中没有任务时,阻塞想要获取任务的线程,使其进入wait状态,释放cpu资源。

阻塞队列带有阻塞和唤醒的功能,不需要额外处理,无任务执行时,线程池利用阻塞队列的take方法挂起,从而维持核心线程的存活,不至于一直占用cpu资源。

为什么先添加队列而不是先创建最大线程
在创建新线程的时候,是要获取全局锁的,这时候其他线程会被阻塞,影响整体效率。

在核心线程已满时,如果任务继续增加那么放在队列中,等队列满了而任务还在增加那么就要创建临时线程了,这样代价低。

30、线程的生命周期及状态

新建状态(NEW):新创建一个线程对象。

就绪状态(RUNNABLE):线程对象创建后,调用该对象的start()方法。该状态的线程等待被线程调度选中,获取CPU的使用权。

运行状态(RUNNING):就绪状态(RUNNABLE)的线程获取CPU时间片开始执行程序代码。

阻塞状态(BLOCKED):阻塞状态是指线程因为某种原因让出了CPU使用权,直到线程再次进入就绪状态(RUNNABLE),等待再次获取CPU时间片进入运行状态

1.等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;

2.同步阻塞 – 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;

3.其他阻塞 – 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。

死亡状态(DEAD):run方法正常退出而自认死亡或者异常终止run方法导致线程结束。

就绪状态转换为运行状态:当此线程得到处理器资源;

运行状态转换为就绪状态:当此线程主动调用yield()方法或在运行过程中失去处理器资源。

运行状态转换为死亡状态:当此线程线程执行体执行完毕或发生了异常。

分布式架构

1、雪花算法原理

雪花算法使用64位long类型的数据存储id

41位存储毫秒级时间戳,这个时间截不是存储当前时间的时间截,而是存储时间截的差值(当前时间截 - 开始时间截) * 得到的值),这里的的开始时间截,一般是我们的ID生成器开始使用的时间,一般为项目创建时间,就是下面实现中代码的twepoch 属性,生成器根据时间戳插值进行初次尝试创建ID。

10位存储机器码,最多支持1024台机器,当并发量非常高,同时有多个请求在同一毫秒到达,可以根据机器码进行第二次生成。机器码可以根据实际需求进行二次划分,比如两个机房操作可以一个机房分配5位机器码。

12位存储序列号,当同一毫秒有多个请求访问到了同一台机器后,此时序列号就派上了用场,为这些请求进行第三次创建,最多每毫秒每台机器产生2的12次方也就是4096个id,满足了大部分场景的需求。

总的来说雪花算法有以下几个优点:

  • 能满足高并发分布式系统环境下ID不重复
  • 基于时间戳,可以保证基本有序递增
  • 不依赖第三方的库或者中间件
  • 生成效率极高
2、为什么Zookeeper可以用来作为注册中心

可以利用 Zookeeper 的临时节点和 watch 机制来实现注册中心的自动注册和发现,另外 Zookeeper 中的数据都是存在内存中的,并且 Zookeeper 底层采用了 nio ,多线程模型,所以 Zookeeper 的性能也是比较高的,所以可以用来作为注册中心,但是如果考虑到注册中心应该是注册可用性的话,那么 Zookeeper 则不太合适,因为 Zookeeper 是 CP 的,它注重的是一致性,所以集群数据不一致时,集群将不可用,所以用 Redis 、 Eureka 、 Nacos 来作为注册中心将更合适

3、数据库实现分布式锁的问题及解决方案

要是利用数据库的唯一索引来实现,唯一索引天然具有排他性,这刚好符合我们对锁的要求:同一时刻只能允许一个竞争者获取锁。加锁时我们在数据库中插入一条锁记录,利用业务id进行防重。当第一个竞争者加锁成功后,第二个竞争者再来加锁就会抛出唯一索引冲突,如果抛出这个异常,我们就判定当前竞争者加锁失败。防重业务id需要我们自己来定义,例如我们的锁对象是一个方法,则我们的业务防重id就是这个方法的名字,如果锁定的对象是一个类,则业务防重id就是这个类名。

  1. 针对锁失效问题,我们可以新增一个expire超时字段,在加锁时设置。

然后另起一个线程,负责轮询删除表中超时的数据。

  1. 针对不可重入问题,我们可以再新增一个request_info字段,记录当前获取锁的线程的机器和线程信息,当相同的线程再次访问时,就可以识别放行了。

  2. 针对非阻塞,还是和redis篇一样,写一个while死循环,失败了不断重拾,直到获取锁成功为止。

  3. 针对单点问题,通用方案就是配置主从数据库。

基于数据库实现分布式锁的优点:简单,易上手

缺点:性能问题

4、数据一致性模型有哪些?

强一致性:当更新操作完成后,任何多个后续进行访问时都会返回最新的值,就是用户刚提交就能看到更新了的数据,这对用户是最友好的。但根据CAP理论,这势必也要牺牲可用性。

弱一致性:系统在写入数据成功后,不承诺立即能读到最新的值,也不承诺什么时候能读到,但是过一段时间之后用户可以看到更新后的值。那么用户读不到最新数据的这段时间被称为“不一致窗口时间”。
最终一致性作为弱一致性中的特例,强调的是所有数据副本,在经过一段时间的同步后,最终能够到达一致的状态,不需要实时保证系统数据的强一致性。而到达最终一致性的时间,其实就是上文提到的不一致窗口时间。

根据业务需求的不同,最终一致性中又有很多种情况:

因果一致性:要求有因果关系的操作顺序得到保证,非因果关系的操作顺序则无所谓。例如微信朋友圈的评论以及对评论的答复所构成的因果关系,有兴趣可以了解一下。
会话一致性:在操作顺序得到保证的前提下,保证用户在同一个会话里读取数据时保证数据是最新的,如分布式系统Session一致性解决方案。
单调读一致性:用户读取某个数据值后,其后续操作不会读取到该数据更早版本的值。
单调写一致性:要求数据的所有副本,以相同的顺序执行所有的更新操作,也称为时间轴一致性。

按不完全统计,应该是有15种一致性模型的

5、什么是分布式事务?有哪些实现方案?

分布式系统会把一个应用系统拆分为可独立部署的多个服务,因此需要服务与服务之间远程协作才能完成事务操作,这种分布式系统环境下由不同的服务之间通过网络远程协作完成事务称之为分布式事务

1、两阶段提交方案/XA方案

所谓的 XA 方案,即:两阶段提交,有一个事务管理器的概念,负责协调多个数据库(资源管理器)的事务,事务管理器先问问各个数据库你准备好了吗?如果每个数据库都回复 ok,那么就正式提交事务,在各个数据库上执行操作;如果任何其中一个数据库回答不 ok,那么就回滚事务。

这种分布式事务方案,比较适合单块应用里,跨多个库的分布式事务,而且因为严重依赖于数据库层面来搞定复杂的事务,效率很低,绝对不适合高并发的场景

2、TCC 方案

TCC 的全称是:Try、Confirm、Cancel。

  • Try 阶段:这个阶段说的是对各个服务的资源做检测以及对资源进行锁定或者预留
  • Confirm 阶段:这个阶段说的是在各个服务中执行实际的操作
  • Cancel 阶段:如果任何一个服务的业务方法执行出错,那么这里就需要进行补偿,就是执行已经执行成功的业务逻辑的回滚操作。(把那些执行成功的回滚)

这种方案说实话几乎很少人使用,我们用的也比较少,但是也有使用的场景。因为这个事务回滚实际上是严重依赖于你自己写代码来回滚和补偿了,会造成补偿代码巨大,非常之恶心。

3、本地消息表

本地消息表其实是国外的 ebay 搞出来的这么一套思想。

这个大概意思是这样的:

  1. A 系统在自己本地一个事务里操作同时,插入一条数据到消息表;
  2. 接着 A 系统将这个消息发送到 MQ 中去;
  3. B 系统接收到消息之后,在一个事务里,往自己本地消息表里插入一条数据,同时执行其他的业务操作,如果这个消息已经被处理过了,那么此时这个事务会回滚,这样保证不会重复处理消息
  4. B 系统执行成功之后,就会更新自己本地消息表的状态以及 A 系统消息表的状态;
  5. 如果 B 系统处理失败了,那么就不会更新消息表状态,那么此时 A 系统会定时扫描自己的消息表,如果有未处理的消息,会再次发送到 MQ 中去,让 B 再次处理;
  6. 这个方案保证了最终一致性,哪怕 B 事务失败了,但是 A 会不断重发消息,直到 B 那边成功为止。

这个方案说实话最大的问题就在于严重依赖于数据库的消息表来管理事务啥的,会导致如果是高并发场景咋办呢?咋扩展呢?所以一般确实很少用。

4、可靠消息最终一致性方案

这个的意思,就是干脆不要用本地的消息表了,直接基于 MQ 来实现事务。比如阿里的 RocketMQ 就支持消息事务。

大概的意思就是:

  1. A 系统先发送一个 prepared 消息到 mq,如果这个 prepared 消息发送失败那么就直接取消操作别执行了;
  2. 如果这个消息发送成功过了,那么接着执行本地事务,如果成功就告诉 mq 发送确认消息,如果失败就告诉 mq 回滚消息;
  3. 如果发送了确认消息,那么此时 B 系统会接收到确认消息,然后执行本地的事务;
  4. mq 会自动定时轮询所有 prepared 消息回调你的接口,问你,这个消息是不是本地事务处理失败了,所有没发送确认的消息,是继续重试还是回滚?一般来说这里你就可以查下数据库看之前本地事务是否执行,如果回滚了,那么这里也回滚吧。这个就是避免可能本地事务执行成功了,而确认消息却发送失败了。
  5. 这个方案里,要是系统 B 的事务失败了咋办?重试咯,自动不断重试直到成功,如果实在是不行,要么就是针对重要的资金类业务进行回滚,比如 B 系统本地回滚后,想办法通知系统 A 也回滚;或者是发送报警由人工来手工回滚和补偿。
  6. 这个还是比较合适的,目前国内互联网公司大都是这么玩儿的,要不你举用 RocketMQ 支持的,要不你就自己基于类似 ActiveMQ?RabbitMQ?自己封装一套类似的逻辑出来,总之思路就是这样子的。

5、最大努力通知方案

这个方案的大致意思就是:

  1. 系统 A 本地事务执行完之后,发送个消息到 MQ;
  2. 这里会有个专门消费 MQ 的最大努力通知服务,这个服务会消费 MQ 然后写入数据库中记录下来,或者是放入个内存队列也可以,接着调用系统 B 的接口;
  3. 要是系统 B 执行成功就 ok 了;要是系统 B 执行失败了,那么最大努力通知服务就定时尝试重新调用系统 B,反复 N 次,最后还是不行就放弃。
6、什么是ZAB协议

ZAB ,Zookeeper Atomic Broadcast,zk 原子消息广播协议,是专为 ZooKeeper 设计的一 种支持崩溃恢复的原子广播协议。在 Zookeeper 中,基于该协议,ZooKeeper 实现了一种主从模式的系统架构来保持集群中各个副本之间的数据一致性。

7、如何设计一个分布式锁

分布式锁的本质:就是在所有进程都能访问到的一个地方,设置一个锁资源,让这些进程都来竞争锁资源。数据库、zookeeper, Redis。。通常对于分布式锁,会要求响应快、性能高、与业务无关。

Redis实现分布式锁:SETNX key value:当key不存在时,就将key设置为value,并返回1。如果key存在,就返回0。EXPIRE key locktime: 设置key的有效时长。 DEL key: 删除。 GETSET key value: 先GET,再SET,先返回key对应的值,如果没有就返回空。然后再将key设置成value。

1、最简单的分布式锁: SETNX 加锁, DEL解锁。问题: 如果获取到锁的进程执行失败,他就永远不会主动解锁,那这个锁就被锁死了。

2、给锁设置过期时长: 问题: SETNX 和EXPIRE并不是原子性的,所以获取到锁的进程有可能还没有执行EXPIRE指令,就挂了,这时锁还是会被锁死。

3、将锁的内容设置为过期时间(客户端时间+过期时长),SETNX获取锁失败时,拿这个时间跟当前时间比对,如果是过期的锁,就先删除锁,再重新上锁。 问题: 在高并发场景下,会产生多个进程同时拿到锁的情况。

4、setNX失败后,获取锁上的时间戳,然后用getset,将自己的过期时间更新上去,并获取旧值。如果这个旧值,跟之前获得的时间戳是不一致的,就表示这个锁已经被其他进程占用了,自己就要放弃竞争锁。

public boolean tryLock(RedisnConnection conn){
    long nowTime= System.currnetTimeMillis();
    long expireTIme = nowTime+1000;
    if(conn.SETNX("mykey",expireTIme)==1){
        conn.EXPIRE("mykey",1000);
        return true;
    }else{
        long oldVal = conn.get("mykey");
        if(oldVal != null && oldVal < nowTime){
            long currentVal = conn.GETSET("mykey",expireTime);
            if(oldVal == curentVal){
                conn.EXPIRE("mykey",1000);
                return true;
            }
            return false;
        }
        return false;
    }
}
DEL

5、上面就形成了一个比较高效的分布式锁。分析一下,上面各种优化的根本问题在于SETNX和EXPIRE两个指令无法保证原子性。Redis2.6提供了直接执行lua脚本的方式,通过Lua脚本来保证原子性。redission。

8、什么是CAP理论

CAP即:

  • Consistency(一致性)
  • Availability(可用性)
  • Partition tolerance(分区容忍性)

①**一致性:**对于客户端的每次读操作,要么读到的是最新的数据,要么读取失败。换句话说,一致性是站在分布式系统的角度,对访问本系统的客户端的一种承诺:要么我给您返回一个错误,要么我给你返回绝对一致的最新数据,不难看出,其强调的是数据正确。

②**可用性:**任何客户端的请求都能得到响应数据,不会出现响应错误。换句话说,可用性是站在分布式系统的角度,对访问本系统的客户的另一种承诺:我一定会给您返回数据,不会给你返回错误,但不保证数据最新,强调的是不出错。

③**分区容忍性:**由于分布式系统通过网络进行通信,网络是不可靠的。当任意数量的消息丢失或延迟到达时,系统仍会继续提供服务,不会挂掉。换句话说,分区容忍性是站在分布式系统的角度,对访问本系统的客户端的再一种承诺:我会一直运行,不管我的内部出现何种数据同步问题,强调的是不挂掉。

CAP原则又称CAP定理,指的是在一个分布式系统中,一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance)。CAP 原则指的是,这三个要素最多只能同时实现两点,不可能三者兼顾。

CAP是没办法同时达到的,要么是CP,要么是CA,要么是AP,是不可能存在CAP的,因为如下:

假如分布式情况下数据库1和数据库2,用户上传一张图片必须同时同步成功才满足一致性(Consistency),并且用户可以看到信息也满足了(可用性),当突发场景数据库1和数据库2突然间因为网络断电原因,某一个直接宕机,那还有另外一个数据库可以提供分区容错性,但是这时候已经无法满足一致性了,所以这种没办法实现。

9、什么是RPC?

远程过程调用协议,一种通过网络从远程计算机上请求服务,而不需要了解底层网络技术的协议。RPC它假定某些协议的存在,例如TPC/UDP等,为通信程序之间携带信息数据。在OSI网络七层模型中,RPC跨越了传输层和应用层,RPC使得开发,包括网络分布式多程序在内的应用程序更加容易。RPC 框架需提供一种透明调用机制,让使用者不必显式的区分本地调用和远程调用。

RPC的三个过程

1:通讯协议 比如:你需要找人在国外干活,那么你可以直接飞过去或者打电话或者通过互联网的形式,去找人,这个找人的过程就是通讯协议

2:寻址 既然要找人干活,肯定要知道地址在哪,飞过去需要找到详细地址,打电话需要知道电话号码,互联网需要知道IP是多少

3:数据序列化 就是说,语言需要互通,才能够让别人干活,之间需要一个大家都懂的语言去交流

10、如何实现接口幂等性?

唯一索引
使用唯一索引可以避免脏数据的添加,当插入重复数据时数据库会抛异常,保证了数据的唯一性。

乐观锁
这里的乐观锁指的是用乐观锁的原理去实现,为数据字段增加一个version字段,当数据需要更新时,先去数据库里获取此时的version版本号

更新数据时首先和版本号作对比,如果不相等说明已经有其他的请求去更新数据了,提示更新失败。

悲观锁
乐观锁可以实现的往往用悲观锁也能实现,在获取数据时进行加锁,当同时有多个重复请求时其他请求都无法进行操作

分布式锁
幂等的本质是分布式锁的问题,分布式锁正常可以通过redis或zookeeper实现;在分布式环境下,锁定全局唯一资源,使请求串行化,实际表现为互斥锁,防止重复,解决幂等。

token机制
token机制的核心思想是为每一次操作生成一个唯一性的凭证,也就是token。一个token在操作的每一个阶段只有一次执行权,一旦执行成功则保存执行结果。对重复的请求,返回同一个结果。token机制的应用十分广泛。

11、什么BASE理论?

BASE理论是由eBay工程师提出,是对可用性和一致性的权衡。BASE是由 Basically Available(基本可用),Soft state(软状态),和 Eventually consistent(最终一致性)三个短语的缩写。

Basically Available(基本可用)
算作完全可用和完全不可用中的一种折中,互联网上的应用,如果是完全不可用的,那这个系统就没存在必要了;而在互联网上,用户量等有时候难以预见,就造成了用户超出系统设计的标准,想一直保持完全可用就很难,所以折中下,我们可以通过延迟响应,流量削峰等手段来保障系统的核心功能的正常,从而实现基本可用。

Eventually consistent(最终一致性) 我们希望获取的数据就是正确的,这像一句废话,如果获取到的数据是不确定正确与否,那我们拿这些错误的数据干嘛。但是由于这样那样的问题,我们不能随时都保障数据的一致,所以我们有了数据的中间状态,即软状态,经过一定时间后,数据最终回归于最终一致,这些短暂的数据不一致性,对用户的影响很小,比如你更新一条微博动态,可能有的地方用户可以看到你这条微博消息,另外的用户看不到这条微博消息,这个影响不大,只要最终所有用户都可以看到你这条微博消息就可以了。

最终一致性的系统不承诺写入数据成功后,立刻就从系统中读出最新的数据,也不承诺具体多久之后可以读到最新的数据,而是尽可能保障特定时间级别之后的数据可用,这取决于很多因素,比如网络快慢,比如副本的多少等。如果我们设置过DNS域名就知道,DNS域名我们配置A记录后,域名和IP的关系不是立刻生效的,过多久也不好说,这就是个最新一致性的系统。

Soft state(软状态) 软状态故名思意就是可以变动的状态,强调的是数据状态处于一种临界状态。相对于软状态,就是硬状态,就是数据的状态是确定的。对于满足ACID的数据状态是硬状态。最终一致性的系统中,数据的读出来的不一定是最新的,我理解就是一种软状态即一种短暂的临时状态。

12、简述paxos算法

(29条消息) 【分布式面试题】1. 简述paxos算法_paxos算法面试_镰刀韭菜的博客-CSDN博客

13、请谈谈ZK对事务性的支持

它对于事务性的支持主要依赖于四个函数,zoo_create_op_init, zoo_delete_op_init, zoo_set_op_init以及zoo_check_op_init。每一个函数都会在客户端初始化一个operation,客户端程序有义务保留这些operations。当准备好一个事务中的所有操作后,可以使用zoo_multi来提交所有的操作,由zookeeper服务来保证这一系列操作的原子性。也就是说只要其中有一个操作失败了,相当于此次提交的任何一个操作都没有对服务端的数据造成影响。Zoo_multi的返回值是第一个失败操作的状态信号。

Zookeeper通过版本号来保证操作的实效性。zoo_set的最后一个形参就是version number。如果提交的数和服务端在该节点的版本号对不上,那么此次设值操作就失败了。这可以解决这样的情况:client1.get, client2.get, client2.set, client1.set。从client1的角度出发,set和get之前操作的变量值已经发生改变了,zookeeper非常负责的保证的最后一次set将不会成功,客户端可以重新get, set一次。

14、简述你对RMI的理解

Java远程方法调用(RMI) - 知乎 (zhihu.com)

一篇文章让你搞懂RMI和RPC究竟用哪个? - 知乎 (zhihu.com)

Java RMI,即 远程方法调用(Remote Method Invocation),它的实现依赖于Java虚拟机(JVM),RMI允许在一个Java虚拟机中运行的对象调用在另一个Java虚拟机中运行的对象上的方法。RMI是Java的一组拥护开发分布式应用程序的API,RMI使用Java语言接口定义了远程对象,它集合了Java序列化和Java远程方法协议(Java Remote Method Protocol),RMI能直接传输序列化后的Java对象和分布式垃圾收集,因此它仅支持从一个JVM到另一个JVM的调用。RMI提供了用Java编程语言编写的程序之间的远程通信。

15、简述raft算法

Raft算法详解 - 知乎 (zhihu.com)

不同于Paxos算法直接从分布式一致性问题出发推导出来,Raft算法则是从多副本状态机的角度提出,用于管理多副本状态机的日志复制。Raft实现了和Paxos相同的功能,它将一致性分解为多个子问题:Leader选举(Leader election)、日志同步(Log replication)、安全性(Safety)、日志压缩(Log compaction)、成员变更(Membership change)等。同时,Raft算法使用了更强的假设来减少了需要考虑的状态,使之变的易于理解和实现。

Raft将系统中的角色分为领导者(Leader)、跟从者(Follower)和候选人(Candidate):

  • Leader:接受客户端请求,并向Follower同步请求日志,当日志同步到大多数节点上后告诉Follower提交日志。
  • Follower:接受并持久化Leader同步的日志,在Leader告之日志可以提交之后,提交日志。
  • Candidate:Leader选举过程中的临时角色。

Raft要求系统在任意时刻最多只有一个Leader,正常工作期间只有Leader和Followers。

16、简述ZK中的观察者机制

zookeeper的观察者详解 - 知乎 (zhihu.com)

Zookeeper中的Watcher机制到底是啥? - 掘金 (juejin.cn)

ZooKeeper 提供了分布式数据发布/订阅功能,一个典型的发布/订阅模型系统定义了一种一对多的订阅关系,能让多个订阅者同时监听某一个主题对象,当这个主题对象自身状态变化时,会通知所有订阅者,使他们能够做出相应的处理。

在 ZooKeeper 中,引入了 Watch 机制来实现这种分布式的通知功能。

ZooKeeper 允许客户端向服务端注册一个 Watch 监听,当服务端的一些事件触发了这个 Watch ,那么就会向指定客户端发送一个事件通知,来实现分布式的通知功能。

Watcher实现由三个部分组成:

  • Zookeeper服务端;
  • Zookeeper客户端;
  • 客户端的ZKWatchManager对象

客户端首先将Watcher注册到服务端,同时将Watcher对象保存到客户端的Watch管理器中。当ZooKeeper服务端监听的数据状态发生变化时,服务端会主动通知客户端,接着客户端的Watch管理器会触发相关Watcher来回调相应处理逻辑,从而完成整体的数据发布/订阅流程

17、简述zk的数据模型

https://blog.csdn.net/nrsc272420199/article/details/106797580

ZooKeeper的视图结构和标准的Unix文件系统类似,其中每个节点称为“数据节点”或ZNode,每个znode可以存储数据,还可以挂载子节点,因此可以称之为“树”。
需要注意的是 创建znode时,每个znode都必须指定值,如果没有值,节点是不能创建成功的。

在Zookeeper中,znode是一个跟Unix文件系统路径相似的节点,可以往这个节点存储或获取数据 ; 且通过客户端可对znode进行增删改查的操作,还可以注册watcher监控znode的变化

18、简述zk的典型应用场景

(29条消息) Zookeeper八种应用场景_用 zookeeper的场景_码码码码码码—农的博客-CSDN博客

19、简述zk的命名服务、配置管理、集群管理

命名服务:
通过指定的名字来获取 资源或者服务地址 。Zookeeper可以创建⼀个 全局唯⼀ 的路径,这个路径就可以作为⼀个名字。被命名的实体可以是集群中的机器,服务的地址,或者是远程的对象等。⼀些分布式服务框架(RPC、RMI)中的服务地址列表,通过使⽤命名服务,客户端应⽤能够根据特定的名字来获取 资源的实体 、 服务地址 和 提供者信息 等
配置管理:
实际项⽬开发中,经常使⽤ .properties 或者 xml 需要配置很多信息,如 数据库连接信息 、 fps地址端⼝ 等等。程序分布式部署时,如果把程序的这些配置信息保存在zk的znode节点下,当你要修改配置,即znode会发⽣变化时,可以通过改变zk中某个⽬录节点的内容,利⽤watcher通知给各个客户端,从⽽更改配置。
集群管理:
集群管理包括 集群监控 和 集群控制 ,就是监控集群机器状态,剔除机器和加⼊机器。zookeeper可以⽅便集群机器的管理,它可以实时监控znode节点的变化,⼀旦发现有机器挂了,该机器就会与zk断开连接,对应的临时⽬录节点会被删除,其他所有机器都收到通知。新机器加⼊也是类似。
————————————————
版权声明:本文为CSDN博主「图图是个好孩纸~」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/m0_70734549/article/details/127870852

20、简述TCC事务模型

分布式事务之解决方案(TCC) - 腾讯云开发者社区-腾讯云 (tencent.com)

TCC是Try、Confirm、Cancel三个词语的缩写,TCC要求每个分支事务实现三个操作 :预处理Try、确认Confirm、撤销Cancel。Try操作做业务检查及资源预留,Confirm做业务确认操作,Cancel实现一个与Try相反的操作既回滚操作。TM首先发起所有的分支事务的try操作,任何一个分支事务的try操作执行失败,TM将会发起所有分支事务的Cancel操作,若try操作全部成功,TM将会发起所有分支事务的Confirm操作,其中Confirm/Cancel操作若执行失败,TM会进行重试

TCC分为三个阶段 :

  1. Try阶段是做业务检查(一致性)及资源预留(隔离),此阶段仅是一个初步操作,它和后续的Confirm一起才能真正构成一个完整的业务逻辑。
  2. Confirm阶段是做确认提交,Try阶段所有分支事务执行成功后开始执行Confirm。通常情况下,采用TCC则认为Confirm阶段是不会出错的。即 :只要Try成功,Confirm一定成功。若Confirm阶段真的出错了,需引入重试机制或人工处理。
  3. Cancel阶段是在业务执行错误需要回滚的状态下执行分支事务的业务取消,预留资源释放。通常情况下,采用TCC则认为Cancel阶段也是一定成功的。若Cancel阶段真的出错了,需引入重试机制或人工处理。
  4. TM事务管理器 TM事务管理器可以实现为独立的服务,也可以让全局事务发起方充当TM的角色,TM独立出来是为了成为公用组件,是为了考虑结构和软件复用。 TM在发起全局事务时生成全局事务记录,全局事务ID贯穿整个分布式事务调用链条,用来记录事务上下文,追踪和记录状态,由于Confirm和Cancel失败需进行重试,因此需要实现为幂等性是指同一个操作无论请求多少次,其结果都相同。
21、负载均衡的策略有哪些?

1.轮询(默认)每个请求按时间顺序逐一分配到不同的后端服务器,如果后端服务器down掉,能自动剔除。

2、指定权重,指定轮询几率,weight和访问比率成正比,用于后端服务器性能不均的情况。

3、IP绑定 ip_hash,每个请求按访问ip的hash结果分配,这样每个访客固定访问一个后端服务器,可以解决session的问题。

4、fair(第三方)按后端服务器的响应时间来分配请求,响应时间短的优先分配。

5、url_hash(第三方)按访问url的hash结果来分配请求,使每个url定向到同一个后端服务器,后端服务器为缓存时比较有效

22、负载均衡算法、类型

(29条消息) 负载均衡算法、类型_Boy-F的博客-CSDN博客

23、分布式锁的使用场景?

【分布式】分布式锁都有哪些实现方案? - 知乎 (zhihu.com)

分布式技术 - 分布式锁的应用场景 - 知乎 (zhihu.com)

在以下2中场景下使用分布式锁:

1 . 提升效率:如果不使用分布式锁,回导致业务重复执行一些没有意义的工作。
2 . 正确性: 使用分布式锁可以防止对数据的并发访问,避免数据不一致,数据损失等。

24、分布式缓存寻址算法

(29条消息) 详解分布式寻址算法_Code我敲你的博客-CSDN博客

hash算法 :根据key进⾏hash函数运算、结果对 分⽚数取模 ,确定分⽚适合固定分⽚数的场景,扩展分⽚或者减少分⽚时,所有数据都需要 重新计算分⽚、存储

⼀致性hash :将整个 hash值 得区间组织成⼀个闭合的圆环,计算每台服务器的hash值、映射到圆环中。使⽤相同的hash算法计算数据的hash值,映射到圆环,顺时针寻找,找到的第⼀个服务器就是数据存储的服务器。新增及减少节点时只会影响节点到他逆时针最近的⼀个服务器之间的值 存在hash环倾斜的问题,即服务器分布不均匀,可以通过虚拟节点解决

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

hash算法比较适合固定分区或者分布式节点的集群架构。一致性hash算法比较适合需要动态扩容的分布式架构以及一些动态负载均衡的分布式中间件和RPC中间件。hash slot是Redis对hash算法的一种实现。

25、对比两阶段,三阶段有哪些改进?

(5条消息) 08 对比两阶段提交,三阶段协议有哪些改进?_久违の欢喜的博客-CSDN博客

26、定时任务实现原理

从单机角度,定时任务实现主要有以下 3 种方案:

  • while + sleep 组合
  • 最小堆实现
  • 时间轮实现

(5条消息) 定时任务实现原理详解_定时任务的原理_谢月的博客-CSDN博客

27、Zookeeper中的领导者选举的流程是怎样的?

【分布式】Zookeeper的Leader选举 - leesf - 博客园 (cnblogs.com)

二.核心选举原则:
Zookeeper集群中只有超过半数以上的服务器启动,集群才能正常工作;
在集群正常工作之前,myid小的服务器给myid大的服务器投票,直到集群正常工作,选出Leader;
选出Leader之后,之前的服务器状态由Looking改变为Following,以后的服务器都是Follower。
如果集群没有Leader(非全新选举),Epoch大的服务器当选leader;
如果Epoch相等,比较ZXID(事物ID),事物ID大的,当选leader;
如果Epoch相等,ZXID相等,则比较myId(服务器id),服务器id大的当选Leader,服务器id是不重复;

三.选举机制类型:
1、全新集群选举(第一次启动):
假设目前有三台服务器,分别是1号、2号、3号;
首先1号启动,启动一次选举,1号投给自己一票,由于其他服务器没有启动,无法收到1号的投票信息,此时1号处于Looking(竞选状态);
2号启动,启动选举,2号给自己投一票,并且与1号交换信息,此时1号发现2号的myId比自己投票服务器(服务器1)的myId大,此时1号0票,2号2票,2>(3/2),服务器2的票数最多,超过半数,那么2号当选leader,1号更改状态为following,2号更改状态为leading;
3号启动,启动选举,给自己投一票,此时与之前启动的1号、2号交换信息,此时1号、2号并没有处于Looking(竞选中)状态,不会更改选举状态,3号一票,服从多数,此时3号更改状态为following;
注意:当集群服务器有5台时,前面的服务器1和服务器2,都是LOOKING,服务器1票数为0,服务器2票数为2,当启动第三台服务器时,服务器3的myid大,服务器2会把票数给服务器3,(服务器启动会给自己投一票)这时服务器3票数为3选为Leader,其他的都是follow,之后启动的服务器也是follow;

2、非全新集群选举(非第一次启动):
如果leader服务器挂了,那么整个集群将暂停对外服务,进入新一轮leader选举,其过程和启动时期的leader选举过程基本一致。Leader挂掉后,余下的服务器都将自己的服务器状态变更为looking,然后开始进入Leader选举过程。服务器1号、2号、3号,此时2号是Leader,如果2号停电挂掉之后,1号、3号无法连接到Leader,知道Leader挂了,他们就知道必须选出一个新的 Leader,于是纷纷将自己的状态都修改为 LOOKING 状态:
比如1号的ZXID(事物id)为:77,Epoch(任期代号)为:1,myid(服务器id):1;3号的ZXID(事物id)为:80,Epoch(任期代号)为:1,myid(服务器id):3;此时3号为leader。

如果follow挂掉了,此时集群还是可以对外提供服务,挂掉一个是达到整个集群总数的半数以上的,如果挂掉的follow恢复之后,还是以 Follower 的身份加入到集群中来,并且仍然以当前 Leader 的信息来同步,即使它的Epoch大于其他的服务器。

28、Zookeeper集群中节点之间数据是如何同步的

⾸先集群启动时,会先进⾏领导者选举,确定 哪个节点是Leader ,哪些节点是 Follower 和 Observer

然后Leader会和其他节点进⾏ 数据同步 ,采⽤ 发送快照 和 发送Diff⽇志 的⽅式

集群在⼯作过程中,所有的写请求都会交给Leader节点来进⾏处理,从节点只能处理读请求

Leader节点收到⼀个写请求时,会通过 两阶段机制 来处理

Leader节点会将该写请求对应的⽇志发送给其他Follower节点,并等待Follower节点持久化⽇志成功

Follower节点收到⽇志后会进⾏持久化,如果持久化成功则发送⼀个Ack给Leader节点

当Leader节点收到半数以上的 Ack 后,就会开始提交,先更新Leader节点本地的内存数据

然后发送 commit 命令给 Follower 节点,Follower节点收到commit命令后就会 更新各⾃本地内存数据

同时Leader节点还是将当前写请求直接发送给Observer节点,Observer节点收到Leader发过来的写请求后直接执⾏更新本地内存数据

最后Leader节点返回客户端写请求响应成功

通过 同步机制 和 两阶段提交机制 来达到 集群中节点数据⼀致

29、zk和eureka的区别

Eureka和zookeeper的区别 - 经典鸡翅 - 博客园 (cnblogs.com)

eureka是基于ap的。zookeeper是基于cp的

Eureka可以很好的应对因网络故障导致部分节点失去联系的情况,而不会像zookeeper那样使整个注册服务瘫痪。Eureka作为单纯的服务注册中心来说要比zookeeper更加“专业”,因为注册服务更重要的是可用性,我们可以接受短期内达不到一致性的状况。

30、分布式session方案

(5条消息) 4种分布式session解决方案_断橋殘雪的博客-CSDN博客

方案一:客户端存储
直接将信息存储在cookie中
cookie是存储在客户端上的一小段数据,客户端通过http协议和服务器进行cookie交互,通常用来存储一些不敏感信息

缺点:

数据存储在客户端,存在安全隐患
cookie存储大小、类型存在限制
数据存储在cookie中,如果一次请求cookie过大,会给网络增加更大的开销

方案二:session复制
session复制是小型企业应用使用较多的一种服务器集群session管理机制,在真正的开发使用的并不是很多,通过对web服务器(例如Tomcat)进行搭建集群。

存在的问题:

session同步的原理是在同一个局域网里面通过发送广播来异步同步session的,一旦服务器多了,并发上来了,session需要同步的数据量就大了,需要将其他服务器上的session全部同步到本服务器上,会带来一定的网路开销,在用户量特别大的时候,会出现内存不足的情况
优点:

服务器之间的session信息都是同步的,任何一台服务器宕机的时候不会影响另外服务器中session的状态,配置相对简单
Tomcat内部已经支持分布式架构开发管理机制,可以对tomcat修改配置来支持session复制,在集群中的几台服务器之间同步session对象,使每台服务器上都保存了所有用户的session信息,这样任何一台本机宕机都不会导致session数据的丢失,而服务器使用session时,也只需要在本机获取即可

方案三:session绑定:
Nginx介绍:
Nginx是一款自由的、开源的、高性能的http服务器和反向代理服务器

Nginx能做什么:
反向代理、负载均衡、http服务器(动静代理)、正向代理

如何使用nginx进行session绑定
我们利用nginx的反向代理和负载均衡,之前是客户端会被分配到其中一台服务器进行处理,具体分配到哪台服务器进行处理还得看服务器的负载均衡算法(轮询、随机、ip-hash、权重等),但是我们可以基于nginx的ip-hash策略,可以对客户端和服务器进行绑定,同一个客户端就只能访问该服务器,无论客户端发送多少次请求都被同一个服务器处理

缺点

  • 容易造成单点故障,如果有一台服务器宕机,那么该台服务器上的session信息将会丢失
  • 前端不能有负载均衡,如果有,session绑定将会出问题

优点

  • 配置简单

方案四:基于redis存储session方案

优点

  • 这是企业中使用的最多的一种方式
  • spring为我们封装好了spring-session,直接引入依赖即可
  • 数据保存在redis中,无缝接入,不存在任何安全隐患
  • redis自身可做集群,搭建主从,同时方便管理

缺点

  • 多了一次网络调用,web容器需要向redis访问
31、dubbo和springcloud的区别

(5条消息) SpringCloud与Dubbo的区别_IsToRestart的博客-CSDN博客

两者都是现在主流的微服务框架,但却存在不少差异:

初始定位不同:SpringCloud定位为微服务架构下的一站式解决方案;Dubbo 是 SOA 时代的产物,它的关注点主要在于服务的调用和治理
生态环境不同:SpringCloud依托于Spring平台,具备更加完善的生态体系;而Dubbo一开始只是做RPC远程调用,生态相对匮乏,现在逐渐丰富起来。
调用方式:SpringCloud是采用Http协议做远程调用,接口一般是Rest风格,比较灵活;Dubbo是采用Dubbo协议,接口一般是Java的Service接口,格式固定。但调用时采用Netty的NIO方式,性能较好。
组件差异比较多,例如SpringCloud注册中心一般用Eureka,而Dubbo用的是Zookeeper

ES

(5条消息) ElasticSearch搜索引擎常见面试题总结_elasticsearch面试_张维鹏的博客-CSDN博客

ElasticSearch面试题 - 知乎 (zhihu.com)

网络与IO面试题

(5条消息) IO网络编程面试题(2022)_io面试题_律二萌萌哒的博客-CSDN博客

1、什么OAuth2.0协议

(5条消息) OAuth2.0 的简介_oauth2.0是什么_Firm陈的博客-CSDN博客

2、描述下HTTP和HTTPS的区别

HTTP: 是互联网上应用最为广泛的一种网络通信协议,基于TCP,可以使浏览器工作更为高效,减少网络传输。

HTTPS: 是HTTP的加强版,可以认为是HTTP+SSL(Secure Socket Layer)。在HTTP的基础上增加了一系列的安全机制。一方面保证数据传输安全,另一位方面对访问者增加了验证机制。是目前现行架构下,最为安全的解决方案。

主要区别:

​ 1、HTTP的连接是简单无状态的,HTTPS的数据传输是经过证书加密的,安全性更高。

​ 2、HTTP是免费的, 而HTTPS需要申请证书,而证书通常是需要收费的,并且费用一般不低。

​ 3、他们的传输协议不通过,所以他们使用的端口也是不一样的, HTTP默认是80端口,而HTTPS默认是443端口。

HTTPS的缺点:

​ 1、HTTPS的握手协议比较费时,所以会影响服务的响应速度以及吞吐量。

​ 2、HTTPS也并不是完全安全的。他的证书体系其实并不是完全安全的。并且HTTPS在面对DDOS这样的攻击时,几乎起不到任何作用。

​ 3、证书需要费钱,并且功能越强大的证书费用越高。

3、什么是SSO?

单点登录,Single Sign On(SSO)指的是在多个应用系统中,只需登录一次,就可以访问其他相互信任的应用系统。

4、什么是跨域请求?有什么问题?怎么解决?

1.CORS全称Cross-Origin Resource Sharing,意为跨域资源共享。当一个资源去访问另一个不同域名或者同域名不同端口的资源时,就会发出跨域请求。如果此时另一个资源不允许其进行跨域资源访问,那么访问就会遇到跨域问题。
2.跨域是指浏览器不能执行来自其它网站的脚本,是由浏览器的同源策略造成的,是浏览器对JavaScript 施加的安全限制。(需要注意的是,跨域并不是请求发不出去,请求能发出去,服务端能收到请求并正常返回结果,只是结果被浏览器拦截了)

  1. 使用JSONP:前端技术使用 jQuery的ajax解决方案,服务端使用JSON.toJSONString。
  2. 使用CORS:在响应头上添加Access-Control-Allow-Origin属性,指定同源策略的地址。同源策略默认地址是网页的本身。只要浏览器检测到响应头带上了CORS,并且允许的源包括了本网站,那么就不会拦截对应的请求响应。(对比jsonp,优点在于功能更加强大支持各种HTTP Method,缺点是兼容性不如JSONP)
    前端:支持原生ajax、jQuery ajax、vue、axios 服务端:支持Java、Nodejs、Python、PHP等
  3. 指定的页面设置document.domain属性:指定的页面就可以共享Cookie。
  4. Nginx反向代理:配置nginx(修改nginx目录下的nginx.conf),在这个服务器上配置多个前缀来转发http/https请求到多个真实的服务器即可。这样,这个服务器上所有url都是相同的域名、协议和端口。这样对于浏览器来说,这些url都是同源的,就不会有跨域限制了。
  5. controller方法的CORS配置,您可以向@RequestMapping注解处理程序方法添加一个@CrossOrigin注解,以便启用CORS(默认情况下,@CrossOrigin允许在@RequestMapping注解中指定的所有源和HTTP方法
  6. 项目中前后端分离部署,所以需要解决跨域的问题。 我们使用cookie存放用户登录的信息,在spring拦截器进行权限控制,当权限不符合时,直接返回给用户固定的json结果。 当用户登录以后,正常使用;当用户退出登录状态时或者token过期时,由于拦截器和跨域的顺序有问题,出现了跨域的现象。 我们知道一个http请求,先走filter,到达servlet后才进行拦截器的处理,如果我们把cors放在filter里,就可以优先于权限拦截器执行。
5、HTTPS是如何实现安全传输的

在https中的数据传输过程中对数据进行加密处理,HTTPS是使用对称加密和非对称加密以及签名算法(签名算法不是用来做加密的)还有证书机制来对消息进行处理来达到一个安全的有效传输。

HTTPS是基于HTTP的上层添加了一个叫做TLS的安全层,对数据的加密等操作都是在这个安全层中进行处理的,其底层还是应用的HTTP。HTTPS通信先是使用非对称加密进行密钥的协商,协商出一个对称加密的密钥,之后的通信则采用这个对称密钥进行对称加密进行密文传输。因为非对称加密其算法及其复杂,导致解密效率低下,而对称加密效率则明显高出百倍。

6、Netty的线程模型是什么样的?

果有,session绑定将会出问题

优点

  • 配置简单

方案四:基于redis存储session方案

优点

  • 这是企业中使用的最多的一种方式
  • spring为我们封装好了spring-session,直接引入依赖即可
  • 数据保存在redis中,无缝接入,不存在任何安全隐患
  • redis自身可做集群,搭建主从,同时方便管理

缺点

  • 多了一次网络调用,web容器需要向redis访问
31、dubbo和springcloud的区别

(5条消息) SpringCloud与Dubbo的区别_IsToRestart的博客-CSDN博客

两者都是现在主流的微服务框架,但却存在不少差异:

初始定位不同:SpringCloud定位为微服务架构下的一站式解决方案;Dubbo 是 SOA 时代的产物,它的关注点主要在于服务的调用和治理
生态环境不同:SpringCloud依托于Spring平台,具备更加完善的生态体系;而Dubbo一开始只是做RPC远程调用,生态相对匮乏,现在逐渐丰富起来。
调用方式:SpringCloud是采用Http协议做远程调用,接口一般是Rest风格,比较灵活;Dubbo是采用Dubbo协议,接口一般是Java的Service接口,格式固定。但调用时采用Netty的NIO方式,性能较好。
组件差异比较多,例如SpringCloud注册中心一般用Eureka,而Dubbo用的是Zookeeper

ES

(5条消息) ElasticSearch搜索引擎常见面试题总结_elasticsearch面试_张维鹏的博客-CSDN博客

ElasticSearch面试题 - 知乎 (zhihu.com)

网络与IO面试题

(5条消息) IO网络编程面试题(2022)_io面试题_律二萌萌哒的博客-CSDN博客

1、什么OAuth2.0协议

(5条消息) OAuth2.0 的简介_oauth2.0是什么_Firm陈的博客-CSDN博客

2、描述下HTTP和HTTPS的区别

HTTP: 是互联网上应用最为广泛的一种网络通信协议,基于TCP,可以使浏览器工作更为高效,减少网络传输。

HTTPS: 是HTTP的加强版,可以认为是HTTP+SSL(Secure Socket Layer)。在HTTP的基础上增加了一系列的安全机制。一方面保证数据传输安全,另一位方面对访问者增加了验证机制。是目前现行架构下,最为安全的解决方案。

主要区别:

​ 1、HTTP的连接是简单无状态的,HTTPS的数据传输是经过证书加密的,安全性更高。

​ 2、HTTP是免费的, 而HTTPS需要申请证书,而证书通常是需要收费的,并且费用一般不低。

​ 3、他们的传输协议不通过,所以他们使用的端口也是不一样的, HTTP默认是80端口,而HTTPS默认是443端口。

HTTPS的缺点:

​ 1、HTTPS的握手协议比较费时,所以会影响服务的响应速度以及吞吐量。

​ 2、HTTPS也并不是完全安全的。他的证书体系其实并不是完全安全的。并且HTTPS在面对DDOS这样的攻击时,几乎起不到任何作用。

​ 3、证书需要费钱,并且功能越强大的证书费用越高。

3、什么是SSO?

单点登录,Single Sign On(SSO)指的是在多个应用系统中,只需登录一次,就可以访问其他相互信任的应用系统。

4、什么是跨域请求?有什么问题?怎么解决?

1.CORS全称Cross-Origin Resource Sharing,意为跨域资源共享。当一个资源去访问另一个不同域名或者同域名不同端口的资源时,就会发出跨域请求。如果此时另一个资源不允许其进行跨域资源访问,那么访问就会遇到跨域问题。
2.跨域是指浏览器不能执行来自其它网站的脚本,是由浏览器的同源策略造成的,是浏览器对JavaScript 施加的安全限制。(需要注意的是,跨域并不是请求发不出去,请求能发出去,服务端能收到请求并正常返回结果,只是结果被浏览器拦截了)

  1. 使用JSONP:前端技术使用 jQuery的ajax解决方案,服务端使用JSON.toJSONString。
  2. 使用CORS:在响应头上添加Access-Control-Allow-Origin属性,指定同源策略的地址。同源策略默认地址是网页的本身。只要浏览器检测到响应头带上了CORS,并且允许的源包括了本网站,那么就不会拦截对应的请求响应。(对比jsonp,优点在于功能更加强大支持各种HTTP Method,缺点是兼容性不如JSONP)
    前端:支持原生ajax、jQuery ajax、vue、axios 服务端:支持Java、Nodejs、Python、PHP等
  3. 指定的页面设置document.domain属性:指定的页面就可以共享Cookie。
  4. Nginx反向代理:配置nginx(修改nginx目录下的nginx.conf),在这个服务器上配置多个前缀来转发http/https请求到多个真实的服务器即可。这样,这个服务器上所有url都是相同的域名、协议和端口。这样对于浏览器来说,这些url都是同源的,就不会有跨域限制了。
  5. controller方法的CORS配置,您可以向@RequestMapping注解处理程序方法添加一个@CrossOrigin注解,以便启用CORS(默认情况下,@CrossOrigin允许在@RequestMapping注解中指定的所有源和HTTP方法
  6. 项目中前后端分离部署,所以需要解决跨域的问题。 我们使用cookie存放用户登录的信息,在spring拦截器进行权限控制,当权限不符合时,直接返回给用户固定的json结果。 当用户登录以后,正常使用;当用户退出登录状态时或者token过期时,由于拦截器和跨域的顺序有问题,出现了跨域的现象。 我们知道一个http请求,先走filter,到达servlet后才进行拦截器的处理,如果我们把cors放在filter里,就可以优先于权限拦截器执行。
5、HTTPS是如何实现安全传输的

在https中的数据传输过程中对数据进行加密处理,HTTPS是使用对称加密和非对称加密以及签名算法(签名算法不是用来做加密的)还有证书机制来对消息进行处理来达到一个安全的有效传输。

HTTPS是基于HTTP的上层添加了一个叫做TLS的安全层,对数据的加密等操作都是在这个安全层中进行处理的,其底层还是应用的HTTP。HTTPS通信先是使用非对称加密进行密钥的协商,协商出一个对称加密的密钥,之后的通信则采用这个对称密钥进行对称加密进行密文传输。因为非对称加密其算法及其复杂,导致解密效率低下,而对称加密效率则明显高出百倍。

6、Netty的线程模型是什么样的?

彻底搞懂 netty 线程模型 - luoxn28 - 博客园 (cnblogs.com)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值