万字面试知识点助力金九银十

关注微信公众号:CoderLi,回复面试获取PDF版本

说明

本文档为本人整理网上资源以及自己的一些知识点、为面试准备的。当时整理的时候并没有考虑到发布出来、所以对于引用整理的网上的一些文章链接可能并没有列出来、抱歉!如有请评论告知,谢谢

引用

  • 周志明-深入理解JVM(第三版)
  • Redis 深度历险
  • 深入理解 Kafka
  • Mysql 技术内幕
  • 高性能Mysql
  • paxos 到 Zookeeper
  • Spring Cloud 微服务实战
  • https://juejin.im/post/6844903860658503693
    https://blog.csdn.net/ThinkWon/article/details/104397516
    https://www.jianshu.com/p/86ef67514a0f
    https://juejin.im/post/6844903635252412430
    https://juejin.im/post/6844903830442737671
    https://learnku.com/articles/38925#389a5d
    https://www.cnblogs.com/xiaolincoding/p/12442435.html
    https://blog.csdn.net/qzcsu/article/details/72861891

技术

  • JVM 调优
    • cms
    • g1
    • 调优工具
  • Spring
    • 看我自己的 Blog 就好了
    • 面试题
  • Redis
  • Kafka
  • Mysql
  • Zookeeper
  • Java
  • Spring Boot /Cloud

项目相关

  • 重复生单 ZK 分布式锁 (重复生单事故)
  • 分布式 ID
  • 支付订单状态以及预存款扣除
  • Kafka 事故

业务逻辑与数据

  • 虚拟拼接

  • B2B 相关流程

JVM 调优

运行时数据区域

  • 程序计数器
  • 虚拟机栈
  • 本地方法栈
  • 方法区

垃圾收集器需要做的三件事情

  • 哪些内存需要回收
  • 什么时候回收
  • 怎么回收

回收方法区

判断一个类型不再被使用

  • 该类的所有实例都被回收
  • 加载该类的类加载器被回收掉了
  • 该类的 Class 对象没有被任何地方引用

分代假说

  • 弱分代假说、绝大多数对象都是朝生夕灭的
  • 强分代假说、熬过 GC 次数越多的对象、就很难会消亡
  • 跨代引用相对于同代引用来说仅仅是占少数

标记-清除

  • 标记阶段
  • 清除阶段

标记存活的对象、统一回收未被标记的对象

缺点

  • 执行效率不稳定、当堆中存在大量对象、大部分需要被回收、那么清除阶段需要进行大量的工作
  • 内存碎片

标记-复制

缺点

  • 不能使用全部的内存进行对象的分配
  • 如果按比例的话、就需要进行分配担保
  • 存活对象较多的时候、需要很多复制、效率会低

标记-整理

  • 移动对象则内存回收时更复杂
  • 不移动则内存分配时更复杂

从垃圾收集的停顿时间来看、不移动对象停顿时间更加短、甚至可以不停顿、但是从整个程序的吞吐量来看、移动对象会更加划算。因为内存分配和访问相比垃圾收集的频率要高很多,这部分的耗时增加、吞吐量就会下降了。

Parallel Old 使用的就是标记整理算法、关注点就是吞吐量

CMS 使用的就是标记-清除算法、关注点就是停顿时间

还有一种就是和稀泥的方法、就是 CMS 那样、平时采用标记-清除算法、暂时容忍内存碎片、直到内存碎片化程度大到影响对象分配、再采用标记-整理算法收集一次、以获得内存的规整

记忆集和卡表

垃圾收集器在新生代中建立了名为记忆集 Remember Set 的数据结构、用来避免整个老年代加进 GC Roots 扫描

记忆集是一种用于记录从非收集区域指向收集区域集合的抽象数据结构

收集器只要通过记忆集判断出某一块非收集区域是否存在指向了收集区域的指针就可以了、并不需要了解这些跨代引用的详细细节。

第三种就是称为 卡表 的方式去实现的。

记忆集是抽象概念、卡表是具体的实现。

卡表最简单的形式可以只是一个字节数组、HotSpot 确实也是这么做的。

字节数组中每个元素都对应着其标示的内存区域中一块特定大小的内存,这一块内存称为卡页。

万字面试知识点助力金九银十_第1张图片

三色标记

  • 白色、表示对象尚未被垃圾收集器访问过、如果在可达性分析开始阶段、所有对象都是白色的、如果在结束阶段、对象依然是白色、则代表对象不可达
  • 黑色、表示对象已经被垃圾收集器访问过了、并且整个对象的所有引用都扫描过了、黑色代表这个对象存活、如果有其他对象指向黑色对象、无须重新扫描一遍、黑色对象不可能直接指向白色对象
  • 灰色、表示对象已经被垃圾收集器访问过、对这个对象至少存在一个引用没有被扫描过

当且仅当都满足以下两个条件、会产生对象消失的问题

  • 插入一条或多条从黑色对象到白色对象的新引用
  • 删除了全部从灰色对象到该白色对象的直接或间接引用

要解决这个并发扫描时对象消息的问题、只需要破坏其中的任意一条就可以了

  • 增量更新、破坏的是第一条 CMS
  • 原始快照、破坏的是第二条、G1

经典的垃圾收集器

万字面试知识点助力金九银十_第2张图片

Serial

单线程工作的收集器

万字面试知识点助力金九银十_第3张图片

ParNew

万字面试知识点助力金九银十_第4张图片

ParNew 收集器出了支持多线程并行收集之外、其他与Serial 收集器相比并无太多创新。

还有一个重要的特性 : 除了 Serial 收集器、它是第二个可以与 CMS 收集器配合工作

  • 并行 Parallel:并行描述的是多条垃圾收集器线程之间的关系、说明同一时间有多条这样的线程协同工作、通常默认此时用户线程处于等待状态
  • 并发 Concurrent:并发描述的是垃圾收集器与用户线程之间的关系、说明同一时间垃圾收集器线程与用户线程都在工作。

Parallel Scavenge

新生代收集器、同样是基于标记-复制算法实现的收集器。

Parallel Scavenge 收集器的目标是达到一个可控制的吞吐量,CMS 收集器则更加关注缩短垃圾收集器用户线程停止的时间。

  • -XX:MaxGCPauseMillis 允许的值是一个大于 0 的毫秒数、收集器将尽力保证内存回收花费的时间不超过用户设定值。垃圾收集器停顿时间缩短是以牺牲吞吐量和新生代空间为代价换取的:系统把新生代调得小一些、收集 300MB 新生代肯定比收集 500MB 新生代快、但也导致了垃圾收集的频率变得更加频繁了,原来 10s 收集一次每次停顿 100 毫秒,现在变成5秒收集一次,每次停顿 70 毫秒。停顿确实在下降、但是吞吐量也下来了。
  • -XX:GCTimeRatio参数的值应该是一个大于 0 小于 100 的数、也就是垃圾收集时间占总时间的比率,相当于吞吐量的倒数。比如设置参数的值为 19 那么允许的最大垃圾收集时间就占总时间的 5% 因为 1/(19+1) 。
  • -XX:+UseAdaptiveSizePolicy 当这个参数被激活后、就不需要人工置顶新生代的大小、Eden、Survivor的比例、晋升老年代对象大小等细节参数。虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或最大吞吐量。这种调节方式称为垃圾收集的自适应调节策略(GC Ergonomics)

Serial Old

是 Serial 收集器的老年代版本,单线程收集器、使用标记整理算法。

用途

  • JDK5 以及之前的版本中与 Parallel Scavenge 收集器搭配使用
  • 另外一种就是作为 CMS 收集器发生失败时的后备预案,在并发收集发生 Concurrent Mode Failure 是使用

万字面试知识点助力金九银十_第5张图片

Parallel Old

是 Parallel Scavenge 收集器的老年代版本、支持多线程并发收集,基于标记整理算法实现。

万字面试知识点助力金九银十_第6张图片

CMS

concurrent mark sweep 收集器是以获取最短停顿时间为目标的收集器。是基于标记-清除算法实现的。

  • 初始标记
  • 并发标记
  • 并发预清理
  • 并发可取消预清理
  • 最终标记
  • 并发清除
  • 并发重置

初始标记

STW 事件、此阶段的目标主要是标记老年代中所有存活的对象、包括 GC Roots 的直接引用、以及由年轻代存活对象所引用的对象(这个也很重要、因为老年代是独立进行回收的)

万字面试知识点助力金九银十_第7张图片

并发标记 在此阶段、垃圾收集器遍历老年代、标记所有存活的对象。此阶段、垃圾收集线程与应用线程并发执行、不用暂停应用。在此阶段、并非所有老年代中存活的对象都在此阶段被被标记、因为标记过程中对象的引用关系还在发生变化。

万字面试知识点助力金九银十_第8张图片

并发预清理 同样不需要 STW 、此阶段记录在并发标记过程中新增的关系引用。然后以此关系链中的黑色对象作为根、重新遍历其引用关系、标记新增的对象。这个在三色标记中有提及到。

万字面试知识点助力金九银十_第9张图片

万字面试知识点助力金九银十_第10张图片

并发可取消预清理 同样不需要 STW 、尝试在 最终标记之前尽可能多做一些工作。

最终标记 最后一次 STW 。本阶段的目标是完成老年代中所有存活对象的标记。因为之前的预清理阶段是并发的、有可能对象之间的引用关系变化没有很好的记录下来、所有需要 STW 来处理。

在这个阶段之后、老年代中的所有存活对象都被标记了

并发清除 不需要 STW、目标是删除未使用的对象、并回收他们占用的空间。

万字面试知识点助力金九银十_第11张图片

并发重置 不需要 STW 、重置 CMS 相关的内部数据、为下次 GC 准备。

CMS 有三个缺点

  • 对处理器资源非常敏感。在并发阶段预应用程序并发执行,虽然不用 STW、但是也会因为占用了一部分线程而导致应用程序变慢,降低吞吐量。CMS 默认启动对回收线程数是:(处理器核心数量 + 3) /4。如果核心数量大于等于4,那么不会占用超过 25% 的处理器运算资源,但是当处理器资源不足四个时,CMS 对程序影响就会变得很大。
  • 由于 CMS 收集器无法处理浮动垃圾、有可能出现 “Concurrent Mode Failure“ 进而导致另一次完全 STW 的 Full GC 产生 。浮动垃圾 : 在 CMS 并发标记和并发清除阶段、用户线程还是继续在运行、这个阶段肯定会有新的垃圾不断产生、但这一部分垃圾对象是出现在标记过程结束以后、CMS 无法在当次垃圾收集中处理掉它们,只好留到下一次垃圾收集时再清掉。同样也是由于在垃圾收集阶段用户线程还需要持续运行、需要预留足够内存空间提供给用户线程使用、因此 CMS 收集器不能像其他收集器那样等待到老年代几乎完全被填满了再进行收集、必须预留一部分空间供并发收集时程序运作使用。JDK6 时、CMS 收集器启动的阈值时 92% ,这样子会面临一种风险、要是 CMS 运行期间预留的内存无法满足程序分配对象的需要、就会出现一次并发失败、这时候虚拟机将不得不启动后备预案、暂停用户现象、临时启用 Serial Old 收集器来进行老年年代的垃圾收集,但这样子的话、停顿的时间就很长了。
  • 内存碎片化问题、因为 CMS 时基于标记清除算法的。空间碎片过多时、将会给打对象分配带来很大麻烦,往往会出现老年代结束时还有很多剩余空间、但是无法找到足够大的连续空间分配当前对象而不得不触发一次 Full GC。所以默认提供了一个参数、用于在 CMS 不得不进行 Full GC 时开启内存碎片的合并整理、但是内存整理也是需要 STW 的、所以停顿时间也会变长了。

G1

是一款面向服务器的垃圾收集器,支持新生代和老年代的垃圾收集,主要针对配备多核处理器及大容量内存多机器。

G1 最主要的设计目标是 : 可预测可配置的 STW 停顿时间。

G1 有一些独特的实现。首先、堆不再分成连续的年轻代和老年代空间(不再坚持固定大小以及固定数量的分代区域划分)。而是划分为多个大小相等的存放对象的小堆区。每个小堆区可能是Eden Survivor 区或者 Old 区、但是在同一时刻只能属于某个代。

在逻辑上, 所有的Eden区和Survivor区合起来就是新生代,所有的Old区合起来就是老年代,且新生代和老年代各自的内存Region区域由G1自动控制,不断变动

在 G1 收集器出现之前的所有其他收集器、包括 CMS 在内、垃圾收集的目标范围要么是整个新生代、要么是整个老年代、要么就是整个堆。G1 跳出这个樊笼、 它可以面向堆内存任何部分来组成回收集 Collection Set CSet、衡量标准不再是它属于哪个分代、而是哪块内存的垃圾数量最多、回收收益最大、这就是 G1 的 Mixed GC 模式。

万字面试知识点助力金九银十_第12张图片

当对象大小超过 Region 的一半、则认为是巨型对象,直接被分配到老年代的巨型对象区 Humongous Regions 。

每个 Region 中最多只有一个巨型对象、巨型对象可以占多个 Region。

G1 把堆内存划分为一个个 Region 的意义在于

  • 每次 GC 不必都去整理整个堆、而是处理一部分的 Region、实现大容量内存的 GC
  • 通过计算每个 Region 的回收价值、包括回收所需要时间、可回收的空间、在有限时间内尽可能回收更多的内存,把垃圾回收造成的停顿时间控制在预期配置的时间范围内。

解决跨 Region 引用

使用记忆集避免全堆作为 GC Roots 扫描、但在 G1 收集器上记忆集的应用要复杂很多、它的每个 Region 都维护有自己的记忆集、这些记忆集都会记录下别的 Region 指向自己的指针,并标记这些指针分别在哪些卡页的范围之内。G1 的记忆集在存储结构的本质上是一个哈希表、key是 Region 的起始地址、Value 是一个集合、里面存储的元素是卡表的索引号。这种双向的卡表结构(卡表是我指向谁、这种结构还记录了谁指向我)。由于每个 Region 都需要维护一个记忆集、所以G1 收集器比其他传统的垃圾收集器有着更高的内存占用负担。G1 耗费相当于 Java 堆容量的 10% - 20% 的额外内存来维持收集器的工作。

并发标记用户线程与 GC 线程互相干扰问题

CMS 采用增量更新、G1 采用原始快照来实现。此外垃圾收集对用户线程的影响还体现在回收过程中新建对象的内存分配上、程序要继续运行肯定会持续有新对象被创建,G1 为每个 Region 创建了两个名为 TAMS(Top At Mark Start) 的指针、把Region 中的一部分空间划分出来用于并发回收过程中的新对象分配,并发回收时新分配的对象地址必须要在这两个指针位置以上。G1 收集器默认在这个地址以上的对象时被隐式标记过的、默认它们是存活的、不纳入回收的范围。与CMS 中的 Concurrent Mode Failure 失败会导致 Full GC 类似,如果内存回收的速度赶不上内存分配的速度、G1 收集器也要被冻结用户线程、导致 Full GC 而产生长时间 STW

建立停顿预测模型

用户通过 -XX:MaxGCPauseMills参数指定停顿时间、这个时间只是垃圾收集器发生前的期望。G1 收集器的停顿预测模式是以衰减均值为理论基础实现的、在垃圾收集过程中、G1 收集器会记录每个 Region 的回收耗时、每个Region 里藏卡数量等各个可测量的步骤花费的成本。

G1 工作模式

针对新生代和老年代、G1 提供了两种 GC 模式、Young GC 和 Mixed GC 、都会导致 STW

  • Young GC 当新生代空间不足时、G1 触发 Young GC回收新生代空间、Young GC 主要对 Eden 区进行回收、它在 Eden 空间耗尽时触发、基于分代回收思想和复制算法、每次 Young GC 都会选定所有新生代的 Region,同时计算下次 Young GC 所需 Eden 区和 Survivor 区的空间、动态调整新生代占 Region个数来控制 Young GC 开销。
  • Mixed GC 当老年代空间达到阈值就会触发 Mixed GC、选定所有新生代的 Region、根据全局并发标记阶段统计得出收集收益高的若干老年代 Region。在用户指定的开销目标范围内、尽可能选择收益高的老年代 Region 进行 GC ,通过选择哪些老年代 Region和选择多少 Region来控制 Region 来控制 Mixed GC 开销。

暂停转移:纯年轻代模式 Evacuation Pause Fully Young

在应用程序刚启动时、G1 还未执行过并发阶段、也就没有获得额外的信息,处于初始的 fully young 模式。

在年轻代空间用满之后、应用线程被暂停、年轻代中存活的对象被复制到存活区、如果没有存活区、则选择任意一部分空闲的小堆区作为存活区。

复制过程称为转移 Evacuation,这和前面讲过的年轻代收集器基本是一样的工作原理。

全局并发标记

主要是为了 Mixed GC 计算找出回收收益较高的 Region 区域。

当堆内存的总体使用比例达到一定数值时、会触发并发标记、默认值是 45% 、可以通过参数 InitiatingHeapOccupancyPercent 来设置。

阶段1 初始标记 此阶段标记所有从 GC Root直接可达的对象。当达到出发条件时、G1 并不会立即发起并发标记周期、而是等待下一次新生代收集、利用新生代收集的 STW 时间段、完成初始标记、这种方式称为借道

阶段2 Root Region 扫描 在初始标记暂停之后、新生代收集也完成对象复制到 Survivor 的工作、应用线程也开始活跃起来、此时为了保证标记算法的正确性,所有新复制到 Survivor 分区的对象、需要找出哪些对象存在对老年代对象的引用,把这些对象标记成根。这个过程称为根分区扫描。同时扫描的 Survivor 分区也被称为根分区;根分区的扫描必须要在下一次新生代垃圾收集启动前完成、因为每次 GC 产生新的存活对象集合。

阶段3 并发标记 标记线程与应用线程并发执行、标记各个堆中 Region的存活对象信息、这个步骤可能会被新的 Young GC 打断、所有标记任务必须在堆满前就扫描完成、如果并发标记耗时很长、那么有可能在并发标记过程中、又经历几次新生代收集

阶段4 再次标记 STW 以完成标记过程、标记在并发阶段发生变化的对象和未被标记的对象、同时完成存活数据计算

阶段5 清理

  • 更新每个 Region 各自的 Remember Set。
  • 回收不包含存活对象的 Region
  • 统计计算回收收益高的老年代分区集合。

CMS 与 G1 比较

与 CMS 相比、G1 有很多优点、暂不论可以指定最大暂停时间、分Region的内存布局、按收益动态确定收集 这些创新性设计带来的红利。单从最传统的算法理论看、G1 也更有发展潜力、与CMS 的标记清除算法不同、G1 从整体上看是基于标记整理算法实现、但从局部(两个 Region 之间) 上看又是基于标记复制算法实现的、但是无论如何这两种算法都以为着 G1 运作期间不会产生内存碎片、垃圾收集完成之后能提供规整的可用内存。这个特性有利于程序长时间运作

比起 CMS G1 弱项也可以列举不少、如在用户程序运行过程中、G1 无论是为了垃圾收集产生的内存占用还是程序运行时的额外执行负载都要比 CMS 高。

Minor GC 的过程

  • Eden 区没有足够的内存分配给新对象

对象进入老年代

  • 大对象直接在老年代分配
  • 分配担保、Minor GC 时、to Space 存不下存活的对象
  • 长期存活的对象直接进入老年代
  • 动态年龄判断、如果在 From Survivor 中的对象大小大于 From 和 To 的和一半、那么大于的那一部分的年龄的对象将会被放入到老年代

空间分配担保

在发生 Minor GC 之前、虚拟机会检查老年代中最大可用的连续空间是否大于所有新生代对象的总空间、如果大于、那么 Minor GC 是安全的

如果不成立、则检查老年代中最大可用连续空间是否大于历次晋升到老年代的对象的平均大小、如果大于、那么进行 Minor GC 、如果小于、则进行 Full GC

如果执行完 Minor GC 发现晋升的对象很多、老年代无法存放、还是要进行 Full GC

Full GC 的过程

  • System.gc 建议 JVM 进行 Full GC
  • Minor GC 之前、老年代中的连续空间小于历次晋升的对象的平均大小
  • Minor GC 执行完发现晋升到老年代的对象大于老年代能存放的空间
  • Metaspace 每次扩容之后大于 MetaspaceSize 参数
  • CMS / G1 并发标记的时候失败

GC 调优过程

项目相关

调优常用的参数

打印 JVM 初始参数

-XX:+PrintFlagsFinal
# 或者
-XX:+PrintFlagsInitial

查看 Java 进程

jps

查看进程中某个参数的值

jinfo -flag XXXX <pid>

查看 Java 进程内存的容量和使用量

jstat -gc <pid>

查看 JVM 内线程的情况

jstack <pid>

内存抖动

Java

Java 枚举

本质上是一个语法糖、继承自 Enum 类。会自动生成 valuse 方法、还有 valueOf 方法

Enum 的 == 和 euqal 是一样的。

Enum 不支持 Clone 方法

Enum 方法支持序列化反序列化,但是反序列化出来的对象还是 JVM 中原来的对象

Java synthetic

由编译器生成的,在源代码中没有出现的,都会被标记为 synthetic。当然有一些例外的情况:默认的构造函数、类的初始化方法、以及枚举类中的 valuevalueOf 方法

Java 序列化/反序列化

序列化就是将对象的状态信息转为可以存储或者传输的形式的过程

  • Serializable & Externalizable
  • transient 不参加序列化/反序列化
  • ObjectOutputStream & ObjectInputStream
  • Externalizable 必须提供一个无参构造方法、写入顺序要与读取顺序一致
  • 序列化 ID 是根据这个类的信息区生成的、
  • 枚举是直接调用它的 Enum.valueOf 方法

https://www.cnblogs.com/-coder-li/p/13100015.html

Java 集合

  • Collection
    • List
    • Set
    • Queue
  • Map

Iterable

Iterator

我们都知道在 ArrayList 中 forEach 中的时候 remove 会导致 ConcurrentModificationException

ArrayList

  • 动态数组
  • 线程不安全
  • 元素允许为 null
  • 连续的内存空间
  • 增加和删除都会导致 modCount 的值改变
  • 扩容默认为 一半

Vector

  • 线程安全
  • 扩容是上次的一倍
  • 存在 modCount
  • 每个操作都加上了 synchronized

CopyOnWriteArrayList

  • 写时复制、加锁
  • 耗内存
  • 实时性不高
  • 不存在 并发修改异常
  • 数据量最好不要太大
  • 使用 ReentrantLock 进行加锁

Collections.synchronizedList

  • synchronized 代码块
  • 对象锁可以传进去
  • 需要传 List 对象进去

LinkedList

  • ArrayList 增删效率低、改查效率高、而 LinkedList 刚刚相反
  • 链表实现
  • for 循环的时候、根据 index 是靠近前半段还是后半段来决定是顺序还是逆序
  • 增删的时候会改变 modCount

HashMap

数组 + 链表+红黑树

万字面试知识点助力金九银十_第13张图片

table 的默认长度是16 loadFactor 的值是 0.75

下标的计算

  • 获取 key 的 hashCode、然后 hashCode 与 hashCode >>> 16 进行亦或
  • 然后使用数组的长度对其进行取模运算

JDK 1.7 HashMap 扩容的时候、会因为链表元素的倒置进而导致循环链表

容量为2,负载因子为1、可以使加入5之后进行扩容

万字面试知识点助力金九银十_第14张图片

再进一步

万字面试知识点助力金九银十_第15张图片

进而

万字面试知识点助力金九银十_第16张图片

在 JDK1.8 的时候、在扩容处理链表的时候、增加了头尾两个元素、将链表元素倒置问题解决了、循环链表的问题也就解决了。

但是无论如何并发的情况下、元素还是会丢失

HashTable

遗留类、很多功能和 HashMap 类似、但是它是线程安全的、任意时刻只有一个线程写 HashTable 、并发性不如 ConcurrentHashMap

LinkedHashMap

继承自 HashMap、在 HashMap 的基础上、通过维护一条双向链表、解决了 HashMap 不能随时保持遍历顺序和插入顺序一致的问题

万字面试知识点助力金九银十_第17张图片

我们可以通过重写 removeEldestEntry 方法来实现一个 LRU 队列

Set

依赖于 HashMap 实现的

Queue

PriorityQueue

默认最小顶堆

面向对象的三大特性

  • 继承
  • 封装
  • 多态

final 关键字

  • 修饰类、不能被继承
  • 修饰方法、不能被重写
  • 修饰属性、要么在声明变量的时候赋值、要么在构造函数中赋值、不能再次修改、可见性

访问修饰符

修饰符 本类 同包 子类 其他
private
default
protected
public

基本类型

  • byte
  • short
  • int
  • float
  • long
  • double
  • char
  • boolean

byte、short、int、long 都缓存了 -128 到 127

character 缓存了 0 到 127

Java 线程安全程度

  • 不可变、如 final 修饰的、可靠性最高
  • 绝对线程安全 一个类要达到 不管运行时环境如何、调用者都不需要任何额外的同步措施、通常需要很大的代价、甚至是不切实际的
  • 相对线程安全 保证对这个对象单独操作是安全的、但是对于一些特定顺序的连续调用、需要额外的手段去保证其正确性、我们常见的线程安全类都是这种级别的、HashTable 、Vector、synchronizedCollections
  • 线程兼容 常说的线程不安全、对象本身线程不安全、需要在调用端正确使用同步手段来保证线程安全
  • 线程独立 无论调用端是否采用了同步措施、都无法在多线程下安全的使用

抽象类和接口的区别

  • 抽象类中的成员变量可以是各种类型的、而接口中的成员变量只能是 public static final 类型的
  • 一个类只能继承一个抽象类、而一个类却可以实现多个接口
  • 设计思想上的区别、抽象类是对相关性的一类事物的抽象、而接口是对能力的抽象

Java 异常

万字面试知识点助力金九银十_第18张图片

万物皆可抛

Error 代表 JVM 本身的错误、并不是程序员能通过代码处理的、Error 很少出现的。OutOfMemoryError、StackOverflowError

Exception 能被程序员处理的

异常分为

  • 受检异常 程序代码中必须使用 try catch 去处理 文件找不到啊、SQL 异常啊、IO 异常啊
  • 非受检异常 RuntimeException 以及它的子类都是非受检异常、不会提示我们程序代码去处理这些异常。空指针异常、类型转换错误、数组越界

需要明确的是:检查和非检查是对于javac来说的,这样就很好理解和区分了。

当finally遇上return

也就是说:try…catch…finally中的return 只要能执行,就都执行了,他们共同向同一个内存地址(假设地址是0×80)写入返回值,后执行的将覆盖先执行的数据,而真正被调用者取的返回值就是最后一次写入的。那么,按照这个思想,下面的这个例子也就不难理解了。

finally中的return 会覆盖 try 或者catch中的返回值。

public static void main(String[] args)
    {
        int result;
 
        result  =  foo();
        System.out.println(result);     /2
 
        result = bar();
        System.out.println(result);    /2
    }
 
    @SuppressWarnings("finally")
    public static int foo()
    {
        trz{
            int a = 5 / 0;
        } catch (Exception e){
            return 1;
        } finally{
            return 2;
        }
 
    }

    @SuppressWarnings("finally")
    public static int bar()
    {
        try {
            return 1;
        }finally {
            return 2;
        }
    }

finally中的return会抑制(消灭)前面try或者catch块中的异常

class TestException
{
    public static void main(String[] args)
    {
        int result;
        try{
            result = foo();
            System.out.println(result);           //输出100
        } catch (Exception e){
            System.out.println(e.getMessage());    //没有捕获到异常
        }
 
        try{
            result  = bar();
            System.out.println(result);           //输出100
        } catch (Exception e){
            System.out.println(e.getMessage());    //没有捕获到异常
        }
    }
 
    //catch中的异常被抑制
    @SuppressWarnings("finally")
    public static int foo() throws Exception
    {
        try {
            int a = 5/0;
            return 1;
        }catch(ArithmeticException amExp) {
            throw new Exception("我将被忽略,因为下面的finally中使用了return");
        }finally {
            return 100;
        }
    }
 
    //try中的异常被抑制
    @SuppressWarnings("finally")
    public static int bar() throws Exception
    {
        try {
            int a = 5/0;
            return 1;
        }finally {
            return 100;
        }
    }
}

finally中的异常会覆盖(消灭)前面try或者catch中的异常

class TestException
{
    public static void main(String[] args)
    {
        int result;
        try{
            result = foo();
        } catch (Exception e){
            System.out.println(e.getMessage());    //输出:我是finaly中的Exception
        }
 
        try{
            result  = bar();
        } catch (Exception e){
            System.out.println(e.getMessage());    //输出:我是finaly中的Exception
        }
    }
 
    //catch中的异常被抑制
    @SuppressWarnings("finally")
    public static int foo() throws Exception
    {
        try {
            int a = 5/0;
            return 1;
        }catch(ArithmeticException amExp) {
            throw new Exception("我将被忽略,因为下面的finally中抛出了新的异常");
        }finally {
            throw new Exception("我是finaly中的Exception");
        }
    }
 
    //try中的异常被抑制
    @SuppressWarnings("finally")
    public static int bar() throws Exception
    {
        try {
            int a = 5/0;
            return 1;
        }finally {
            throw new Exception("我是finaly中的Exception");
        }
 
    }
}

上面的3个例子都异于常人的编码思维,因此我建议:

不要在fianlly中使用return。

不要在finally中抛出异常。

减轻finally的任务,不要在finally中做一些其它的事情,finally块仅仅用来释放资源是最合适的。

将尽量将所有的return写在函数的最后面,而不是try … catch … finally中。

反射

反射是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为 Java 语言的反射机制。

利用放射创建数组

cls = Class.forName("java.lang.String");
Object array = Array.newInstance(cls,25);
//往数组里添加内容
Array.set(array,0,"hello");

泛型

参数化类型

有三种使用方式: 泛型类、泛型接口、泛型方法

Java 实现泛型的方式是 类型擦除式泛型

C# 选择的泛型实现方式是 具现化式泛型

在 C# 里面 List 和 List 是两种类型,而 Java 的泛型只存在源代码中、编译之后的字节码文件中、全部泛型都被替换你为原来的裸类型,并且在相应的地方插入了强制转型代码、对于 List List 它们是同一个类型

signature 是新加入的一个属性、它的作用存储参数化类型的具体类型

擦除法仅仅是对方法对 Code 属性中的字节码进行擦除,实际上元数据中还是保留了泛型信息

IO

主要的超类有哪些

  • InputStream
  • OutputStream
  • Reader
  • Writer

说说RandomAccessFile?

随机存取并不意味着你可以在真正随机的位置进行读写操作,它只是意味着你可以跳过文件中某些部分进行操作,并且支持同时读写,不要求特定的存取顺序。

这使得RandomAccessFile可以覆盖一个文件的某些部分、或者追加内容到它的末尾、或者删除它的某些内容,当然它也可以从文件的任何位置开始读取文件。

它在java.io包中是一个特殊的类,既不是输入流也不是输出流,它两者都可以做到。他是Object的直接子类。通常来说,一个流只有一个功能,要么读,要么写。但是RandomAccessFile既可以读文件,也可以写文件。

多线程

线程状态

万字面试知识点助力金九银十_第19张图片

线程的六个状态

  • NEW
  • Runnable
  • Blocked
  • Waiting
  • Time Waiting
  • Terminated

终止线程

不正确的方式

  • stop
  • destroy

stop 终止线程时、会抛出 ThreadDeath 的 Error

正确的方式

  • 使用 interrupt 方法
  • 额外使用 volatile 标志符

如果线程处于 wait 、join 、 sleep 阻塞时、那么 interrupt 会生效、该线程的中断状态标志将会被清除、抛出 InterruptionException

如果线程是被 IO/NIO 阻塞、那么IO 操作将会抛出特殊的异常值、达到终止线程的目标

  • Thread#interrupt
  • Thread#isInterruptd
  • Thread#interrupted 返回状态、并且清除

使用 volatile 标志位、虽然是经常使用的、但是如果线程处于休眠状态、那么就永远不会去检查这个 volatile 的标志位了

Thread#yield

执行该方法的线程会放弃当前的 CPU 时间片、与其他线程一起等待 CPU 的调度、但是可能再次被 CPU 调度到它

wait / notify

必须持有对象锁、才能调用

wait 方法导致当前线程等待、加入该对象的等待集合中、并且放弃当前持有的对象锁

notify、notifyAll 方法唤醒一个或所有正在等待这个对象锁的线程

虽然 wait 会自动解锁、但是对顺序有要求、如果在 notify 被调用之后、才开始调用 wait 方法、线程永远处于 waiting 状态

park/unpark

park 等待许可、unpark 为指定线程提供许可、但是不能叠加、其实就是一个标志位

多次调用 unpark 之后、再调用 park 线程会直接运行、但不会叠加、连续多次调用 park 只会第一次拿到许可执行、后续会进入等待状态。

伪唤醒

官方建议应该在循环中检测等待条件、原因是处于等待状态的线程可能会收到错误警报和伪唤醒、如果不在循环中检查等待条件、程序就会在没有满足条件的情况下退出

伪唤醒是指线程并非因为 notify、notifyAll 、unpark 等 api 调用而唤醒、而是更底层原因导致的

值得注意的一点是、park 是不会释放锁的、这个也会造成死锁

锁降级

锁降级指的是写锁降级成为读锁。锁降级是指把持住当前拥有的写锁的同时、再获取到读锁、随后释放写锁的过程。

class CachedData {
   Object data;
   volatile boolean cacheValid;
   final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

   void processCachedData() {
     rwl.readLock().lock();
     // 缓存无效
     if (!cacheValid) {
        // Must release read lock before acquiring write lock
        // 释放读锁
        rwl.readLock().unlock();
        // 尝试获取写锁
        rwl.writeLock().lock();
        try {
          // Recheck state because another thread might have
          // acquired write lock and changed state before we did.
          // 再次判断获取是否无效
          if (!cacheValid) {
              // 获取数据
            data = ...
            cacheValid = true;
          }
          // Downgrade by acquiring read lock before releasing write lock
          // 锁降级
          rwl.readLock().lock();
        } finally {
          rwl.writeLock().unlock(); // Unlock write, still hold read
        }
     }
     // 经过很长的时间做一些处理、而且不想我刚刚自己更新的数据被别人改了
     try {
       use(data);
     } finally {
       rwl.readLock().unlock();
     }
   }
 }
 

如果只是希望最后使用数据的时候,拿到的是最新的数据,而不一定是自己刚修改过的数据,那么先解写锁,再上读锁,然后使用数据也无妨

lock()与lockInterruptibly()的区别

  • lock 优先考虑获取锁、待获取锁成功之后、才响应中断
  • lockInterruptibly 优先考虑响应中断、而不是响应获取锁

lockInterruptibly 允许在等待时由其他线程调用等待线程的 Thread.interrupt 方法来中断等待线程的等待而返回,这时不用获取锁、而会抛出一个 InterruptedException

lock 方法不允许 Thread#interrupt中断、即即使检测到 Thread#isInterrupted 一样会继续尝试获取锁、失败则继续休眠、只是在最后获取锁成功之后才响应中断。

多线程有什么用

  • 发挥多核 CPU 的优势
  • 单核 CPU 的时候、防止阻塞

线程是不是越多越好

  • 线程在 Java 中本身就是一个对象、更是操作系统的资源、如果创建时间+销毁时间>执行时间 、那么就很不划算了
  • Java 对象占用堆内存、然后一个线程默认最大栈空间大小为 1M
  • 操作系统切换上下文会影响性能

如何确定线程的数量

  • 如果是 CPU 密集型 线程池的大小可以设置为 N+1
  • 如果是 IO 密集型应用、则线程池大小设置为 2N+1

为啥使用线程池

  • 线程复用
  • 控制最大并发数
  • 管理线程

线程池的核心参数

  • corePoolSize 线程池中常驻的核心线程
  • maximumPoolSize 线程池最大能容乃的最大线程数、必须大于等于 1
  • keepAliveTime 空余线程存活的时间
  • unit keepAliveTime 的时间单位
  • workQueue 任务队列 被提交但尚未执行的任务
  • threadFactory 生成线程池中工作线程的工程
  • handler 满了的时候如何处理的策略

处理策略

  • 拒绝并抛出异常
  • 拒绝但不抛出异常
  • 提交任务的线程去执行任务
  • 丢弃最久的任务

JDK 自带线程池的缺陷

  1. OOM

newFixedThreadPool 创建的任务队列是无界的、导致提交的任务可以无限的放置到该任务队列中(堆积大量的请求)、将内存涨爆

newSingleThreadExecutor 同样也是因为任务队列是无界的

newCachedThreadPool 的话、因为它使用的任务队列是 SynchronousQueue 、这个队列只能存储一个任务、而缓存线程池的最大线程数是 Integer.MAX_VALUE 可以创建无限多的线程、同样也会导致 OOM

newScheduledThreadPool 同样也是因为可以无限创建线程

  1. 不能自定义拒绝策略
  2. 无法指定线程的名称、出错无法追溯原因

CyclicBarrier和CountDownLatch的区别

  • CyclicBarrier 可重用、countdownlatch 不可重用
  • CountDownlatch 是两组线程、第一组负责计数器减一、第二组是阻塞线程、第一组线程将计数器减到0时、第二组线程才开始执行。而 CyclicBarrier

Unsafe

用于执行低级别、不安全的方法。如直接访问/管理 内存资源、用于提升 Java 执行效率、增强 Java 对底层资源的操作能力。

Unsafe 类使 Java 语言拥有了类似 C 语言指针一样操作内存空间的能力

堆外内存

使用堆外内存的原因

  • 对垃圾收集停顿的改善。由于堆外内存是直接受操作系统管理而不是 JVM ,当我们使用堆外内存,即可保持较小的堆内内存规模。从而减少 GC 时减少回收停顿对于应用的影响
  • 提升程序 I/O 操作的性能。通常在 I/O 通信过程中、会存在堆内内存到堆外内存的数据拷贝操作,对于需要频繁进行内存间数据拷贝且生命周期较短的暂存数据、建议放在堆外内存

典型的应用:

DirectByteBuffer 是 Java 用于实现堆外内存的一个重要类,通常用在通信过程中做缓冲池、如 Netty、MINA 等NIO 框架中应用广泛。

DirectByteBuffer 对于堆外内存的创建、使用、销毁等逻辑均由 Unsafe 提供的 API 来实现

使用 Cleaner 对象用于跟踪 DirectByteBuffer 对象的垃圾回收、以实现当 DirectByteBuffer 被垃圾回收时、分配的堆外内存一起被释放。

Cleaner 继承自 Java 四大引用类型之一的虚引用、众所周知、无法通过虚引用获取与之关联的对象实例、且当对象仅被虚引用引用时、在任何发生 GC 的时候、其均可被回收。

当某个被 Cleaner 引用的对象被回收时、JVM 垃圾收集器会将此对象的引用加入到对象引用中的 pending 链表中、等待 Reference-Handler 进行相关处理、其中 Reference-Handler 为一个拥有最高优先级的守护线程、会循环不断的处理 pending 链表中的对象引用、执行 Cleaner 的 clean 方法进行相关清理工作

CAS 相关

比较并替换

并发计算的时候经常使用到的一种技术、CAS 操作包含三个操作数、内存地址、期望值、新值。

执行 CAS 操作的时候、将内存地址的值与预期值进行比较、如果匹配、则将该内存地址的值设置为新值,否则处理器不做任何操作

CAS 是一条 CPU 的原子指令、不会造成所谓的数据不一致问题

典型应用

AtomicInteger、ConcurrentHashMap 都有应用到 CAS

带来的问题

  • 在多写的环境下、CPU 会有很大的开销
  • 只能保证一个变量的原子操作
  • BAB 问题

AtomicInteger

AtomicReference

AtomicStampedReference 解决了 ABA 问题、加了版本号

  • 公平锁、多个线程获取锁按照申请的顺序来、先来先到
  • 非公平锁、多个线程获取锁不一定按照申请顺序来、可能某个后到的线程先拿到锁、会造成饥饿现象

非公平锁比公平锁有更好的性能、因为公平锁获取锁的线程永远都是等待最久的线程、而非公平锁可能是刚刚到来的线程、减少了上下文的切换

可重入锁

线程可以进入任意它已经获取了锁的同步代码块/方法中、最大的作用就是避免死锁

自旋锁

是指尝试获取锁的过程不会立即阻塞、而是采用循环的方式尝试获取锁。这样的好处是减少上下文切换、坏处是循环会消耗 CPU

synchronized 和 lock的区别

  • synchronized 属于 JVM 层面、monitorenter、monitorexit 底层通过 monitor 对象来完成。Object的wait/notify 都是依赖 monitor 对象的;lock 是具体的类、API 层面的锁
  • synchronized 不需要用户手动释放锁;lock 需要
  • 等待是否可以被中断、synchronized 不可以、lock 可以
  • 是否是公平锁 synchronized 非公平锁、lock 两者都可
  • 是否可以判断锁的状态、lock 可以、synchronized 不可以

内存模型

一个老古董、它是与计算机硬件有关的一个概念。

计算机多核缓存多线程的问题、

  • 原子性 指在一个操作中、CPU 不可以中途暂停然后再调度、即不被中断操作、要么执行完成、要么不执行
  • 可见性 指当多个线程访问同一个变量时、一个线程修改了这个变量的值、其他线程能够立即看到修改的值
  • 有序性程序执行的顺序按照代码的先后顺序执行

为了保证共享内存的正确性、内存模型定义了共享内存系统中多线程程序读写操作行为的规范

通过这些规则来规范对内存的读写操作、从而保证指令执行的正确性。

内存模型解决并发问题主要采用的两种方式

  • 限制处理器优化
  • 使用内存屏障

Java 内存模型

是一种符合内存模型规范、屏蔽了各种硬件和操作系统的访问和差异、保证了 Java 程序在各种平台对内存的访问都能保证一致的机制和规范。

Java 内存模型规定了所有的变量都存储在主内存中、每条线程还有自己的工作内存。

线程的工作内存保存了该线程中用到的变量的主内存副本拷贝、线程对变量的所有操作都必须在工作内存中进行、而不能直接读写主内存。

JMM 就作用于工作内存和主存之间数据同步的过程、它规定了如何做数据同步以及什么时候做数据同步。

  • 原子性
    • 在 Java 中、为了保证原子性、提供了两个高级的字节码指令 Monitorenter 和 monitorexit 。Java 中 synchronized 就是基于此实现的、所以可以使用 synchronized 来实现原子性
  • 可见性
    • Java 内存模型是通过变量修改后将新值同步回主内存、在变量读取前从主内存刷新新变量值的这种依赖主内存作为传递媒介的方式实现。volatile、synchronized、final 都可以实现可见性
  • 有序性
    • synchronized 、 volatile 保证多线程之间操作的有序性。实现方式有所区别 volatile 关键字会禁止指令重排。synchrnized 关键字保证同一时刻只允许一条线程操作。

触发对类加载条件

  • new 一个对象
  • 访问静态变量
  • 访问静态方法
  • 反射
  • main 方法对应的主类
  • 子类加载时父类未加载

类的生命周期

  • 加载
    • 根据全类名获取对应的二进制流
    • 将流中的静态数据结构转为运行时的数据结构
    • 生成一个代表该类的 Class 对象
  • 验证
    • 确保二进制流中的数据合法
    • 各种验证
  • 准备
    • 为静态变量分配内存并设置为初始值
    • final 变量会被设置为程序设置的值
  • 解释
    • 符号引用变成直接引用
  • 初始化
    • 设置静态变量的值
  • 使用
  • 卸载

类加载器

  • BootstrapClassLoader 启动类加载器、加载 Java 核心库
  • ExtClassLoader 扩展类加载器、加载 ext 包
  • AppClassLoader 应用程序类加载器

双亲委派机制

万字面试知识点助力金九银十_第20张图片

  • 自底向上的判断类是否已经加载
  • 自顶向下加载类

越是基础的类越是由上层的类加载器进行加载

破坏双亲委派

第一次破坏、双亲委派机制未出的时候

第二次破坏

服务提供接口 Service Provider Interface SPI

一个典型的例子便是 JNDI服务、JNDI 现在已经是 Java 的标准服务、它的代码由启动类加载器来完成从、肯定属于 Java 中很基础的类型。

但 JNDI 存在的目的就是对资源进行查找和集中管理、它需要调用其他厂商实现的 SPI 代码,现在的问题是、启动类绝不可能认识或加载这些类。

为了解决这个问题、Java 的引入了一个不太优雅的设计

线程上下文类加载器。这个类加载器可以通过 Thread setContextClassLoader 方法进行设置、如果创建线程时没有设置、它将会从父线程中继承一个、如果在应用程序全局都没有设置过、那这个类加载器默认就是应用程序类加载器

第三次破坏

  • 模块热部署
  • 代码热替换

jdk动态代理&cglib动态代理

万字面试知识点助力金九银十_第21张图片

  • 静态代理

    • 基于继承
    • 聚合的方式实现静态代理
  • 动态代理

    • JDK 动态代理

      实现 InvocationHandler

      Proxy.newProxyInstance 创建一个代理对象

    • Cglib

JDK 和 Cglib 动态代理对比

  • JDK 动态代理只能代理实现了接口的类、没有实现接口的类不能实现JDK动态代理
  • Cglib 动态代理是针对类实现代理的、运行时动态生成代理类的子类拦截父类的方法调用、因此不能代理 final 类型的类和方法

代理对象的所有接口方法调用都会转发到 InvocationHandler.invoke 方法、

对于从 Object 中继承的方法、JDK Proxy 会把 hashCode equals toString 这三个非接口的方法转发给 InvocationHandler 、其余的 Object 方法不会转发

Spring

总体架构

  • Test
  • Core 工具类
  • Beans IOC 相关
  • Context ApplicationContext
  • Expression Language
  • AOP
  • Aspect
  • ORM
  • JMS
  • JDBC
  • Transaction
  • Web

Spring Alias

AliasRegistry 其中的一个实现类 SimpleAliasRegistry

private final Map<String, String> aliasMap = new ConcurrentHashMap<>(16);

key 是alias,value 是 name

Spring 资源

public interface Resource extends InputStreamSource 
  • FileSystemResource
  • ClassPathResource
  • URLResource
  • ByteArrayResource

ResourceLoader 资源加载的抽象

DefaultResourceLoader

设计模式

  • 责任链模式 ResourceLoader 加载资源的时候解释资源路径的协议
    • 纯的责任链模式 : 要么处理、要么不处理、不会半吊子。这个请求最终肯定会被其中一个 Handler 处理
  • 策略模式
    • 在 GenericAppcationContext 中可以设置 ResourceLoader

Spring 容器初始化

  • Resource 的获取
  • 使用 ThreadLocal 判断资源是否循环加载
  • 使用 DocumentLoader 将 Resource 转换为 Document
  • 解释 bean 标签、解释 import 标签、解释 alias 标签、解释 beans 标签
  • 构造 BeanDefinition 注册 Alias 和 BeanDefinition

相关组件

  • BeanDefinitionReader 读取解释配置文件
  • BeanDefinitionRegistry beanDefinition 的注册中心
  • DocumentLoader 将 Resource 转为 Document 对象
  • BeanDefinitionDocumentReader 读取 Document 向 BeanDefinitionRegistry 注册

Spring BeanDefinition

  • BeanDefinition
    • AbstractBeanDefinition
      • ChildBeanDefinition
      • RootBeanDefinition
      • GenericBeanDefinition

本质上没有什么区别只是在使用上有约束比如说 ChildBeanDefinition 必须有 父的 BeanDefinition、而 RootBeanDefinition 不能有。

在容器初始化的时候、往 BeanDefinitionRegistry 注册使用的是 GenericBeanDefinition

而在 getBean 的时候回将其整合为 RootBeanDefinition 继承它的父 BeanDefinition

Spring 循环依赖

循环依赖、就是两个或两个以上的 bean 互相持有对方。

Spring 中的循环包括

  • 构造器循环
  • setter 循环

对于构造器循环、Spring 无法解决

对于 Setter 循环、Spring 只能解决 singleton 类型的。通过三级缓存来实现

第一级缓存 : 创建好并且是填充好属性完成初始化的 bean

第二级缓存 : 创建好但是没有完成填充和初始化的 bean

第三级缓存 : 包裹着创建好但是没有完成填充和初始化的 bean 的 ObjectFactory ,第二级缓存是来源于第三级的 getObject 方法返回的 bean

为啥三级缓存、二级不行吗?

其实是可以的、但是这样却无法给用户一个扩展接口。

假如我们取消了上面所说的第三级缓存、只留下第一级缓存和第二级缓存、那么当出现一个循环依赖的时候、我们就没办法在它获取一个没完成属性填充的 bean 的时候做一些额外的事情、因为这个时候你只是单纯的在一个 Map 里面取出一个 value、无法给用户一个扩展接口

假如我们取消了上面所说的第二级缓存、只留下第一级缓存和第三级缓存、那么当存在 A 与 B 互相依赖、A 与 C 互相依赖的时候、ObjectFactory 的 getBean 方法就会被调用两次、也就是说 SmartInstantiationAwareBeanPostProcessor 的 getEarlyBeanReference 会被调用两次。

假如我们把第二级缓存和第一级缓存整合成一个、那问题更加大了、其他线程直接从这一层缓存获取到一个没有填充属性没有初始化的 bean 直接使用、直接爆炸

为啥不能解决构造器循环依赖

扯蛋吧、都没办法创建出一个 bean 怎么搞

Spring 获取单例

  • 找到这个参数的 beanName
  • 去三级缓存中看看能否找到对应的 bean
  • 如果找到则根据参数判断是返回一个 单纯的 bean 还是一个 factoryBean、如果获取到的是一个 factoryBean 返回需要的是一个 bean、那么要将其从 factoryBean 中获取出来并缓存起来
  • 看看是否存在父容器、存在则直接调用父容器的 getBean 方法
  • 如果不存在则获取对应的 BeanDefinition 然后去实例化其指定的 DependOn 的实例对象。
  • 根据指定 bean 的创建策略创建出一个不完整的 bean
  • 将不完整的 bean 放入到第三级缓存中
  • 对属性进行填充、回调各种初始方法
  • 加入到第一级缓存中、移除第二级第三级缓存
  • 返回 bean

Spring FactoryBean

  • SingletonRegistry Spring 容器注册 bean 的时候就用到这个接口
  • DefaultSingletonRegistry 三级缓存就是放在这里了、实现了 SingletonRegistry 接口
  • FactoryBeanRegistrySupport FactoryBean 产出的 bean 的缓存的地方、继承自 DefaultSingletonRegistry

Spring Aware 介绍

  • BeanNameAware
  • BeanClassLoaderAware
  • BeanFactoryAware

ApplicationContextAwareProcessor 实现了 BeanPostProcessor。

在 postProcessBeforeInitialization 中帮助

  • EnvironmentAware
  • ResourceLoaderAware
  • ApplicationEventPublisherAware
  • ApplicationContextAware

实现注入这些对象

Spring BeanPostProcessor

private final List<BeanPostProcessor> beanPostProcessors;

在 ApplicationContext 中 它会去主动注册 BeanPostProcessor registerBeanPostProcessors

PriorityOrdered、Ordered 接口

Spring PropertyEditor

PropertyEditor 是提供给 AWT 使用的。Java 也提供了默认实现 PropertyEditorSupport

我们使用 xml 配置文件为某个属性设置值的时候、写入的都是一个字符串类型、但是属性的实际类型可能是 Integer、Object 等、这个时候就需要做一层转换。

  • PropertyRegistry
  • PropertyRegistrySupport

Spring AOP

AOP 能将那些与业务模块无关、但是却公共的一些逻辑、比如说日志、事物处理等封装起来,减少了系统重复代码、降低了模块间的耦合,有利于扩展和维护

Spring AOP 就是基于动态代理的、如果代理的对象、实现了某个接口、那么 Spring AOP 会使用 JDK Proxy 去创建兑现、而对于没有实现接口的对象、就无法使用 JDK Proxy 去进行代理、这时候 Spring AOP 就会使用 cglib、这个时候 Spring AOP 会使用 CGlib 生成一个被代理对象的子类作为代理。

当然我们可以使用 AspectJ ,Spring AOP 已经集成了 AspectJ、AspectJ 应该算得上是 Java 生态系统中最完整的 AOP 框架。

Spring AOP 和 AspectJ AOP 有什么区别

Spring AOP 属于运行时增强、而AspectJ 是编译时增强。Spring AOP 基于代理、而 AspectJ 基于字节码操作。

如果我们切面比较少、两者性能差异不大、如果切末太多、最好选择 AspectJ 它比 Spring AOP 快很多。

Spring 中 bean 的作用域有哪些

  • singleton 整个 Spring 容器中只有唯一一个实例、默认作用域
  • prototype 每次请求都会创建一个新的实例
  • request 每次 HTTP 请求都会产生一个新的 bean、该 bean 仅在 当前 HTTP request 内有效
  • session 每一次 HTTP 都会产生一个新的 bean、该 bean 仅在当前 HTTP session内有效

Spring 中单例 bean 的现场安全问题了解

常见的两种解决办法

  • 尽量保持对象的无状态化
  • 将有状态的属性放置在 ThreadLocal

Spring 中的生命周期

  • BeanNameAware
  • BeanClassLoaderAware
  • BeanFactoryAware
  • *Aware
  • BeanPostProcessor#postProcessBeforeInitialization
  • InitializationBean#afterPropertiesSet
  • BeanPostProcessor#postProcessAfterInitialization
  • DisposableBean#destroy

Spring MVC 流程

  • 客户端发送请求、直接请求到 DispatcherServlet
  • DispatcherServlet 根据请求信息调用 HandlerMapping
  • Handler 就是我们常说的 Controller 开始由 HandlerAdapter 适配器处理
  • HandlerAdapter 会根据 Handler 来调用真正的处理器来处理请求、并处理相应的业务逻辑
  • 处理器处理完业务之后、会返回一个 ModelAndView 对象、Model 是返回的数据对象、View 是逻辑上的 View
  • ViewResolver 会根据逻辑 View 查找实际的 View
  • DispatcherServlet 会把返回的 Model 传给 View
  • 把 View 返回给请求者

Spring 中使用到哪些设计模式

  • 工厂方法 Spring 中的 FactoryBean
  • 单例模式
  • 模版方法 Spring 中的 jdbcTemplate、RedisTemplate
  • 责任链模式 ResourceLoader 的 ProtocolResolver
  • 策略模式 GenericApplicationContext 中使用不同的资源加载器
  • 观察者模式 Spring 的事件与监听

@Component 和 @Bean 的区别是什么

  • 作用对象不同 @Component 作用在类上、@Bean 作用在方法
  • @Component 通常是通过类路径扫描来自动侦测以及自动装配到 Spring 容器中 @ComponentScan 扫描。而@Bean 通常是我们在表用该注解的地方产生这个 bean、告诉 Spring 、如果我需要这个类的实例、你要将这个对象返回给我
  • @Bean 注解比 Component 注解的自定义性更强、而且很多地方只能通过 @Bean 注解来注册 bean。比如当我们引用第三库中的类需要装配到 Spring 容器时、则只能通过 @Bean 来实现

Spring 管理事务的方式有几种

  • 编程式事务、在代码中硬编码
  • 声明式事务、在配置文件中配置

声明式事务

  • 基于 XML 的声明式事务
  • 基于注解的声明式事务

Spring 事务中的隔离级别有几种

TransactionDefinition 接口中定义了五个表示隔离级别常量

  • TransactionDefinition.ISOLATION_DEFAULT 使用数据库默认的隔离级别、Mysql 默认采用 Repeatable read 隔离级别、Oracle 采用 read committ
  • TransactionDefinition.ISOLATION_READ_UNCOMMITTED 未提交读、可能会导致脏读、幻读和不可重复读
  • TransactionDefinition.ISOLATION_READ_COMMITTED 提交读、不会导致脏读、但是会不可重复读、幻读
  • TransactionDefinition.ISOLATION_REPEATABLE_READ 可重复读、可以阻止脏读和不可重复读、但是可能会幻读
  • TransactionDefinition.ISOLATION_SERIALIZABLE 序列化事务 所有的事务依次逐个执行、这样事务就完全不可能产生干扰、该级别下、可以防止脏读、幻读、不可重复读

Spring 事务中的事务传播行为

支持当前事务的

  • Propagation require 如果当前存在事务、则加入;如果当前没有事务、则新建一个
  • Propagation support 如果当前存在事务、则加入;如果没有、则以非事务的方式运行
  • Propagation mandatory 如果当前存在事务、则加入;如果没有、则抛出异常

不支持当前事务的

  • propagation require new 创建一个新的事务、如果当前存在事务、则将当前事务挂起
  • propagation not support 以非事务的形式运行、如果当前存在事务、则挂起
  • propagation nerver 以非事务运行、如果当前存在事务、则抛出异常

其他的

  • propagation nested 如果当前存在事务、则创建一个事务作为当前事务的嵌套事务来运行、如果当前没有事务、则新建一个事务

@Autowired 过程

在启动 Spring 容器的时候、有一个 AutowiredAnnotationBeanPostProcessor ,当扫描到有

  • 在容器中按类型查找 bean
  • 如果查询结果只有一个、那么就直接注入进去
  • 如果查询结果不止一个、那么将会根据名称来查找
  • 如果上述结果为空、则抛出异常、除非使用 require=false

自动装配有哪些局限性

  • 你需要配置定义依赖
  • 不能自动装配基本类型、String 、类
  • 模糊匹配

JDK动态代理和CGLIB动态代理的区别

Spring AOP 中动态代理主要有两种方式、JDK 动态代理和 CGLIB 动态代理

  • JDK 动态代理只提供接口的代理、不支持类的代理。核心 InvocationHandler 接口和 Proxy 类。InvocationHandler 通过 invoke 方法反射来调用目标类中的代码,将通用的逻辑和业务代码编织在一起。Proxy 利用 InvocationHandler 动态创建一个符合某一接口的实例、生成目标类的代理对象。
  • 如果代理类没有实现 InvocationHandler 接口、那么 Spring AOP 会选择使用 CGLIB 来动态代理目标类。可以在运行时动态的生成指定类的一个字类对象、并覆盖其中特定方法并添加增强代码、从而实现 AOP。CGLIB 是通过集成的方式做动态代理、如果某个类被标记为 final、那么就无法使用 CGLIB 做动态代理。

静态代理与动态代理区别在于生成 AOP 代理对象的时机不同、相对来说 AspectJ 的静态代理方式具有更好的性能、但是静态编译需要特定的编译器处理、而 Spring AOP 则无需特定的编译器处理。

https://juejin.im/post/6844903860658503693

https://blog.csdn.net/ThinkWon/article/details/104397516

Redis

Redis 的作用

  • 缓存
  • 分布式锁

常见的数据结构

  • string
  • list
  • hash
  • set
  • zset

string

类似 Java 的 ArrayList 采用预分配冗余空间的方式减少内存频繁分配

万字面试知识点助力金九银十_第22张图片

扩容机制

  • 字符串的长度小于 1M 前、扩容空间采用加倍策略、保留100%的冗余空间
  • 长度超过 1M 之后、为了避免加倍后冗余空间过大而浪费、每次扩容只会加多 1M

常用命令

set name1 value1
set name2 value2
mget name1 name2
mset name1 boy name2 girl name3 unknown
expire name1 5 #5s 后过期
setex name1 5 ljx
setnx name1 ljx

list

相当于 Java 中的 LinkedList 是链表

插入和删除操作非常快、时间复杂度为 O(1) 索引定位很慢 O(n)

当列表弹出最后一个元素后、该数据结构自动被删除、内存被回收

lpush listName values #往列表中的头部加入一个或多个元素
lpop listName#从列表中的头部取出一个元素

rpush listName values #往列表中的尾部加入一个或多个元素
rpop listName #从列表中的尾部取出一个元素

lindex listName index #相当于 Java 的 get(index)

lrange listName start stop #返回列表中指定区间的元素、0可以代表第一个元素、-1可以代表最后一个元素

快速链表

image-20200827014139101

redis 底层存储的不是一个简单的 linkedList 而是称为快速链表的一个结构

在列表元素较小的情况下、会使用一块连续的内存存储、即 ziplist、它将所有元素紧挨着存储、分配的是一块连续的内存、当数据量比较多才会改成 quicklist、因为普通链表的附加指针空间太大、会比较浪费空间、会加重内存碎片化

hash

相当于 Java 中 HashMap 数据结构同样是数组+链表二维结构

当 hash 移除了最后一个元素之后、该数据结构会自动删除、内存被回收

万字面试知识点助力金九银十_第23张图片

不同的是 redis 中 hash 的 key 只能是字符串、另外 rehash 的方式也不一样,java 中的 rehash 是一次性进行 rehash 的、 是一个耗时操作、为了提高性能、不阻塞服务、所以采取了渐进性 rehash 策略

渐进性 rehash 会在 rehash 的同时、保留新旧两个 hash 结构、查询时会同时查询两个 hash 结构、然后再后续的定时任务重以及 hash 的子指令重、循序渐进将旧的 hash 内容一点点迁移到新的 hash 结构中

set

相当于 Java 中的 HashSet 、value 都是 null

当集合中最后一个元素移除后、数据结构自动删除、内存被回收

zset

类似 Java 中 SortedSet 和 HashMap 的结合体、一方面是个 Set 保证内部value 的唯一性、另一方面给每个 value 赋予一个 score 代表这个 value 的排序权重、使用的是跳跃列表。

当集合中最后一个元素被移除后、数据结构自动删除、内存被回收

如果 score 值都是一样的、那么它会比较 value 的值

通用规则

  • 如果容器不存在则创建一个、再进行操作、比如 rpush没有list、则自动创建、然后再 rpush 进去
  • 如果容器中没有元素、那么就会立即删除这个数据结构、释放内存
  • 所有的数据结构都可以设置过期时间、时间到了、就会删除相应的对象、设置过期时间的单位是对象、所以对一个容器而言、不能单独设置某个元素的过期时间

限流

限流需求中存在一个滑动时间窗口、我们可以使用 zset 的 score 来圈出这个时间窗口。我们只要保留这个时间窗口、窗口之外的数据都可以砍掉。zset 的 value 填毫秒的时间戳

一个用户单独使用 zset 、每个请求作为这个 zset 的值存在里面、score 为请求到达的时间、value 为达到的时间戳

万字面试知识点助力金九银十_第24张图片

  • 先增加
  • 再移除窗口之外的数据
  • 统计窗口内的数据个数
  • 判断是否大于限制

因为连续的几个 Redis 操作都是针对同一个 key 的、使用 pipeline 可以显著提升 Redis 的存取效率。

但是这中方式去做限流、缺点很明显、一定时间窗口内允许大量的请求的话、会花费大量的存储空间

使用 pipeline 的好处 https://www.jianshu.com/p/86ef67514a0f

multi 和 pipeline 的区别 https://juejin.im/post/6844903635252412430

漏斗限流

漏斗算法

public class FunnelRateLimiter { 
  static class Funnel {
		int capacity; // 漏斗的容量
		float leakingRate; // 漏斗的流速
    int leftQuota; // 漏斗的剩余容量
    long leakingTs; // 上一次放水的时间
    
    public Funnel(int capacity, float leakingRate) { 
      this.capacity = capacity;
    	this.leakingRate = leakingRate; 
      this.leftQuota = capacity;
    	this.leakingTs = System.currentTimeMillis(); 
    }
    
    void makeSpace() {
      
      long nowTs = System.currentTimeMillis();
      long deltaTs = nowTs - leakingTs;
      int deltaQuota = (int) (deltaTs * leakingRate);//可释放的容量
      
      if (deltaQuota < 0) { // 间隔时间太长,整数数字过大溢出
      	this.leftQuota = capacity; this.leakingTs = nowTs; return;
      }
      
      if (deltaQuota < 1) { // 腾出空间太小,最小单位是 1
      	return; 
      }
      
      this.leftQuota += deltaQuota;
      this.leakingTs = nowTs;
      
      if (this.leftQuota > this.capacity) {
      	this.leftQuota = this.capacity; 
      }
      
    }
    
    boolean watering(int quota) { makeSpace();
    	if (this.leftQuota >= quota) {
    		this.leftQuota -= quota;
    		return true;
      }
    	return false;
     }
 }
private Map<String, Funnel> funnels = new HashMap<>();

public boolean isActionAllowed(String userId, String actionKey, int capacity, float leakingRate) {
  
  String key = String.format("%s:%s", userId, actionKey);
	Funnel funnel = funnels.get(key);
	
  if (funnel == null) {
		funnel = new Funnel(capacity, leakingRate);
    funnels.put(key, funnel); 
  }
	
  return funnel.watering(1); // 需要 1 个 quota }
}

分布式的漏斗算法可以使用 hash 这个数据结构来完成。将 Funnel 对象的内容按字段存储到一个 hash 的结构中、灌水将 hash 结构的字段取出来之后进行逻辑运算、再将新值回填到 hash 结构、完成一次限流检查。

但是有个问题就是、这三个过程无法保证是原子性、如果想要原子性、那就要涉及到加锁控制、而一旦加锁、意味着加锁失败、加锁失败就意味着重试或者放弃

Redis 4.0 提供了限流模块 redis-cell、使用的就是漏斗算法 、并且提供了原子的限流指令

万字面试知识点助力金九银十_第25张图片

万字面试知识点助力金九银十_第26张图片

多路复用

Redis 是单线程程序

除 Redis 之外、Node.js 也是单线程的、Nginx 也是单线程的

Redis 单线程为什么还能那么快

因为所有的数据都在内存中、所有的运算都是内存基本的

Redis 单线程如何处理那么多并发客户端连接

多路复用 API :Epoll

事件轮询 API、操作系统提供给用户程序的

输入是读写描述符、输出是预知对应的可读可写事件。同时提供一个 timeout 参数、如果没有任何事件到来、那么就等待 timeout 时间,线程就处于阻塞状态。一旦期间有任何事件到来就可以立即返回。

这个其实是一个死循环、事件循环。

服务器套接字 serversocket 对象的读操作是指调用 accept 接受客户端新连接

指令队列

Redis 会将每个客户端套接字都关联一个指令队列、客户端的指令通过队列来排队进行顺序处理、先到先服务。

响应队列

Redis 同样会为每个客户端套接字关联一个响应队列。Redis 服务器通过响应队列来将指令的结果返回给客户端。

定时任务

服务器除了要响应 IO 事件外、还要除了其他事情、比如定时任务就是非常重要的一件事、如果线程阻塞在 select 系统调用上、定时任务将无法得到准时的调度

Redis 的定时任务会记录在一个称为最小堆的数据结果中、这个堆中、最快要执行的任务排在堆的最上方。在每个循环周期、Redis 都会将最小堆里面已经到点的任务立即进行处理。处理完毕后、将最快要执行的任务还需要的时间记录下来、这个时间就是系统调用的 timeout 参数。因为 Redis 知道未来的 timeout 时间内、没有其他定时任务需要处理、所以安心的进行系统调用等待

分布式锁

分布式锁的本质实现的目标就是在 redis 里面占一个茅坑、当别的进程要进来占的时候、发现已经有人蹲了、只好放弃或稍后再试

占坑使用的是 setnx (set if not exists) 只允许一个客户端占坑、先到先得

使用完之后、调用 del 删除茅坑

setnx lockName value
do something
del lockName

但是有可能异常并没有执行 del 操作、那么锁就会得不到释放、那么我们可以在拿到锁之后加上一个过期时间、保证锁会被释放

setnx lockName value
expire lockName 5
do something
del lockName

但是也有可能在 setnx 和 expire 之间进程突然挂掉、导致 expire没有执行、也会造成死锁

这种问题的一个原因就是 setnx 和 expire 两个指令不是一个原子操作

redis 2.8 中加入了 set 的扩展参数、setnx 和 expire 可以一起执行

set lockName value ex 5 nx

超时问题

在加锁和释放锁之间执行时间太长、导致 redis 过期删除了锁、那么这个时候、第二个线程重新持有了这把锁、紧接着第一个线程就把这个锁释放掉、那么第三个线程就会在第二个线程逻辑执行完拿到了这个锁。

为了避免这个问题、redis 分布式锁不要用于执行较长时间的任务

还有一个比较安全的做法是、就是 set 指令存放的 value 是一个随机数、释放锁之前先判断锁释放一致、然后再删除 key、

但是比较 value 和 删除不是一个原子操作、redis 也没有提供相关的指令、这个时候可以使用 lua 脚本、因为 lua 脚本可以保证多个指令的原子性执行

Redisson 有解决这个问题、当发现超时后、业务还没执行完、会有监控线程去重新设置这个时间

https://juejin.im/post/6844903830442737671

可重入锁

可重入锁是指线程在持有锁的情况下、再次请求锁。如果一个锁支持同一个线程多次获取锁、那么这个锁就是可重入锁

Redis 中分布式锁要支持可重入、那么就要重写 set 方法、使用 ThreadLocal 存储当前持有的锁

RedLock

为了使用 RedLock 需要提供多个 Redis 实例、这些实例之间相互独立没有主从关系、同很多分布式算法一样、redLock 也是采用大多数机制

加锁时、它会向过半节点发送 set key value ex exTime nx 指令、只要过半节点成功、则认为加锁成功、释放锁时、需要向所有节点发送del 指令

队列

Redis 的 list 数据结构可以作为消息队列、使用 lpush、rpush 结合使用 rpop lpop

万字面试知识点助力金九银十_第27张图片

如果队列空了的话、那么客户端就会陷入 pop 的死循环、不断的 pop

这不仅浪费客户端的 cpu 而且也会拉高 redis 的 qps

在客户端可以通过 sleep 来实现、让线程休眠、但是这样子会导致消息的延迟。虽然客户端的 CPU 下来了、Redis 的 QPS 也下来了

这个时候我们可以使用 blpop / brpop

阻塞读是在队列没有数据的时候进入休眠、而在消息到来的时候马上醒过来、消息的延迟几乎为 0

但是需要主要的是、如果客户线程一致阻塞在那里、那么 redis 会把这个链接当成是空闲链接、闲置过久、服务器会主动断开这个链接、

这个时候 brpop blpop 都会抛出异常、注意捕获异常重试

延时队列

可以使用 zset 来实现、以消息的到达时间为 score 、然后客户端多个线程轮询获取 zset 的到期任务进行处理

考虑并发问题、zrem 返回大于 0 才算成功夺取到任务

万字面试知识点助力金九银十_第28张图片

位图

位图不是特殊的数据结构、内容其实就是普通的字符串、也就是 byte 数组。可以使用 get/set 设置或获取整个位图的内容。

也可以使用getbit / setbit把它作为数组处理

统计和查找

位图统计指令 bitcount 用来统计指定范围内 1 的个数

位图查找指令 bitpos 用来查找指定范围内出现的第一个0 或 1

指定范围 [ start end ] 必须是 8的倍数

万字面试知识点助力金九银十_第29张图片

布隆过滤器

布隆过滤器可以理解为不怎么精确的 set 结构、当你使用 contains 方法的时候、它可能会误判

  • 当它说一个 key 存在的时候、它可能存在
  • 当它说一个 key 不存在的时候、那么它一定不存在

Redis 官方提供的布隆过滤器在 4.0 的时候才以插件的形式出现

布隆过滤器有两个指令、一个是 add 一个是 exist

布隆过滤器对于已经见过的元素肯定不会误判、只会误判那些没见过的元素

布隆过滤器有两个参数、一个是错误率一个是初始大小

初始大小过大、浪费空间、过小导致错误率上升

万字面试知识点助力金九银十_第30张图片

向布隆过滤器添加 key 时、会使用多个 hash 函数对 key 进行 hash 、每个 hash 都会算到一个不同的位置、然后再把对应位置的值变为1

exist 则是判断其对应的各个位置是否都是为1 、只要有一个不为1 则这个key 是不存在于布隆过滤器中的

过期策略

redis 会将每个设置了过期时间的 key 放入到一个独立的字典中、以后会定期的遍历整个集合、来删除过期的 key

除了定时遍历之外、还会采用惰性删除过期的 key 、所谓的惰性删除就是每次客户端来获取key的时候、发现这个 key 已经过期了、就会将其删除掉

定时删除时集中处理、惰性删除时零散处理

定时删除

每秒进行十次

  • 从过期字段中随机选取 20 个 key
  • 删除这 20 个 key 中过期的 key
  • 如果过期的 key 的比率超过 1/4 那么重复步骤 1

为了保证扫描不回出现循环过度、导致线程卡死现象、算法还增加了扫描时间上限、默认不回超过 25ms

从库的过期策略

从库不回进行过期扫描、所以主库过期的 key 的 del 指令没有及时同步到从库的话、会出现主从数据的不一致。主库没有的数据在从库还存在。比如上面说的分布式锁的算法漏洞就是因为同步延迟产生的。

持久化

有两种方式

  • 快照、序列化二进制数据
  • AOF 日志 append only file 指令文本、长期运行过程中会变得很大、需要定期对 AOF 文件瘦身
  • 混合持久化、快照和 AOF 结合

快照

redis 使用操作系统的多进程、cpw copy on write 机制来实现快照持久化

万字面试知识点助力金九银十_第31张图片

  • 900 秒内如果超过 1 key 被修改、则发起快照保存
  • 300 秒内如果超过 10 个key 被修改、则发起快照保存

redis 在持久化时会调用 glibc 的函数 fork 产生一个子进程,父进程继续处理客户端请求、子进程和它的父进程共享内存里面的代码段和数据段。子进程做数据持久化、他不会修改内存数据结构、只会对数据结构进行遍历读取、然后持久化到磁盘中。

而父进程响应客户端的请求、对内存数据结构进行修改。

这个时候会使用到操作系统的 copy on write 机制来对数据段页面进行分离

数据段是由很多操作系统的页组成的、当父进程对其中一个页面进行数据修改的时候、只会将那个共享页复制一份出来,只对这个复制出来的页面进行修改。所以对子进程来说、数据还是 fork 进程那一瞬间的数据

万字面试知识点助力金九银十_第32张图片

AOF

aof 日志存储的是 redis 服务器的顺序指令序列、AOF 只记录对内存进行修改的指令记录

Redis 会在收到客户端修改指令后、先进行参数检验、如果没有问题、就立即将该指令文本存储到 AOF 中、也就是先存到磁盘、然后再执行指令

AOF 重写

提供了 bgaofrewrite 指令对 aof 日志进行瘦身、远离就是开辟一个子进程对内存进行遍历转换成一系列 Redis 操作指令、序列化到一个新的 AOF 日志文件中。序列化完毕后再将操作期间发生的增量 AOF 日志追加到新的 AOF 文件中、追加后替换旧的 AOF 日志文件】瘦身工作就完成了。

fsync

aof 日志是以文件的形式存在的、当程序对 aof 进行 write 操作时、实际上是写到了内核文件描述分配的一个内存缓冲中、然后内核会异步将脏数据刷新到磁盘。这就意味着如果机器突然宕机,AOF 日志内容可能还没有来得及完全刷到磁盘中,这个时候就会出现日志丢失

Linux 提供了 fsync 函数可以将指定文件的内容强制从内核刷新到磁盘。只要 Redis 进程实时调用 fsync 函数就可以保证 aof 日志不丢失。但是 fsync 是一个磁盘 IO 操作、他很慢。如果Redis 没执行一条指令就要 fsync 一次,那么Redis 高性能的地位就不保了。

三种刷盘策略

  1. 永不主动调用 fsync 、让操作系统来决定合适调用然后才同步到磁盘
  2. 1s 执行一次 fsync
  3. 来一个指令就执行一次 fsync

混合持久化

重启 Redis 时、我们很少使用 rdb 来恢复内存、因为会丢失大量的数据、我们通常使用 AOF 日志重放、但重放 AOF 日志性能相对于 rdb 来说满的很多。这样在 Redis 实例很大的情况、启动需要花费很长的时间。

Redis 4.0 为了解决这个问题、提出了新的持久化策略–混合持久化。将 rdb 文件的内容和增量的 AOF 文件存在一起。这里的 AOF 不再是全量日志而是自持久化开始之后的

于是在 Redis 重启的时候、先加载rdb 的内容、然后重放增量 AOF 日志即可

完全代替之前的 AOF 全量文件重放、因此重启效率大幅提升。

LRU

配置参数 maxmemory 来限制使用内存的大小

当使用的内存大小大于配置的内存大小、redis 提供了几种可选的策略来让用户决定该如何腾出新的空间以继续提供读写服务

  • volatile-lru 尝试淘汰设置了过期时间的 key 最少使用的 key 优先被淘汰、没有设置的 key 不会被淘汰
  • allkeys-lru 尝试淘汰所有的 key 优先淘汰最少使用的 key
  • volatile-random 尝试淘汰设置了过期时间的 key、随机选择 key 进行淘汰
  • allkeys-random 尝试淘汰所有的 key、随机选择 key 进行淘汰
  • volatile-ttl 淘汰设置了 过期时间的 key、剩余存活时间最大的key 被优先淘汰
  • noeviction 拒绝写请求、但是可以接受 del 请求 查询请求、这样可以保证不丢失已经存在redis 上的数据、但不能提供写请求、默认策略

处理 key 过期方式有几种处理和懒惰处理、但是对于 LRU 的淘汰只能是懒惰处理

当 redis 执行写操作的时候、发现内存超出 maxmemory、那么就会采样出 5 个 key、然后淘汰掉最旧的 key、如果淘汰之后还是超出 maxmemory 那就继续随机采样、直至低于 maxmemory

如何采样就是看 maxmemory policy 的配置的、如果是 allkeys 则从所有的 key 中、如果是 volatile 就从设置了过期时间的 key 中。

每次采样的个数、则是通过 maxmemory_samples 的设置、默认为5

淘汰组是一个数组、它的大小是 maxmemory_samples 在每一次淘汰循环中、新随机出来的 key 列表和 淘汰池中的 key 进行融合、淘汰掉最旧的 key 之后、保留剩余较旧的 key 放入淘汰组、等待下一个循环

每个 key 都增加了一个额外的小字段、这个字段的长度是 24个 bit 也就是最后一次访问的时间戳

del 的惰性删除

redis 内部实际上并不是只有主线程、还有几个异步线程专门来处理一些特别耗时的操作

del —> unlink

对删除操作进行懒处理、丢给后台线程异步回收内存

主线程将对象从 “大树” 中摘除后、会将这个key 的内存回收操作包装成一个任务、塞进一部任务队列、后台线程会从这个异步队列中取任务。

不是所有的 unlink 操作都要延后处理、如果 key 所占用的内存很少、延后处理就没有必要、这个时候 redis 会将对应的 key 内存立即回收、跟 del 指令一样

scan

从海量 key 中查出特定规则的 key、最简单暴力就是使用 keys 命令

但是它是有缺点的

  • 没有 offset 、limit。一次性查询出所有满足条件的 key
  • keys 是遍历算法、复杂度为 O(n) 如果redis 实例上有千万以上、这个指令会导致 redis 服务卡顿

增加了一个新指令 scan

scan cursor Match keyPattern Count countNumber
  • 虽然复杂的还是 O(n) 但是通过游标分步进行、不会阻塞线程
  • 提供 limit 参数、可以控制每次返回结果的最大条数、 limit 只是一个 hint、返回结果可多可少
  • 同 keys 一样、提供模式匹配功能
  • 服务器不保存游标、游标返回给客户端
  • 返回的结果可能重复、客户端要去重
  • 遍历过程如果又数据修改、改动的数据不能遍历
  • 单次返回的结果为空不意味着遍历结束、要看返回的游标值是否为 0

集群方案

哨兵模式

万字面试知识点助力金九银十_第33张图片

我们可以将 sentinel 集群看作是一个 ZK 集群、它是集群高可用的心脏、一般由 3-5个节点、这样子挂了个别节点、集群还能正常运转

sentinel 负责持续监控主从节点的健康、当主节点挂掉时、自动选择一个最优的从节点切换为主节点。

客户端来连接集群时、会首先连接 sentinel 通过sentinel 查询主节点的地址、然后再去连接主节点进行数据交互、当主节点发生故障时、客户端会重新 sentinel要地址、sentinel 会将最新的主节点地址告诉客户端。如此一来应用程序就可以自动完成节点的切换

万字面试知识点助力金九银十_第34张图片

旧的 Master 重新上线

万字面试知识点助力金九银十_第35张图片

消息丢失

Redis 采用异步复制、意味着当主节点挂掉时、从节点可能没有收到全部同步消息、这部分同步消息就丢失了。如果主从延迟特别打、那么丢失的数据就可能会特别多。sentinel无法保证消息完全不丢失,但是尽可能保证消息少丢失。有两个选项可以限制主从延迟过大

  • min-slave-to-write 1
  • min-slave-max-lag 10

第一个参数表示主节点必须至少有一个从节点再进行正常复制、否则就停止对外写服务、丧失可用性

何为正常?第二个参数就是控制整个的、单位是秒、表示如果 10s 没有收到从节点的反馈、就意味着从节点不正常、要么网络断开、要么一直没有非反馈

sentinel 切换主从

sentinel 进行主从切换的时候、客户端如何知道地址变更了

  • 连接池建立新连接时、会查询主库地址、如何跟内存中的主库地址对比、如果不一致则断开所有连接、然后用新地址建立新连接。如果主库挂掉、那么所有正在使用的连接都会被关闭,然后重连时会用上新地址
  • 如果是 sentinel 主动进行主从切换、主库并没有挂掉、而之前的连接已经在使用、他会在处理命令时捕获一个特殊的异常 ReadOnlyError 在这个异常中旧连接全部被关闭、后续指令会进行重连。所有修改性的命令都会抛出 ReadOnlyError 如果没有修改性指令、虽然不回的到切换、但是数据不会被破坏,所以即使不切换也没关系

Codis

万字面试知识点助力金九银十_第36张图片

万字面试知识点助力金九银十_第37张图片

Codis 时无状态的、知识一个转发的代理中间件

万字面试知识点助力金九银十_第38张图片

每个槽位都会映射到后面多个 Redis 之一

不同Codis 实例之间槽位关系如何同步?

万字面试知识点助力金九银十_第39张图片

将槽位关系存储在 zk 上、并且提供了 dashboard 可以用来观察和修改槽位关系,当槽位关系发生变化时、Codis Proxy 会监听到变化并重新同步槽位关系

扩容时如何找到槽位对应的 key

在迁移的过程中、 codis 还是会接收到新的请求搭载当前正在迁移的槽位上、因为当前槽位的数据同时存在新旧两个槽位中、当Codis 接收到位于前一槽位中的key后、会立即强制对当前的单个key 进行迁移、迁移完成后、再将请求转发到 新的 Redis 实例。

Codis 的代价

  • 不再支持事务、事务的隔离性不再是串行化了、事务只能在单个 Redis 实例中完成
  • 单个key 不宜过大、否则的话造成迁移卡顿
  • 多了一个代理层、网络开销比单个 Redis 大
  • 要使用到 zk

优点

  • 设计上比 Redis Cluster 简单、将分布式的问题交给了第三方 zk去负责

Redis Cluster

去中心化

万字面试知识点助力金九银十_第40张图片

将所有数据划分为16384 个槽、相比 codis 的1024 更为精细,每个节点负责其中一部分槽位。槽位的信息存储在每个节点。

当客户端来连接集群时、他会的到一份集群槽位的配置信息、这样、但客户要查找某个 key 时、可以直接定位到目标节点

可能下线和确定下线

Redis 集群采用 Gossip 协议来广播自己的状态以及自己对整个集群认知的改变、比如发现某个节点失联了、它会将这个信息向整个集群广播、其他节点也就可以收到这个失联信息、如果一个节点收到了某个节点失联的数量已经到达集群的大多数、就可以标记为该节点下线、然后向整个集群广播、强迫其他节点也接受这个节点下线的事实

事务

  • mutil 指示事务开始

  • exec 指示事务执行

  • discard 指示事务丢弃

事务仔遇到指令失败后、后面的指令还继续执行

Redis 的事务根本不能算原子性、仅仅是满足事务的 隔离性、隔离性中的串行化、当前执行的事务有着不被其他事务打断的权利

Redis 为事务提供了一个 discard 指令、用于丢弃事务缓存队列中所有的指令、执行 exec 之前

可以结合 pipeline 进行优化

watch

乐观锁

watch 会在事务开始之前盯住一个或多个关键变量、当事务执行时、也就是服务器收到 exec 指令要顺序执行缓存的事务队列时、Redis 会检测关键变量自 watch 知乎、是否被修改了、包括当前事务所在的客户端。如果关键变量被人动过了、exec 指令就会返回 null 回复告知客户端事务执行失败

Redis 禁止在 mutil 和 exec 之间执行 watch 指令、而必须在 mutil 之前做好盯住关键变量、否则就会出错

内存回收机制

Redis 并不总是可以将空闲内存立即归还操作系统

如果当前 Redis 内存有 10G 、当你删除 1GB 的 key 之后、你再去观察内存、内存不有太大的变化。因为操作系统回收内存是以页为单位的,如果这个页上只要有一个 key 还在使用、那么他就不能被回收。所以即使你删掉了 1GB 的key 但是这些key 分散在很多页面中、每个页面还有其他key 存在、导致内存不会马上被回收

如果你执行 flushdb 再观察内存、会发现内存确实被回收了、原因是所有的key都被干掉了、大部分之前使用的页面都干净了、会立即被操作系统回收

虽然 Redis 无法保证立即回收已经删除的 key 的内存、但是会重用那些尚未被回收的空闲内存、就好比电影院的人走了、但是座位还在、下一波观众来了、直接坐就行了、而操作系统回收则是把作为都给搬走

主从同步

CAP 原理

  • 一致性
  • 可用性
  • 分区容错性

主从同步

Redis 支持主从同步和从从同步、从从同步时新增功能、为了减轻主库同步负担

万字面试知识点助力金九银十_第41张图片

增量同步

Redis 同步是指令流的、主节点会将那些对自己的状态产生修改的指令记录在内存的 buffer 中、然后异步的将 buffer 的指令同步到从节点、从节点一遍执行主节点同步过来的指令流达到和主节点一样的状态、像主节点反馈自己同步到哪里了(偏移量)

因为内存的 buffer 是有限的、所以 Redis 主库不能将所有的指令都记录在内存 buffer 中、Redis 复制内存buffer 是一个定长的环形数组、如果数组内容满了就会从头开始覆盖前面的内容

如果因为某些原因、从节点在短时间无法和主节点进行同步、Redis 的主节点那些没有同步的指令在 buffer 中已经被覆盖了、那么从节点不能通过指令流来进行同步、这个时候需要用到更加复杂的同步机制、快照同步

快照同步

快照同步时一个非常耗费资源的操作、他首先需要在主库进行一次 bgsave 将当前内存数据全部快照到磁盘中、然后再将快照文件的内容全部传送到从节点。从节点将快照文件接受完毕之后(收到之后落盘)、立即执行一次全量加载、加载之前清空内存的数据、加载完之后再通知主节点进行增量同步

再整个快照同步进行的过程中、主节点的复制 buffer 还在不停的往前移动、如果快照同步的时间过长或复制 buffer 过小、都会导致同步期间的复制buffer 再次被覆盖、这样会导致再次发起快照同步

增加从节点

当从节点刚刚加入到集群时、它必须先进行一次快照同步、同步完成之后在进行增量同步

无盘复制

所谓的无盘复制、指主服务器直接通过套接字将快照内容发送到从节点、生成快照时一个遍历过程、主节点会一边遍历内存、一边将序列化内容发送到从节点、从节点还是跟之前一样、先将接收到内容存储到磁盘、然后一次性加载

wait 指令

Redis 的复制是异步执行的、wait 指令可以让异步复制变成同步复制

wait 1 0 

第一个参数是从库的数量、第二个是时间、单位是毫秒

表示等待 N 个从库同步、最大等待时间为 t 如果为 0 则达标为无线等待

wait 阻塞服务

Kafka

基本认知

  • 消息系统:系统解耦、流量削峰、异步通信、分区消息顺序性保证、回溯消费
  • 存储系统:把消息持久化到磁盘,持久化、多副本
  • 流式处理平台

组成

  • 若干 Producer
  • 若干 Consumer
  • 若干 Broker
  • 一个 ZK 集群 负责集群元数据的管理、控制器的选举

在 Kafka 中还有两个特别重要的概念、主题和分区。Kafka 中的消息以主题为单位进行归类、producer 负责将消息发送到特定主题、而消费者负责订阅主题进行消费。

主题是一个逻辑上的概念、它还可以细分为多个分区、一个分区只属于一个主题,可以称它为主题分区。

Kafka 保证的是分区有序而不是主题有序。

Kafka 为分区引入了多副本机制、通过增加副本数量可以提升容灾能力。同一分区的不同副本保存的是相同的消息(在同一时刻,副本之间并发完全一样),副本之间是 一主多从的关系、其中 Leader 副本负责处理读写请求、follower 副本只与 leader 副本的进行消息同步。

副本位于不同的 broker 中、当leader 副本出现故障时、从 follower 副本中重新选举新的 leader 副本对外提供服务,kafka 通过多副本机制实现了故障自动转移。

Kafka 消费端也具备一定的容灾能力、Consumer 使用拉模式从服务端拉取消息、并保存消费的具体位置、当消费者当即后恢复上线时可以更加之前的消费位置继续拉消息、不会造成消息丢失。

分区中的所有副本统称为AR assined replicas 、所有与 leader 副本保持一定程度同步的副本 包括 leader 副本组成 ISR in sync repilcas ,ISR 是 AR 的一个子集。

消息会先发到 leader 副本、然后 follower 副本才能从 leader 副本中拉取消息进行同步,同步期间、follower 副本相对于 leader 副本会有一定程度的滞后。这时一个可接受的滞后范围

与 leader 副本滞后过多的副本、不包括 leader 副本、组成 OSR out of sync replicas

AR = ISR + OSR 、正常情况下、OSR 应该为 空

leader 副本负责维护和跟踪 ISR 集合所有 follower 副本滞后的状态、当 follower 副本落后太多或失效时、leader 副本会把它从 ISR 集合中剔除。如果 follower 副本追上了 leader 副本、那么 leader 副本会把它从 OSR 及中中转移至 ISR 集合。

默认情况下、当 leader 副本发生故障时、只有在 ISR 集合中的副本才有资格被选举为新的 leader、而在 OSR 及中中的副本没有任何机会

ISR 和 HW hight watermark 以及 LEO log end offset 有紧密的关系

万字面试知识点助力金九银十_第42张图片

消费者只能拉取 HW 之前的消息、而 LEO 标识当前日志文件中下一条待写入消息的 offset。LEO 的大小相当于当前日志分区中最后一条消息的offset 加 1 。

分区 ISR 集合中每个副本都会维护自身的 LEO、而 ISR中最小的 LEO则为分区的 HW、对消费者而言只能消费HW之前的消息。

万字面试知识点助力金九银十_第43张图片

万字面试知识点助力金九银十_第44张图片

万字面试知识点助力金九银十_第45张图片

万字面试知识点助力金九银十_第46张图片

由此可见、Kafka 的复制机制既不是完全的同步复制也不是单纯的异步复制。事实上、同步复制要求所有工作的 follower 副本都复制完、这条消息才被认为成功提交,这种方式极大地影响了性能。而在异步复制方式下、follower 副本异步地从 leader 副本中复制数据、数据只要被 leader 副本写入就被认为是成功提交、这种情况下、如果 follower 副本还没有完全复制完 leader 副本、突然 leader 宕机、则会造成数据丢失。

Kafka 使用的这种 ISR 的方式有效地权限了数据可靠性和性能之间的关系

生产者

producer 是线程安全的、可以在多个线程中共享单个 kafka Producer 实例

对于同一个分区而言、如果消息 records1 与 records2 之前发送、那么 producer 就可以保证对应的 callback1 在 callback2 之前调用、也就是说 回调也可以保证分区有序。

序列化

生产者需要用序列化器将对象转换成字节数组才能通过网络发送给 Kafka、而消费者需要用反序列化器将字节数组转换成响应的对象。

分区器

消息通过 send 方法发往 broker 的过程中、有可能需要经过拦截器、序列化器和分区器的一系列作用滞后才能被正在的发往 broker

如果在 record 中指定了 partition 字段、那么就不需要分区器、如果没有指定则依赖分区器、根据 key 这个值来计算 partition 的值、分区器的作用就是为消息分配分区。

如果 key 部位 null 则默认的分区器对 key 进行哈希、然后根据hash 值来计算分区号、如果 key 为 null 那么消息将会以轮询的方式发往主题的各个可用分区。

拦截器

生产者拦截器、消费者拦截器

整体架构

万字面试知识点助力金九银十_第47张图片

主线程和 sender 线程、缓存到 RecordAccumulator 中、Sender 从 RecordAccumulator 中获取消息将其发送到Kafka

消息收集器的作用是为了Sender 线程能够批量发送、进而减少网络资源的消耗提升性能

ProducerBatch 一个消息批次、ProducerRecord 被包含在 ProducerBatch 中

Sender 从 RecordAccumulator 中获取缓存消息之后、会进一步将原本的 <分区,Deque> 保存形式变为

然后 这样就可以将 Request 请求发送到各个 Node

请求从 Sender 线程发往Kafka 之前还会保存到 InFlightRequests 中、InFlightRequests 保存对象的具体形式为 Map 它的主要作用是缓存已经发出去但没有收到响应的请求,如果未收到响应的节点的请求很多、那么就会不再发送请求给这个节点了。

元数据的更新

从 InFlightRequests 中找到负载最小的 Node、然后获取和更新这些元数据

元数据是指 Kafka 集群的元数据、这些元数据具体记录了集群中有哪些主题、这些主题有哪些分区、每个分区的 leader 副本在哪里节点上、follower 副本分配在哪些节点上、那些副本在AR ISR 中、集群中有哪些节点、控制器又是哪一个等信息。

当客户端没有需要使用的元数据信息时、或者到期更新元数据就会发起请求去负载最低的节点中获取元数据

重要的参数

  • asks 这个参数用来制定分区中必须要有多少副本收到这条消息之后、生产者才会认为这条细哦洗成功写入、acks 是 生产者客户端中非常重要的参数、它涉及到消息的可靠性和吞吐量之间的权衡。acks 有三种类型的值、都是字符串
    • acks = 1 默认值为 1 生产者发送消息之后、只要分区的 leader 副本成功写入消息、那么他就会收到服务端的成功响应、如果消息无法写入 leader 副本、比如 leader 副本崩溃、重新选举新的 leader 副本,那么生产者将会收到一个错误的响应,为了避免消息丢失、消费者可以选择重新发送消息。是消息可靠性和吞吐量之间的折中方案
    • acks = 0 生产者发送消息之后不需要等待任何服务端 的响应。在其他配置一样的情况下、acks=0 的吞吐量最大
    • acks =-1 或 acks =all 生产者在消息发送以后、需要等待 ISR 中所有副本都成功写入消息后、才能收到服务端的成功响应、在其他配置相同的情况下、ack=-1 可以达到最强的可靠性。但是并不意味消息就一定可靠、因为 ISR 中可能只有 leader 副本、这样子就退化成 acks=1 的情况。
  • retries 和 retry.backoff.ms 重试次数默认是0、重试的时候的间隔时间、默认为 100
    • Kafka 可以保证同一个分区的消息是有序的、如果生产者按照一定的顺序发送消息、那么这些消息也会顺序地写入分区、进而消费者也可以按照同样的顺序消费它们、如果 acks 设置为 非零值、并且 max.in.flight.requests.per.connection 参数配置大于 1、那么就会出现错序的现象、如果第一批写入失败、第二批次消息写入成功、那么生产者会重试第一批次的消息、此时如果第一批次消息写入成功、那么这两个批次的消息就会错序
  • compression.type 消息的压缩方式、默认为 none、默认情况下消息不回被压缩。对消息压缩可以极大地减少网络传输量、降低网络 IO 、从而提高整体性能、时间换空间、如果对时延有要求、不建议进行压缩
  • linger.ms 指定生产者发送 ProducerBatch 之前等待更多 ProducerRecord 加入 ProducerBatch 的时间,默认值为0.生产者客户端会在 ProducerBatch 被填满或等待时间超过 linger.ms 值时发送出去。增大这个值会增加参数的时延、但是能提升吞吐量

消费者

消费者负责订阅Kafka 中的主题、并且从订阅的主题上拉取消息。

在 Kafka 的消费中还有消费组、每个消费者都有一个对应的消费组、当消息发布到主题后、只会投递给订阅它的每个消费组的一个消费者

如果消费者过多、出现了消费者的个数大于分区的个数、就会有消费者分配不到任何分区

一个正常的消费逻辑需要具备一下几个步骤

  1. 配置消费者客户端参数创建相应的消费者实例
  2. 订阅主题
  3. 拉取消息并消费
  4. 提交消费唯一
  5. 关闭消费者实例

消息消费

消息的消费一般有两种模式、推模式和拉模式。推模式时服务端主动将消息推送给消费者、而拉模式时消费者主动向服务端发起请求来拉取消息。 Kafka 时基于拉模式的

可以简单地认为 poll 方法只是拉取一下消息而已、但就其内部逻辑而言并不简单、它涉及消费的位移、消费者协调器、组协调器、分区分配的分发、再均衡的逻辑、心跳等内容。

位移的提交

消费者的位移存储在 Kafka 内部主题 __consumer_offsets

对于位移的提交的具体时间的把握也很有把握、可能会造成重复消费和消息丢失的现象。

万字面试知识点助力金九银十_第48张图片

当前 poll 操作所拉取的消息集为 [x+2,x+7]、x+2 代表上上一次提交的消费位移、说明已经完成了一次 x+1 之前包括x+1的所有消息的消费 x+5 表示当前正在处理的位置,如果拉取到消息之后就位移提交了、也就是提交 x+8、那么当消费 x+5 的时候遇到异常、在故障恢复之后、我们重新拉取的消息从 x+8开始的、也就是说、x+5 到 x+7之间到消息并没有被消费、如此便发生了消息丢失的现象。

如果位移提交的动作是在消费完所有拉取到的消息之后菜执行的、那么当消费 x+5的时候遇到异常、在故障恢复之后、我们重新拉取的消息是从 x+2 开始的、也就是 x+2到 x+4之间到消息又重新消费了一遍、

在 Kafka 中默认的消费位移提交的方式是自动提交、当然这个默认提交不是没消费一条消息就提交一次、而是定期提交、这个定期提交的周期时间默认是 5s、

在默认的情况下、消费者每隔5s会将拉取到的每隔分区中最大的消息位移进行提交。提交的动作是在poll 方法的逻辑里完成的。每次真正向服务端发起拉取请求之前会检查释放可以进行位移提交、如果可以、就提交上次轮询的位移。

commitAsync 提交的时候同样也会有失败的情况发生、我们可以设置一个在本保存它提交的位移、每次提交前就改变它、再遇到位移提交失败需要重试的时候、可以检查所提交的位移和序号的值的大小,如果提交的位移小于序号的值、那就说明有更大的位移已经提交了,不需要进行重新提交、如果两者相同、则可以提交。代码正确的情况下、是不会出现提交位移的大小大于序号的大小的。

指定位移消费

当一个新的消费组简历的时候、他根本没有可以查找的消费位移、或者消费组订阅了一个新的主题、他也没有可以查找的消费位移。

在 kafka 中每当消费者查找不到所记录的消费位移时、就会根据消费者客户端的桉树的配置来决定从何处开始进行消费、这个参数默认值时 latest 表示从分区末尾开始消费消息。如果将参数配置为 earliest 那么消费者会从最开始处消费。

当然还可以将其设置为 none 那么当找不到消费位移的时候就会报错

再均衡

指分区的所属权从一个消费者转移到另一个消费者的行为

在再均衡期间、消费组内的消费者无法读取到消息。在再均衡发生的这小段时间内、消费组会变得不可用、另外、当一个分区重新分配给另一个消费者时、消费者当前的状态就会丢失。比如消费者消费完某个分区的一部分消息时还没来得及提交位移消费就发生了再均衡、之后这个分区分配给了另一个组内的消费者、原来被消费的那部分消息又被重新消费一遍、也就发生了重复消费。

可以配合 ConsumerRebalanceListener 里面的两个方法、一个是在再均衡开始之前和消费者停止读取消息之后被调用,可以通过这个方法提交消费位移

还有一个方法是在再均衡完成之后消费可以开始读取消费之前被调用

万字面试知识点助力金九银十_第49张图片

或者配合 seek 指定拉取的位置

万字面试知识点助力金九银十_第50张图片

消费者拦截器

多线程实现

KafkaProducer 是线程安全的、KafkaConsumer 却是非线程安全的,consumer 中定义了一个 acquire 方法用来检查当前是否只有一个线程在操作、若有其他线程正在操作则会抛出 ConcurrentModificationException

万字面试知识点助力金九银十_第51张图片

优点是每个线程可以按顺序消费各个分区中的消息、缺点也很明显每个线程都在维护一个独立的TCP 连接 这个会造成不小的系统开销

万字面试知识点助力金九银十_第52张图片

每个处理消息的 RecordHandler 类在处理完消息之后都将对应的消费位移保存到共享变量 offsets 中

每次 poll 之前将其提交

但是这样子存在风险、假如有个处理线程 RecordHandler1 正在处理 0-99 的消息、而另一个 RecordHandler2 已经处理完 offset 为 100-199 的消息并进行位移提交、此时如果 1 发生异常、则之后消费只能从 200 开始而无法再次消费 0-99的消息、造成消息丢失的现象。

万字面试知识点助力金九银十_第53张图片

配置参数

  • max.poll.records

    配置 consumer 在一次拉取请求中拉取的最大消息数、默认值为 500条、如果消息的大小都比较小、可以适当调大这个参数值来提升一定的消费速度

  • hearbeat.interval.ms 默认值 3000 当使用 Kafka 分组管理功能时、心跳到消费者协调器之间的预计时间、心跳用于确保消费者绘画保持活动状态、当所有消费者加入或离开时方便重新平衡、该值必须比 session.timeout.ms 小、通常是 1/3 也可以调整得更低。

  • session.timeout.ms 默认值 10000 组管理协议中用来检查消费者释放失效的超市时间

  • max.poll.interval.ms 默认值 300000 当通过消费组管理消费者时、该配置指定拉取消息最长空闲时间、若超过这个时间间隔还没发起 poll 操作、则消费组认为该消费者已经离开了消费组、将进行再均衡操作

主题和分区

万字面试知识点助力金九银十_第54张图片

配置

  • cleanup.policy 日志压缩策略、默认值是 delete 还可以配置 compact
  • compression.type 消息的压缩类型 默认值为 producer 表示保留生产者中所示有的原始压缩类型

分区管理

分区使用多副本机制来提升可靠性、但只有 leader 副本对外提供读写服务、而 follower 副本只负责在内部进行消息同步

日志清理

提供两种日志清理策略

  • 日志删除 按照一定的保留策略直接删除不符合条件的日志分段
  • 日志压缩 针对每个消息的key 进行整合、对于有相同 key 的不同value 的值、只保留最后一个版本

日志删除

  • 基于时间
  • 基于日志大小
  • 基于日志起始偏移量

磁盘存储

Kafka 是依赖于磁盘来存储和缓存消息的

顺序写盘的速度不仅比随机写盘的速度快、而且比随机写内存的速度快。

Kafka 在设计时采用了文件追加的方式来写入消息、即只能在日志文件的尾部追加新的消息、并且不允许修改写入的消息、这种方式属于典型的顺序写盘操作,所以计算Kafka 使用磁盘作为存储介质、他所能承载的吞吐量也不容小觑

页缓存

页缓存是操作系统实现的一种主要的磁盘缓存、以此来减少对磁盘 IO 的操作,具体来说就是将磁盘的数据缓存到内存,把磁盘的访问变为对内存的访问

Linux操作系统中的参数用来指定当脏页数量达到系统内存的百分之几后会触发 pdflush 处理脏页

对一个进程而已、他会在进程内缓存处理所需的数据、然而这些数据有可能还缓存在操作系统的页缓存中、同一份数据可能被缓存两次、除非使用 Direct IO 的方式

此外 Java 对象的内存开销非常大、通常是真实数据大小的几倍甚至更多、空间使用率低下。

还有就是垃圾回收会随着堆内数据增多而变得越来越慢

所以使用操作系统的页缓存不仅可以省去来进程内的缓存消耗,同时还可以通过结构紧凑的字节码代替对象节省更多的空间、而且大内存的时候不用担心 GC 带来的性能问题。此外 Kafka 重启 页缓存依然是有效的、页缓存和文件之间的一致性交由操作系统来负责、比进程内维护更加安全有效。

Kafka 使用了大量的页缓存、这也是 Kafka 实现高吞吐的重要因素之一。

零拷贝

零拷贝指的是数据直接从磁盘文件复制到网卡设备中、而不需要经由应用程序之手。零拷贝大大提高了应用程序的性能、减少了内核和用户模式之间的上下文切换。

万字面试知识点助力金九银十_第55张图片

从上面的过程中、数据平白无故地从内核模式到用户模式走了一圈、浪费了两次复制过程、第一次是从内核模式复制到用户模式、第二次是从用户模式再复制回内核模式、上面的 2、3 步。而且在上面的过程中内核和用户模式的上下文切换也是 4 次。

万字面试知识点助力金九银十_第56张图片

零拷贝技术通过 DMA 技术将文件内容复制到内核模式下的 Read Buffer 中、不过没有数据复制到Socket Buffer 相反只有包含数据的位置和长度信息的文件描述符被加到 Socket Buffer 中。DMA 引擎将数据从内核模式中传递到网卡设备、这里只经历了2次复制就从磁盘中创送出去、上下文切花也变成了 2 次。

零拷贝是针对内核模式而言的、数据在内核模式下实现了零拷贝。

控制器

Kafka 集群中会有一个或多个 broker ,其中一个 broker 会被选举为控制器,他负责管理整个集群中所有分区和副本的状态、当某个分区的 leader 副本出现故障时、由控制器复制为该分区选举新的 leader 副本、当检查到某个分区的 ISR 集合发生变化时,由控制器负责同志所有 broker 更新其元数据信息

可靠性

  • 就Kafka 而言、越多的副本越能够保证数据的可靠性
  • 生产者中的 acks 参数、设置为 -1 。所有的isr 中的副本写入成功才算成功
  • 当然也可能出现 isr 中只剩下 leader 副本、所以我们可以配置 isr 的副本数量最小为2 确保 消息能在 leader 副本 和 follower副本存在
  • 因为 Kafka 使用大量的页缓存、刷盘的时间也可以配置、默认是交由操作系统去做同步
  • 然后我们应该关闭 当 leader 副本失效重新选举 leader 的时候、只能够从 isr 中选取、而不是 ar中
  • 消费者那边也要注意位移提交的问题

Spring Boot /Cloud

Spring Cloud Eureka

  • 服务注册
  • 服务发现
@EnableEurekaServer
  • eureka.clent.register-with-eureka: 由于该应用为注册中心,所以设置为 false, 代表不向注册中心注册自己。
  • eureka.client.fetch-registry: 由于注册中心的职责就是维护服务实例,它并不需要去检索服务, 所以也设置为 false

服务提供者

@EnableDiscoveryClient

高可用的时候

  • eureka.clent.register-with-eureka: 不需要改变、默认为true 就好
  • eureka.client.fetch-registry: 不需要改变、默认为true 就好

然后配置集群的某个地址

eureka.instance.hostname = peerl
eureka.client.serviceUrl.defaultZone = http://peer2:1112/eureka/

服务消费者

再在 RestTemplate 加上 @LoadBalance 注解

  • 服务提供者
    • 服务注册:服务提供者会在启动时发送请求将自己注册到Eureka、附带上自己的一些元数据信息
    • 服务同步:两个 Eureka Server 之间会进行数据同步
    • 服务续约
      • 服务提供者会维护一个新探告诉 Eureka 他还活着、renew
      • 默认时 30s renew 一次、90s 没renew 就认为服务失效了
  • 服务消费者
    • 获取服务
      • 30s 更新一次服务列表
    • 服务调用
      • Ribbon 默认会采用轮询的方式进行调用、从而实现客户端负载均衡
      • 对于访问实例的选择、Eureka 中有 Region 和 Zone 的概念、一个 Region 中可以包含多个 Zone、每个服务都要被注册到一个 Zone 中、所以一个客户端对应一个 Region 和 一个Zone、在进行服务调用的时候、优先访问同处一个 Zone 中的服务提供方、若访问不到、则访问其他 Zone
    • 服务下线
  • 服务注册中心
    • 失效剔除:定时任务 60s、将当前清单中超时 90s 没有renew 的服务剔除出去
    • 自我保护:Eureka 会统计心跳失败比例在 15分钟内是否低于 85% 如果出现低于的情况下、Eureka 会将当前实例注册信息保护起来、让这些实例不过期(生产上经常是因为网络不稳定导致的)。但是在保护期间若实例出现问他、客户端很可能拿到这些实际不存在的服务实例、出现调用失败的原因

客户端负载均衡 Spring Cloud Ribbon

工具类框架

使用客户端负载均衡调用非常简单

  • 服务提供者只需要启动多个服务实例、然后注册到注册中心
  • 服务消费者直接通过被 @LoadBalanced 注解修饰过的 RestTemplate 来实现服务的接口调用

负载均衡策略

  • 随机
  • 线性轮询
  • 权重
  • 最小负载
  • Zone 区域亲和

Eureka 服务治理机制强调了 CAP 原理中的AP、即可用性和可靠性、他与 ZK 强调 CP 一致性、可靠性 的服务治理框架最大的区别就是、Eureka 为了实现更高的服务可用性、失去了一定的一致性 、在极端情况下它宁愿接受故障的实例也不要丢弃”健康“的实例。

服务容错保护 Hystrix

具备功能、服务降级、服务熔断、线程和信号隔离、请求缓存、请求合并以及服务监控等

@EnableCircuitBreaker 开启断路器功能

  1. 结果是否被缓存
  2. 断路器是否打开
  3. 线程池/请求队列/信号量是否占满
  4. hystrix 会将 成功、失败、拒绝、超时 等信息报告给断路器、断路器会用计数器维护这些数据
  5. fallback 处理
  6. 返回成功响应

Spring Cloud Feign

@EnbleFeignClients

Mysql

概述

逻辑架构

万字面试知识点助力金九银十_第57张图片

  • 第一层、连接处理、授权认证、安全等
    • 每个客户端连接在服务端进程中拥有一个线程
  • 查询解析、分析、优化、缓存

  • 存储引擎

并发控制

写锁比读锁有更高的优先级、一个写锁请求可能会被插入到读锁队列的前面。

  • 行锁
    • 大开销
    • 只在存储引擎实现、MySQL 服务器没有实现
  • 表锁
    • 开销小
    • 粒度大

事务

acid

  • 原子性
    • 一个事务必须被视为一个不可分割的最小工作单元、整个事务中的所有操作要么全部提交成功、要么全部失败回滚、对于一个事务来说、不可能只执行其中的一部分操作、这就是事务的原子性操作
  • 一致性
    • 数据库总是从一个一致性状态转换到另一个一致性状态
  • 隔离性
    • 隔离级别
      • 未提交读
      • 提交读
        • 两次执行同样的查询、可能会得到不一样的结果
      • 可重复读
        • 读取某个范围内的记录时、另一个事务又在该范围插入了新的记录。InnoDb 存储引擎通过多版本并发控制 MVCC 解决了幻读的问题
      • 可串行化
        • 会在读取每一行数据都加锁、所以可能导致大量的超时和锁争用问题
  • 持久性
    • 一旦事务提交、其所做的修改会永久保存在数据库中。

死锁

死锁是指两个或多个事务在同一资源上相互占用、并请求锁定对方占用的资源、从而导致恶性循环的现象

为了解决这种问题、InnoDB 存储引擎、检测到死锁的循环依赖、并立即返回一个错误。还有一种解决方式、查询时间到达锁等待超时的设定后放弃锁请求。

InnoDB 目前处理死锁的方法是、将持有最少行排它锁的事务进行回滚

事务日志

事务日志采用追加的方式、顺序IO

预写式日志、修改数据需要写两次磁盘

Mysql 中的锁

InnoDB 采用两阶段锁定协议、在事务执行过程中、随时都可以执行锁定、锁只有在执行 Commit 或者 Rollback 的时候才会释放、并且所有锁是在同一时刻被释放的

显式锁定

  • select… lock in share mode
  • select… for update

多版本并发控制

可以认为 MVCC 是行级锁的一个变种、但是它在很多情况下避免了加锁操作、因此开销更低。

MVCC 的实现、是通过保存数据在某个时间点的快照来实现的、也就是说、不管需要执行多长时间、每个事务看到的数据都是一致的。

根据事务开始的时间不同、每个事务对同一张表、同一时刻看到的数据可能是不一样的。

InnoDB 的 MVCC 通过在每行记录后面保存两个隐藏的列来实现、这两个列、一个保存行创建的系统版本号、一个保存行的删除时候的系统版本号。每开始一个新的事务、系统版本号就会递增、事务开始的时刻系统版本号会作为事务的版本号、用来和查询每行记录的版本号进行比较。

万字面试知识点助力金九银十_第58张图片

MVCC 只在 RR 和 RC 两个隔离级别下工作、其他的隔离级别都和 MVCC 不兼容。

InnoDB 引擎

InnoDB 的数据存储在表空间中、由一系列的数据文件组成。InnoDB 可以将每个表的数据和索引放在单独的文件中。

InnoDB 采用 MVCC 来保持高并发、并且实现了四个标准的隔离级别。默认级别是 RR、并且通过间隙锁策略防止幻读的出现。间隙锁使得 InnoDb 不仅仅锁定查询涉及的行、还会对索引中间的间隙进行锁定、以防止幻影行插入 。

InnoDB 表基于聚集索引建立的、聚集索引对主键查询由很高的性能、不过二级索引必须包含主键列、所以如果主键列很大的话、其他的所有索引都会很大、因此、若表上的索引很多的话、主键应当尽可能小

InnoDB 内部做了很多优化、包括磁盘读取数据时采用的可预测性预读、能够在内存中创建 hash 索引以加速读操作的自适应哈希索引、以及能够加速插入操作的插入缓冲

MyISAM 引擎

不支持事务和行级锁

MyISAM 会将表存储在两个文件中、数据文件和索引文件

  • 加锁与并发
    • 对整张表加锁、而不是对行。读时共享锁、写时排它锁。但是在表读取查询的时候、也可以往表中插入新的记录(并发插入)
  • 索引特性
    • 即使时 BLOB 和 TEXT 等长字段、也可以基于其前500个字符创建索引、支持全文索引
  • 延迟更新索引值
    • 创建表时指定了 DELAY_KEY_WRITE 选项、每次修改执行完时、不会立即将修改的索引写入磁盘 而是写道键缓冲区、只有在清理键缓冲区的时候才将对应索引块吸入磁盘
  • 不会再对表进行修改操作、那么这样的表或许适合采用 MyISAM 的压缩表

Memory 引擎

  • 数据不会被修改、重启之后丢失也没关系、那么就用Memory 引擎
  • 数据都是保存在内存中、重启之后表结构还在、但是数据会丢失
  • 支持 Hash 索引、因此查找操作非常快
  • 表级锁、写入性能低
  • 不支持BLOB、TEXT类型的列、
  • 并且每行的长度时固定的、即使指定了 VARCHAR 列、实际存储时也会转成 CHAR 会导致内存的浪费

如果 Mysql 在执行查询过程中需要使用到临时表来保存中间结果、内部使用的临时表就是 Memory 表、如果超出了 Memory 表的限制、或者含有 Text 字段、则临时表转换成 MyISAM 表

  • 临时表可以是Memory存储引擎的、临时表在单个连接中可见、当连接断开时、临时表将不复存在

选择不同的存储引擎

  • 事务
    • InnoDB 支持热备份
  • 备份
    • InnoDB 在线热备份
  • 崩溃恢复
    • MyISAM 崩溃后发生的损坏概率比 InnoDB 大、而且恢复速度也慢
  • 特有的特性

Schema 与数据类型优化

  • 更少的通常更好
  • 简单就好
  • 尽量避免 NULL

varchar 和 char

varchar

  • 比定长类型更省空间
  • 需要使用 1 或 2 个额外子接记录字符串长度、如果列的最大长度小于或等于 255 字节、则只使用 1 个字节表示、否则使用 2 个字节。varchar(10) 需要11个字节的存储空间 varchar(1000) 则需要 1002个字节
  • update 的时候需要做额外的工作、因为可能会变长

适合用 varchar

  • 字符串列的最大长度比平均长度大很多
  • 列的更新很少
  • 使用 UTF8 字符集的时候

char

  • 定长
  • 采用空格进行填充

适合用 char

  • 定长、如 MD5
  • 非常短的列

datetime & timestamp

  • datetime 存储范围大、从 1001 到 9999 精确到秒、他把日期和时间封装到格式为YYYYMMDDHHMMSS的整数中、与时区无关、使用 8 字节存储空间
  • timestamp 类型保存了 从 1970.01.01午夜以来的秒数、它和Unix 时间戳相同、使用 4个字节存储、表示范围从 1970-2038 ;依赖时区、Mysql 服务器、操作系统、以及客户端连接都有时区设置
  • timestamp 列默认为 not null 这和其他数据类型不一样
  • 如果插入时候没有指定第一个 timestamp 列的值、mysql 则会设置这个列的值为当前时间

索引

索引的类型

B-Tree

通常意味着所有的值都是按顺序存储的、并且每个叶子页到根的距离相同

万字面试知识点助力金九银十_第59张图片

B-Tree 索引能够加快访问数据的速度、因为存储引擎不再需要进行全表扫描来获取需要的数据、取而代之的是从索引的根节点进行搜索。根节点的槽中存放了指向子节点的指针、存储引擎根据这些指针向下查找。通过比较节点页的值和要查找的值可以找到合适的指针进入下层子节点、这些指针实际上定义了子节点页中值的上限和下限。最终存储引擎要么找到对应的值、要么该记录不存在 。

B-Tree 对索引列是顺序组织存储的、所以很适合范围查找。

万字面试知识点助力金九银十_第60张图片

索引对多个值进行排序的依据是创建索引时的顺序、

B-Tree 索引适用于全键值、键值范围或键前缀查找(最左前缀查找)

  • 全键值匹配
  • 匹配最左前缀
  • 匹配范围值
  • 精确匹配某一列并范围匹配另一列

索引还可以用于查询中的 order by 才足以、如果 order by 子句满足上面所说的查询类型

https://learnku.com/articles/38925#389a5d

B-Tree 索引的限制

  • 如果不是按照索引的最左列开始查找、则无法使用索引
  • 不能跳过索引中的列
  • 如果查询中由某个列的范围查询、则其右边所有列都无法使用索引优化查询

哈希索引

只有精确匹配索引的所有的列查询才有效

对于每一行数据、存储引擎都会对所有的索引列计算一个哈希码、哈希码是一个较小的值。哈希索引将所有的哈希码存储在索引中、同时在哈希表中保存指向每个数据行的指针 。

只有 Memory 引擎显式支持哈希索引、这也是 Memory 引擎表,默认的索引类型、Memory 引擎同时也支持 B-Tree 索引。值得一提的是、memory 引擎是支持非唯一哈希索引的、如果多个列的哈希值相同、索引会以链表的方式存放多个记录指针到同一个哈希条目中 。

优点

  • 索引自身只需存储对应的哈希值、所以索引结构十分紧凑、哈希索引查找的速度非常快

缺点

  • 哈希索引只包含哈希值和行指针、而不存储字段值、所以不能使用索引中的值来避免行读取。不过内存中的行读取很快
  • 哈希索引数据并不是按照索引值顺序存储的、所以无法用于排序
  • 不支持部分索引列的匹配查找
  • 只支持等值比较查询、不支持任何范围查询
  • 访问哈希索引的数据非常快、除非有很多哈希冲突

InnoDB 引擎有一个特殊的功能叫做 “自适应哈希索引”。当 InnoDB 注意到某些索引值被使用得非常频繁时、它会在内存中基于 B-Tree 索引之上再创建一个哈希索引、这样就让B-Tree 索引具有哈希索引的一些优点,比如快速的哈希查找。

索引的优点

  • 索引大大减少了服务器需要扫描的数据量
  • 避免排序和临时表
  • 随机IO 变成顺序 IO

高性能索引策略

  • 独立的列–索引列不能是表达式的一部分也不能是函数的参数
  • 前缀索引–索引很长的字符列、会让索引变得大且慢。我们可以索引开始的部分字符、这样可以大大节约索引空间、提高索引效率
  • 索引的选择性–指的是不重复的索引值和数据表记录总数的比值。索引的选择性越高、查询效率越高、唯一索引的选择性是 1、这是最好的索引选择性

多列索引

选择合适的索引列顺序

  • 将选择性最高的列放到索引最前列。这个建议在某些场景下可能有帮助、但通常不如避免随机 IO 和排序那么重要。
  • 也要考虑查询条件的具体值、也就是值得分布有关

聚集索引

是一种数据存储的方式。

InnoDB的聚集索引实际上在同一个结构中保存了 B-Tree 索引和数据行

聚集索引的数据行实际上是存放在索引的叶子页中。聚集 表示数据行和相邻的键值紧凑地存储在一起。一个表只能有一个聚集索引。

叶子页包含了行的全部数据、但是节点页只包含了索引的列

万字面试知识点助力金九银十_第61张图片

聚集索引的列就是主键列

优点

  • 相关数据保存在一起
  • 数据访问更加快
  • 覆盖索引扫描可以直接在非叶子节点中得到主键值

缺点

  • 聚集索引最大限度的提高了IO密集型应用的性能、如果数据全部放在内存中、则访问的顺序就没有那么重要了、聚集索引也没什么优势了
  • 插入速度严重依赖于插入顺序。按主键的顺序插入是加载数据到 InnoDB 表中速度最快的方式。如果不是按照主键顺序加载数据、那么加载完成之后最好使用 Optimize table 命令重新组织下表
  • 插入新行的时候可能会面临 页分裂问题
  • 聚集索引可能会导致全表扫描变慢、尤其是行比较稀疏、或者由于页分裂导致数据存储不连续的时候
  • 二级索引可能比想象中要大、因为二级索引中的叶子节点包含了行的主键列
  • 二级索引访问需要两次索引查找、而不是一次。要记住、二级索引叶子节点保存的不是指向行的物理位置的指针、而是行的主键值。对于 InnoDB 、自适应哈希索引能够减少这样重复工作

MyISAM 和 InnoDB 的索引对比

  • MyISAM 中主键索引和其他索引在结构上没有什么不同
  • InnoDB 中聚集索引就是表、不像MyISAM 那样需要独立的行存储
  • 聚集索引的每个叶子节点都包含了主键值、事务ID、用于事务和 MVCC 的回滚指针、剩余的列
  • 二级索引存储的是主键值、而不是行指针、好处是InnoDB 在移动时无须更新二级索引的这个值、坏处是让二级索引占用了更多的空间

万字面试知识点助力金九银十_第62张图片

InnoDB 表中按主键顺序插入行

最好使用 Auto_increment 自增列、这样可以保证数据行是按顺序写入

UUID 主键插入行不仅花费时间更长,而且索引占用的空间也更大。这一方面由于主键字段更长、另一方面毫无疑问由于页分裂和碎片导致的。

万字面试知识点助力金九银十_第63张图片

当达到页的最大填充因子时、默认是页大小的 15/16 ,剩余的空间用于以后的修改。下一条记录就会写入新的页中。

按照这种方式、主键页就会近似地被填满。这就是期望的结果。(二级索引页可能是不一样的)

而使用 UUID 插入

因为新行的主键值不一定比之前插入的大、所以 InnoDB 无法简单地总是把新行插入到索引的最好、而是需要为新行找一个合适的位置–通常是已有数据的中间位置-并且分配空间。这会增加很多额外的工作、导致数据分布不够优化、缺点如下

  • 写入的目标页可能已经刷到磁盘上并从缓存中移除、或者还没有被加入到缓存中、InnoDB 不得不在插入前先找到并从磁盘读取目标页到内存中。这将导致大量的随机 IO
  • 因为写入是乱序的、InnoDB 不得不频繁地做页分裂操作、以便为新的行分配空间。页分裂会导致移动大量数据、一次插入最少需要修改三个页、而不是一个页
  • 由于频繁的页分裂、页会变得稀疏并被不规则地填充、最终会有数据碎片

把这些随机值载入到聚集索引以后、也会需要做 optimize table 来重建表并优化页的填充。

InnoDB 应该尽可能地按主键顺序插入数据、并且尽可能使用单调增加聚集键的值来插入新行

顺序主键什么时候回造成更坏的结果

  • 高并发工作负载、在InnoDB 中按主键顺序插入可能回造成明显的争用、因为所有的插入都发生在这里、并发插入可能会导致间隙锁竞争。另一个热点可能是 auto_increment 锁机制

覆盖索引

如果索引的叶子节点中已经包含要查询的数据、那么就不需要进行回表。如果一个索引包含所有需要查询的字段的值、我们就称之为覆盖索引

  • 索引条目通常远小于数据行大小、所以如果只需要读取索引、那么Mysql 就会极大地减少数据访问量。覆盖索引对于 IO 密集型的应用也有帮助、因为索引比数据更小、更容易全部放入内存。
  • 因为索引是按照列值顺序存储的(至少在单个页内是如此)、所以对于 IO 密集型的范围查询会比随机从磁盘读取一行数据的 IO 少得多
  • 一些存储引起如 MyISAM 在内存中只缓存索引、数据则依赖操作系统来缓存、因为访问数据需要一次系统调用、这可能会导致严重的性能问题
  • 由于InnoDB 的聚集所以、覆盖索引对 InnoDB 表特别有用,可以避免对主键索引对二次查询

体系结构

  • 数据库 物理操作系统文件或其他形式文件类型的集合。比如说 frm、myd、myi、ibd 结尾的文件
  • 实例 mysql 数据库由后台线程以及一个共享内存区组成

InnoDB 存储引擎支持事务、行锁设计、支持外键、并支持类似于 Oracle 的非锁定读、即默认读取操作不会产生锁。

InnoDB存储引擎的表单独存放到一个独立的 ibd 文件中

InnoDB 通过使用多版本控制并发 MVCC 来获得高并发性、并且实现了 SQL 标准的 4 种隔离级别、默认为 RR 级别。同时使用一种称为 next-key locking 的策略避免幻读现象的产生。

InnoDB 存储引擎还提供了插入缓冲、二次写、自适应哈希索引、预读。

对于表中数据的存储、InnoDB 存储引起采用聚集的方式、每张表的存储都是按主键的顺序进行存放的、如果没有显式地在表定义的时候指定主键、InnoDB 存储引擎会在为每一个行生成一个6字节的 ROWID、并以此为主键。

MyISAM 引擎不支持事务、表锁设计、由MYI 和 MYD 组成、它的缓冲池只缓存索引文件、而不缓存数据文件

Memory 将表中的数据放在内存中、如果数据库发生重启或崩溃、表中的数据将会消失、非常适合存储临时数据的临时表

Memory 存储引擎默认使用哈希索引、而不是我们熟悉的 B+树索引。只支持表锁、并发性能差、并且不支持 BLOB 和TEXT 类型。最重要的是变长字段是按照定长字段方式存储的、因此会浪费内存。还有一点就是、Mysql 数据库使用 Memory 作为临时表来存放查询的中间结果、如果中间结果集大于 Memory 存储引擎的容量设置、又或者中间结果含有 TEXT 或 BLOB 类型的字段、就会使用 MyISAM 存储表作为临时表、但是MyISAM 只缓存索引数据、不缓存数据文件、因此查询性能不太好。

后台线程

后台线程主要的作用是负责刷新内存池中的数据、保证缓冲池的内存缓存是最近的数据、此外将已修改的数据文件刷新到磁盘文件、同时保证在数据库异常的情况下 InnoDB 能恢复到正常运行的状态。

  • Master Thread

    核心后台线程、主要负责将缓冲池的数据一步刷新到磁盘、保证数据的一致性、包括脏页的刷新、合并插入缓冲、UNDO 页的回收

  • IO Thread

    在InnoDB 中大量使用了 AIO 来处理写IO请求、这样可以提高数据库的性能。wirte、read、inset buffer 、log IO

  • Purge Thread

    事务提交之后、其使用的 undolog 可能不再需要、因为需要 Purge Thread 来回收已经使用并分配的 undo 页。

  • page cleaner thread

    刷新脏页

内存

万字面试知识点助力金九银十_第64张图片

  • 索引页
  • 数据页
  • undo 页
  • 插入缓冲
  • 自适应哈希索引
  • 锁信息
  • 数据字典信息

LRU List Free List Flush List

通常来说、数据库中的缓冲池是通过 LRU 算法进行管理的、即最频繁使用的页在 LRU 列表的前端、而最少使用的页在 LRU 列表的尾端。当缓冲池不能存放新读取的页时、先释放 LRU 列表中尾端的页

缓冲池的页的大小是 16KB、它将传统的 LRU 算法做了一些优化、在LRU 列表中加入了 midpoint 位置、新读取到的页虽然是最新访问的页、但是不能直接放入 LRU 列表的首部、而是放入 midpoint 。这个位置大概是 LRU 列表长度的 5/8

midpoint 之后的列表称为 old 列表、之前的称为 new 列表、可以认为 new 列表中的页都是最为活跃的数据热点数据

LRU 列表从 old 部分加入到 new 部分时、称此事发生的操作为 page made young、而因为 innodb_old_blocks_time 的设置而导致页没有从old 转到 new 部分到操作称为 page not made young

自适应哈希索引、Lock 信息、insert buffer 等页不需要LRU算法进行维护

LRU 用来管理已经读取的页、但当数据库刚启动时、LRU 列表是空的、没有任何页、这时页都放在 Free 列表中。当需要从缓冲池中分页时、首先从 Free 列表中查找释放有可用的空闲页、如果有则从Free 中删除、放入LRU 列表中。否则根据 LRU 算法淘汰 LRU 列表末尾的页。

在 LRU 列表中的页被修改后、称为脏页、也就是缓冲池中的页和磁盘的页数据产生了不一致。这时候 DB 会通过 checkpoint 机制将脏页刷新回磁盘、脏页即存在 LRU 列表、也存在 Flush 列表中

Http

摘抄自小林大神 https://www.cnblogs.com/xiaolincoding/p/12442435.html

  1. Http 是什么

    超文本传输协议、是在计算机世界里面专门在两点之间传输文字、图片、音效、视频等超文本数据等约定和规范

  2. 五大类 HTTP 状态码

    具体含义 常见状态码
    1xx 提示信息、表示目前协议处理等中间状态、还需要后续的操作
    2xx 成功、报文已经收到并被正确处理 200、204、206
    3xx 重定向、资源位置发生变动、需要客户端重新发送请求 301、302、304
    4xx 客户端错误、请求报文有误、服务器无法处理 400、403、404
    5xx 服务器错误、服务器在处理请求时内部发生了错误 500、501、502
     1xx 属于提示信息、协议处理中的一种中间状态、实际用到的比较少
    
     2xx 表示服务成功处理了客户端的请求、
    
     	200 OK 表示一切正常、如果非 HEAD 请求、服务器返回的响应都会有 body 数据
    
     	204 no content 常见成功状态码、与200 基本相同、只是没有了 body 数据
    
     	206 partial content 时用于 HTTP 分块下载或断点续传、表示返回的 body 数据并不是资源的全部、而是其中的一部分
    
     3xx 
    
     	301 表示永久重定向、说明资源已经不在了、需要用新的 URL访问
    
     	302 表示临时重定向、说明资源还在、但暂时需要用另一个URL访问
    
     	301 302 都会在响应头里面使用 Location 字段、指明后续需要跳转的 URL、浏览器回自动重定向到新的 URL
    
     	304 not modified 不含有跳转到含义、表示资源未修改、重定向已存在的缓冲文件、页陈伟缓存重定向、用于缓存控制
    
     4xx
    
     	400 bad request 请求报文有错误、笼统的错误
    
     	403 forbidden 服务器禁止访问资源、并不是客户端请求出错
    
     	404 not found 表示请求的资源在服务区上不存在或未找到、所以无法提供给客户端
    
     5xx 服务器内部发生了错误
    
     	500 与 400 一样、笼统的错误
    
     	501 not implement 请求的功能还没有实现
    
     	502 bad gateway 作为网关或者代理返回的错误码
    
     	503 service unavailable 表示服务器当前很忙、暂时无法响应服务器
    

    http 常见的字段

  • host 字段、客户端发送请求的时、用来指定服务器域名的
  • content-length 服务器在返回数据时、会有 content-length 字段、表明这次返回的数据长度
  • connection 用于客户端要求服务器使用 TCP 持久连接、以便其他请求的复用。HTTP/1.1 版本默认连接时持久连接、但是为了兼容老版本、需要指定 connection 字段
  • content-type 用于服务器响应时、告诉客服端、这次数据是什么格式
  • accept 客户端发送请求的时候、告诉服务端我能接受哪些数据格式
  • content-encoding 数据的压缩方式、表示服务器返回的数据用什么压缩格式
  • accept-encoding 客户端能接受哪些压缩方式

get 和 post

get 方法的含义是从服务器获取资源、这个资源可以是静态的文本、页面、图片视频等

post 方法则是相反操作、他向 URI 指定的资源提交数据、数据就在报文的 body 里

get 和 post 都是安全和幂等的吗

  • 在 HTTP 协议中、所谓的安去啊是指请求方法不回破坏服务器上的资源
  • 所谓的幂等、意识是多次执行相同的操作、结果都是相同的

get 是安全且幂等的

post 是不安全也不是幂等的

Http 特性

优点

  • 简单、header + body

  • 灵活和易于扩展 http 协议里各类请求方法、URL/URI、状态码、头字段等每个组成要求都没有被固定死、允许自定义和扩充

    同时 http 在 osi 第七层、则它的下层可以随意变化

    https 就是在 http 和tcp 层之间增加了 ssl 和tls 安全传输层、http/3 设置把tcp层换成udp的quic

  • 应用广泛和跨平台

缺点

  • 无状态 好处是不需要额外资源记录状态信息、减轻服务端负担 坏处就是做一些关联系操作时会非常麻烦

    cookie

  • 明文传输

  • 不安全

    • 明文
    • 不验证通信方身份
    • 无法保证明文的完整性

    https 的方式解决、引入 SSL/TLS 层

Http/1.1 性能如何

  • 持久连接

    万字面试知识点助力金九银十_第65张图片

  • 管道网络

    管道机制运行同时发出A请求和B请求,但是服务器还是先回应A请求、完成后在回应B请求。要是前面回应特别慢、后面就会有许多请求排队等着、这称为队头阻塞

    万字面试知识点助力金九银十_第66张图片

  • 队头阻塞

    万字面试知识点助力金九银十_第67张图片

    Http/1.1 的性能一般后续的http/2 和 http/3 就是在优化http性能

http 和 https

区别

  • http 信息明文传输、存在安全风险问题、https 则解决了不安全的缺陷、在tcp 和http 之间加入了ssl/tls 安全协议
  • http 连接简历相对简单、tcp 三次握手之后便可以进行报文传输、https 则在 tcp三次握手之后还需要进行 ssl/tls 握手的过程、才可进行加密报文传输
  • http端口80 https 443
  • https 协议需要向 CA 申请数字证书、用来确保服务器的身份是可信的

https 解决了什么问题

  • 信息加密、无法获取到信息
  • 校验机制 无法改变通信内容
  • 身份证书

https 如何解决的

  • 混合加密
    • 对称加密
    • 非对称加密
  • 摘要算法
  • 数字证书

SSL/TLS 握手、四次通信

  1. 客户端向服务器发起加密通信请求

    发送的信息:客户端支持的 SSL/TLS 协议版本、

    客户端生产的随机数 用于产生会话密钥

    客户端支持的加密套件 RSA

  2. 服务端收到请求后、响应如下内容

    确认SSL/TLS 版本、如果不支持、则关闭加密通信

    服务器生产随机数 用于产生会话密钥

    确认密码套件

    返回数字证书

  3. 浏览器收到回应后、确认证书的真实性、从证书中取出公钥、使用它加密报文

    一个随机数 用于产生会话密钥

    加密通信算法改变通知、表示随后的信息将用会话密钥加密通信

    客户端握手结束通知、这一项同时把之前所有的内容的发生的数据做成一个摘要、供服务端检验

  4. 服务端收到第三个随机数之后、通过协商加密算、计算出本次通信的会话密钥、然后向客户端发送最后的信息

    加密通信算法改变通知

    服务器握手结束通知

万字面试知识点助力金九银十_第68张图片

Http 有哪些方法

  • get
  • post
  • put
  • delete

get post 的区别

  • 数据传输的方式不同、get将请求参数放在url中、而 post 通过请求体
  • 安全性不同、post的数据放在请求体内、所以有一定的安全保证、而get 放在url 可以通过历史记录看到
  • get 是安全幂等的、post 不安全不幂等

put 和 post 区别

  • put 是幂等的、而post则是非幂等的
  • 通常情况下 put 指向的是单一个资源、而post 是一个资源集合。如在一篇新的文章集合中新增一篇文章、post 多次的话就会新增多篇了。而 put 则是指向单一资源、比如更新一下文章

http 报文

请求报文

  • 请求行
    • 请求方法、URL http协议版本字段
  • 请求头
    • 键值对、一个一行
    • host
    • accept
  • 空行
  • 请求体
    • post 、put 存放的

万字面试知识点助力金九银十_第69张图片

响应报文

  • 响应行
    • 协议版本、状态码、状态码的原因
  • 响应头
  • 空行
  • 响应体

万字面试知识点助力金九银十_第70张图片

cookie 和 session的区别

http 是无状态的、让http 具备状态、解决方法有两种、一种是在客户端中保持状态、一种是在服务端保持状态

cookie 介绍

服务端发送客户端特殊的信息、以文本的方式存放在客户端、存放在请求头、响应头。客户端再次请求相同的服务器时、将cookie 回发、服务器收到之后惠根据cookie 生成与客户端一致的内容

session机制

在服务器中保存用户的信息、检查是否包含对应的状态信息

那这样子也是需要客户端那边支持、提供一个id给服务器去判断客户端是谁

  • cookie
  • url 重写

两者比较

  • cookie数据存放在客户端、session 数据存放在服务端
  • session 比 cookie 安全
  • 减轻服务端的负担、就使用cookie

计算机网络

  • 应用层
  • 表示层
  • 会话层
  • 传输层
  • 网络层
  • 数据链路层
  • 物理层

传输层

UDP

  • 无连接的
  • 尽最大努力交付、不可靠交付
  • UDP 面向报文的
    • 应用层交给 UDP 多长的报文、UDP 加上头部之后就直接交给IP 层、不做任何的合并和拆分
  • UDP 没有拥塞控制
  • 支持一对一、一对多、多对多、多对一
  • 头部开销小、只有 8 字节、而 TCP 要 20字节

TCP

  • 面向连接的
  • 一对一通信
  • 可靠交付
  • 全双工的
  • 面向字节流的

滑动窗口

发送的滑动窗口

接收的滑动窗口

万字面试知识点助力金九银十_第71张图片

TCP 连接的建立

万字面试知识点助力金九银十_第72张图片

A 向 B 发出连接请求报文段、这时首部中的 SYN=1 seq=x、SYN=1 报文不能携带数据、但是要消耗一个序号、这时客户端进入一个同步已发送的状态

B 收到了连接请求的报文段后、如果同意建立连接、则向A发送确认、在确认报文中 SYN=1 ACK=1 seq=y ack=x+1 、这个也是不能携带数据、但是需要消耗一个序号、这时B 进入到一个同步收到的状态

A 收到B的确认之后、还要给B确认、ACK=1 seq=x+1 ack = y+1 这个报文可以携带数据、如果不携带则不消耗序号、下一次数据报文段序号依旧可以使用 seq=x+1 此时tcp 连接已经确认了、A进入连接确认状态

https://blog.csdn.net/qzcsu/article/details/72861891

为啥需要需要三次握手

一句话,主要防止已经失效的连接请求报文突然又传送到了服务器,从而产生错误。

如果使用的是两次握手建立连接,假设有这样一种场景,客户端发送了第一个请求连接并且没有丢失,只是因为在网络结点中滞留的时间太长了,由于TCP的客户端迟迟没有收到确认报文,以为服务器没有收到,此时重新向服务器发送这条报文,此后客户端和服务器经过两次握手完成连接,传输数据,然后关闭连接。此时此前滞留的那一次请求连接,网络通畅了到达了服务器,这个报文本该是失效的,但是,两次握手的机制将会让客户端和服务器再次建立连接,这将导致不必要的错误和资源的浪费。

如果采用的是三次握手,就算是那一次失效的报文传送过来了,服务端接受到了那条失效报文并且回复了确认报文,但是客户端不会再次发出确认。由于服务器收不到确认,就知道客户端并没有请求连接。

TCP连接的释放

客户端主动关闭、服务端被动关闭

万字面试知识点助力金九银十_第73张图片

为啥客户端最后需要等待 2MSL

MSL maximum segment lifetime 最大的报文寿命、TCP 允许不同的实现可以设置不同的MSL 值

  1. 保证客户端最后一个 ACK 报文能达到服务器、因为这个 ACK 报文可能丢失、站在服务器的角度看来、喔已经发送了FIN+ACK报文请求断开了、客户端还没给我回应、应该是我发送的报文他没有收到、于是服务器会重新发送一次、而客户端能在2MSL内收到这个重传的报文、接着给出回应报文、并重制2MSL计时器
  2. 防止类似与三次握手中提到了已经失效的连接请求报文出现在本链接中。客户端发送完最后的一个确认报文、在这个 2MSL 时间中、就可以使本连接持续的时间内产生的所有报文都从网络中消失。这样新的连接不回出现旧连接的请求报文

你可能感兴趣的:(Java,java,面试)