2020下半年Java面试题汇总

小声BB:此贴大部分内容都是抄的(引用),转至各大博主以及大牛,面试题都是本人实际面试过程中被问到的题目(基础较多,人菜勿喷T. T),当然其中也有自己的一些理解,大家多指教纠正!猛男飞吻 MUA~~~ ~(等我牛X了, 一定回来把这句话删掉,全是我的!咩哈哈 --更新中…)

一. Java基础

1. Spring

1.1 IOC和AOP

首先声明:IOC & AOP 不是 Spring 提出来的,它们在 Spring 之前其实已经存在了,只不过当时更加偏向于理论。Spring 在技术层次将这两个思想进行了很好的实现。

01: IOC
IoC (Inversion of control )控制反转/反转控制。它是一种思想不是一个技术实现。描述解决的是:Java 开发领域对象的创建以及管理的问题。
例如:现有类 A 依赖于类 B

  • 传统的开发方式 :往往是在类 A 中手动通过 new 关键字来 new 一个 B 的对象出来

  • 使用 IoC 思想的开发方式 :不通过 new 关键字来创建对象,而是通过 IoC 容器(Spring 框架) 来帮助我们实例化对象。我们需要哪个对象,直接从 IoC 容器里面获取即可。

从以上两种开发方式的对比来看:我们 “丧失了一个权力” (创建、管理对象的权力),从而也得到了一个好处(不用再考虑对象的创建、管理等一系列的事情)

为什么叫控制反转
控制 :指的是对象创建(实例化、管理)的权力

反转 :控制权交给外部环境(Spring 框架、IoC 容器)
IOC
:IoC 解决了什么问题

IoC 的思想就是两方之间不互相依赖(解耦),由第三方容器来管理相关资源。这样有什么好处呢?

  • 对象之间的耦合度或者说依赖程度降低;
  • 资源变的容易管理;比如你用 Spring 容器提供的话很容易就可以实现一个单例。

:IOC容器的初始化过程

*资源定位=====>BeanDefinition的载入=====>XML解析文件=====>注册BeanDefinition*
第一个过程是Resource资源定位。这个Resouce指的是BeanDefinition的资源定位。这个过程就是容器找数据的过程,就像水桶装水需要先找到水一样。(找到applicationContetx.xml等配置文件)
第二个过程是BeanDefinition的载入过程。这个载入过程是把用户定义好的Bean表示成Ioc容器内部的数据结构,而这个容器内部的数据结构就是BeanDefition。

  • 构造一个BeanFactory,也就是IOC容器
  • 调用XML解析器得到document对象
  • 按照Spring的规则解析BeanDefition

第三个过程是向IOC容器注册这些BeanDefinition的过程,这个过程就是将前面的BeanDefition保存到HashMap中的过程。

上面提到的过程一般是不包括Bean的依赖注入的实现。在Spring中,Bean的载入和依赖注入是两个独立的过程。依赖注入一般发生在应用第一次通过getBean向容器索取Bean的时候。下面的一张图描述了这三个过程调用的主要方法,图中的四个过程其实描述的是上面的第二个过程和第三个过程:
IOC

:DI 依赖注入: 三种注入方式:构造方法注入,setter注入,基于注解的注入。我们常用的是基于注解的注入
bean是如何创建— 工厂模式
数据是如何注入-------反射

:IoC 和 DI 的区别
IoC(Inverse of Control:控制反转)是一种设计思想 或者说是某种模式。这个设计思想就是 将原本在程序中手动创建对象的控制权,交由 Spring 框架来管理。 IoC 在其他语言中也有应用,并非 Spring 特有。IoC 容器是 Spring 用来实现 IoC 的载体, IoC 容器实际上就是个 Map(key,value),Map 中存放的是各种对象。
IoC 最常见以及最合理的实现方式叫做依赖注入(Dependency Injection,简称 DI)。
参考:《IOC容器初始化过程

02:AOP
AOP:Aspect oriented programming 面向切面编程,AOP 是 OOP(面向对象编程)的一种延续。
传统横切逻辑代码存在的问题:

  • 代码重复问题
  • 横切逻辑代码和业务代码混杂在一起,代码臃肿,不便维护

AOP 就是用来解决这些问题的
AOP 另辟蹊径,提出横向抽取机制,将横切逻辑代码和业务逻辑代码分离
AOP
AOP 解决了什么问题
通过上面的分析可以发现,AOP 主要用来解决:在不改变原有业务逻辑的情况下,增强横切逻辑代码,根本上解耦合,避免横切逻辑代码重复。

:概念定义

Aspect(切面): Aspect 声明类似于 Java 中的类声明,在 Aspect 中会包含着一些 Pointcut 以及相应的 Advice。
Joint point(连接点):表示在程序中明确定义的点,典型的包括方法调用,对类成员的访问以及异常处理程序块的执行等等,它自身还可以嵌套其它 joint point。
Pointcut(切点):表示一组 joint point,这些 joint point 或是通过逻辑关系组合起来,或是通过通配、正则表达式等方式集中起来,它定义了相应的 Advice 将要发生的地方。
Advice(增强):Advice 定义了在 Pointcut 里面定义的程序点具体要做的操作,它通过 before、after 和 around 来区别是在每个 joint point 之前、之后还是代替执行的代码。
Target(目标对象):织入 Advice 的目标对象.。
Weaving(织入):将 Aspect 和其他对象连接起来, 并创建 Adviced object 的过程

然后举一个容易理解的例子:
看完了上面的理论部分知识, 我相信还是会有不少朋友感觉到 AOP 的概念还是很模糊, 对 AOP 中的各种概念理解的还不是很透彻. 其实这很正常, 因为 AOP 中的概念是在是太多了, 我当时也是花了老大劲才梳理清楚的.
下面我以一个简单的例子来比喻一下 AOP 中 Aspect, Joint point, Pointcut 与 Advice之间的关系.
让我们来假设一下, 从前有一个叫爪哇的小县城, 在一个月黑风高的晚上, 这个县城中发生了命案. 作案的凶手十分狡猾, 现场没有留下什么有价值的线索. 不过万幸的是, 刚从隔壁回来的老王恰好在这时候无意中发现了凶手行凶的过程, 但是由于天色已晚, 加上凶手蒙着面, 老王并没有看清凶手的面目, 只知道凶手是个男性, 身高约七尺五寸. 爪哇县的县令根据老王的描述, 对守门的士兵下命令说: 凡是发现有身高七尺五寸的男性, 都要抓过来审问. 士兵当然不敢违背县令的命令, 只好把进出城的所有符合条件的人都抓了起来.
来让我们看一下上面的一个小故事和 AOP 到底有什么对应关系.
首先我们知道, 在 Spring AOP 中 Joint point 指代的是所有方法的执行点, 而 point cut 是一个描述信息, 它修饰的是 Joint point, 通过 point cut, 我们就可以确定哪些 Joint point 可以被织入 Advice. 对应到我们在上面举的例子, 我们可以做一个简单的类比, Joint point 就相当于 爪哇的小县城里的百姓,pointcut 就相当于 老王所做的指控, 即凶手是个男性, 身高约七尺五寸, 而 Advice 则是施加在符合老王所描述的嫌疑人的动作: 抓过来审问.

  • Joint point : 爪哇的小县城里的百姓: 因为根据定义, Joint point 是所有可能被织入 Advice 的候选的点, 在 Spring AOP中, 则可以认为所有方法执行点都是 Joint point. 而在我们上面的例子中, 命案发生在小县城中, 按理说在此县城中的所有人都有可能是嫌疑人.

  • Pointcut :男性, 身高约七尺五寸: 我们知道, 所有的方法(joint point) 都可以织入 Advice, 但是我们并不希望在所有方法上都织入 Advice, 而 Pointcut 的作用就是提供一组规则来匹配joinpoint, 给满足规则的 joinpoint 添加 Advice. 同理, 对于县令来说, 他再昏庸, 也知道不能把县城中的所有百姓都抓起来审问, 而是根据凶手是个男性, 身高约七尺五寸, 把符合条件的人抓起来. 在这里 凶手是个男性, 身高约七尺五寸 就是一个修饰谓语, 它限定了凶手的范围, 满足此修饰规则的百姓都是嫌疑人, 都需要抓起来审问.

  • Advice :抓过来审问, Advice 是一个动作, 即一段 Java 代码, 这段 Java 代码是作用于 point cut 所限定的那些 Joint point 上的. 同理, 对比到我们的例子中, 抓过来审问 这个动作就是对作用于那些满足 男性, 身高约七尺五寸 的爪哇的小县城里的百姓.

  • Aspect::Aspect 是 point cut 与 Advice 的组合, 因此在这里我们就可以类比: “根据老王的线索, 凡是发现有身高七尺五寸的男性, 都要抓过来审问” 这一整个动作可以被认为是一个 Aspect.
    AOP

其他的一些内容
AOP中的Joinpoint可以有多种类型:构造方法调用,字段的设置和获取,方法的调用,方法的执行,异常的处理执行,类的初始化。也就是说在AOP的概念中我们可以在上面的这些Joinpoint上织入我们自定义的Advice,但是在Spring中却没有实现上面所有的joinpoint,确切的说,Spring只支持方法执行类型的Joinpoint。
Advice 的类型
before advice, 在 join point 前被执行的 advice. 虽然 before advice 是在 join point 前被执行, 但是它并不能够阻止 join point 的执行, 除非发生了异常(即我们在 before advice 代码中, 不能人为地决定是否继续执行 join point 中的代码)

  • after return advice, 在一个 join point 正常返回后执行的 advice
  • after throwing advice, 当一个 join point 抛出异常后执行的 advice
  • after(final) advice, 无论一个 join point 是正常退出还是发生了异常, 都会被执行的 advice.
  • around advice, 在 join point 前和 joint point 退出后都执行的 advice. 这个是最常用的 advice.
  • introduction,introduction可以为原有的对象增加新的属性和方法。

在Spring中,通过动态代理和动态字节码技术实现了AOP

AOP的代理实现
代理模式
代理模式作为23种经典设计模式之一,其比较官方的定义为“为其他对象提供一种代理以控制对这个对象的访问,简单点说就是,之前A类自己做一件事,在使用代理之后,A类不直接去做,而是由A类的代理类B来去做。代理类其实是在之前类的基础上做了一层封装。java中有静态代理、JDK动态代理、CGLib动态代理的方式。静态代理指的是代理类是在编译期就存在的,相反动态代理则是在程序运行期动态生成的

静态代理
静态代理,简单点来说就是在程序运行之前,代理类和被代理类的关系已经确定。静态代理的实现方式一般有以下几个步骤,首先要定义一个公共的接口,供代理类和被代理类实现,

对比,使用静态代理和不使用静态代理,可以发现使用了代理之后,可以在被代理方法的执行前或后加入别的代码,实现诸如权限及日志的操作。
但,静态代理也存在一定的问题,如果被代理方法很多,就要为每个方法进行代理,增加了代码维护的成本。有没有其他的方式可以减少代码的维护,那就是动态代理。

JDK动态代理
代理类是由Proxy这个类通过newProxyInstance方法动态生成的,生成对象后使用“实例调用方法”的方式进行方法调用,那么代理类的被代理类的关系只有在执行这行代码的时候才会生成,因此称为动态代理。

JDK的动态代理也存在不足,即被代理类必须要有实现的接口,如没有接口则无法使用JDK动态代理(从newProxyInstance方法的第二个参数可得知,必须传入被代理类的实现接口),那么就需要使用CGLib动态代理。

CGLib动态代理
CGLib动态代理是一个第三方实现的动态代理类库,不要求被代理类必须实现接口,它采用的是继承被代理类,使用其子类的方式,弥补了被代理类没有接口的不足,

总结
对静态代理、JDK动态代理、CGLib动态代理做一个总结,静态代理的维护成本比较高,有一个被代理类就需要创建一个代理类,而且需要实现相同的接口。JDK动态代理模式和CGLib动态代理的区别是JDK动态代理需要被代理类实现接口,而CGLib则是生成被代理类的子类,要求被代理类不能是final的,因为final类无法被继承。

参考:《细说Spring——AOP详解(AOP概览)》《springAOP之代理模式

2. JVM

Java从编码到执行过程
一个Java源文件如Hello.java通过编译命令javac编译成class文件Hello.class,在JVM中ClassLoader加载该class文件到JVM内存中,执行时候通过字节码解释器或是即时编译器解释后,交给执行引擎,执行引擎与OS硬件交互去完成执行。
在这里插入图片描述

  1. 运行时内存
    在这里插入图片描述
    1 、程序计数器(线程私有)
    程序计数器是一块很小的内存空间,它是线程私有的,可以认作为当前线程的行号指示器。
    是当前线程锁执行字节码的行号指示器,每条线程都有一个独立的程序计数器,这类内存也称为“线程私有”的内存。正在执行java方法的话,计数器记录的是虚拟机字节码指令的地址(当前指令的地址)。如果是Natice方法,则为空。
    2、JVM栈(线程私有)
    虚拟机栈就是我们平常讲JVM内存堆栈中的栈,它也是线程私有的,每个虚拟机栈的生命周期都与一个线程相同。虚拟机栈是线程用来执行方法的内存区域,后入先出(Last In First Out,LIFO)。如下图所示:
    在这里插入图片描述
    一个线程在执行方法时,每调用一个方法,就是将该方法作为栈帧压入自己的虚拟机栈;方法里调用另一个方法,就是将另一个方法的栈帧再压入虚拟机栈;线程当前执行的方法就是栈顶帧。
    每个栈帧对应一个方法,其内部包括以下内容:
    局部变量表:对应方法参数与局部变量,其类型是Java的8种基本数据类型加上对象引用。注意是对象的引用,不是对象本身。
    操作数栈:线程执行方法内部字节码操作指令时使用的后入先出栈,各种指令会往操作栈中写入和提取信息。Java虚拟机的解释执行引擎被称为“基于栈的执行引擎”,里面的“栈”就是操作栈。
    动态连接:每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,栈帧持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking),即,调用一个方法是通过该引用找到运行时常量池中的方法信息的。
    方法返回地址:方法执行结束,不管是正常退出还是异常退出,都需要返回到该方法被调用的位置。
  2. 本地方法栈(线程私有)
    本地方法栈与虚拟机栈类似,不同的是,虚拟机栈是用来执行java方法(class字节码)的,而本地方法栈是用来执行本地方法(native)的。所谓本地方法即JVM进程所在机器的OS的本地函数库,例如linux的.so或windows的.dll这些可执行类库中的方法。Java语法上,是用JNI调用这些native接口的。
  3. 堆区(线程共享)
    Java堆是JVM内存中最大的一块区域,JVM几乎所有的对象实例都在堆里分配内存并创建。堆内部区域的划分取决于JVM的垃圾回收策略,即GC策略。目前主流的GC策略大部分是基于分代收集算法的,如 parNew+CMS,或者G1等等。因此我们可以将Java堆再划分为新生代,老年代。如下图所示:
    堆中内存分配
    分代收集算法大致过程:
    1、JVM新创建的对象会放在eden区域。
    2、当eden区域快满时,触发Minor GC新生代GC,通过可达性分析法或者引用计数法将失去引用的对象销毁,剩下的对象移动到幸存者区S1,并清空eden区域,此时S2是空的。
    3、当eden区域又快满时,再次触发Minor GC,对eden和S1的对象进行可达性分析,销毁失去引用的对象,同时将剩下的对象全部移动到另一个幸存者区S2,并清空eden和S1。
    4、每次eden快满时,重复上述第3步,触发Minor GC,将幸存者在S1与S2之间来回倒腾。
    5、在历次Minor GC中一直存活下来的幸存者,或者太大了会导致新生代频繁Minor GC的对象,或者Minor GC时幸存者对象太多导致S1或S2放不下了,那么这些对象就会被放到老年代。
    6、老年代的对象越来越多,最终会触发Major GC或Full GC,对老年代甚至整堆的对象进行清理。通常Major GC或Full GC会导致较长时间的STW,暂停GC以外的所有线程,因此频繁的Major GC或Full GC会严重影响JVM性能。
  4. 方法区(线程共享)
    方法区(Method Area)与Java堆一样,是各个线程共享的内存区域。
    它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
    要注意的是,《Java虚拟机规范》中的方法区是一个逻辑上的区域,不同的JVM对它都有不同的实现。另外,它有一个别名叫作“非堆”(Non-Heap),目的是与Java堆区分开来。
    在Java8之前,HotSpot虚拟机将方法区实现为永久代,能够通过分代收集的GC来管理其内存区域。但这种设计导致Java应用经常遇到内存溢出问题,很多JVM都需要在启动时添加参数-XX:MaxPermSize来调整永久代的大小。因此在Java7的时候,就先将方法区中的字符串常量池,静态变量等转移到了Java堆中;而到了Java8,就直接移除了永久代,将其中剩下的内容如类的元信息,方法元信息,class常量池,运行时常量池等移动到了一个新的区域Metaspace元数据区,将JIT即时编译的代码缓存放到了CodeCache区域。
    不管是Java8之前的永久代,还是Java8以后的元数据区与CodeCache,还是Java7以后堆中的字符串常量池,它们在逻辑上都属于方法区。只是不同JVM在不同版本中的具体实现不一样罢了。
  5. 内存区域异常类型
    JVM内存的异常有两种,分别是内存溢出和栈溢出。
    内存溢出:是OutOfMemoryError,一般对应线程共享区域如堆和元数据区。当内存不足以分配对象空间,而堆或方法区又无法扩展时,就会抛出该异常。比如对应堆区的OutOfMemoryError: Java heap space,对应元数据区的OutOfMemoryError: Metaspace。如果Java虚拟机栈容量可以动态扩展 ,当栈扩展时无法申请到足够的内存也会抛出OutOfMemoryError。
    栈溢出:是StackOverflowError,对应虚拟机栈和本地方法栈,当线程请求的栈深度大于虚拟机所允许的深度时就会抛出该异常。
  6. GC
    1、什么是垃圾(Garbage)呢?
    垃圾是指在运行程序中没有任何指针指向的对象,这个对象就是需要被回收的垃圾。
    2、JVM如何判断一个对象可以回收的?
    主要有两种方式:引用计数算法和可达性分析算法。
    引用计数算法:所谓的引用计数算法,就是通过一个对象的引用计数器来判断该对象是否被引用,对象被引用的时候,计数器就加1,引用失效计数器就减1。
    计数器的值为0 的时候就说明这个对象没有被引用了,可以被JVM回收了。需要注意的是,引用计数算法虽然实现方式简单,但是会出现循环引用的问题。
    可达性分析算法:可达性分析算法的基础是GC Roots,是所有对象的根对象,在JVM加载时,会创建一些对象引用正常对象,这些对象作为这些正常对象的起始点,在垃圾回收时,JVM会从GC Roots开始向下搜索,如果一个对象到GC Roots没有任何引用链相连时,就证明这个对象可以回收了。
    3,垃圾回收线程是如何回收对象的?
    JVM去回收对象主要遵从两个特性:自动性、不可预期性。
    自动性:JVM会创建一个系统级的线程来跟踪每一块被分配出去的内存,在JVM空闲时,就会自动的检查每一块分配出去的内存空间,然后自动回收每一块内存。
    不可预期性:不可预期性主要是一个对象没有被引用的时候,是立马就被回收的吗,这个答案是未知的,有可能立马就被回收,有可能隔了很久依然在内存中。
    4、GC算法
    JVM给我们提供了多种回收算法来实现回收机制,一般来说,市面上常见的垃圾收集器的回收算法主要分为四类:
    01:标记-清除算法(Mark-Sweep)
    优点:不需要移动对象,简单高效
    确定:标记-清除的过程效率低,会产生内存碎片。
    02:复制算法(Copying)
    优点:简单高效,不会产生内存碎片:
    缺点:内存使用率低,还有可能产生频繁复制的问题。
    03:标记-整理算法(Mark-Compact)
    优点:效率高,不产生内存碎片
    缺点:需要移动局部对象
    04:分代收集算法(Gennerational Collection)
    优点:分区回收
    缺点:对于长期存活对象的回收效果不太好。
    常用的几种垃圾回收器:在这里插入图片描述
    5、Minor GC和Full GC,这两种GC有什么不一样吗?
    01:Minor GC又称为新生代GC : 指的是发生在新生代的垃圾收集。因为Java对象大多都具备朝生夕灭的特性,因此Minor GC(采用复制算法)非常频繁,一般回收速度也比较快。
    02:Full GC 又称为老年代GC或者Major GC : 指发生在老年代的垃圾收集。出现了Major GC,经常会伴随至少一次的Minor GC(并非绝对,在Parallel Scavenge收集器中就有直接进行Full GC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上。
    一般是把Java堆分为新生代和老年代。在新生代中,每次垃圾回收都有大批对象死去,只有少量存活,因此我们采用复制算法;而老年代中对象存活率高、没有额外空间对它进行分配担保,就必须采用"标记-清理"或者"标记-整理"算法。
    6、GC调优!
    通过分析GC日志,找出影响性能的问题,接下来就该有针对性的调优了,简单介绍几种常用的调优策略,主要是降低Minor GC和Full GCd 频率
    01:降低Minor GC频率
    :Minor GC主要是针对Eden区的对象回收,由于新生代空间一般比较小,Eden区很块就会满,就会导致Minor GC的频率比较高,我们的解决办法通常是增大新生代空间来降低Minor GC的频率。
    在讲衡量GC性能指标的时候,我们提到增大内存会增加回收时候的卡顿时间。Minor GC也会导致应用程序的卡顿,只是时间非常短暂,那么扩大Eden区会不会导致Minor GC的时间增长,还得深入看一下一次Minor GC发生了什么:
    每次Minor GC主要做了两件事,扫描新生代(A)和复制存活对象(B)。其中复制对象的耗时是远高于扫描对象的。我们举个例子,如果一个对象在Eden区域存活500ms,Minor GC的频率是300ms一次,正常情况下,在一次Minor GC中用时就说A+B的时间,这个时候我们通过gc日志分析,把Eden扩容,变成了600ms才进行一次Minor GC,此时这个对象在Eden区中已经被回收,就不用复制对象了,就省去了复制存活对象的时间,在这一次Minor GC中只是增加了扫描新生代的时间。从而提高了效率!
    小结:单次 Minor GC 时间更多取决于 GC 后存活对象的数量,而非 Eden 区的大小。如果堆内存中存活时间比较长的对象多,增加年轻代的空间,单次Minor GC的时间反而会增加,如果是堆内存中短期对象多,那么扩容后,单词Minor GC的时间不会明显的增加,还降低了Minor GC频率。
    02:降低Full GC频率
    Full GC的触发通常是因为堆内存空间不足或者老年代对象太多造成的,Full GC又会带来上下文切换,都知道上下文切换会降低系统的性能。我们可以通过下边几个方向来降低Full GC的频率:
    减少创建大对象:有时候因为一些编程习惯的问题,为了省事就一次性从数据库查询一个大对象用于web端显示,这种大对象会被直接创建在老年代,哪怕是创建在新生代,由于新生代的空间一般很小,通过一次Minor GC就会进入老年代,这样的大对象攒多了就会触发Full GC,所以还是要养成良好的习惯,减少一些不必要字段的查询。
    增大堆内存空间:堆内存不足这种情况就直接增大堆内存的空间,把初始化内存空间就设置成最大堆内存空间,这样就可以显著降低Full GC频率
    合适的GC回收器:上边我们也介绍了多种回收器,根据我们的业务场景,选择合适的回收器往往可以达到不错的效果。
  7. JVM调优(虚)
    JVM调整的核心步骤:
    01:确定调优的目的,选择合适的GC collector
    02:调整JVM heap的大小
    03:调整young generation在整个JVM heap中所占的比重.

3. 多线程

面试题01:守护线程与非守护线程的区别
java中两类线程:User Thread(用户线程)和Daemon Thread(守护线程)。
任何一个守护线程都会守护整个JVM中所有的非守护线程,只要当前JVM中还有任何一个非守护线程没有结束,守护线程就全部工作,当所有的非守护线程全部结束后,守护线程也会随着JVM一同结束。守护线程最典型的应用就是GC(垃圾回收器)
需要注意的地方:
1、thread.setDaemon(true)方法必须在thread.start()之前设置,否则会报IllegalThreadStateException异常,不能把正在运行的常规线程设置为守护线程。
2、在守护线程中产生的新线程也是守护线程。
3、不是所有应用都可以分配守护线程来进行服务,比如读写操作或是计算逻辑等。因为如果非守护线程都结束了,但是读写或计算逻辑没有完成,守护线程也会停止。
判断线程是否为守护线程的方法是:isDaemon(),返回true为守护线程,返回false为非守护线程


乐观锁、悲观锁、AQS、sync和Lock

乐观锁CAS
CAS(Compare And Swap 比较并且替换)是乐观锁的一种实现方式,是一种轻量级锁,JUC(我是一个链接^. ^) 中很多工具类的实现就是基于 CAS 的。
CAS 是怎么实现线程安全的?
CAS模式中,在读取数据时是不加锁的,在写数据时先去获取原来的值,操作时再判断当前值是否与原来值相同,是否被别的线程修改,如果未被修改则进行写操作,如果已经被修改则进入自旋,重新读取数据

举个栗子:现在一个线程要修改数据库的name,修改前我会先去数据库查name的值,发现name=“帅丙”,拿到值了,我们准备修改成name=“三歪”,在修改之前我们判断一下,原来的name是不是等于“帅丙”,如果被其他线程修改就会发现name不等于“帅丙”,我们就不进行操作,如果原来的值还是帅丙,我们就把name修改为“三歪”,至此,一个流程就结束了。
Tip:比较+更新 整体是一个原子操作
CAS
CAS存在什么问题呢?
01:ABA问题。
02:无限自旋,CPU开销
03:只能保证一个共享变量原子操作
01:ABA问题
ABA
执行顺序:

  1. 线程1去读取A,准备进行操作
  2. 线程2读取到A,此时CAS比对通过,进行写操作,将A修改为B
  3. 线程3读取到B,CAS比对通过,进行操作,将B修改为A
  4. 此时线程1进行写操作,CAS比对通过,符合预期,写操作成功

整个过程中没有那个线程异常,各个CAS都会通过,虽然结果是对的,但是 中间数据发生了多次变化,线程1没有意识到数据的变化。
02:循环时间长开销大的问题:
是因为CAS操作长时间不成功的话,会导致一直自旋,相当于死循环了,CPU的压力会很大。
可以通过自旋次数加校验
03:只能保证一个共享变量的原子操作:
CAS操作单个共享变量的时候可以保证原子的操作,多个变量就不行了,JDK 5之后 AtomicReference可以用来保证对象之间的原子性,就可以把多个对象放入CAS中操作。那我就拿AtomicInteger举例,他的自增函数 incrementAndGet()就是这样实现的,其中就有大量循环判断的过程,直到符合条件才成功。
那我就拿AtomicInteger举例,他的自增函数incrementAndGet()就是这样实现的,其中就有大量循环判断的过程,直到符合条件才成功。
在这里插入图片描述
大概意思就是循环判断给定偏移量是否等于内存中的偏移量,直到成功才退出
乐观锁在项目开发中的实践?
比如我们在很多订单表,流水表,为了防止并发问题,就会加入CAS的校验过程,保证了线程的安全
开发过程中ABA你们是怎么保证的?
加标志位(version),例如搞个自增的字段,操作一次就自增加一,或者搞个时间戳,比较时间戳的值。

举个栗子:现在我们去要求操作数据库,根据CAS的原则我们本来只需要查询原本的值就好了,现在我们一同查出他的标志位版本字段vision。
像这样之前不能防止ABA的正常修改:
update table set value = newValue where value = #{oldValue}
//oldValue就是我们执行前查询出来的值
带版本号能防止ABA的修改:
update table set value = newValue ,vision = vision + 1 where value = #{oldValue} and vision = #{vision}
// 判断原来的值和版本号是否匹配,中间有别的线程修改,值可能相等,但是版本号100%不一样
除了版本号,像什么时间戳,还有JUC工具包里面也提供了这样的类,想要扩展的小伙伴可以去了解一下。


悲观锁
JVM层面的synchronized
synchronized加锁,synchronized 是最常用的线程同步手段之一,上面提到的CAS是乐观锁的实现,synchronized就是悲观锁了。
synchronized,代表这个方法加锁,相当于不管哪一个线程(例如线程A),运行到这个方法时,都要检查有没有其它线程B(或者C、 D等)正在用这个方法(或者该类的其他同步方法),有的话要等正在使用synchronized方法的线程B(或者C 、D)运行完这个方法后再运行此线程A,没有的话,锁定调用者,然后直接运行。

synchronized 对 对象、方法和代码块三方面加锁,是怎么保证线程安全的:

  • synchronized 对对象进行加锁,在 JVM 中,对象在内存中分为三块区域:对象头(Header)、实例数据(Instance
    Data)和对齐填充(Padding)。
    对象头:我们以Hotspot虚拟机为例,Hotspot的对象头主要包括两部分数据:Mark Word(标记字段)、Klass Pointer(类型指针)。
    Mark Word:默认存储对象的HashCode分代年龄锁标志位信息。它会根据对象的状态复用自己的存储空间,也就是说在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。
    Klass Point:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
    你可以看到在对象头中保存了锁标志位和指向 monitor 对象的起始地址,如下图所示,右侧就是对象对应的 Monitor 对象。
    对象头
  • synchronized 应用在方法上时,在字节码中是通过方法的 ACC_SYNCHRONIZED 标志来实现的。
    其他线程进这个方法就看看是否有这个标志位,有就代表有别的仔拥有了他,你就别碰了。
  • synchronized 应用在同步块上时,在字节码中是通过 monitorentermonitorexit 实现的。

小结
同步方法和同步代码块都是通过monitor来实现的
两者的区别:同步方法是通过方法中的access_flags中设置ACC_SYNCHRONIZED标志来实现,同步代码块是通过monitorenter和monitorexit来实现。

问:synchronized为什么说不算是重量级锁?
在多线程并发编程中 synchronized 一直是元老级角色,很多人都会称呼它为重量级锁。

但是,随着 Java SE 1.6 对 synchronized 进行了各种优化之后,有些情况下它就并不那么重,Java SE 1.6 中为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁轻量级锁

针对 synchronized 获取锁的方式,JVM 使用了锁升级的优化方式,就是先使用偏向锁优先同一线程然后再次获取锁,如果失败,就升级为 CAS 轻量级锁,如果失败就会短暂自旋,防止线程被系统挂起。最后如果以上都失败就升级为重量级锁
锁升级
Tips : 锁只能升级,不能降级。
锁
ReentrantLock
ReentrantLock但是在介绍这玩意之前,我觉得我有必要先介绍AQS(AbstractQueuedSynchronizer)。

AQS:也就是队列同步器,这是实现 ReentrantLock 的基础。

AQS 有一个 state 标记位,值为1 时表示有线程占用,其他线程需要进入到同步队列等待,同步队列是一个双向链表。
在这里插入图片描述
当获得锁的线程需要等待某个条件时,会进入 condition 的等待队列,等待队列可以有多个。
当 condition 条件满足时,线程会从等待队列重新进入同步队列进行获取锁的竞争。

ReentrantLock 就是基于 AQS 实现的,如下图所示,ReentrantLock 内部有公平锁和非公平锁两种实现,差别就在于新来的线程是否比已经在同步队列中的等待线程更早获得锁。
和 ReentrantLock 实现方式类似,Semaphore 也是基于 AQS 的,差别在于 ReentrantLock 是独占锁,Semaphore 是共享锁。
reLLock
从图中可以看到,ReentrantLock里面有一个内部类Sync,Sync继承AQS(AbstractQueuedSynchronizer),添加锁和释放锁的大部分操作实际上都是在Sync中实现的。

它有公平锁FairSync和非公平锁NonfairSync两个子类。

ReentrantLock默认使用非公平锁,也可以通过构造器来显示的指定使用公平锁。

4. 事物

5. 集合

问:16是2的幂,8也是,32也是,为啥偏偏选了16?
我觉得就是一个经验值,定义16没有很特别的原因,只要是2次幂,其实用 8 和 32 都差不多。
用16只是因为作者认为16这个初始容量是能符合常用而已。

问:Hashmap中的链表大小超过八个时会自动转化为红黑树,当删除小于六时重新变为链表,为啥呢?
根据泊松分布,在负载因子默认为0.75的时候,单个hash槽内元素个数为8的概率小于百万分之一,所以将7作为一个分水岭,等于7的时候不转换,大于等于8的时候才进行转换,小于等于6的时候就化为链表。
注意这里有个面试点: 到8之后是必须转为红黑树么?
答案是:不是必须,通过观察源码我们会发现在Put元素之后,判断为单项列表的第8个节点之后,还会继续判断当前HashMap的长度length()是否超过64,如果没有那么会进行resize()处理,超过64之后才会进行变树操作。
问:HashMap线程安全问题

  • 使用Collections.synchronizedMap(Map)创建线程安全的map集合;
  • Hashtable
  • ConcurrentHashMap

ConcurrentHashMap & HashTable

  • HashTable
    和HashMap比HashTable是线程安全的,但是由于引入了重量级锁,所以他的效率比较低下。
    HashTable上锁
    问:HashMap与HashTable的区别?
    01:线程安全问题
    02:HashMap允许键或者值为null,(此时null必定在头一个元素),HashTable不允许键或者值为null,当HashTable在put时有null就会抛出控指针异常,但是HashMap对null做了判断:

    因为Hashtable使用的是快速失败机制(fail-fast),这种机制会使你此次读到的数据不一定是最新的数据。
    如果你使用null值,就会使得其无法判断对应的key是不存在还是为空,因为你无法再调用一次contain(key)来判断Key是否存在,ConcurrentHashMap同理。
    03:实现方式不同:Hashtable 继承了 Dictionary类,而 HashMap 继承的是 AbstractMap 类
    04:初始化容量不同:HashMap 的初始容量为:16,Hashtable 初始容量为:11,两者的负载因子默认都是:0.75。
    05:扩容机制不同:当现有容量大于总容量 * 负载因子时,HashMap 扩容规则为当前容量翻倍,Hashtable 扩容规则为当前容量翻倍 + 1。
    06:迭代器不同:HashMap 中的 Iterator 迭代器是 fail-fast 的,而 Hashtable 的 Enumerator 不是 fail-fast 的。
    所以,当其他线程改变了HashMap 的结构,如:增加、删除元素,将会抛出ConcurrentModificationException 异常,而 Hashtable 则不会。
    java.util包下的集合类都是快速失败(file-fast)的,不能在多线程下发生并发修改(迭代过程中被修改)算是一种安全机制吧。
    java.util.concurrent包下的容器都是安全失败(file-safe),可以在多线程下并发使用,并发修改。
  • ConcurrentHashMap
    ConcurrentHashMap底层是由数组+列表实现的,不过jdk1.7和1.8有不同的实现:
    先看1.7:
    1.7
    如图所示:他是由Segment和HashEntry组合实现的,和HashMap一样,任然是数组加链表。
    Segment 是 ConcurrentHashMap 的一个内部类,主要的组成如下:在这里插入图片描述
    HashEntry跟HashMap差不多的,但是不同点是,他使用volatile去修饰了他的数据Value还有下一个节点next。
    volatile的特性是啥?
    01:保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。(实现可见性)
    02:禁止进行指令重排序。(实现有序性)
    volatile 只能保证对单次读/写的原子性。i++ 这种操作不能保证原子性。
    那么那你能说说他并发度高的原因么?
    原理上来说,ConcurrentHashMap 采用了分段锁技术,其中 Segment 继承于 ReentrantLock。
    不会像 HashTable 那样不管是 put 还是 get 操作都需要做同步处理,理论上 ConcurrentHashMap 支持 CurrencyLevel (Segment 数组数量)的线程并发。
    每当一个线程占用锁访问一个 Segment 时,不会影响到其他的 Segment。
    就是说如果容量大小是16他的并发度就是16,可以同时允许16个线程操作16个Segment而且还是线程安全的。
    在这里插入图片描述
    他先定位到Segment,然后再进行put操作。
    我们看看他的put源代码,你就知道他是怎么做到线程安全的了,关键句子我注释了。
    在这里插入图片描述
    首先第一步的时候会尝试获取锁trylock(),如果获取失败肯定就有其他线程存在竞争,则利用 scanAndLockForPut() 自旋获取锁。
    01:尝试自旋获取锁。
    02:如果重试的次数达到了 MAX_SCAN_RETRIES 则改为阻塞锁获取,保证能获取成功。
    那他get的逻辑呢?
    get 逻辑比较简单,只需要将 Key 通过 Hash 之后定位到具体的 Segment ,再通过一次 Hash 定位到具体的元素上。
    由于 HashEntry 中的 value 属性是用 volatile 关键词修饰的,保证了内存可见性,所以每次获取时都是最新值。
    ConcurrentHashMap 的 get 方法是非常高效的,因为整个过程都不需要加锁。
    由于1.7基本上还是数组加链表的方式,我们去查询的时候,还得遍历链表,会导致效率很低,这个跟jdk1.7的HashMap是存在的一样问题,所以他在jdk1.8完全优化了。
    再看1.8
    1.8中抛弃了原有的 Segment 分段锁,而采用了 CAS + synchronized 来保证并发安全性。
    跟HashMap很像,也把之前的HashEntry改成了Node,但是作用不变,把值和next采用了volatile去修饰,保证了可见性,并且也引入了红黑树,在链表大于一定值的时候会转换(默认是8)。
    ConcurrentHashMap在进行put操作的还是比较复杂的,大致可以分为以下步骤:
    01:根据 key 计算出 hashcode 。
    02:判断是否需要进行初始化。
    03:即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。
    04:如果当前位置的 hashcode == MOVED == -1,则需要进行扩容。
    05:如果都不满足,则利用 synchronized 锁写入数据。
    06:如果数量大于 TREEIFY_THRESHOLD 则要转换为红黑树。
    在这里插入图片描述
    问:你在上面提到CAS是什么?自旋又是什么?
    CAS 是乐观锁的一种实现方式,是一种轻量级锁,JUC 中很多工具类的实现就是基于 CAS 的。
    CAS 操作的流程如下图所示,线程在读取数据时不进行加锁,在准备写回数据时,比较原值是否修改,若未被其他线程修改则写回,若已被修改,则重新执行读取流程。
    这是一种乐观策略,认为并发操作并不总会发生。
    CAS自旋
    问:CAS就一定能保证数据没被别的线程修改过么?
    并不是的,比如很经典的ABA问题,CAS就无法判断了。
    什么是ABA?
    就是说来了一个线程把值改回了B,又来了一个线程把值又改回了A,对于这个时候判断的线程,就发现他的值还是A,所以他就不知道这个值到底有没有被人改过,其实很多场景如果只追求最后结果正确,这是没关系的。
    但是实际过程中还是需要记录修改过程的,比如资金修改什么的,你每次修改的都应该有记录,方便回溯。
    那怎么解决ABA问题?
    用版本号去保证就好了,就比如说,我在修改前去查询他原来的值的时候再带一个版本号,每次判断就连值和版本号一起判断,判断成功就给版本号加1。
    update a set value = newValue ,vision = vision + 1 where value = #{oldValue} and vision = #{vision} // 判断原来的值和版本号是否匹配,中间有别的线程修改,值可能相等,但是版本号100%不一样
    问:CAS性能很高,但是我知道synchronized性能可不咋地,为啥jdk1.8升级之后反而多了synchronized?
    synchronized之前一直都是重量级的锁,但是后来java官方是对他进行过升级的,他现在采用的是锁升级的方式去做的。
    针对 synchronized 获取锁的方式,JVM 使用了锁升级的优化方式,就是先使用偏向锁优先同一线程然后再次获取锁,如果失败,就升级为 CAS 轻量级锁,如果失败就会短暂自旋,防止线程被系统挂起。最后如果以上都失败就升级为重量级锁
    所以是一步步升级上去的,最初也是通过很多轻量级的方式锁定的。
    ConcurrentHashMap的get操作又是怎么样子的呢?
    01:根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值。
    02:如果是红黑树那就按照树的方式获取值。
    03:就不满足那就按照链表的方式遍历获取值。
    GET
    小结 :1.8 在 1.7 的数据结构上做了大的改动,采用红黑树之后可以保证查询效率(O(logn)),甚至取消了 ReentrantLock 改为了 synchronized,这样可以看出在新版的 JDK 中对 synchronized 优化是很到位的。
    总结:
    Hashtable&ConcurrentHashMap跟HashMap基本上就是一套连环组合,我在面试的时候经常能吹上很久,经常被面试官说:好了好了,我们继续下一个话题吧哈哈。
    是的因为提到HashMap你肯定会聊到他的线程安全性这一点,那你总不能加锁一句话就搞定了吧,java的作者们也不想,所以人家写开发了对应的替代品,那就是线程安全的Hashtable&ConcurrentHashMap。
    两者都有特点,但是线程安全场景还是后者用得多一点,原因我在文中已经大篇幅全方位的介绍了,这里就不再过多赘述了。
    你们发现了面试就是一个个的,你说到啥面试官可能就怼到你啥,别问我为啥知道嘿嘿。
    你知道不确定能不能为这场面试加分,但是不知道肯定是减分的,文中的快速失败(fail—fast)问到,那对应的安全失败(fail—safe)也是有可能知道的,我想读者很多都不知道吧,因为我问过很多仔哈哈。
    还有提到CAS乐观锁,你要知道ABA,你要知道解决方案,因为在实际的开发场景真的不要太常用了,sync的锁升级你也要知道。

6. IO

7. 设计模式

二. 数据库

Mybatis

什么是 Mybatis?
MyBatis 的优点
MyBatis 框架的缺点
MyBatis 框架适用场合
MyBatis 与 Hibernate 有哪些不同?
#{}和${}的区别是什么?
当实体类中的属性名和表中的字段名不一样 ,怎么办 ?
模糊查询 like 语句该怎么写?
Mapper 接口的工作原理是什么?Mapper 接口里的方法,参数不同时,方法能重载吗?
Mybatis 是如何进行分页的?分页插件的原理是什么?
Mybatis是如何将sql执行结果封装为目标对象并返回的?都有哪些映射形式?
如何执行批量插入?
如何获取自动生成的(主)键值?
在 mapper 中如何传递多个参数?
Mybatis 动态 sql 有什么用?执行原理?有哪些动态 sql?
Xml 映射文件中,除了常见的 select|insert|updae|delete 标签之外,还有哪些标签?
Mybatis 的 Xml 映射文件中,不同的 Xml 映射文件,id 是否可以重复?
为什么说 Mybatis 是半自动 ORM 映射工具?它与全自动的区别在哪里?
一对一、一对多的关联查询 ?
MyBatis 实现一对一有几种方式?具体怎么操作的?
Mybatis 是否支持延迟加载?如果支持,它的实现原理是什么?
Mybatis 的一级、二级缓存
什么是 MyBatis 的接口绑定?有哪些实现方式?
使用 MyBatis 的 mapper 接口调用时有哪些要求?
简述 Mybatis 的插件运行原理,以及如何编写一个插件
Mybatis经典25问

MySql

索引:
数据库的神!

问:mysql中的慢查询是什么?
1.概念
MySql的慢查询,全名是慢查询日志,是MySql提供的一种日志记录功能,用来记录响应时间超过设定阈值的sql语句。
具体环境中,运行时间超过long_query_time值的SQL语句,则会被记录到慢查询日志中。
long_query_time的默认值为10,意思是记录运行10秒以上的语句。
默认情况下,MySQL数据库并不启动慢查询日志,需要手动来设置这个参数。
当然,如果不是调优需要的话,一般不建议启动该参数,因为开启慢查询日志会或多或少带来一定的性能影响。

2 .参数
MySQL 慢查询的相关参数解释:
slow_query_log:是否开启慢查询日志,1表示开启,0表示关闭。
log-slow-queries :旧版(5.6以下版本)MySQL数据库慢查询日志存储路径。可以不设置该参数,系统则会默认给一个缺省的文件host_name-slow.log
slow-query-log-file:新版(5.6及以上版本)MySQL数据库慢查询日志存储路径。可以不设置该参数,系统则会默认给一个缺省的文件host_name-slow.log
long_query_time:慢查询阈值,当查询时间多于设定的阈值时,记录日志。
log_queries_not_using_indexes:未使用索引的查询也被记录到慢查询日志中(可选项)。
log_output:日志存储方式。log_output=‘FILE’表示将日志存入文件,默认值是’FILE’。log_output='TABLE’表示将日志存入数据库

参考:《mysql慢查询

优化

三. 框架

SpringMVC

SpingBoot

  1. 什么是 Spring Boot?
    Spring Boot 是 Spring 开源组织下的子项目,是 Spring 组件一站式解决方案,主要是简化了使用 Spring 的难度,简省了繁重的配置,提供了各种启动器,开发者能快速上手。

  2. 为什么要用 Spring Boot?
    Spring Boot 优点非常多,如
    独立运行
    简化配置
    自动配置
    无代码生成和XML配置
    应用监控
    上手容易

  3. Spring Boot 和 Spring MVC 的区别?
    Spring 框架就像一个家族,有众多衍生产品例如 boot、security、jpa等等。
    但他们的基础都是Spring 的 ioc和 aop, ioc 提供了依赖注入的容器 ,aop 解决了面向横切面的编程,然后在此两者的基础上实现了其他延伸产品的高级功能。

1:Spring MVC是基于 Servlet 的一个 MVC 框架 主要解决 WEB 开发的问题,
SpringMVC是一种web层mvc框架,用于替代servlet处理或响应请求,获取表单参数,表单校验等。
SpringMVC是一个MVC的开源框架,SpringMVC=struts2+spring,springMVC就相当于是Struts2加上Spring的整合。
2:Spring Boot实现了自动配置,降低了项目搭建的复杂度。
Springboot是一个微服务框架,延续了spring框架的核心思想IOC和AOP,简化了应用的开发和部署。Spring Boot是为了简化Spring应用的创建、运行、调试、部署等而出现的,使用它可以做到专注于Spring应用的开发,而无需过多关注XML的配置。提供了一堆依赖打包,并已经按照使用习惯解决了依赖问题

  1. Spring Boot 的核心配置文件有哪几个?它们的区别是什么?
    Spring Boot 的核心配置文件是 application 和 bootstrap 配置文件。

1.application 配置文件这个容易理解,主要用于 Spring Boot 项目的自动化配置。
2.bootstrap 配置文件有以下几个应用场景。
一 使用 Spring Cloud Config 配置中心时,这时需要在 bootstrap 配置文件中添加连接到配置中心的配置属性来加载外部配置中心的配置信息;
一些固定的不能被覆盖的属性;
一些加密/解密的场景;

  1. Spring Boot 的核心注解是哪个?它主要由哪几个注解组成的?
    启动类上面的注解是@SpringBootApplication,是 Spring Boot 的核心注解,主要组合包含了以下 3 个注解:

1.@SpringBootConfiguration :
标注当前类是配置类,这个注解继承自@Configuration。并会将当前类内声明的一个或多个以@Bean注解标记的方法的实例纳入到srping容器中,并且实例名就是方法名。
2.@EnableAutoConfiguration:
打开自动配置的功能,这个注解会根据我们添加的组件jar来完成一些默认配置,比如我们做微服时会添加spring-boot-starter-web等组件jar的pom依赖,这样配置会默认配置springmvc 和 tomcat。
也可以关闭某个自动配置的选项,
如关闭数据源自动配置功能: @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })。
3.@ComponentScan:
扫描当前包及其子包下被@Component,@Controller,@Service,@Repository注解标记的类并纳入到spring容器中进行管理。等价于<-context:component-scan->的xml配置文件中的配置项。

  1. Spring Boot 自动配置原理是什么?
    注解 @EnableAutoConfiguration, @Configuration, @ConditionalOnClass 就是自动配置的核心,首先它得是一个配置文件,其次根据类路径下是否有这个类去自动配置。
    具体看这篇文章《Spring Boot自动配置原理、实战》。
    可参考《Spring Boot自动配置原理》。

  2. 你如何理解 Spring Boot 中的 Starters?
    Starters可以理解为启动器,它包含了一系列可以集成到应用里面的依赖包,你可以一站式集成 Spring 及其他技术,而不需要到处找示例代码和依赖包。如你想使用 Spring JPA 访问数据库,只要加入 spring-boot-starter-data-jpa 启动器依赖就能使用了。
    Starters包含了许多项目中需要用到的依赖,它们能快速持续的运行,都是一系列得到支持的管理传递性依赖。
    具体请看这篇文章《Spring Boot Starters启动器》。

  3. Spring Boot 有哪几种读取配置的方式?
    Spring Boot 可以通过
    @PropertySource,
    @Value,
    @Environment,
    @ConfigurationProperties 来绑定变量
    具体请看这篇文章《Spring Boot读取配置的几种方式》。

  4. SpringBoot 实现热部署有哪几种方式?
    主要有两种方式:
    Spring Loaded
    Spring-boot-devtools
    Spring-boot-devtools 使用方式可以参考这篇文章《Spring Boot实现热部署》。

  5. Spring Boot 配置加载顺序?
    在 Spring Boot 里面,可以使用以下几种方式来加载配置。他们的执行顺序是:
    1)properties文件;
    2)YAML文件;
    3)系统环境变量;
    4)命令行参数;
    等等……
    具体请看这篇文章《Spring Boot 配置加载顺序详解》。

  6. Spring Boot 如何实现多环境配置?

1.编写多个properties文件
对应不同的环境,在application.properties文件中配置指定使用哪个配置文件
例如你可以另外建立3个环境下的配置文件:
applcation.properties
application-dev.properties
application-test.properties
application-prod.properties
然后在applcation.properties文件中指定当前的环境spring.profiles.active=test,这时候读取的就是application-test.properties文件。
2.使用注解@Profile
在JAVA配置代码中也可以加不同Profile下定义不同的配置文件,@Profile注解只能组合使用@Configuration和@Component注解。

  1. Spring Boot 可以兼容老 Spring 项目吗,如何做?
    可以兼容,使用 @ImportResource 注解导入老 Spring 项目配置文件。

SpringCloud & Alibaba

  1. 什么是单体应用
    做的所有应用程序 放置在一个 项目中 最后 将之后的war 或者jar 部署在你的服务器。这种模式随着发展终将会被淘汰 是因为出现的问题 将随之而来 并发 耦合 等 问题 刻不容缓。

  2. 什么是SOA架构
    面向服务的架构(SOA)是一个组件模型,它将应用程序的不同功能单元(称为服务)进行拆分,并通过这些服务之间定义良好的接口和协议联系起来。接口是采用中立的方式进行定义的,它应该独立于实现服务的硬件平台、操作系统和编程语言。这使得构建在各种各样的系统中的服务可以以一种统一和通用的方式进行交互。

  3. 什么是微服务
    微服务是一种架构模式,叫微服务架构更合理,就是把一个系统中的各个功能点都拆开为一个个的小应用然后单独部署,同时因为这些小应用多,所以需要一些办法来管理这些小应用。
    总的来说,微服务的主旨是将一个原本独立的系统拆分成多个小型服务,这些小型服务都在各自独立的进程中运行,服务之间通过基于HTTP的RESTful API进行通信协作,并且每个服务都维护着自身的数据存储、业务开发、自动化测试以及独立部署机制。

  4. 微服务的优点、缺点、特点
    优点:
    1、独立开发 – 所有微服务都可以根据各自的功能轻松开发
    2、独立部署 – 基于其服务,可以在任何应用程序中单独部署它们
    3、故障隔离 – 即使应用程序的一项服务不起作用,系统仍可继续运行
    4、混合技术堆栈 – 可以使用不同的语言和技术来构建同一应用程序的不同服务
    5、粒度缩放 – 单个组件可根据需要进行缩放,无需将所有组件缩放在一起
    缺点:
    1、分布式的复杂性
    2、数据的最终一致性
    3、运维的复杂性
    4、多模块测试测复杂性
    特点:
    1、解耦 – 系统内的服务很大程度上是分离的。每个微服务可独立运行在自己的进程里;因此,整个应用程序可以轻松构建,更改和扩展
    2、组件化 – 微服务被视为可以轻松更换和升级的独立组件,一系列独立运行的微服务共同构建起了整个系统;
    3、业务能力 – 微服务非常简单,专注于单一功能,每个服务为独立的业务开发,一个微服务一般完成某个特定的功能,比如:订单管理、用户管理等;
    4、自治 – 开发人员和团队可以彼此独立工作,从而提高速度
    5、持续交付 – 通过软件创建,测试和批准的系统自动化,允许频繁发布软件
    6、责任 – 微服务不关注应用程序作为项目。相反,他们将应用程序视为他们负责的产品
    7、分散治理 – 重点是使用正确的工具来做正确的工作。这意味着没有标准化模式或任何技术模式。开发人员可以自由选择最有用的工具来解决他们的问题
    8、敏捷 – 微服务支持敏捷开发。任何新功能都可以快速开发并再次丢弃
    在这里插入图片描述
    1、客户端 – 来自不同设备的不同用户发送请求。
    2、身份提供商 – 验证用户或客户身份并颁发安全令牌。(Security、OAuth2)
    3、API 网关 – 处理客户端请求。(Zuul1.0、Gateway)
    4、静态内容 – 容纳系统的所有内容。(CDN)
    5、管理 – 在节点上平衡服务并识别故障。(Hystrix)
    6、服务发现 – 查找微服务之间通信路径的指南。(Eureka、Consul、ZK、Nacos)
    7、内容交付网络 – 代理服务器及其数据中心的分布式网络。
    8、远程服务 – 启用驻留在 IT 设备网络上的远程访问信息。
    9、配置中心 – 实现环境数据配置,动态修改实时更新。(Config、nacos、Apollo)

  5. 什么是 Spring Cloud?
    Spring cloud 流应用程序启动器是基于 Spring Boot 的 Spring 集成应用程序,提供与外部系统的集成。Spring cloud Task,一个生命周期短暂的微服务框架,用于快速构建执行有限数据处理的应用程序。

  6. 为什么使用SpringCloud?
    SpringCloud是一套非常完整的微服务解决方案,俗称“微服务全家桶”,几乎内置了微服务所使用的各种技术,可以不必集成第三方依赖。

  7. SpringCloud各大组件
    在介绍Spring Cloud 全家桶之前,首先要介绍一下Netflix ,Netflix 是一个很伟大的公司,在Spring Cloud项目中占着重要的作用,Netflix 公司提供了包括Eureka、Hystrix、Zuul、Archaius等在内的很多组件,在微服务架构中至关重要,Spring在Netflix 的基础上,封装了一系列的组件,命名为:Spring Cloud Eureka、Spring Cloud Hystrix、Spring Cloud Zuul等。
    在这里插入图片描述

下边对各个组件进行分别得介绍:
5. SpringCloud Eureka 注册中心
我们使用微服务,微服务的本质还是各种API接口的调用,那么我们怎么产生这些接口、产生了这些接口之后如何进行调用那?如何进行管理哪?
答案就是Spring Cloud Eureka,我们可以将自己定义的API 接口注册到Spring Cloud Eureka上,Eureka负责服务的注册于发现,如果学习过Zookeeper的话,就可以很好的理解,Eureka的角色和 Zookeeper的角色差不多,都是服务的注册和发现,构成Eureka体系的包括:服务注册中心、服务提供者、服务消费者。
1、Eureka服务注册中心构成的服务注册中心的主从复制集群;
2、然后服务提供者向注册中心进行注册、续约、下线服务等;
3、服务消费者向Eureka注册中心拉去服务列表并维护在本地(这也是客户端发现模式的机制体现!);
4、然后服务消费者根据从Eureka服务注册中心获取的服务列表选取一个服务提供者进行消费服务。
5. Spring Cloud Ribbon 负载均衡
在上Spring Cloud Eureka描述了服务如何进行注册,注册到哪里,服务消费者如何获取服务生产者的服务信息,但是Eureka只是维护了服务生产者、注册中心、服务消费者三者之间的关系,真正的服务消费者调用服务生产者提供的数据是通过Spring Cloud Ribbon来实现的。
在Eureka中提到了服务消费者是将服务从注册中心获取服务生产者的服务列表并维护在本地的,这种客户端发现模式的方式是服务消费者选择合适的节点进行访问服务生产者提供的数据,这种选择合适节点的过程就是Spring Cloud Ribbon完成的。
Spring Cloud Ribbon客户端负载均衡器由此而来。
负载平衡旨在优化资源使用,最大化吞吐量,最小化响应时间并避免任何单一资源的过载!提高可靠性和可用性!
5. Spring Cloud Feign 声明式接口调用
上述9,10中我们已经使用最简单的方式实现了服务的注册发现和服务的调用操作,如果具体的使用Ribbon调用服务的话,你就可以感受到使用Ribbon的方式还是有一些复杂,因此Spring Cloud Feign应运而生。
Spring Cloud Feign 是一个声明web服务客户端,这使得编写Web服务客户端更容易,使用Feign 创建一个接口并对它进行注解,它具有可插拔的注解支持包括Feign注解与JAX-RS注解,Feign还支持可插拔的编码器与解码器,Spring Cloud 增加了对 Spring MVC的注解,Spring Web 默认使用了HttpMessageConverters, Spring Cloud 集成 Ribbon 和 Eureka 提供的负载均衡的HTTP客户端 Feign。
简单的可以理解为:Spring Cloud Feign 的出现使得Eureka和Ribbon的使用更为简单。
5. Spring Cloud Hystrix 断路器
我们通过上面组件知道了使用Eureka进行服务的注册和发现,使用Ribbon实现服务的负载均衡调用,还知道了使用Feign可以简化我们的编码。但是,这些还不足以实现一个高可用的微服务架构。
例如:当有一个服务出现了故障,而服务的调用方不知道服务出现故障,若此时调用放的请求不断的增加,最后就会等待出现故障的依赖方 相应形成任务的积压,最终导致自身服务的瘫痪。导致服务雪崩。
Spring Cloud Hystrix正是为了解决这种情况的,防止对某一故障服务持续进行访问。Hystrix的含义是:断路器,断路器本身是一种开关装置,用于我们家庭的电路保护,防止电流的过载,当线路中有电器发生短路的时候,断路器能够及时切换故障的电器,防止发生过载、发热甚至起火等严重后果。

什么是服务熔断?什么是服务降级
熔断机制 :是应对雪崩效应的一种微服务链路保护机制。当某个微服务不可用或者响应时间太长时,会进行服务降级,进而熔断该节点微服务的调用,快速返回“错误”的响应信息。当检测到该节点微服务调用响应正常后恢复调用链路。在SpringCloud框架里熔断机制通过Hystrix实现,Hystrix会监控微服务间调用的状况,当失败的调用到一定阈值,缺省是5秒内调用20次,如果失败,就会启动熔断机制。

服务降级 :一般是从整体负荷考虑。就是当某个服务熔断之后,服务器将不再被调用,此时客户端可以自己准备一个本地的fallback回调,返回一个缺省值。这样做,虽然水平下降,但好歹可用,比直接挂掉强。

  1. Spring Cloud Config 配置中心
    对于微服务还不是很多的时候,各种服务的配置管理起来还相对简单,但是当成百上千的微服务节点起来的时候,服务配置的管理变得会复杂起来。
    分布式系统中,由于服务数量巨多,为了方便服务配置文件统一管理,实时更新,所以需要分布式配置中心组件。
    在Spring Cloud中,有分布式配置中心组件Spring Cloud Config ,它支持配置服务放在配置服务的内存中(即本地),也支持放在远程Git仓库中。在Cpring Cloud Config 组件中,分两个角色,一是Config Server,二是Config Client。
    Config Server用于配置属性的存储,存储的位置可以为Git仓库、SVN仓库、本地文件等
    Config Client用于服务属性的读取。
    在这里插入图片描述

  2. Spring Cloud Zuul 服务网关
    我们使用Spring Cloud Netflix中的Eureka实现了服务注册中心以及服务注册与发现;而服务间通过Ribbon或Feign实现服务的消费以及均衡负载;通过Spring Cloud Config实现了应用多环境的外部化配置以及版本管理。为了使得服务集群更为健壮,使用Hystrix的融断机制来避免在微服务架构中个别服务出现异常时引起的故障蔓延。
    目前的结构是这样的:这里写图片描述
    先来说说这样架构需要做的一些事儿以及存在的不足:
    1、首先,破坏了服务无状态特点。为了保证对外服务的安全性,我们需要实现对服务访问的权限控制,而开放服务的权限控制机制将会贯穿并污染整个开放服务的业务逻辑,这会带来的最直接问题是,破坏了服务集群中REST API无状态的特点。从具体开发和测试的角度来说,在工作中除了要考虑实际的业务逻辑之外,还需要额外可续对接口访问的控制处理。
    2、其次,无法直接复用既有接口。当我们需要对一个即有的集群内访问接口,实现外部服务访问时,我们不得不通过在原有接口上增加校验逻辑,或增加一个代理调用来实现权限控制,无法直接复用原有的接口。
    面对类似上面的问题,我们要如何解决呢?下面进入本文的正题:服务网关!
    为了解决上面这些问题,我们需要将权限控制这样的东西从我们的服务单元中抽离出去,而最适合这些逻辑的地方就是处于对外访问最前端的地方,我们需要一个更强大一些的均衡负载器,它就是本文将来介绍的:服务网关。
    服务网关是微服务架构中一个不可或缺的部分。通过服务网关统一向外系统提供REST API的过程中,除了具备服务路由、均衡负载功能之外,它还具备了权限控制等功能。Spring Cloud Netflix中的Zuul就担任了这样的一个角色,为微服务架构提供了前门保护的作用,同时将权限控制这些较重的非业务逻辑内容迁移到服务路由层面,使得服务集群主体能够具备更高的可复用性和可测试性。

  3. Spring Cloud Bus 消息总线
    在Spring Cloud Config中,我们知道的配置文件可以通过Config Server存储到Git等地方,通过Config Client进行读取,但是我们的配置文件不可能是一直不变的,当我们的配置文件放生变化的时候如何进行更新哪?
    一种最简单的方式重启一下Config Client进行重新获取,但Spring Cloud绝对不会让你这样做的,Spring Cloud Bus正是提供一种操作使得我们在不关闭服务的情况下更新我们的配置。
    Spring Cloud Bus官方意义:消息总线!
    当然动态更新服务配置只是消息总线的一个用处,还有很多其他用处。
    在这里插入图片描述

  4. 常见注册中心比较
    在这里插入图片描述
    上图基本表达了注册中心的交互过程, 体现出三种角色之间关系:
    1.服务提供者 Service Provider (Server): 服务启动后向RegistryCenter注册 自己的一个实例 定期向RegistryCenter发送心跳(heartbeat), 证明自己还能苟一会 服务关闭时向RegistryCenter发起注销
    2.服务消费者 Service Consumer(Client): 服务启动后RegistryCenter订阅所需要使用的服务(Server), 并缓存到实例列表中 向对应服务(Server)发起调用时,从内存中的该服务的实例列表选择一个,进行远程调用. 服务关闭时向RegistryCenter取消订阅
    3.注册中心 Service Registry Center: Server超过一定时间未心跳时,从服务的实例列表移除. 服务的实例列表发生变化(新增或者移除)时,通知订阅该服务的 Consumer,从而让 Consumer 能够刷新本地缓存. 有些 注册中心不提供这项功能, 例如Eureka,二手Client 定期轮询更新本地缓存
    大多数情况下一个服务可能既是Client又是 Consumer
    1.CAP理论是分布式架构中重要理论
    一致性(Consistency) :(所有节点在同一时间具有相同的数据)
    可用性(Availability) :(保证每个请求不管成功或者失败都有响应)
    分隔容忍(Partition tolerance): (系统中任意信息的丢失或失败不会影响系统的继续运作)
    由于C与A的特性无法共存.CAP 不可能都取,只能取其中2个,要么AP要么CP
    2.各个注册中心的区别:
    在这里插入图片描述

参考《微服务架构中常见的注册中心

其他面试题可参考《SpringCloudAlibaba个人面试汇总

ZooKepper

  1. ZK是什么?
    ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,是Google的Chubby一个开源的实现。
    它是集群的管理者,监视着集群中各个节点的状态根据节点提交的反馈进行下一步合理操作。最终,将简单易用的接口和性能高效、功能稳定的系统提供给用户。

Dubbo

中间件

消息队列

  1. 消息队列
    01:为什么使用消息队列?
    传统的公司本身的业务体量很小,所以直接单机一把梭啥都能搞定了,但是随着业务体量不断扩大,采用微服务的设计思想分布式的部署方式,所以拆分了很多的服务,随着体量的增加以及业务场景越来越复杂,很多场景单机的技术栈和中间件以及不够用了,而且对系统的友好性也下降了,最后做了很多技术选型的工作,所以决定引入消息队列中间件
    02:消息队列解决的核心问题
    异步、削峰、解耦
    03:01-异步:
    我们之前的场景里面有很多步骤都是在一个流程里面需要做完的,就比如说我的下单系统吧,本来我们业务简单,下单了付了钱就好了,流程就走完了。
    但是后面来了个产品经理,搞了个优惠券系统,OK问题不大,流程里面多100ms去扣减优惠券。
    后来产品经理灵光一闪说我们可以搞个积分系统啊,也行吧,流程里面多了200ms去增减积分。
    再后来后来隔壁的产品老王说:下单成功后我们要给用户发短信,也将就吧,100ms去发个短信。

    在这里插入图片描述
    链路长了就慢了,但是我们发现上面的流程是同时做的呀,你支付成功后,我去校验优惠券的同时我可以去增减积分啊,还可以同时发个短信啊。
    那正常的流程我们是没办法实现的呀,怎么办,异步。
    你对比一下是不是发现,这样子最多只用100毫秒用户知道下单成功了,至于短信你迟几秒发给他他根本不在意是吧。
    在这里插入图片描述
    如果说此处你想用线程或者线程池实现的话,会引入大量的开发,并且维护成本很高,指不定多会儿就会有坑!
    03:02-解耦:
    下单之后,你就把你支付成功的消息告诉别的系统,他们收到了去处理就好了,你只用走完自己的流程,把自己的消息发出去,那后面要接入什么系统简单,直接订阅你发送的支付成功消息,你支付成功了他们监听就好了。
    在这里插入图片描述
    说白了:核心系统只需要关注自身的业务逻辑即可,与其他系统解耦,各个系统之间独立维护,降低系统的风险。
    03:03-削峰:
    平时流量很低,但是你要做秒杀活动00 :00的时候流量疯狂怼进来,你的服务器,Redis,MySQL各自的承受能力都不一样,你直接全部流量照单全收肯定有问题啊,直接就打挂了。
    所以把请求放到队列里面,然后至于每秒消费多少请求,就看自己的服务器处理能力,你能处理5000QPS你就消费这么多,可能会比正常的慢一点,但是不至于打挂服务器,等流量高峰下去了,你的服务也就没压力了。
    你看阿里双十一12:00的时候这么多流量瞬间涌进去,他有时候是不是会慢一点,但是人家没挂啊,或者降级给你个友好的提示页面,等高峰过去了又是一条好汉了。
    04:消息队列的劣势
    系统复杂性
    本来简单的一个系统,现在凭空接入一个中间件在那,就需要考虑去维护他,而且使用的过程中更要考虑各种问题,比如消息重复消费、消息丢失、消息的顺序消费等等。
    数据一致性
    这个其实是分布式服务本身就存在的一个问题,不仅仅是消息队列的问题,但是放在这里说是因为用了消息队列这个问题会暴露得比较严重一点。
    就像开头说的,下单的服务自己保证自己的逻辑成功处理了,成功发了消息,但是优惠券系统,积分系统等等这么多系统,他们成功还是失败你就不管了?
    所有的服务都成功才能算这一次下单是成功的,那怎么才能保证数据一致性呢?
    使用分布式事务:把下单,优惠券,积分。。。都放在一个事务里面一样,要成功一起成功,要失败一起失败。
    可用性
    搞个系统本身没啥问题,现在突然接入一个中间件在那放着,万一挂了怎么办?我下个单MQ挂了,优惠券不扣了,积分不加了,这不是杀一个程序员能搞定的吧,感觉得杀一片。
    需要使用集群部署,分布式部署等解决
    05:常用的消息中间件
    在这里插入图片描述
    大家其实一下子就能看到差距了,就拿吞吐量来说,早期比较活跃的ActiveMQ 和RabbitMQ基本上不是后两者的对手了,在现在这样大数据的年代吞吐量是真的很重要。
    比如现在突然爆发了一个超级热点新闻,你的APP注册用户高达亿数,你要想办法第一时间把突发全部推送到每个人手上,你没有大吞吐量的消息队列中间件用啥去推?
    再说这些用户大量涌进来看了你的新闻产生了一系列的附带流量,你怎么应对这些数据,很多场景离开消息队列基本上难以为继。
    就部署方式而言前两者也是大不如后面两个天然分布式架构的哥哥,都是高可用的分布式架构,而且数据多个副本的数据也能做到0丢失。
    我们再聊一下RabbitMQ这个中间件其实还行,但是这玩意开发语言居然是erlang,我敢说绝大部分工程师肯定不会为了一个中间件去刻意学习一门语言的,开发维护成本你想都想不到,出个问题查都查半天。
    至于RocketMQ(阿里开源的),git活跃度还可以。基本上你push了自己的bug确认了有问题都有阿里大佬跟你试试解答并修复的,我个人推荐的也是这个,他的架构设计部分跟同样是阿里开源的一个RPC框架是真的很像(Dubbo)可能是因为师出同门的原因吧。
    Kafka我放到最后说,你们也应该知道了,压轴的这是个大哥,大数据领域,公司的日志采集,实时计算等场景,都离不开他的身影,他基本上算得上是世界范围级别的消息队列标杆了。
    参考大神敖丙三太子面试官问我:什么是消息队列?什么场景需要他?用了会出现什么问题?》(ps:可以多关注此人,分享的帖子着实不错!非广告不认识纯仰慕)
    ================================================================================

Redis

  1. Redis中间件常见面试题
    缓存常见问题
    在这里插入图片描述
    02:001:缓存的类型
    本地缓存:
    本地缓存就是在进程的内存中进行缓存,比如我们的 JVM 堆中,可以用 LRUMap 来实现,也可以使用 Ehcache 这样的工具来实现。
    本地缓存是内存访问,没有远程交互开销,性能最好,但是受限于单机容量,一般缓存较小且无法扩展。
    分布式缓存:
    分布式缓存可以很好得解决这个问题。
    分布式缓存一般都具有良好的水平扩展能力,对较大数据量的场景也能应付自如。缺点就是需要进行远程请求,性能不如本地缓存。
    多级缓存:
    为了平衡这种情况,实际业务中一般采用多级缓存,本地缓存只保存访问频率最高的部分热点数据,其他的热点数据放在分布式缓存中。
    在目前的一线大厂中,这也是最常用的缓存方案,单考单一的缓存方案往往难以撑住很多高并发的场景。
    02:002:淘汰策略
    不管是本地缓存还是分布式缓存,为了保证较高性能,都是使用内存来保存数据,由于成本和内存限制,当存储的数据超过缓存容量时,需要对缓存的数据进行剔除。
    一般的剔除策略有
    FIFO 淘汰最早数据
    LRU 剔除最近最少使用
    LFU 剔除最近使用频率最低的数据几种策略。
    noeviction:返回错误当内存限制达到并且客户端尝试执行会让更多内存被使用的命令(大部分的写入指令,但DEL和几个例外)
    allkeys-lru: 尝试回收最少使用的键(LRU),使得新添加的数据有空间存放。
    volatile-lru: 尝试回收最少使用的键(LRU),但仅限于在过期集合的键,使得新添加的数据有空间存放。
    allkeys-random: 回收随机的键使得新添加的数据有空间存放。
    volatile-random: 回收随机的键使得新添加的数据有空间存放,但仅限于在过期集合的键。
    volatile-ttl: 回收在过期集合的键,并且优先回收存活时间(TTL)较短的键,使得新添加的数据有空间存放。
    如果没有键满足回收的前提条件的话,策略volatile-lru, volatile-random以及volatile-ttl就和noeviction 差不多了。
    其实在大家熟悉的LinkedHashMap中也实现了Lru算法的,实现如下:
    在这里插入图片描述
    当容量超过100时,开始执行LRU策略:将最近最少未使用的 TimeoutInfoHolder 对象 evict 掉。
    ================================================================================
    02:002:Redis
    在这里插入图片描述
    与 MC 不同的是,Redis 采用单线程模式处理请求。这样做的原因有 2 个:一个是因为采用了非阻塞的异步事件处理机制;另一个是缓存数据都是内存操作 IO 时间不会太长,单线程可以避免线程上下文切换产生的代价。
    Redis 支持持久化,所以 Redis 不仅仅可以用作缓存,也可以用作 NoSQL 数据库。
    相比 MC,Redis 还有一个非常大的优势,就是除了 K-V 之外,还支持多种数据格式,例如 list、set、sorted set、hash 等。
    Redis 提供主从同步机制,以及 Cluster 集群部署能力,能够提供高可用服务。
    ================================================================================
    问:Redis为什么这么快!!!
    Redis采用的是基于内存的采用的是单进程单线程模型的 KV 数据库,由C语言编写,官方提供的数据是可以达到100000+的QPS(每秒内查询次数)。
    01完全基于内存: 绝大部分请求是纯粹的内存操作,非常快速。它的,数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1);
    02数据结构简单: 对数据操作也简单,Redis中的数据结构是专门进行设计的;
    03采用单线程: 避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题 不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;
    04使用多路I/O复用模型: 非阻塞IO;
    05使用底层模型不同: 它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求
    在这里插入图片描述
    ================================================================================
    02:003:Redis数据类型
    (一)String
    这是最简单的类型,就是普通的 set 和 get,做简单的 KV 缓存。
    String的实际应用场景比较广泛的有:
    缓存功能:String字符串是最常用的数据类型,不仅仅是Redis,各个语言都是最基本类型,因此,利用Redis作为缓存,配合其它数据库作为存储层,利用Redis支持高并发的特点,可以大大加快系统的读写速度、以及降低后端数据库的压力。
    计数器:许多系统都会使用Redis作为系统的实时计数器,可以快速实现计数和查询的功能。而且最终的数据结果可以按照特定的时间落地到数据库或者其它存储介质当中进行永久保存。
    共享用户Session:用户重新刷新一次界面,可能需要访问一下数据进行重新登录,或者访问页面缓存Cookie,但是可以利用Redis将用户的Session集中管理,在这种模式只需要保证Redis的高可用,每次用户Session的更新和获取都可以快速完成。大大提高效率。
    (二)Hash
    这个是类似 Map 的一种结构,这个一般就是可以将结构化的数据,比如一个对象(前提是这个对象没嵌套其他的对象)给缓存在 Redis 里,然后每次读写缓存的时候,可以就操作 Hash 里的某个字段。
    但是这个的场景其实还是多少单一了一些,因为现在很多对象都是比较复杂的,比如你的商品对象可能里面就包含了很多属性,其中也有对象。我自己使用的场景用得不是那么多。
    (三)List
    List 是有序列表,这个还是可以玩儿出很多花样的。
    比如可以通过 List 存储一些列表型的数据结构,类似粉丝列表、文章的评论列表之类的东西。
    比如可以通过 lrange 命令,读取某个闭区间内的元素,可以基于 List 实现分页查询,这个是很棒的一个功能,基于 Redis 实现简单的高性能分页,可以做类似微博那种下拉不断分页的东西,性能高,就一页一页走。
    比如可以搞个简单的消息队列,从 List 头怼进去,从 List 屁股那里弄出来。
    List本身就是我们在开发过程中比较常用的数据结构了,热点数据更不用说了。
    01消息队列
    Redis的链表结构,可以轻松实现阻塞队列,可以使用左进右出的命令组成来完成队列的设计。比如:数据的生产者可以通过Lpush命令从左边插入数据,多个数据消费者,可以使用BRpop命令阻塞的“抢”列表尾部的数据。
    (02文章列表或者数据分页展示的应用
    比如,我们常用的博客网站的文章列表,当用户量越来越多时,而且每一个用户都有自己的文章列表,而且当文章多时,都需要分页展示,这时可以考虑使用Redis的列表,列表不但有序同时还支持按照范围内获取元素,可以完美解决分页查询功能。大大提高查询效率。
    (四)Set
    Set 是无序集合,会自动去重的那种。
    直接基于 Set 将系统里需要去重的数据扔进去,自动就给去重了,如果你需要对一些数据进行快速的全局去重,你当然也可以基于 JVM 内存里的 HashSet 进行去重,但是如果你的某个系统部署在多台机器上呢?得基于Redis进行全局的 Set 去重。
    可以基于 Set 玩儿交集、并集、差集的操作,比如交集吧,我们可以把两个人的好友列表整一个交集,看看俩人的共同好友是谁?对吧。
    反正这些场景比较多,因为对比很快,操作也简单,两个查询一个Set搞定。
    (五)Sorted Set
    Sorted set 是排序的 Set,去重但可以排序,写进去的时候给一个分数,自动根据分数排序。
    有序集合的使用场景与集合类似,但是set集合不是自动有序的,而Sorted set可以利用分数进行成员间的排序,而且是插入时就排序好。所以当你需要一个有序且不重复的集合列表时,就可以选择Sorted set数据结构作为选择方案。
    排行榜:有序集合经典使用场景。例如视频网站需要对用户上传的视频做排行榜,榜单维护可能是多方面:按照时间、按照播放量、按照获得的赞数等。
    用Sorted Sets来做带权重的队列:比如普通消息的score为1,重要消息的score为2,然后工作线程可以选择按score的倒序来获取工作任务。让重要的任务优先执行。
    微博热搜榜:就是有个后面的热度值,前面就是名称
    (六)高级用法
    pub/sub:
    功能是订阅发布功能,可以用作简单的消息队列;
    Bitmap :
    位图是支持按 bit 位来存储信息,可以用来实现 布隆过滤器(BloomFilter);
    HyperLogLog:
    供不精确的去重计数功能,比较适合用来做大规模数据的去重统计,例如统计 UV(独立访客);
    Geospatial:
    可以用来保存地理位置,并作位置距离计算或者根据半径计算位置等。有没有想过用Redis来实现附近的人?或者计算最优地图路径?
    Pipeline:
    可以批量执行一组指令,一次性返回全部结果,可以减少频繁的请求应答。
    Lua:
    Redis 支持提交 Lua 脚本来执行一系列的功能。
    事务:
    最后一个功能是事务,但 Redis 提供的不是严格的事务,Redis 只保证串行执行命令,并且能保证全部执行,但是执行命令失败时并不会回滚,而是会继续执行下去。
    ================================================================================
    02:004:Redis的持久化
    Redis 提供了 RDB 和 AOF 两种持久化方式,RDB 是把内存中的数据集以快照形式写入磁盘,实际操作是通过 fork 子进程执行,采用二进制压缩存储;AOF 是以文本日志的形式记录 Redis 处理的每一个写入或删除操作。
    RDB: 把整个 Redis 的数据保存在单一文件中,比较适合用来做灾备,但缺点是快照保存完成之前如果宕机,这段时间的数据将会丢失,另外保存快照时可能导致服务短时间不可用。
    AOF :对日志文件的写入操作使用的追加模式,有灵活的同步策略,支持每秒同步、每次修改同步和不同步,缺点就是相同规模的数据集,AOF 要大于 RDB,AOF 在运行效率上往往会慢于 RDB。
    两种方式都可以把Redis内存中的数据持久化到磁盘上,然后再将这些数据备份到别的地方去,RDB更适合做冷备AOF更适合做热备,比如我杭州的某电商公司有这两个数据,我备份一份到我杭州的节点,再备份一个到上海的,就算发生无法避免的自然灾害,也不会两个地方都一起挂吧,这灾备也就是异地容灾,地球毁灭他没办法。
    问:那这两种机制各自优缺点是啥?
    先说RDB吧
    优点
    他会生成多个数据文件,每个数据文件分别都代表了某一时刻Redis里面的数据,这种方式,有没有觉得很适合做冷备,完整的数据运维设置定时任务,定时同步到远端的服务器,比如阿里的云服务,这样一旦线上挂了,你想恢复多少分钟之前的数据,就去远端拷贝一份之前的数据就好了。
    RDB对Redis的性能影响非常小,是因为在同步数据的时候他只是fork了一个子进程去做持久化的,而且他在数据恢复的时候速度比AOF来的快。
    缺点
    RDB都是快照文件,都是默认五分钟甚至更久的时间才会生成一次,这意味着你这次同步到下次同步这中间五分钟的数据都很可能全部丢失掉。AOF则最多丢一秒的数据,数据完整性上高下立判。
    还有就是RDB在生成数据快照的时候,如果文件很大,客户端可能会暂停几毫秒甚至几秒,你公司在做秒杀的时候他刚好在这个时候fork了一个子进程去生成一个大快照,哦豁,出大问题。
    再来说说AOF
    优点:
    上面提到了,RDB五分钟一次生成快照,但是AOF是一秒一次去通过一个后台的线程fsync操作,那最多丢这一秒的数据。
    AOF在对日志文件进行操作的时候是以append-only的方式去写的,他只是追加的方式写数据,自然就少了很多磁盘寻址的开销了,写入性能惊人,文件也不容易破损。
    AOF的日志是通过一个叫非常可读的方式记录的,这样的特性就适合做灾难性数据误删除的紧急恢复了,比如公司的实习生通过flushall清空了所有的数据,只要这个时候后台重写还没发生,你马上拷贝一份AOF日志文件,把最后一条flushall命令删了就完事了。
    tip:我说的命令你们别真去线上系统操作啊,想试去自己买的服务器上装个Redis试,别到时候来说,啊你真是个渣男,害我把服务器搞崩了,Redis官网上的命令都去看看,不要乱试!!!
    缺点:
    一样的数据,AOF文件比RDB还要大。
    AOF开启后,Redis支持写的QPS会比RDB支持写的要低,他不是每秒都要去异步刷新一次日志嘛fsync,当然即使这样性能还是很高,我记得ElasticSearch也是这样的,异步刷新缓存区的数据去持久化,为啥这么做呢,不直接来一条怼一条呢,那我会告诉你这样性能可能低到没办法用的,大家可以思考下为啥哟。
    所以你单独用RDB你会丢失很多数据,你单独用AOF,你数据恢复没RDB来的快,真出什么问题的时候第一时间用RDB恢复,然后AOF做数据补全,真香!冷备热备一起上,才是互联网时代一个高健壮性系统的王道。
    ================================================================================
    02:005:Redis高可用
    来看 Redis 的高可用。Redis 支持主从同步,提供 Cluster 集群部署模式,通过 Sentine l哨兵来监控 Redis 主服务器的状态。当主挂掉时,在从节点中根据一定策略选出新主,并调整其他从 slaveof 到新主。
    选主的策略简单来说有三个:
    01:slave 的 priority 设置的越低,优先级越高;
    02:同等情况下,slave 复制的数据越多优先级越高;
    03:相同的条件下 runid 越小越容易被选中。
    在 Redis 集群中,sentinel 也会进行多实例部署,sentinel 之间通过 Raft 协议来保证自身的高可用。
    Redis Cluster 使用分片机制,在内部分为 16384 个 slot 插槽,分布在所有 master 节点上,每个 master 节点负责一部分 slot。数据操作时按 key 做 CRC16 来计算在哪个 slot,由哪个 master 进行处理。数据的冗余是通过 slave 节点来保障。
    哨兵
    哨兵必须用三个实例去保证自己的健壮性的,哨兵+主从并不能保证数据不丢失,但是可以保证集群的高可用。
    在这里插入图片描述
    M1所在的机器挂了,哨兵还有两个,两个人一看他不是挂了嘛,那我们就选举一个出来执行故障转移不就好了。
    小结下哨兵组件的主要功能
    集群监控:负责监控 Redis master 和 slave 进程是否正常工作。
    消息通知:如果某个 Redis 实例有故障,那么哨兵负责发送消息作为报警通知给管理员。
    故障转移:如果 master node 挂掉了,会自动转移到 slave node 上。
    配置中心:如果故障转移发生了,通知 client 客户端新的 master 地址。
    主从
    提到这个,就跟我前面提到的数据持久化的RDB和AOF有着比密切的关系了。
    我先说下为啥要用主从这样的架构模式,前面提到了单机QPS是有上限的,而且Redis的特性就是必须支撑读高并发的,那你一台机器又读又写,这谁顶得住啊,不当人啊!但是你让这个master机器去写,数据同步给别的slave机器,他们都拿去读,分发掉大量的请求那是不是好很多,而且扩容的时候还可以轻松实现水平扩容
    主从
    启动一台slave 的时候,他会发送一个psync命令给master ,如果是这个slave第一次连接到master,他会触发一个全量复制。master就会启动一个线程,生成RDB快照,还会把新的写请求都缓存在内存中,RDB文件生成后,master会将这个RDB发送给slave的,slave拿到之后做的第一件事情就是写进本地的磁盘,然后加载进内存,然后master会把内存里面缓存的那些新命名都发给slave。后续有新的数据时使用AOF进行更新!
    ================================================================================
    02:005:缓存常见问题
    【缓存更新方式】
    这是决定在使用缓存时就该考虑的问题。
    缓存的数据在数据源发生变更时需要对缓存进行更新,数据源可能是 DB,也可能是远程服务。更新的方式可以是主动更新。数据源是 DB 时,可以在更新完 DB 后就直接更新缓存
    当数据源不是 DB 而是其他远程服务,可能无法及时主动感知数据变更,这种情况下一般会选择对缓存数据设置失效期,也就是数据不一致的最大容忍时间。这种场景下,可以选择失效更新,key 不存在或失效时先请求数据源获取最新数据,然后再次缓存,并更新失效期。但这样做有个问题,如果依赖的远程服务在更新时出现异常,则会导致数据不可用。改进的办法是异步更新,就是当失效时先不清除数据,继续使用旧的数据,然后由异步线程去执行更新任务。这样就避免了失效瞬间的空窗期。另外还有一种纯异步更新方式,定时对数据进行分批更新。实际使用时可以根据业务场景选择更新方式。
    【数据不一致】
    第二个问题是数据不一致的问题,可以说只要使用缓存,就要考虑如何面对这个问题。缓存不一致产生的原因一般是主动更新失败,例如更新 DB 后,更新 Redis 因为网络原因请求超时;
    或者是异步更新失败导致。解决的办法是,如果服务对耗时不是特别敏感可以增加重试;如果服务对耗时敏感可以通过异步补偿任务来处理失败的更新,或者短期的数据不一致不会影响业务,那么只要下次更新时可以成功,能保证最终一致性就可以。
    【缓存击穿】
    缓存击穿,就是某个热点数据失效时,大量针对这个数据的请求会穿透到数据源。
    解决这个问题有如下办法。
    可以使用互斥锁更新,保证同一个进程中针对同一个数据不会并发请求到 DB,减小 DB 压力。
    使用随机退避方式,失效时随机 sleep 一个很短的时间,再次查询,如果失败再执行更新。
    针对多个热点 key 同时失效的问题,可以在缓存时使用固定时间加上一个小的随机数,避免大量热点 key 同一时刻失效。
    【缓存穿透】
    缓存穿透。产生这个问题的原因可能是外部的恶意攻击,例如,对用户信息进行了缓存,但恶意攻击者使用不存在的用户id频繁请求接口,导致查询缓存不命中,然后穿透 DB 查询依然不命中。这时会有大量请求穿透缓存访问到 DB。
    解决的办法如下。
    对不存在的用户,在缓存中保存一个空对象进行标记,防止相同 ID 再次访问 DB。不过有时这个方法并不能很好解决问题,可能导致缓存中存储大量无用数据
    使用 BloomFilter 过滤器,BloomFilter 的特点是存在性检测,如果 BloomFilter 中不存在,那么数据一定不存在;如果 BloomFilter 中存在,实际数据也有可能会不存在。非常适合解决这类的问题。(可以确定一定不存在!)
    【缓存雪崩】
    缓存雪崩,产生的原因是缓存挂掉,这时所有的请求都会穿透到 DB。
    解决方法:
    使用快速失败的熔断策略,减少 DB 瞬间压力;
    使用主从模式集群模式来尽量保证缓存服务的高可用
    实际场景中,这两种方法会结合使用。

分布式锁

参考资料:通俗讲解分布式锁
1.分布式锁的实现有哪些?
有数据库方式
有Redis分布式
有Zookeeper分布式锁等等。

2.Redis为什么能实现分布式锁?
首先是因为Redis是单线程的,这里的单线程是指Redis的网络请求模块使用的是单线程,即一个线程处理所有网络请求。其他模块任然是多线程的。
Redis可以实现分布式锁主要是因为 setnx key value 属性的存在,当设置key成功时返回 :1 失败时返回 :0
当其他线程在设置时判断返回值即可得知当前资源是否有被其他线程占用。若被占用则继续等待,若key处理完之后被删除,则重新设置时返回:1
通过上面的方式,我们好像是解决了分布式锁的问题,但是想想还有没有什么问题呢?
对,问题还是有的,可能会有死锁的问题发生,比如服务器1设置完之后,获取了锁之后,忽然发生了宕机。
那后续的删除key操作就没法执行,这个key会一直在Redis中存在,其他服务器每次去检查,都会返回0,他们都会认为有人在使用锁,我需要等。
为了解决这个死锁的问题,我们就需要给key设置有效期了。
设置的方式有2种:
第一种就是在set完key之后,直接设置key的有效期 “expire key timeout” ,为key设置一个超时时间,单位为second,超过这个时间锁会自动释放,避免死锁。
这种方式相当于,把锁持有的有效期,交给了Redis去控制。如果时间到了,你还没有给我删除key,那Redis就直接给你删了,其他服务器就可以继续去setnx获取锁。

第二种方式,就是把删除key权利交给其他的服务器,那这个时候就需要用到value值了,比如服务器1,设置了value也就是timeout为当前时间+1秒 ,这个时候服务器2通过get发现时间已经超过系统当前时间了,那就说明服务器1没有释放锁,服务器1可能出问题了,服务器2就开始执行删除key操作,并且继续执行setnx操作。

3.为什么Zookeeper可实现分布式锁?
ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,是Google的Chubby一个开源的实现,是Hadoop和Hbase的重要组件。
ZooKeeper就像是我们的电脑文件系统,我们可以在d盘中创建文件夹a,并且可以继续在文件夹a中创建文件夹a1,a2。
zk主要有四种类型的节点:
持久性节点
持久性顺序节点
临时性节点
临时性顺序节点

顺序性节点,顺序性节点是指,在创建节点的时候,ZooKeeper会自动给节点编号比如0000001,0000002这种的。

4.在Zookeeper中如何加锁?
使用临时性顺序节点在并发请求的同时创建多个临时顺序节点,每个节点监听上一个节点,当上一个节点处理完毕之后即可进行处理。
例如当001节点处理完毕,删除节点后,002收到通知,去获取锁,开始执行,执行完毕,删除节点,通知003~以此类推。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值