基础知识

面向对象的特征?

面向对象的特征:封装、继承、多态、抽象。 封装:就是把对象的属性和行为(数据)结合为一个独立的整体,并尽可能隐藏对象的内 部实现细节,就是把不想告诉或者不该告诉别人的东西隐藏起来,把可以告诉别人的公开,别人只能用我提供的功能实现需求,而不知道是如何实现的。增加安全性。 继承:子类继承父类的数据属性和行为,并能根据自己的需求扩展出新的行为,提高了代 码的复用性。 多态:指允许不同的对象对同一消息做出相应。即同一消息可以根据发送对象的不同而采 用多种不同的行为方式(发送消息就是函数调用)。封装和继承几乎都是为多态而准备 的,在执行期间判断引用对象的实际类型,根据其实际的类型调用其相应的方法。 抽象:表示对问题领域进行分析、设计中得出的抽象的概念,是对一系列看上去不同,但是本质上相同的具体概念的抽象。在 Java 中抽象用 abstract 关键字来修饰,用abstract修饰类时,此类就不能被实例化,从这里可以看出,抽象类(接口)就是为了继承而存在的。

常见的RuntimeException

  1. java.lang.NullPointerException 空指针异常;出现原因:调用了未经初始化的对象或者是不存在 的对象。
  2. java.lang.ClassNotFoundException 指定的类找不到;出现原因:类的名称和路径加载错误;通常 都是程序试图通过字符串来加载某个类时可能引发异常。
  3. java.lang.NumberFormatException 字符串转换为数字异常;出现原因:字符型数据中包含非数 字型字符。
  4. java.lang.IndexOutOfBoundsException 数组角标越界异常,常见于操作数组对象时发生。
  5. java.lang.IllegalArgumentException 方法传递参数错误。
  6. java.lang.ClassCastException 数据类型转换异常。
  7. java.lang.NoClassDefFoundException 未找到类定义错误。
  8. SQLException SQL 异常,常见于操作数据库时的 SQL 语句错误。
  9. java.lang.InstantiationException实例化异常。
  10. java.lang.NoSuchMethodException方法不存在异常。

常见的引用类型

java的引用类型一般分为四种:强引用软引用、弱引用、虚引用

  • 强引用:普通的变量引用
  • 软引用:将对象用SoftReference软引用类型的对象包裹,正常情况不会被回收,但是GC做完后发现释放不出空间存放新的对象,则会把这些软引用的对象回收掉。软引用可用来实现内存敏感的高速缓存。
  • 弱引用:将对象用WeakReference软引用类型的对象包裹,弱引用跟没引用差不多,GC会直接回收掉,很少用
  • 虚引用:虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系,几乎不用

JVM

jvm内存模型

对象创建过程

  1. 类加载检查:虚拟机遇到一条new指令时,首先将去检查这个类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程:类加载过程中会通过jvm自带的三个类加载器按照双亲委派机制进行类加载过程,我们常见的ClassNotFoundException就是在这里发生的,我们代码中的静态代码块里面的内容也是在这个过程中执行的。
  2. 分配内存:在类加载检查通过后,虚拟机将为新生对象分配内存。对象所需内存的大小在类加载完成后便可完全确定,为对象分配空间的任务等同于把 一块确定大小的内存从Java堆中划分出来。
  3. 初始化零值:内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
  4. 设置对象头:初始化零值之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息(即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例)、对象的哈希码(HashCode)、对象的GC分代年龄、锁状态标志等信息。这些信息存放在对象的对象头Object Header之中。
  5. 执行方法:执行方法,即对象按照程序员的意愿进行初始化。对应到语言层面上讲,就是为属性赋值(注意,这与上面的赋零值不同,这是由程序员赋的值),和执行构造方法

垃圾回收算法

  1. 引用计数法
  2. 可达性分析法

对于可达性分析法,我们知道需要存在一个GC Root的对象作为起点,从这个节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连(就是从GC Roots到对象不可达)时,则证明此对象是不可用的。

GC Root有哪些?

  • 虚拟机栈中的引用对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用对象
  • 本地方法栈中JNI引用对象

垃圾回收器有哪些?

  1. Serial收集器(-XX:+UseSerialGC -XX:+UseSerialOldGC):Serial(串行)收集器是一个单线程收集器,新生代采用复制算法,老年代采用标记-整理算法。
  2. Parallel Scavenge收集器(-XX:+UseParallelGC(年轻代),-XX:+UseParallelOldGC(老年代)) :Parallel收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和Serial收集器类似。默认的收集线程数跟cpu核数相同
  3. ParNew收集器(-XX:+UseParNewGC):只有它能与CMS收集器(真正意义上的并发收集器,后面会介绍到)配合工作
  4. CMS收集器(-XX:+UseConcMarkSweepGC(old)):收集器是一种以获取最短回收停顿时间为目标的收集器CMS收集器是一种 “标记-清除”算法实现的。CMS等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。所谓吞吐量就是CPU中用于运行用户代码的时间与CPU总消耗时间的比值。
  5. G1收集器(-XX:+UseG1GC):G1 (Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征。G1将Java堆划分为多个大小相等的独立区域(Region),JVM最多可以有2048个Region。一般Region大小等于堆大小除以2048,比如堆大小为4096M,则Region大小为2M
  6. Shenandoah:可以看成是G1升级版
  7. ZGC收集器:ZGC是一款JDK 11中新加入的具有实验性质的低延迟垃圾收集器。1、支持TB量级的堆;2、最大GC停顿时间不超10ms;3、奠定未来GC特性的基础;4、最糟糕的情况下吞吐量会降低15%

CMS运行过程,缺点?

整个过程分为四个步骤

  1. 初始标记(STW): 暂停所有的其他线程(STW),并记录下gc roots直接能引用的对象,速度很快
  2. 并发标记: 并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程, 这个过程耗时较长但是不需要停顿用户线程,因为用户程序继续运行,可能会有导致已经标记过的对象状态发生改变。
  3. 重新标记(STW): 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短。
  4. 并发清理: 开启用户线程,同时GC线程开始对未标记的区域做清扫。
  5. 并发重置:重置本次GC过程中的标记数据。

缺点:

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

G1运行过程

G1收集器一次GC的运作过程大致分为以下几个步骤:

  1. 初始标记(initial mark,STW):暂停所有的其他线程,并记录下gc roots直接能引用的对象,速度很快 ;
  2. 并发标记(Concurrent Marking):同CMS的并发标记
  3. 最终标记(Remark,STW):同CMS的重新标记
  4. 筛选回收(Cleanup,STW):G1采用复制算法回收几乎不会有太多内存碎片

G1适合什么场景

  1. 50%以上的堆被存活对象占用
  2. 对象分配和晋升的速度变化非常大
  3. 垃圾回收时间特别长,超过1秒
  4. 8GB以上的堆内存(建议值)
  5. 停顿时间是500ms以内

判断元空间是无用的类

法区(元空间)主要回收的是无用的类,那么如何判断一个类是无用的类呢?类需要同时满足下面3个条件才能算是 “无用的类”

  • 该类所有的对象实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
  • 加载该类的 ClassLoader 已经被回收。(条件苛刻,自定义类加载器会被回收掉)
  • 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

安全点和安全区域

安全点:就是指代码中一些特定的位置,当线程运行到这些位置时它的状态是确定的,这样JVM就可以安全的进行一些操作,比如GC等,所以GC不是想什么时候做就立即触发的,是需要等待所有线程运行到安全点后才能触发。这些特定的安全点位置主要有以下几种:

  1. 方法返回之前
  2. 调用某个方法之后
  3. 抛出异常的位置
  4. 循环的末尾

安全区域:如果一个线程处于 Sleep 或中断状态,它就不能响应 JVM 的中断请求,引用关系不会发生变化。在这个区域内的任意地方开始 GC 都是安全的。

类加载器?双亲委派,好处?

JDK自带有三个类加载器:bootstrapClassLoader(引导类加载器)、ExtClassLoader(扩展类加载器)、AppClassLoader(系统类加载器),bootstrapClassLoader是ExtClassLoader的父类加载器(并不是集成关系,是属性关系)默认加载%JAVA_HOME%/lib下的jar包和class文件ExtClassLoader是AppClassLoader的父类加载器,默认加载%JAVA_HOME%/lib/ext文件夹下的jar包和class文件AppClassLoader是自定义类加载器,负责加载classpath下的类文件

双亲委派:分为向上委派和向下查找,每一个类加载器都有各自的缓存,都会记录自己加载过的类,当一个类需要加载,AppClassLoader先查找自己的缓存有没有加载过这个类,没有加载过就会调用ExtClassLoader去查找,ExtClassLoader没有加载过,就会调用bootstrapClassLoader类去加载,如果bootstrapClassLoader没有加载过,就会去他的类加载路径查找,如果没有找到ExtClassLoader就会查找他的类加载路径,向上委派就是查找缓存,查找到bootstrapClassLoader位置,向下查找就是查找类加载路径,查找到发起的类加载器为止。

好处:1、安全性,避免自己写的类替换掉java核心类;2、避免类重复加载,相同class文件不同的类加载器加载的也是两个类。

YGC和FGC发生的场景

YGC:对新生代堆进行gc。频率比较高,因为大部分对象的存活寿命较短,在新生代里被回收。性能耗费较小。edn空间不足,执行

FGC:全堆范围的gc。默认堆空间使用到达80%(可调整)的时候会触发fgc。生产环境,一般比较少会触发fgc,有时10天或一周左右会有一次。

老年代空间不足,永久区空间不足,调用方法System.gc() ,ygc时的悲观策略, dump live的内存信息时(jmap –dump:live),都会执行full gc

jstack,jmap,Jstat作用

jmap:可以用来查看内存信息,实例个数以及占用内存大小

  • jmap -heap 进程号:查看堆内存信息
  • jmap ‐dump:format=b,file=eureka.hprof 进程号: 堆内存的快照信息,添加jvm参数也可以设置内存溢出自动导出dump文件

jstack: 可以获得java线程的运行情况,可以查看死锁,阻塞,等待

  • Jstack -l PID >> 123.txt 打印某个java进程的堆栈信息

Jstat:jstat命令可以查看堆内存各部分的使用量,以及加载类的数量

  • jstat -gc pid 最常用,可以评估程序内存使用及GC压力整体情况

多线程

死锁

  • 死锁:是指一组互相竞争资源的线程,因为互相等待,导致永久阻塞的现象。

  • 原因:必须同时满足以下四个条件

    1. 共享互斥条件:共享资源x和y只能被一个线程占用
    2. 占有且等待:线程t1已经占有共享资源x,在等待共享资源y的时候不释放共享资源x
    3. 不可抢占:其他线程不能强行抢占线程t1占有的资源
    4. 循环等待:线程t1等待线程t2占有的资源,线程t2等待线程t1占有的资源
  • 如何避免死锁:

    1. 既然产生死锁必然满足四个条件,那我们只要打破四个条件中的一个就可以避免,第一个互斥条件是无法被破坏的,因为锁本身就是通过互斥来解决线程安全的
    2. 针对后三个条件,我们逐一分析,占有且等待这个条件我们可以一次性申请所有资源,不存在等待;
    3. 不可抢占这个条件:占有部分资源的线程进一步申请其他资源的时候如果申请不到,可以主动释放已占有的资源,这样不可抢占这条件就破坏了;
    4. 循环等待这个条件:可以按照顺序去申请资源进行预防,就是说资源是有线性顺序的,申请的时候可以先申请资源序号小的,再申请资源序号大的,线性化之后,就不存在循环等待了

sleep和wait区别

  1. sleep 是 Thread 类的静态本地方法,wait 则是 Object 类的本地方法
  2. sleep方法不会释放锁,但是wait会释放,而且会加入到等待队列中
  3. sleep方法不依赖于同步器synchronized,但是wait需要依赖synchronized关键字
  4. sleep不需要被唤醒(休眠之后推出阻塞),但是wait需要(不指定时间需要被别人中断)
  5. sleep 一般用于当前线程休眠,或者轮循暂停操作,wait 则多用于多线程之间的通信
  6. sleep 会让出 CPU 执行时间且强制上下文切换,而 wait 则不一定,wait 后可能还是有机会重新竞争到锁继续执行的。

sleep就是把cpu的执行资格和执行权释放出去,不再运行此线程,当定时时间结束再取回cpu资源,参与cpu的调度,获取到cpu资源后就可以继续运行了。而如果sleep时该线程有锁,那么sleep不会释放这个锁,而是把锁带着进入了冻结状态,也就是说其他需要这个锁的线程根本不可能获取到这个锁。也就是说无法执行程序。如果在睡眠期间其他线程调用了这个线程的interrupt方法,那么这个线程也会抛出interruptexception异常返回,这点和wait是一样的。

当我们调用wait()方法后,线程会放到等待池当中,等待池的线程是不会去竞争同步锁。只有调用了notify()或notifyAll()后等待池的线程才会开始去竞争锁,notify()是随机从等待池选出一个线程放到锁池,而notifyAll()是将等待池的所有线程放到锁池当中

Volitile作用

  1. 可见性:对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。内存屏障->汇编Lock关键字->缓存一致性协议
  2. 原子性:对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性
  3. 有序性:对volatile修饰的变量的读写操作前后加上各种特定的内存屏障来禁止指令重排序来保障有序性

Volitile如何保证可见性

  1. 缓存一致性:在共享内存多处理器系统中,每个处理器都有一个单独的缓存内存,共享数据在主内存和各处理器私有缓存同时存在。当数据的一个副本发生更改,其他副本需要感知到最新修改数据。缓存一致性的手段主要有:
  2. 总线锁定:总线锁定就是使用处理器提供的一个 LOCK#信号,当其中一个处理器在总线上输出此信号时,其它处理器的请求将被阻塞,那么该处理器可以独占共享内存。
  3. 总线窥探(Bus snooping):是缓存中的一致性控制器窥探总线事务的一种方案(该方案由Ravishankar和Goodman于1983年提出)当数据被多个缓存共享时,一个处理器修改了共享数据的值,可以是刷新缓存块或使缓存块失效还可以通过缓存块状态的改变,来达到缓存一致性的目的,主要取决于窥探协议类型:
    1. 写失效(Write-invalidate) :当一个处理器写入共享缓存时,其他缓存中的所有共享副本都会通过总线窥探失效。确保处理器只能读写一个数据副本,其他缓存中的所有其他副本都无效。这是最常用的窥探协议。MSI、MESI、MOSI、MOESI和MESIF协议属于该类型。
    2. 写更新(Write-update):当处理器写入一个共享缓存块时,其他缓存的所有共享副本都会通过总线窥探更新。这个方法将写数据广播到总线上的所有缓存中。它比写失效协议引起更大的总线流量,因此这种方法不常见。Dragon和firefly协议属于此类别。

指令重排序

Java语言规范规定JVM线程内部维持顺序化语义,即只要程序的最终结果与它顺序化情况的结果相等,那么指令的执行顺序可以与代码顺序不一致,此过程叫指令的重排序。指令重排序的意义::JVM能根据处理器特性(CPU多级缓存系统、多核处理器等)适当的对机器指令进行重排序,使机器指令能更符合CPU的执行特性,最大限度的发挥机器性能。

Java线程的生命周期

  1. NEW(初始化状态)
  2. RUNNABLE(可运行状态+运行状态)
  3. BLOCKED(阻塞状态)
  4. WAITING(无时限等待)
  5. TIMED_WAITING(有时限等待)
  6. TERMINATED(终止状态)

在操作系统层面,有五种状态,Java 线程中的 BLOCKED、WAITING、TIMED_WAITING 是一种状态,即前面我们提到的休眠状态。也就是说只要 Java 线程处于这三种状态之一,那么这个线程就永远没有 CPU 的使用权。

linux查看线程命令

  • ps - fe 查看所有进程

  • ps - fT - p 查看某个进程( PID )的所有线程

  • kill 杀死进程

  • top 按大写 H 切换是否显示线程

  • top - H - p 查看某个进程( PID )的所有线程

  • jps 命令查看所有 Java 进程

  • jstack 查看某个 Java 进程( PID )的所有线程状态

  • jconsole 来查看某个 Java 进程中线程的运行情况(图形界面)

Java线程的实现方式

  1. 方式1:使用 Thread类或继承Thread类
  2. 实现 Runnable 接口配合Thread
  3. 使用有返回值的 Callable,借助线程池使用
  4. 使用 lambda

本质上Java中实现线程只有一种方式,都是通过new Thread()创建线程,调用Thread#start启动线程最终都会调用Thread#run方法

Thread常用方法

sleep方法

  • 调用 sleep 会让当前线程从 Running 进入TIMED_WAITING状态,不会释放对象锁
  • 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出InterruptedException,并且会清除中断标志
  • 睡眠结束后的线程未必会立刻得到执行
  • sleep当传入参数为0时,和yield相同

yield方法

  • yield会释放CPU资源,让当前线程从 Running 进入 Runnable状态,让优先级更高(至少是相同)的线程获得执行机会,不会释放对象锁;
  • 假设当前进程只有main线程,当调用yield之后,main线程会继续运行,因为没有比它优先级更高的线程;
  • 具体的实现依赖于操作系统的任务调度器

ThreadLocal的实现原理

ThreadLocal的实现原理,每一个Thread维护一个ThreadLocalMap,key为使用弱引用的ThreadLocal
实例,value为线程变量的副本

ThreadLocal应用场景

  1. 在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束。
  2. 线程间数据隔离
  3. 进行事务操作,用于存储线程事务信息。
  4. 数据库连接,Session会话管理。
    • Spring框架在事务开始时会给当前线程绑定一个Jdbc Connection,在整个事务过程都是使用该线程绑定的connection来执行数据库操作,实现了事务的隔离性。Spring框架里面就是用的ThreadLocal来实现这种隔离

ThreadLocal内存泄露原因,如何避免

  1. 强引用:使用最普遍的引用(new),一个对象具有强引用,不会被垃圾回收器回收。当内存空间不足,
    Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不回收这种对象。
  2. 弱引用:JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中,用
    java.lang.ref.WeakReference类来表示。可以在缓存中使用弱引用。
  3. ThreadLocal正确的使用方法
    • 每次使用完ThreadLocal都调用它的remove()方法清除数据
    • 将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就能保证任
      何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉 。

synchronized和ReentranLock区别?

synchronized是jvm层面的关键字,底层是通过monitor对象来完成,monitor对象底层依赖于操作系统的Mutex互斥量,底层调用的是操作系统的Pthread库,是由操作系统来维护的,而操作系统分为用户空间和内核空间的,JVM运行在用户空间,调用底层操作系统cpu会进行一轮状态切换,这个状态切换是比较重型操作,wait/notify等方法也依赖monitor对象只有在同步块或方法中才能调wait/notify等方法。

Lock是具体类(java.util.concurrent.Locks.Lock)是api层面的锁,是从jdk1.5开始引入的,那个时候synchronized性能还比较差,ReentranLock的出现是为了解决当时性能差的问题

  • 使用方法:synchronized不需要手动释放锁,ReentranLock则需要用户手动释放锁(需要lock()和unlock()配合try/finally使用)

  • 等待是否可中断:synchronized不可中断,除非抛出异常或者执行完成,ReentranLock可中断

  1. 设置超时方法tryLock(long timeout, timeUnit unit)
  2. lockInterruptibly()放代码块忠,调用interrupt()方法可中断
  • 加锁是否公平:synchronized非公平锁;ReentranLock 两者都可以,默认非公平锁,构造方法传入true为公平锁,false为非公平锁

  • 锁绑定多个条件Condition:synchronized 没有;ReentranLock 用来实现分组唤醒需要唤醒的线程,可以精确唤醒,不像synchronized要么随机唤醒一个线程,要么全部唤醒

线程池参数有,核心线程配置,原因?

  1. corePoolSize:线程池中的核心线程数
  2. maximumPoolSize:线程池中允许的最大线程数。如果当前阻塞队列满了,且继续提交任务,则创建新的线程执行任务
  3. keepAliveTime:核心线程外的线程存活超时时间
  4. unit:时间单位
  5. workQueue:用来保存等待被执行的任务的阻塞队列,且任务必须实现Runable接口
  6. threadFactory:用来创建新线程
  7. 线程池的饱和策略:( AbortPolicy:直接抛出异常,默认策略;CallerRunsPolicy:用调用者所在的线程来执行任务;DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;DiscardPolicy:直接丢弃任务)

CPU密集型(CPU-bound)

CPU密集型也叫计算密集型,在多重程序系统中,大部份时间用来做计算、逻辑判断等CPU动作的程序称之CPU bound。例如一个计算圆周率至小数点一千位以下的程序,在执行的过程当中绝大部份时间用在三角函数和开根号的计算,便是属于CPU bound的程序。

线程数 = CPU核数+1 (现代CPU支持超线程)

IO密集型(I/O bound)

IO密集型指的是系统的CPU性能相对硬盘、内存要好很多,此时,系统运作,大部分的状况是CPU在等I/O (硬盘/内存) 的读/写操作

线程数 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目

集合

Set,List,Map有什么区别

  • 结构特点
    1. List 和 Set 是存储单列数据的集合, Map 是存储键和值这样的双列数据的集合;
    2. List中存储的数据是有顺序,并且允许重复; Map 中存储的数据是没有顺序的,其键是不能重复的,它的值是可以有重复的, Set 中存储的数据是无序的,且不允许有重复,但元素在集合中的位置由元素的hashcode决定,位置是固定的(Set集合根据 hashcode 来进行数据的存储,所以位置是固定的,但是位置不是用户可以控制的,所以对于用户来说 set 中的元素还是无序的);

HashMap和HashTable有什么区别?

区别:

  1. HashMap方法没有synchronized修饰,线程非安全,HashTable线程安全;
  2. HashMap允许key和value为null,而HashTable不允许

HashMap底层实现

  1. jdk1.7底层采用的是数组+链表实现;jdk1.8底层采用数组+链表+红黑树实现,相对于1.7提升了查询的性能
  2. 链表过度到红黑树的阈值为8,红黑树退化为链表的阈值是6,原因在源码中332行给了明确解释(Poisson distribution)泊松分布,这是数学里面类似于正态分布的一个问题,这是统计学里面的一些东西
  3. HashMap初始化的数组长度为16(算法导论中的除法散列法讲到 K取模m得到一个对应的位置,根据具体位置插入一个数据值,假如长度是16,长度范围就是015,取模之后我的余数为015之间,而算法导论中指出,一个不太接近2的整数幂的素数(质数:只能被1和它本身整出的数),常常是m的最好选择。但是hashMap中未采用素数,而是采用2的n次方(合数)作为数组长度),原因为:1、更便于位置计算;2、方便做数据迁移。
  4. 为了保证存入的数据均匀的散列分布在数组中,需要设计良好的hash散列算法(hashMap中采用扰动函数来做的:移位和异或相关操作ConcurrentHashMap源码中684行spread方法,即:让int的高16位异或它本身),尽可能的避免hash冲突(概率问题),具体落到数组哪个节点并非是通过取模运算得出,而是通过与上数组长度-1即可(只需要int类型数32位的后四位参与运算),即可得到0~15之间的数,因为取模运算效率远低于位运算,所以这也是hashMap要求底层数组长度必须为2的n次方的原因之一(ConcurrentHashMap源码中514行规定)。

HashMap put流程

  1. HashMap/ConcurrentHashMap 并不是通过构造函数创建默认空间的,而是通过put数据的时候获取到对应的数据空间的,如果数组长度是0,则通过CAS操作后初始化数组,如果有线程正在做初始化数组操作,其他线程则让出时间片

HashMap扩容流程

  1. hashMap规定了当我的数组快放满的时候就要开始扩容了,什么时候算是快放满?HashMap是通过扩容因子来规定的
  2. HashMap规定扩容因子是0.75,如果默认长度是16,也就是说当HashMap底层数组的容量达到12的时候进行扩容操作。0.75则是根据统计学得来的。private static final float LOAD_FACTOR = 0.75f;
  3. 扩容首先要创建新的数组(原来大小左移1位)ConcurrentHaspMap源码2367行开始扩容操作
  4. 转移旧数据到新的数组中去(怎么计算扩容后数据下标位置?)原来:h&(n-1)计算下标位置,扩容后 h$(31),原来占用4个二进制位,扩容后占用5个二进制位,所以只需要看第五位即可,如果第五位是0,扩容后的的下标跟原位置一样,如果是1,新下标位置在原数组的位置的基础上加上原来数组长度即可,这也是数组长度采用2的N次方扩容的第二个原因
  5. 假设原来长度是128,扩容后是256,整体扩容方式是通过多线程方式运行的,但是要保证数据不能乱,将原来数组分段,规定每个线程最少负责16个桶的迁移工作,8个线程可以并行执行,如果小于16个桶,直接单线程执行

为什么选择用红黑树

二叉树在极端情况下会退化为链表,查询时间复杂度跟链表相似,而AVL树,SB树,红黑树,都属于平衡二叉树,尽量保持左子树和右子树的高度差不要相差太大,而这三种树的差别就在于左树和右树的高度规则不同。

  • AVL树:严格意义平衡树,的要求左子树和右子树的高度差不能超过1,损失了部分插入性能,带来了高效的查询
  • SB树:
  • 红黑树:要求最长子树不能超过最短子树的2倍即可,损失了部分查询性能,使得查询效率高于链表的同时,相比于其他树提升了插入性能,尽量做到了插入和查询的一个平衡点,而HashMap则是查多,插入少(这里的插入指的是产生Hash冲突下的插入),所以红黑树更适合HashMap来做底层的存储结构。

ConcurrentHaspMap

  1. ConcurrentHaspMap是线程安全的HashMap,它底层采用大量的CAS操作

spring

spring是一个框架,更像是一个生态环境,在我们的开发流程中,所有的框架基本上都依赖于spring,spring起到了一个IOC容器的作用,用来承载我们整体的bean对象,它帮我们处理了bean对象从创建到销毁的整个生命周期的管理,我们在使用spring的时候,可以使用配置文件,也可以使用注解的方式进行相关开发。spring框架的工作主要流程是,当我们程序开始启动之后,spring把注解或者配置文件定义好的bean对象转化成为BeanDefinitionran,然后通BeanFactoryPostProcessor完成整个的BeanDefinitionran的解析和加载过程,然后根据BeanDefinitionran通过反射的方式创建bean对象,然后进行对象初始化,包括:aware接口相关操作,BeanPostProcessor操作,Init-methord操作完成整个bean的创建过程,之后我们就可以使用了。

Bean的初始化过程

Bean的生命周期

循环依赖

AOP的顺序

spring4和spring5是不一样的

springMVC处理请求流程

spring事务的传播机制

  1. TransactionDefinition.PROPAGATION_REQUIRED:支持当前事务,如果没有事务会创建一个新的事务
  2. TransactionDefinition.PROPAGATION_SUPPORTS:支持当前事务,如果没有事务的话以非事务方式执行
  3. TransactionDefinition.PROPAGATION_MANDATORY:支持当前事务,如果没有事务抛出异常
  4. TransactionDefinition.PROPAGATION_REQUIRES_NEW:创建一个新的事务并挂起当前事务
  5. TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事务方式执行,如果当前存在事务则将当前事务挂起
  6. TransactionDefinition.PROPAGATION_NEVER:以非事务方式进行,如果存在事务则抛出异常
  7. TransactionDefinition.PROPAGATION_NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则进行与PROPAGATION_REQUIRED类似的操作。

RPC框架

你知道的json序列化方式?

  1. 谷歌的Gson
  2. json-smart:号称是速度最快的JSON解析器
  3. Common Lang3(3.1)的SerializationUtils
  4. 阿里巴巴的 FastJson、以及 Jackson

两个系统之间怎么交互的?

  1. 套接字(socket):这是一种更为一般得进程间通信机制,它可用于网络中不同机器之间的进程间通信,应用非常广泛。
  2. 可以通过RPC框架(dubbo、feign等)进行远程调用
  3. 也可以引入消息队列等消息中间件作为系统之间信息交互的桥梁
  4. 共享内存(shared memory):可以说这是最有用的进程间通信方式。它使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据得更

dubbo的序列化?

dubbo 支持 hession、Java 二进制序列化、json、SOAP 文本序列化多种序列化协议。 hessian 是其默认的序列化协议

网络

网络七层模型

  • 第一层:物理层
  • 第二层:数据链路层 802.2、802.3ATM、HDLC、FRAME RELAY
  • 第三层:网络层 IP、IPX、ARP、APPLETALK、ICMP
  • 第四层:传输层 TCP、UDP、SPX
  • 第五层:会话层 RPC、SQL、NFS 、X WINDOWS、ASP
  • 第六层:表示层 ASCLL、PICT、TIFF、JPEG、 MIDI、MPEG
  • 第七层:应用层 HTTP,FTP,SNMP等
  1. 物理层:物理层负责最后将信息编码成电流脉冲或其它信号用于网上传输;
  2. 数据链路层:数据链路层通过物理网络链路􏰁供数据传输。不同的数据链路层定义了不同的网络和协 议特征,其中包括物理编址、网络拓扑结构、错误校验、数据帧序列以及流控。可以简单的理解为:规定了0和1的分包形式,确定了网络数据包的形式;
  3. 网络层:网络层负责在源和终点之间建立连接。可以理解为,此处需要确定计算机的位置,怎么确定?IPv4,IPv6!
  4. 传输层:传输层向高层提供可靠的端到端的网络数据流服务。可以理解为:每一个应用程序都会在网卡注册一个端口号,该层就是端口与端口的通信!常用的(TCP/IP)协议;
  5. 会话层:会话层建立、管理和终止表示层与实体之间的通信会话;建立一个连接(自动的手机信息、自动的网络寻址);
  6. 表示层:表示层提供多种功能用于应用层数据编码和转化,以确保以一个系统应用层发送的信息 可以被另一个系统应用层识别;可以理解为:解决不同系统之间的通信,eg:Linux下的QQ和Windows下的QQ可以通信;
  7. 应用层:OSI 的应用层协议包括文件的传输、访问及管理协议(FTAM) ,以及文件虚拟终端协议(VIP)和公用管理系统信息(CMIP)等;

http1.0和http1.1区别

HTTP1.0最早在网页中使用是在1996年,那个时候只是使用一些较为简单的网页上和网络请求上,而HTTP1.1则在1999年才开始广泛应用于现在的各大浏览器网络请求中,同时HTTP1.1也是当前使用最为广泛的HTTP协议。 主要区别主要体现在:

  1. 缓存处理,在HTTP1.0中主要使用header里的If-Modified-Since,Expires来做为缓存判断的标准,HTTP1.1则引入了更多的缓存控制策略例如Entity tag,If-Unmodified-Since, If-Match, If-None-Match等更多可供选择的缓存头来控制缓存策略。
  2. 带宽优化及网络连接的使用,HTTP1.0中,存在一些浪费带宽的现象,例如客户端只是需要某个对象的一部分,而服务器却将整个对象送过来了,并且不支持断点续传功能,HTTP1.1则在请求头引入了range头域,它允许只请求资源的某个部分,即返回码是206(Partial Content),这样就方便了开发者自由的选择以便于充分利用带宽和连接。
  3. 错误通知的管理,在HTTP1.1中新增了24个错误状态响应码,如409(Conflict)表示请求的资源与资源的当前状态发生冲突;410(Gone)表示服务器上的某个资源被永久性的删除。
  4. Host头处理,在HTTP1.0中认为每台服务器都绑定一个唯一的IP地址,因此,请求消息中的URL并没有传递主机名(hostname)。但随着虚拟主机技术的发展,在一台物理服务器上可以存在多个虚拟主机(Multi-homed Web Servers),并且它们共享一个IP地址。HTTP1.1的请求消息和响应消息都应支持Host头域,且请求消息中如果没有Host头域会报告一个错误(400 Bad Request)。
  5. 长连接,HTTP 1.1支持长连接(PersistentConnection)和请求的流水线(Pipelining)处理,在一个TCP连接上可以传送多个HTTP请求和响应,减少了建立和关闭连接的消耗和延迟,在HTTP1.1中默认开启Connection: keep-alive,一定程度上弥补了HTTP1.0每次请求都要创建连接的缺点。

HTTPS与HTTP的一些区别

  • HTTPS协议需要到CA申请证书,一般免费证书很少,需要交费。
  • HTTP协议运行在TCP之上,所有传输的内容都是明文,HTTPS运行在SSL/TLS之上,SSL/TLS运行在TCP之上,所有传输的内容都经过加密的。
  • HTTP和HTTPS使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。
  • HTTPS可以有效的防止运营商劫持,解决了防劫持的一个大问题。

http三次握手四次挥手

其实这么说是不太准确的,应该说是tcp协议建立连接要3次握手,断开连接要4次挥手,而http是基于tcp协议的,所以通常我们也这么说,tcp可以提供全双工的数据流传输服务

三次握手

  1. TCP服务器进程先创建传输控制块TCB,时刻准备接受客户进程的连接请求,此时服务器就进入了LISTEN(监听)状态;
  2. TCP客户进程也是先创建传输控制块TCB,然后向服务器发出连接请求报文,这是报文首部中的同部位SYN=1,同时选择一个初始序列号 seq=x ,此时,TCP客户端进程进入了 SYN-SENT(同步已发送状态)状态。TCP规定,SYN报文段(SYN=1的报文段)不能携带数据,但需要消耗掉一个序号。
  3. TCP服务器收到请求报文后,如果同意连接,则发出确认报文。确认报文中应该 ACK=1,SYN=1,确认号是ack=x+1,同时也要为自己初始化一个序列号 seq=y,此时,TCP服务器进程进入了SYN-RCVD(同步收到)状态。这个报文也不能携带数据,但是同样要消耗一个序号。
  4. TCP客户进程收到确认后,还要向服务器给出确认。确认报文的ACK=1,ack=y+1,自己的序列号seq=x+1,此时,TCP连接建立,客户端进入ESTABLISHED(已建立连接)状态。TCP规定,ACK报文段可以携带数据,但是如果不携带数据则不消耗序号。
  5. 当服务器收到客户端的确认后也进入ESTABLISHED状态,此后双方就可以开始通信了。

四次挥手

Dubbo

  • 提供者(Provider)启动时,会向注册中心写入自己的元数据信息(调用方式)。
  • 消费者(Consumer)启动时,也会在注册中心写入自己的元数据信息,并且订阅服务提供者,路由和配置元数据的信息。
  • 服务治理中心(duubo-admin)启动时,会同时订阅所有消费者,提供者,路由和配置元数据的信息。
  • 当提供者离开或者新提供者加入时,注册中心发现变化会通知消费者和服务治理中心。

Netty

NIO三大核心组件

Channel(通道), Buffer(缓冲区),Selector(多路复用器)

  1. channel 类似于流,每个 channel 对应一个 buffer缓冲区,buffer 底层就是个数组
  2. channel 会注册到 selector 上,由 selector 根据 channel 读写事件的发生将其交由某个空闲的线程处理
  3. NIO 的 Buffer 和 channel 都是既可以读也可以写

Netty线程模型

Netty通过Reactor模型基于多路复用器接收并处理用户请求,内部实现boss和work两个线程池,其中boss的线程负责处理请求的accept事件,当接收到accept事件的请求时,把对应的socket封装到一个NioSocketChannel中,并交给work线程池,其中work线程池负责请求的read和write事件,交由对应的Handler处理。

  1. Netty 抽象出两组线程池BossGroup和WorkerGroup,BossGroup专门负责接收客户端的连接, WorkerGroup专门负责网络的读写
  2. BossGroup和WorkerGroup类型都是NioEventLoopGroup
  3. NioEventLoopGroup 相当于一个事件循环线程组, 这个组中含有多个事件循环线程 , 每一个事件循环线程是NioEventLoop
  4. 每个NioEventLoop都有一个selector , 用于监听注册在其上的socketChannel的网络通讯
  5. 每个BossNioEventLoop线程内部循环执行的步骤有 3 步
  6. 处理accept事件 , 与client 建立连接 , 生成 NioSocketChannel
  7. 将NioSocketChannel注册到某个worker NIOEventLoop上的selector
  8. 处理任务队列的任务 , 即runAllTasks
  9. 每个workerNIOEventLoop线程循环执行的步骤
    1. 轮询注册到自己selector上的所有NioSocketChannel 的read, write事件
    2. 处理 I/O 事件, 即read , write 事件, 在对应NioSocketChannel 处理业务
    3. runAllTasks处理任务队列TaskQueue的任务 ,一些耗时的业务处理一般可以放入TaskQueue中慢慢处理,这样不影响数据在 pipeline 中的流动处理

Netty模块组件

  • Bootstrap、ServerBootstrap:Bootstrap 意思是引导,一个 Netty 应用通常由一个 Bootstrap 开始,主要作用是配置整个 Netty 程序,串联各个组件,Netty 中 Bootstrap 类是客户端程序的启动引导类,ServerBootstrap 是服务端启动引导类。

  • Future、ChannelFuture:正如前面介绍,在 Netty 中所有的 IO 操作都是异步的,不能立刻得知消息是否被正确处理。但是可以过一会等它执行完成或者直接注册一个监听,具体的实现就是通过 Future 和 ChannelFutures,他们可以注册一个监听,当操作执行成功或失败时监听会自动触发注册的监听事件。

  • Channel:Netty 网络通信的组件,能够用于执行网络 I/O 操作。Channel 为用户提供:

    1. 当前网络连接的通道的状态(例如是否打开?是否已连接?)

    2. 网络连接的配置参数 (例如接收缓冲区大小)

    3. 提供异步的网络 I/O 操作(如建立连接,读写,绑定端口),异步调用意味着任何 I/O 调用都将立即返回,并且不保证在调用结束时所请求的 I/O 操作已完成。

    4. 调用立即返回一个 ChannelFuture 实例,通过注册监听器到 ChannelFuture 上,可以 I/O 操作成功、失败或取消时回调通知调用方。

    5. 支持关联 I/O 操作与对应的处理程序。

    6. 不同协议、不同的阻塞类型的连接都有不同的 Channel 类型与之对应。

    7. NioSocketChannel,异步的客户端 TCP Socket 连接。
      NioServerSocketChannel,异步的服务器端 TCP Socket 连接。
      NioDatagramChannel,异步的 UDP 连接。
      NioSctpChannel,异步的客户端 Sctp 连接。
      NioSctpServerChannel,异步的 Sctp 服务器端连接。
      这些通道涵盖了 UDP 和 TCP 网络 IO 以及文件 IO。
      
  • Selector:Netty 基于 Selector 对象实现 I/O 多路复用,通过 Selector 一个线程可以监听多个连接的 Channel 事件。当向一个 Selector 中注册 Channel 后,Selector 内部的机制就可以自动不断地查询(Select) 这些注册的 Channel 是否有已就绪的 I/O 事件(例如可读,可写,网络连接完成等),这样程序就可以很简单地使用一个线程高效地管理多个 Channel 。

  • NioEventLoop:NioEventLoop 中维护了一个线程和任务队列,支持异步提交执行任务,线程启动时会调用 NioEventLoop 的 run 方法,执行 I/O 任务和非 I/O 任务:I/O 任务,即 selectionKey 中 ready 的事件,如 accept、connect、read、write 等,由 processSelectedKeys 方法触发。非 IO 任务,添加到 taskQueue 中的任务,如 register0、bind0 等任务,由 runAllTasks 方法触发。

  • NioEventLoopGroup:NioEventLoopGroup,主要管理 eventLoop 的生命周期,可以理解为一个线程池,内部维护了一组线程,每个线程(NioEventLoop)负责处理多个 Channel 上的事件,而一个 Channel 只对应于一个线程。

  • ChannelHandler:ChannelHandler 是一个接口,处理 I/O 事件或拦截 I/O 操作,并将其转发到其 ChannelPipeline(业务处理链)中的下一个处理程序。

  • ChannelHandlerContext:保存 Channel 相关的所有上下文信息,同时关联一个 ChannelHandler 对象。

  • ChannelPipline:保存 ChannelHandler 的 List,用于处理或拦截 Channel 的入站事件和出站操作。ChannelPipeline 实现了一种高级形式的拦截过滤器模式,使用户可以完全控制事件的处理方式,以及 Channel 中各个的 ChannelHandler 如何相互交互。在 Netty 中每个 Channel 都有且仅有一个 ChannelPipeline 与之对应,它们的组成关系如下:

一个 Channel 包含了一个 ChannelPipeline,而 ChannelPipeline 中又维护了一个由 ChannelHandlerContext 组成的双向链表,并且每个 ChannelHandlerContext 中又关联着一个 ChannelHandler。read事件(入站事件)和write事件(出站事件)在一个双向链表中,入站事件会从链表 head 往后传递到最后一个入站的 handler,出站事件会从链表 tail 往前传递到最前一个出站的 handler,两种类型的 handler 互不干扰。

Netty粘包拆包

TCP是一个流协议,就是没有界限的一长串二进制数据。TCP作为传输层协议并不不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行数据包的划分,所以在业务上认为是一个完整的包,可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的TCP粘包和拆包问题。面向流的通信是无消息保护边界的。

解决方案

  1. 消息定长度,传输的数据大小固定长度,例如每段的长度固定为100字节,如果不够空位补空格

  2. 在数据包尾部添加特殊分隔符,比如下划线,中划线等,这种方法简单易行,但选择分隔符的时候一定要注意每条数据的内部一定不能出现分隔符。

  3. 发送长度:发送每条数据的时候,将数据的长度一并发送,比如可以选择每条数据的前4位是数据的长度,应用层处理时可以根据长度来判断每条数据的开始和结束。

    Netty提供了多个解码器,可以进行分包的操作,如下:

    1. LineBasedFrameDecoder (回车换行分包)
    2. DelimiterBasedFrameDecoder(特殊分隔符分包)
    3. FixedLengthFrameDecoder(固定长度报文来分包)

数据库

ACID理论

​ 事务处理几乎是每一个信息系统中都会涉及到的问题,它存在的意义就是保证系统中的数据是正确的,不同数据间不会产生矛盾,也就是保证数据状态的一致性(Consistency),理论上,要达成这个目标需要三方面的共同努力:

  1. 原子性(Atomic):在同一项业务处理过程中,事务保证了多个对数据的修改,要么同时成功,要么一起被撤销。原子性是由undolog日志来保证的,它记录了需要回滚的日志信息,事务回滚时,撤销已经执行成功的sql。
  2. 隔离性(Isolation):在不同的业务处理过程中,事务保证了各自业务正在读、写的数据互相独立,不会彼此影响。隔离性是由MVCC来保证的。
  3. 持久性(Durability):事务应当保证所有被成功提交的数据修改都能够正确地被持久化,不丢失数据。持久性由redolog来保证的,mysql修改数据的时候会在redolog中记录一份日志数据,就算数据没有保存成功,只要日志保存成功了,数据仍然不会丢失。

以上就是事务的“ACID”的概念提法。我自己对这种已经形成习惯的“ACID”的提法是不太认同的,因为这四种特性并不正交,A、I、D 是手段,C 是目的,完全是为了拼凑个单词缩写才弄到一块去,误导的弊端已经超过了易于传播的好处。

Mysql索引结构

mysql的索引结构有:二叉树、红黑树、hash表、BTree、B+Tree

  • 二叉树
  • Hash表
  • B+Tree

InnoDB和MyISAM区别

区别 InnoDB MyISAM
事务 支持 不支持
外键 支持 不支持
索引 聚簇索引和非聚簇索引 非聚簇索引
行锁 支持 不支持
表锁 支持 支持
存储文件 frm(表结构),ibd(数据和索引) frm,myi(索引文件),myd(数据文件)
具体行数 全表扫描统计行数 通过变量保存行数
  • MyISAM不支持事务,在执行查询语句SELECT前,会自动给涉及的所有表加读锁,在执行update、insert、delete操作会自动给涉及的表加写锁。
  • InnoDB在执行查询语句SELECT时(非串行隔离级别),(因为有mvcc机制)不会加锁。但是update、insert、delete操作会加行锁。简而言之,就是读锁会阻塞写,但是不会阻塞读。而写锁则会把读和写都阻塞

B树和B+数的区别

B-Tree

B+Tree(B-Tree变种)

  1. B+Tree非叶子节点不存储data,只存储冗余索引,叶子节点包含所有索引字段 。优点:可以放更多的索引,BTree非叶子节点会存储索引和数据
  2. B+Tree叶子节点用指针连接。优点:提高区间访问的性能(范围查找),BTree叶子节点指针为空

事务隔离级别

事务隔离级别 脏读 不可重复读 幻读
读未提交(read-uncomm
读已提交(read-committed)
可重复读(repeatable-read) mysql默认
串行化(serializable)
  • Mysql在可重复度隔离级别下通过MVCC保证事务隔离性
  • 幻读是指当一个事务A读取某一个范围的数据时,另一个事务B在这个范围插入行,A事务再次读取这个范围数据时,会产生幻读

Mysql幻读是怎么解决的

首先要确认一下幻读是怎么产生的,先弄清两个概念,那就是当前读和快照读

  • 当前读

像select lock in share mode(共享锁),select for update,update,insert,delete(排他锁)这些操作都是一种当前读,为什么叫当前读?就是他读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取记录进行加锁

  • 快照读

​ 像不加锁的select操作就是快照读,也就是不加锁的非阻塞读;快照读的前提是隔离级别不是串行化,串行级别的快照读会退化成当前读,快照读的实现是基于多版本并发控制,也就是MVCC,可以认为MVCC是行锁的一个变种,但是很多情况下避免加锁操作,快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本

如果只有快照读是不会产生幻读问题,只有快照读和当前读一起使用的时候才会产生幻读。

Mysql在可重复度隔离级别下可以通过MVCC和临键锁(记录锁+间隙锁)解决幻读问题。

MVCC

对于mvcc的理解,可以从数据库三种并发场景来说

  1. 第一种是读和读的并发:就是两个线程A和B同时进行读操作,这种情况下呢,不会产生任何的并发问题
  2. 第二种是读写并发:就是说两个线程A和B在同一时刻分别进行读写操作,这种情况下可能会对数据库的数据造成一些问题,第一、事务隔离性问题;第二、会造成脏读,幻读,不可重复读的问题
  3. 第三种是写和写的并发:就是两个线程A和B同时进行写操作,这种情况下可能会存在数据更新的丢失问题
  4. MVCC就是为了解决事务操作中并发安全问题的,无锁并发控制技术,全称就是:多版本并发控制,他是通过数据库记录中的隐式字段Undo日志和ReadView来实现的,MVCC主要解决三个问题:第一、通过MVCC可以解决读写并发阻塞问题,从而提高数据库的并发处理能力;第二、MVCC采用的是乐观锁的方式实现,降低了死锁的概率;第三、解决了一致性读的问题,也就是事务启动的时候,根据某个条件去读取到的数据,知道事务结束的时候再去执行相同条件,还是读到同一份数据,不会发生变化变化,而我们在使用MVCC的时候,一般是根据业务场景来选择组合搭配,乐观锁或者悲观锁,MVCC用来解决读写冲突,而乐观锁悲观锁用来解决写和写的冲突,从而最大程度去提高数据库的并发性能。

什么场景会引发幻读

幻读是指在同一个事务中,存在前后两次查询同一个范围的数据,但是第二次查询却看到了第一次查询没看到的行,一般情况下特指事务执行中新增的其他行。

sql在mysql的执行过程

事务怎么保证一致性

redo log是InnoDB存储引擎层的日志,又称重做日志文件,用于记录事务操作的变化,记录的是数据修改之后的值,不管事务是否提交都会记录下来。如数据库掉电,InnoDB存储引擎会使用redo log恢复到掉电前的时刻,以此来保证数据的完整性。在一条更新语句进行执行的时候,InnoDB引擎会把更新记录写到redo log日志中,然后更新内存,此时算是语句执行完了,然后在空闲的时候或者是按照设定的更新策略将redo log中的内容更新到磁盘中,这里涉及到WALWrite Ahead logging技术,他的关键点是先写日志,再写磁盘。

binlog 、undo 、redo

  1. redo log通常是物理日志,记录的是数据页的物理修改,而不是某一行或某几行修改成怎样怎样,它用来恢复提交后的物理数据页(恢复数据页,且只能恢复到最后一次提交的位置)。
  2. undo用来回滚行记录到某个版本。undo log一般是逻辑日志,根据每行记录进行记录。
  3. 二进制日志(binlog)

聚簇和非聚簇索引

  • 聚簇索引也好、非聚簇索引也好,都是索引的一个基本分类,他们最本质的点在于存储引擎,如果我们用InnoDB存储引擎,他的存储文件是.frm和.ibd文件,这意味着InnoDB里面存放数据文件和索引文件是在同一个文件.ibd文件里,所以他的数据和索引是放在一起存储的,这种存储方式称之为聚簇索引。InnerDB在进行数据插入的时候,必须要绑定一个索引列上,默认是主键,如果没有主键,会选择唯一键,如果没有唯一键,会生成6字节的rowid,跟数据绑定在一起

  • 像MyISAM存储引擎,他存储文件是.frm,.myi(索引文件),.myd(数据文件)文件,他是把索引文件和数据文件分开存储的,这种存储方式称为非聚簇索引,InnoDB既有聚簇索引也有非聚簇索引,MyISAM只有非聚簇索引

数据库慢sql优化

  • 尽量创建联合索引,

  • 多表关联查询:所有的join查询,都是通过嵌套循环连接完成的,嵌套循环join有三个变种:

    1. Simple Nested-Loop Join :从表中取出匹配所有列,匹配后合并,开销大。select * from t1,t2(笛卡尔积)
    2. Index Nested-Loop Join : 索引嵌套连接,由于非驱动表有索引,通过索引减少比较,加速查询,我们再做关联查询的时候必须要求关联字段有索引;查询过程:1、根据关联字段索引进行查找,在索引上找到符合的值后再回表查询,只有匹配到索引后才会回表,至于驱动表选择,Mysql优化器一般会选择记录少的作为驱动表,但是当SQL特别复杂的时候不排除会选择错。2、如果非驱动表关联主键,性能会非常高,如果不是主键,关联后返回行数特别多的话,效率也会很低,要多次回表操作。
    3. Block Nested-Loop Join:当连接条件没有索引的时候会用这种方式关联,比Simple Nested-Loop Join 多了一个中间处理过程,有些情况下,可能join的列就是没有索引,那么MySQL会选择Block Nested-Loop Join算法,其实就是使用Join buffer将驱动表的查询Join相关列都缓存到Join buffer中,然后批量与非驱动表进行比较,降低了非驱动表的访问频次。查看join buffer:show variables like 'join_buffer_size;'

索引失效的情况

谈到索引主要从两方面来分析:IO 和数据结构:不管索引数据还是行数据,都是存在磁盘里面的,我们尽可能少的取出数据

  • IO—–>读取次数少、量少——>分块读取——>局部性原理、磁盘预读
  • 数据结构——>B+树——>二叉树、AVL树、红黑树、B树
  1. 组合索引不遵循最左匹配原则
  2. 组合索引前面索引列使用范围查询(<,>,like),会导致后续索引失效;
  3. 不要在索引上做任何操作(计算,函数,类型转换)
  4. is null和is not null 无法使用索引
  5. 尽量少使用or操作符,否则连接时索引会失效
  6. 字符串不添加引号会导致索引失效(隐式类型转换)
  7. 两表关联使用的条件字段中字段的长度,编码不一致会导致索引失效
  8. like语句中,以%开头的模糊查询会导致索引失效
  9. 如果mysql中使用全表扫描比索引快,也会导致索引失效 (force index:强制使用索引)

如何做分库分表

​ 使用mycat或者shardingsphere中间件做分库分表,选择合适的中间件,水平分库,水平分表,垂直分库,垂直分表,在进行分库分表的时候尽量遵循以下原则

  1. 能不切分尽量不要切分
  2. 如果要切分一定要选择合适的切分规则,提前规划好
  3. 如果切分尽量通过数据冗余或表分组来降低跨库Join的可能
  4. 由于数据库中间件对数据Join实现的优劣难以把握,而且实现高性能难度极大,业务尽量少使用多表Join

Mysql主从复制

  1. 从库通过手工执行change master to 语句连接主库,提供连接用户信息(userName、passWord、port、ip)二进制日志的起点位置(file名 position号);start slave
  2. 从库的IO线程和主库的dump线程建立连接
  3. 从库根据change master to 语句提供的file名和position号,IO线程向主库发起binlog请求
  4. 主库dump线程根据从库请求,将本地binlog以events的方式发给从库IO线程
  5. 从库IO线程接受binlog events,并放到本地relay-log中(顺序IO),传送过来的信息会记录到master.info中
  6. 从库SQL线程应用relay-log,并且把应用过的记录到relay-log.info中,默认情况下,已经应用过的relay会自动被清理purge

缓存:Redis/ES

Redis数据类型

  1. String

  2. Hash

  3. List:类似于数组

  4. Set:无序集合 用户列表,求集合的交集,并集等操作

  5. ZSet:有序集合

    • 1)点击新闻:ZINCRBY hotNews:20190819 1 守护香港

    • 2)展示当日排行前十:ZREVRANGE hotNews:20190819 0 9 WITHSCORES

    • 3)七日搜索榜单计算:ZUNIONSTORE hotNews:20190813-20190819 7 hotNews:20190813 hotNews:20190814… hotNews:20190819

    • 4)展示七日排行前十:ZREVRANGE hotNews:20190813-20190819 0 9 WITHSCORES

Redis单线程模型

Redis基于Reactor模式开发的网络事件处理器,这个文件事件处理器是单线程的,采用IO多路复用机制监听多个Socker,根据Socker上的事件类型选择对应的事件处理器处理这个事件,可以实现高性能的网络通信。文件事件处理器包含4个部分,多个Socker,IO多路复用程序,文件事件分派器和事件处理器(命令请求处理器,命令回复处理器,链接应答处理器),多个Socket可能并发产生不同操作,每个操作对应不同的文件事件,但是IO多路复用会监听多个Socket,会将Socket放入一个队列,每次从队列中取出一个Socket给时间分派器,时间分派器把Socket给对应的事件处理器。

单线程快的原因:1.纯内存操作;2.核心是基于非阻塞的IO多路复用机制;3.单线程反而避免多线程的频繁上下文切换

分布式锁怎么实现的?

  1. 基于数据库实现分布式锁:唯一索引,状态机唯一联合索引
  2. 基于缓存(Redis等)实现分布式锁:SET key value NX PX 30000
  3. 基于Zookeeper实现分布式锁;

setnx用到的参数

SET key value NX PX 30000

第三个参数:把key、value set到redis中的策略

  • nx : not exists, 只有key 不存在时才把key value set 到redis
  • xx : is exists ,只有 key 存在是,才把key value set 到redis

第四个参数:过期时间单位

  • ex :seconds 秒
  • px : milliseconds 毫秒

使用其他值,抛出 异常 : redis.clients.jedis.exceptions.JedisDataException : ERR syntax error

第五个参数:有两种可选的值,

int 和long 的time,都是过期时间 ,expx 参数是px的时候,使用long类型的参数,可以表示更多时间

缓存穿透,击穿,雪崩

  • 缓存雪崩:缓存同一时间大面积失效,后面请求都落到数据库上,造成大量请求直接打到数据库。
    1. 过期时间随机,防止同一时间大量数据过期
    2. 缓存预热:项目启动加载缓存到redis
    3. 互斥锁:修改的时候只允许一个线程修改数据库,其他读写线程等待
    4. 加相应缓存标记,记录缓存是否失效,如果失效,更新缓存(内存消耗大,一般不用)
  • 缓存穿透:指数据库没有数据,导致请求落到数据库上
    1. 接口层增加校验,对id进行规则拦截
    2. 缓存取不到数据,设置null值到缓存,设置短时间超时,防止网络攻击
    3. 布隆过滤器,所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据就会被这个bitmap拦截
  • 缓存击穿:缓存没有,数据库中有数据(一般是缓存时间到期),并发用户特别多,同时大量请求落到数据库,缓存击穿指并发查询同一条数据,缓存雪崩是不同数据都过期
    1. key不过期
    2. 加互斥锁

Redis过期键删除策略

Redis对于过期键有三种清除策略:

  1. 被动删除:当读/写一个已经过期的key时,会触发惰性删除策略,直接删除掉这个过期key
  2. 主动删除:由于惰性删除策略无法保证冷数据被及时删掉,所以Redis会定期主动淘汰一批已过期的key
  3. 当前已用内存超过maxmemory限定时,触发主动清理策略

主动清理策略在Redis 4.0 之前一共实现了 6 种内存淘汰策略,在 4.0 之后,又增加了 2 种策略,总共8种:

a) 针对设置了过期时间的key做处理:

  1. volatile-ttl:在筛选时,会针对设置了过期时间的键值对,根据过期时间的先后进行删除,越早过期的越先被删除。
  2. volatile-random:就像它的名称一样,在设置了过期时间的键值对中,进行随机删除。
  3. volatile-lru:会使用 LRU 算法筛选设置了过期时间的键值对删除。
  4. volatile-lfu:会使用 LFU 算法筛选设置了过期时间的键值对删除。

b) 针对所有的key做处理:

  1. allkeys-random:从所有键值对中随机选择并删除数据。
  2. allkeys-lru:使用 LRU 算法在所有数据中进行筛选删除。
  3. allkeys-lfu:使用 LFU 算法在所有数据中进行筛选删除。

c) 不处理:

  1. noeviction:不会剔除任何数据,拒绝所有写入操作并返回客户端错误信息”(error) OOM command not allowed when used memory”,此时Redis只响应读操作。

LRU 算法(Least Recently Used,最近最少使用)

淘汰很久没被访问过的数据,以最近一次访问时间作为参考。

LFU 算法(Least Frequently Used,最不经常使用)

  1. 淘汰最近一段时间被访问次数最少的数据,以次数作为参考。
  2. 当存在热点数据时,LRU的效率很好,但偶发性的、周期性的批量操作会导致LRU命中率急剧下降,缓存污染情况比较严重。这时使用LFU可能更好点。根据自身业务类型,配置好maxmemory-policy(默认是noeviction),推荐使用volatile-lru。如果不设置最大内存,当 Redis 内存超出物理内存限制时,内存的数据会开始和磁盘产生频繁的交换 (swap),会让 Redis 的性能急剧下降。当Redis运行在主从模式时,只有主结点才会执行过期删除策略,然后把删除操作”del key”同步到从结点删除数据。

Redis持久化

  1. RDB快照(snapshot)
    1. Redis 将内存数据快照保存在名字为 dump.rdb 的二进制文件中。可以对 Redis 进行设置, 让它在“ N 秒内数据集至少有 M 个改动”这一条件被满足时, 自动保存一次数据;**# save 60 1000 //** (60秒改1000次进行一次RDB持久化)关闭RDB只需要将所有的save保存策略注释掉即可。
    2. 问题:会阻塞客户端命令。
    3. 还可以手动执行命令生成RDB快照,进入redis客户端执行命令savebgsave可以生成dump.rdb文件,每次命令执行都会将所有redis内存快照到一个新的rdb文件里,并覆盖原有rdb快照文件。
    4. bgsave的写时复制(COW)机制:Redis 借助操作系统提供的写时复制技术(Copy-On-Write, COW),在生成快照的同时,依然可以正常处理写命令。bgsave 子进程是由主线程 fork 生成的,可以共享主线程的所有内存数据。bgsave 子进程运行后,开始读取主线程的内存数据,并把它们写入 RDB 文件。此时,如果主线程对这些数据也都是读操作,那么,主线程和 bgsave 子进程相互不影响。但是,如果主线程要修改一块数据,那么,这块数据就会被复制一份,生成该数据的副本。然后,bgsave 子进程会把这个副本数据写入 RDB 文件,在这个过程中,主线程仍然可以修改原来的数据。
  2. AOF(append-only file)
    1. 如果 Redis 因为某些原因而造成故障停机, 那么服务器将丢失最近写入、且仍未保存到快照中的那些数据。从 1.1 版本开始, Redis 增加了一种完全耐久的持久化方式: AOF 持久化,将修改的每一条指令记录进文件appendonly.aof中(先写入os cache,每隔一段时间fsync到磁盘)

缓存与数据库数据一致性

解决方案:

  1. 对于并发几率很小的数据(如个人维度的订单数据、用户数据等),这种几乎不用考虑这个问题,很少会发生缓存不一致,可以给缓存数据加上过期时间,每隔一段时间触发读的主动更新即可。
  2. 就算并发很高,如果业务上能容忍短时间的缓存数据不一致(如商品名称,商品分类菜单等),缓存加上过期时间依然可以解决大部分业务对于缓存的要求。
  3. 如果不能容忍缓存数据不一致,可以通过加读写锁保证并发读写或写写的时候按顺序排好队,读读的时候相当于无锁
  4. 也可以用阿里开源的canal通过监听数据库的binlog日志及时的去修改缓存,但是引入了新的中间件,增加了系统的复杂度。

ES简介

ES他是建立在全文搜索引擎库ApacheLucene基础上的一个开源搜索和分析引擎,ES本身具有一个分布式存储,检索速度快的特性,所以我们经常用它去实现全文检索这一类的场景,比如像网站搜索,公司内部用ELK做日志聚集和检索,来去快速查找服务器的日志记录,去定位问题,基本上涉及到TB级别的数据场景,用ES是一个比较好的选择

ES查询快的原因

  1. 第一、Lucene是擅长管理大量的索引数据的,另一方面他会对数据进行分词以后在保存索引,这样能搞提升数据的检索效率
  2. 第二、ES采用倒排索引,所谓倒排索引,就是通过属性值来确定数据记录的位置的索引,来避免全表扫描这样一个问题,
  3. 第三、ES采用分片存储机制
  4. 第四、ES扩展性好,我们可以水平扩展增加服务器,提升ES处理性能,可以达到百台服务器节点扩展
  5. 第五、ES内部提供了数据汇总和索引生命周期管理的一些功能,可以方便我们更加高效的存储和搜索数据

使用ES注意事项

  1. ES里面不建议使用复杂的关联查询,会对性能影响很大
  2. 避免深度分页查询,因为ES分页是支持front和size参数,在查询的时候,每个分片必须构造一个长度为front加size的优先队列,然后回传到网关节点,网关节点再对这些队列进行排序再找到正确的size文档。而当front足够大的时候容易造成OOM以及网络传输性能差的一些问题

中间件

消息队列

Rabbitmq生产高可用怎么实现的

  1. 生产部署为集群模式:避免单机模式下MQ服务器挂了导致服务不可用,还可以承载更高的并发量,但是集群模式有个问题,就是在哪个节点上创建队列,该队列只会存在该节点上,其他集群节点不会备份该队列,一旦该节点宕机,该队列中的消息就会丢失(亲测设置持久化也丢失)
  2. 使用镜像队列:可以解决集群模式下,每个机器上只有一个队列的问题,让消息在其他节点再备份一份。开启方式:任何节点添加policy策略即可,该集群就具有镜像队列能力,可设置备份的份数和备份队列规则,即使其中一个节点宕机,也会保证整个集群中保存2份
  3. 高可用负载均衡:haproxy+keepalive,nginx,lvs等实现高可用负载均衡
  4. 其他:Federation Exchange联邦交换机插件解决两地接受消息确认消息延迟问题,也可以用Shovel来做数据同步,需要安装插件

zookeeper

zookeeper的理解

  1. 集群管理:提供了CP模型来保证集群中每个节点的数据一致性
  2. 分布式锁
  3. 集群选举

zookeeper选举机制

  • LeaderElection
  • AuthFastLeaderElection
  • FastLeaderElection (最新默认)

目前有5台服务器,每台服务器均没有数据,它们的编号分别是1,2,3,4,5,按编号依次启动,它们的选择举过程如下:

  • 服务器1启动,给自己投票,然后发投票信息,由于其它机器还没有启动所以它收不到反馈信息,服务器1的状态一直属于Looking(选举状态)。
  • 服务器2启动,给自己投票,同时与之前启动的服务器1交换结果,由于服务器2的编号大所以服务器2胜出,但此时投票数没有大于半数,所以两个服务器的状态依然是LOOKING。
  • 服务器3启动,给自己投票,同时与之前启动的服务器1,2交换信息,由于服务器3的编号最大所以服务器3胜出,此时投票数正好大于半数,所以服务器3成为领导者,服务器1,2成为小弟。
  • 服务器4启动,给自己投票,同时与之前启动的服务器1,2,3交换信息,尽管服务器4的编号大,但之前服务器3已经胜出,所以服务器4只能成为小弟。
  • 服务器5启动,后面的逻辑同服务器4成为小弟。

微服务

全局序号生成规则?

  1. UUID:没顺序,长度过长,作为主键索引效率低
  2. 数据库自增id:实现简单,保证唯一递增,扩展性差,有单点故障风险
  3. redis生成id
  4. 雪花算法
  5. 通过一个序列表记录当前序列号,机器每次从序列表中获取一定步长的序列数然后缓存再本地,等用完后再重新从步长表获取

理论

CAP理论

​ CAP 理论又叫 Brewer 理论,这是加州大学伯克利分校的埃里克 · 布鲁尔(Eric Brewer)教授,在 2000 年 7 月“ACM 分布式计算原理研讨会(PODC)”上提出的一个猜想。CAP理论原稿(那时候还只是猜想)然后到了 2002 年,麻省理工学院的赛斯 · 吉尔伯特(Seth Gilbert)和南希 · 林奇(Nancy Lynch)就以严谨的数学推理证明了这个 CAP 猜想。在这之后,CAP 理论就正式成为了分布式计算领域公认的著名定理。这个定理里,描述了一个分布式的系统中,当涉及到共享数据问题时,以下三个特性最多只能满足其中两个:

  1. 一致性(Consistency):代表在任何时刻、任何分布式节点中,我们所看到的数据都是没有矛盾的。这与 ACID 中的 C 是相同的单词,但它们又有不同的定义(分别指 Replication 的一致性和数据库状态的一致性)。在分布式事务中,ACID 的 C 要以满足 CAP 中的 C 为前提。
  2. 可用性(Availability):代表系统不间断地提供服务的能力。
  3. 分区容忍性(Partition Tolerance):代表分布式环境中,当部分节点因网络原因而彼此失联(即与其他节点形成“网络分区”)时,系统仍能正确地提供服务的能力。

DDD

ddd不是一种架构风格,而是一种方法论,什么是方法论,每个人按照自己的想法来设计就是一套方法论;ddd是一种业务比较认可,对于微服务拆分的一种方法论。

项目

  1. 最近做的比较熟悉的项目是哪个?画一下项目技术架构图
  2. 写两个类,能够实现堆内存溢出和栈内存溢出
  3. 写一个线程安全的单例。
  4. 两个可变有序链表放到新数组中,有序

算法