本文主要内容为找工作复习期间总结的一些新知识。

MYSQL篇

数据库的范式

第一范式

主要是为了确保原子性的,也就是存储的数据具备不可再分性。

1
2
3
4
5
6
SELECT * FROM `zz_student`;
+----------------------+--------+-------+
| student | course | score |
+----------------------+--------+-------+
| 小怪兽,男,185cm | 语文 | 95 |
| 怪兽,男,185cm | 数学 | 100 |

在上述的学生表中,其中有一个student学生列,这一列存储的数据则明显不符合第一范式:原子性的规定,因为这一列的数据还可以再拆分为姓名、性别、身高三项数据
1
2
3
4
5
6
7
SELECT * FROM `zz_student`;
+--------------+-------------+----------------+--------+-------+
| student_name | student_sex | student_height | course | score |
+--------------+-------------+----------------+--------+-------+
|小怪兽 || 185cm | 语文 | 95 |
| 怪兽 || 185cm | 数学 | 100 |

第二范式

第二范式的要求表中的所有列,其数据都必须依赖于主键,也就是一张表只存储同一类型的数据,不能有任何一列数据与主键没有关系,还是上面的那张表数据为例:

1
2
3
4
5
6
7
SELECT * FROM `zz_student`;
+--------------+-------------+----------------+--------+-------+
| student_name | student_sex | student_height | course | score |
+--------------+-------------+----------------+--------+-------+
|小怪兽 || 185cm | 语文 | 95 |
| 怪兽 || 185cm | 数学 | 100 |


虽然此时已经满足了数据库的第一范式,但此刻观察course课程、score分数这两列数据,跟前面的几列数据实际上依赖关系并不大,同时也由于这样的结构,导致前面几列的数据出现了大量冗余,所以此时可以再次拆分一下表结构:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
SELECT * FROM `zz_student`;
+------------+--------+------+--------+--------------+--------------+
| student_id | name | sex | height | department | dean |
+------------+--------+------+--------+--------------+--------------+
| 1 | 竹子 || 185cm | 计算机系 | 竹子老大 |
| 2 | 熊猫 || 170cm | 金融系 | 熊猫老大 |
+------------+--------+------+--------+--------------+--------------+

SELECT * FROM `zz_course`;
+-----------+-------------+
| course_id | course_name |
+-----------+-------------+
| 1 | 语文 |
| 2 | 数学 |
| 3 | 英语 |
+-----------+-------------+

SELECT * FROM `zz_score`;
+----------+------------+-----------+-------+
| score_id | student_id | course_id | score |
+----------+------------+-----------+-------+
| 1 | 1 | 1 | 95 |
| 2 | 1 | 2 | 100 |
| 3 | 1 | 3 | 88 |
| 4 | 2 | 1 | 99 |
| 5 | 2 | 2 | 90 |
| 6 | 2 | 3 | 95 |

经过上述结构优化后,之前的一张表此时被我们拆分成学生表、课程表、成绩表三张,每张表中的id字段作为主键,其他字段都依赖这个主键。无论在那张表中,都可以通过id主键确定其他字段的信息。

第三范式

第三范式要求表中每一列数据不能与主键之外的字段有直接关系。

三范式小结

  • 第一范式:确保原子性,表中每一个列数据都必须是不可再分的字段。
  • 第二范式:确保唯一性,每张表都只描述一种业务属性,一张表只描述一件事。
  • 第三范式:确保独立性,表中除主键外,每个字段之间不存在任何依赖,都是独立的。

巴斯-科德范式

第三范式的要求是:任何非主键字段不能与其他非主键字段间存在依赖关系,也就是要求每个非主键字段之间要具备独立性。而巴斯-科德范式在第三范式的基础上,进一步要求:任何主属性不能对其他主键子集存在依赖。

第三范式只要求非主键字段之间,不能存在依赖关系,但没要求联合主键中的字段不能存在依赖,因此第三范式并未考虑完善,巴斯-科德范式修正的就是这点
例如:一张表中 并没有uuid,和id,而是用联合主键来确保唯一,联合主键是有多列组成。需要确保这多列之间没有以来关系

第四范式

第四范式(4NF) 是在第三范式和 巴斯-科德范式(BCNF) 基础上的进一步规范化,它主要解决 多值依赖(Multivalued Dependency) 带来的数据冗余问题

假设以「用户名、角色、权限」三个字段作为联合主键,先来分析一下这张表是否满足之前的范式:

  • 表中每列数据都不可再分,具备原子性,满足第一范式。
  • 表中数据都仅描述了用户权限这一种业务属性,具备唯一性,满足第二范式。
  • 除主键外,表中其他字段不存在依赖关系,具备独立性,满足第三范式。
  • 联合主键中的用户、角色、权限都为独立字段,不存在依赖性,满足BC范式。

但是这个时候 如果一个用户 可有多个角色,一个角色又有多个权限。这就是多值依赖。也就是一张表中存在一对多的关系 。这时候就需要拆分成三张表,把角色表和权限单独做成一个表

第五范式

第五范式的定义:建立在4NF的基础上,进一步消除表中的连接依赖,直到表中的连接依赖都是主键所蕴含的。 看不懂就先跳过了

总结

第一范式:原子性,每个字段的值不能再分。
第二范式:唯一性,表内每行数据必须描述同一业务属性的数据。
第三范式:独立性,表中每个非主键字段之间不能存在依赖性。
巴斯范式:主键字段独立性,联合主键字段之间不能存在依赖性。
第四范式:表中字段不能存在多值依赖关系。
第五范式:表中字段的数据之间不能存在连接依赖关系。

SQL语句的执行过程

先上图:

  1. SQL接口会接收到 客户端发来的SQL语句
  2. mysql8.0中已经移除了缓存
  3. 解析器 会判断这个SQL语法对不对。不对会抛异常
  4. 优化器根据SQL制定出不同的执行方案,并择选出最优的执行计划。
  5. 工作线程根据执行计划,调用存储引擎所提供的API获取数据。
  6. 存储引擎根据API调用方的操作,去磁盘中检索数据(索引、表数据….)。
  7. 发生磁盘IO后,对于磁盘中符合要求的数据逐条返回给SQL接口。
  8. SQL接口会对所有的结果集进行处理(剔除列、合并数据….)并返回。

索引篇

聚簇索引和非聚簇索引

聚簇索引的特点是:索引结构和数据存储在一起,数据按照索引顺序进行存储。

1
2
3
4
5
普通索引
叶子节点 → 存储主键

聚簇索引
叶子节点 → 存储整行数据

聚簇索引是指索引结构和数据存储在一起的一种索引方式,在InnoDB存储引擎中主键索引就是聚簇索引。它的叶子节点存储的是整行数据,因此通过主键查询可以直接获取数据,不需要回表。由于数据按照索引顺序存储,所以一个表只能有一个聚簇索引。

索引下推

把原本在 Server 层判断的条件,下推到存储引擎层,在扫描索引时就提前过滤数据。

  • 假设有表
    1
    user(id, name, age)
  • 联合索引
    1
    index(name, age)
  • 查询sql
    因为是> 会导致索引失效
    1
    2
    SELECT * FROM user
    WHERE name = 'Tom' AND age > 20;
  • 没有索引下推的流程
    1
    2
    3
    4
    1 通过 name = 'Tom' 查索引
    2 得到很多主键 id
    3 回表查完整数据
    4 再判断 age > 20
  • 有索引下推的流程
    1
    2
    3
    1 通过 name = 'Tom' 查索引
    2 在索引里直接判断 age > 20
    3 只把符合条件的记录回表

MYSQL日志篇

Undo-log撤销日志

Undo即撤销的意思,但咱们通常也习惯称它为回滚日志,在日常开发过程中,如果代码敲错了,一般会习惯性的按下Ctrl+Z撤销,而Undo-log的作用也是如此,但它是用来给MySQL撤销SQL操作的。

undo-log 并不会记录相反的sql。undo-log会生成多版本控制链。当事务回滚的时候,多版本控制链中的指针会移动。

Redo-log重做日志

Undo-log主要用于实现事务回滚和MVCC机制,而Redo-log则用来实现数据的恢复。
Redo-log是一种预写式日志,即在向内存写入数据前,会先写日志,当后续数据未被刷写到磁盘、MySQL崩溃时,就可以通过日志来恢复数据,确保所有提交的事务都会被持久化。

  1. 先刷写一次Redo-log日志到磁盘,后台线程再根据Redo-log日志把数据落盘的原因:

    • 日志比数据先落入磁盘,因此就算MySQL崩溃也可以通过日志恢复数据。
    • 写日志时是以追加形式写到末尾,而写数据时则是计算数据位置,随机插入

      因为写日志会比写数据落盘快,因此日志落盘后返回,比数据落盘后返回要快,对于客户端而言,响应时间会更短

Bin-log变更日志

Bin-log变更日志 和 redo-log日志功能大致相同 。主要是记录所有对数据库表结构变更和表数据修改的操作,对于select、show这类读操作并不会记录。bin-log是MySQL-Server级别的日志,也就是所有引擎都能用的日志,而redo-log、undo-log都是InnoDB引擎专享的,无法跨引擎生效。

  • bin-log 会记录所有表更新的sql(update ,crate inster,drop)。
  • bin-log 是通过二进制文件存储的,先将sql存到内存中,最后刷到磁盘上
  • bin-log 常用于主从同步使用

bin-log ,redo-log的区别

对于Redo-log、Bin-log两者的区别,主要可以从四个维度上来说:

  1. 生效范围不同,Redo-log是InnoDB专享的,Bin-log是所有引擎通用的。
  2. 写入方式不同,Redo-log是用两个文件循环写,而Bin-log是不断创建新文件追加写。

    当redo-log上的数据被刷到磁盘上,则redo-log就不在需要这些数据了。可以被覆盖
    bin-log 的数据满了,会在新创建一个文件 继续追加数据,需要手动清除

  3. 文件格式不同,Redo-log中记录的都是变更后的数据,而Bin-log会记录变更SQL语句。
  4. 使用场景不同,Redo-log主要实现故障情况下的数据恢复,Bin-log则用于数据灾备、同步。

redo-log 的两阶段提交

  1. 如果只写一次的话,那到底先写bin-log还是redo-log呢?
    • 先写bin-log,再写redo-log:当事务提交后,先写bin-log成功,结果在写redo-log时断电宕机了,再重启后由于redo-log中没有该事务的日志记录,因此不会恢复该事务提交的数据。但要注意,主从架构中同步数据是使用bin-log来实现的,而宕机前bin-log写入成功了,就代表这个事务提交的数据会被同步到从机,也就意味着从机会比主机多出一条数据。
    • 先写redo-log,再写bin-log:当事务提交后,先写redo-log成功,但在写bin-log时宕机了,主节点重启后,会根据redo-log恢复数据,但从机依旧是依赖bin-log来同步数据的,因此从机无法将这个事务提交的数据同步过去,毕竟bin-log中没有撒,最终从机会比主机少一条数据。
  2. redo-log就被设计成了两阶段提交模式,设置成两阶段提交后,整个执行过程有三处崩溃点:

    • redo-log(prepare):在写入准备状态的redo记录时宕机,事务还未提交,不会影响一致性。
    • bin-log:在写bin记录时崩溃,重启后会根据redo记录中的事务ID,回滚前面已写入的数据。
    • redo-log(commit):在bin-log写入成功后,写redo(commit)记录时崩溃,因为bin-log中已经写入成功了,所以从机也可以同步数据,因此重启时直接再次提交事务,写入一条redo(commit)记录即可。

通过这种两阶段提交的方案,就能够确保redo-log、bin-log两者的日志数据是相同的,bin-log中有的主机再恢复,如果bin-log没有则直接回滚主机上写入的数据,确保整个数据库系统的数据一致性。

辅助性的日志

  1. error-log:MySQL线上MySQL由于非外在因素(断电、硬件损坏…)导致崩溃时,辅助线上排错的日志。
  2. slow-log:系统响应缓慢时,用于定位问题SQL的日志,其中记录了查询时间较长的SQL。
  3. relay-log:搭建MySQL高可用热备架构时,用于同步数据的辅助日志。

SQL优化篇

  • 尽量不要用select *
    1. 会增加 网络传输数据量
    2. select * 如果不是条件不是主键,那么必然会回表查询。如果查询字段 全部在索引中,就可以走 覆盖索引。
  • 连表查询时尽量不要关联太多表
    1. 数据量会随表数量呈直线性增长,数据量越大检索效率越低
    2. 当关联的表数量过多时,无法控制好索引的匹配,涉及的表越多,索引不可控风险越大
  • 小表驱动大表

    1. 可以减少循环查询的次数
  • 不要使用like左模糊和全模糊查询

    1. 会导致索引失效

JVM篇

如何判定对象是否存活

判定对象是否存活 是垃圾回收的基础。存活的对象不可垃圾回收。这是GC的基础

引用计数算法

创建出的每个对象自身都携带一个引用计数器,主要用于记录自身的引用情况。当一个指针指向当前对象时,该计数器会+1
对象实例被创建出来后,计数器会被初始化为1,因为局部变量指针引用了该实例对象。而后续执行过程中,又有另外一个变量引用该实例时,该对象的引用计数器会+1。而当方法执行结束,栈帧中局部变量表中引用该对象的指针随之销毁时,当前对象的引用计数器会-1。当一个对象的计数器为0时,代表当前对象已经没有指针引用它了,那么在GC发生时,该对象会被判定为“垃圾”,然后会被回收。

这种判断算法的优势在于:实现简单,垃圾便于辨识,判断效率高,回收没有延迟性。但凡事有利必有弊,该算法一方面因为需要额外存储计数器,以及每次引用指向或消失时都需要同步更新计数器,所以增加了存储成本和时间开销;另一方面存在一个致命缺陷,这种算法无法处理两个对象相互引用这种引用循环的状况,如下:

可达性分析算法

简单理解:如果一个对象从 GC Roots 出发无法被访问到,那么这个对象就是垃圾,可以被回收。如下:

1
2
3
4
5
6
7
8
9
10
11
GC Roots


A
/ \
B C
\
D

E → F

对象是否可达
A可达
B可达
C可达
D可达
E不可达
F不可达

垃圾回收算法

标记-清除

标记清除算法是现代GC算法的基础,标-清算法会将回收工作分为标记和清除两个阶段。在标记阶段会根据可达性分析算法,通过根节点标记堆中所有的可达对象,而这些对象则被称为堆中存活对象,反之,未被标记的则为垃圾对象。然后在清除阶段,会对于所有未标记的对象进行清除。

示例:初始GC标志位都为0,也就是未标记状态,假设此时系统堆内存出现不足,那么最终会触发GC机制。GC开始时,在标记阶段首先会停下整个程序,然后GC线程开始遍历所有GC Roots节点,根据可达性分析算法找出所有的存活对象并标记为1

复制算法

复制算法与前面的标-清算法相比,它就可以很好的保证内存回收之后的内存整齐度。因为复制算法会将JVM中原有的堆内存分为两块,在同一时刻只会使用一块内存用于对象分配。在发生GC时,首先会将使用的那块内存区域中的存活对象复制到未使用的这块内存中。等复制完成之后,对当前使用的这块内存进行全面清除回收,清除完成之后,交换两块内存之间的角色,最后GC结束。假设堆空间如下

在发生GC时,首先会将左侧这块内存区域中的存活对象移动到右侧这块空闲内存中,如下:

然后会对于左侧这块内存所有区域进行统一回收,如下:

复制算法是注定不能用于年老代的,因为一方面没有新的区域为年老代提供分配担保,另一方面则是年老代中的对象明显生命周期都很长,存活对象会非常多

标记-整理算法

标记-整理算法也被称为标记-压缩算法,标-整算法适用于存活率较高的场景,它是建立在标-清算法的基础上做了优化。标-整算法也会分为两个阶段,分别为标记阶段、整理阶段:

①标记阶段:和标-清算法一样。在标记阶段时也会基于GcRoots节点遍历整个内存中的所有对象,然后对所有存活对象做一次标记。
②整理阶段:在整理阶段该算法并不会和标-清算法一样简单的清理内存,而是会将所有存活对象移动(压缩)到内存的一端,然后对于存活对象边界之外的内存进行统一回收。

垃圾回收器

CMS垃圾回收器

CMS 是 Java 中的一种 以低停顿时间为目标的垃圾回收器,主要用于 老年代的回收。

  1. CMS 只会回收老年代,所以需要搭配一款其他的垃圾回收器去回收年轻代。 CMS是第一款并行去回收垃圾的。CMS 之前的垃圾回收器在进行GC的时候 会发生STW。导致用户线程等待。
  2. CMS 会产生内存碎片

特点说明
低停顿适合对响应时间敏感的系统
并发回收GC 与用户线程同时执行
标记-清除算法不压缩内存
只回收老年代新生代通常使用 ParNew

G1垃圾回收器

在JDK1.9之前。JVM中的堆空间的布局都是采用分代存储的方式,无论从逻辑上还是从物理内存上,都是分代的。大概长这个样子

但是到了Java9的时候,因为默认GC器改为了G1,所以堆中的内存区域被划为了一个个的Region区。

G1垃圾回收器不在物理采用分代的堆。而是把整个堆空间 分成了多个小区域,每个小区域称为 Region区。
但是 G1在逻辑上仍然分代。G1将Region区进行了分类。如下图


虽然堆空间中已经不在分代了。但是G1分成了四个板块

  • E = Eden 新创建对象
  • S = Survivor 对象晋升过程暂时存放的区域 (Eden → Survivor → Old)
  • O = Old 长期存活对象
  • H = Humongous 用于存放 大对象(对象大小 > Region 的 50%)

G1的GC类型

G1中主要存在YoungGC、MixedGC以及FullGC三种GC类型,这三种GC类型分别会在不同情景下被触发。

  1. YoungGC
    顾名思义 YoungGC 是对年轻代 也就是 edge区域进行垃圾回收。

YoungGC并非说Eden区放满了就会立马被触发,在G1中,当新生代区域被用完时,G1首先会大概计算一下回收当前的新生代空间需要花费多少时间,如果回收时间远远小于参数-XX:MaxGCPauseMills设定的值,那么不会触发YoungGC,而是会继续为新生代增加新的Region区用于存放新分配的对象实例。直至某次Eden区空间再次被放满并经过计算后,此次回收的耗时接近-XX:MaxGCPauseMills参数设定的值,那么才会触发YoungGC。

  1. MixedGC
    MixedGC翻译过来的意思为混合型GC,而并非是指FullGC。当老年代的空间不足时对老年代进行垃圾回收,但也往往会伴随着年轻代一起回收
    当整个堆中年老代的区域占有率达到参数-XX:InitiatingHeapOccupancyPercent设定的值后触发MixedGC,发生该类型GC后,会回收所有新生代Region区、部分年老代Region区(会根据期望的GC停顿时间选择合适的年老代Region区优先回收)以及大对象Humongous区。

  2. FullGC
    当整个堆空间中的空闲Region不足以支撑拷贝对象或由于元数据空间满了等原因触发,在发生FullGC时,G1首先会停止系统所有用户线程,然后采用单线程进行标记、清理和压缩整理内存,以便于清理出足够多的空闲Region来供下一次MixedGC使用。但该过程是单线程串行收集的,因此这个过程非常耗时的(ShenandoahGC中采用了多线程并行收集)。

G1总结

一个Region花120ms能回收10MB垃圾,而另外一个Region花80ms能回收20MB垃圾,在回收时间有限情况下,G1当然会优先选择后面这个“性价比”更高的Region回收。这种使用Region划分内存空间以及有优先级的区域回收方式,能够确保G1收集器在有限时间内,可以尽可能达到更高的收集效率。

ZGC垃圾回收器

JDK11的时候 推出了ZGC垃圾回收器。是真正意义上的不分代收集器,因为它无论是从逻辑上,还是物理上都不再保留分代的概念。
在ZGC中,也会把堆空间划分为一个个的Region区域,但ZGC中的Region区不存在分代的概念,它仅仅只是简单的将所有Region区分为了大、中、小三个等级,如下

  • 小型区/页(Small):固定大小为2MB,用于分配小于256KB的对象。
  • 中型区/页(Medium):固定大小为32MB,用于分配>=256KB ~ <=4MB的对象。
  • 大型区/页(Large):没有固定大小,容量可以动态变化,但是大小必须为2MB的整数倍,专门用于存放>4MB的巨型对象。但值得一提的是:每个Large区只能存放一个大对象,也就代表着你的这个大对象多大,那么这个Large区就为多大,所以一般情况下,Large区的容量要小于Medium区,并且需要注意:Large区的空间是不会被重新分配的(稍后分析)。
特点说明
超低停顿GC 停顿通常 < 10ms
支持大堆支持 TB 级内存
并发回收大部分 GC 工作并发执行
着色指针使用 Colored Pointer 技术
读屏障使用 Load Barrier

ZGC,G1,CMS的区别

特性CMSG1ZGC
JDK版本JDK5JDK7+JDK11+
堆结构新生代 + 老年代RegionRegion
GC算法标记-清除标记-整理并发压缩
停顿时间较低可预测极低
是否整理内存❌ 不整理✅ 整理✅ 整理
内存碎片
最大堆支持几十GB几百GBTB级
主要目标低停顿平衡吞吐量和停顿极低停顿