pull/1/head
595208882@qq.com 3 years ago
parent c098f3c7ec
commit edf7bbc4a5

@ -2190,7 +2190,7 @@ https://www.cnblogs.com/skywang12345/category/508186.html
### 冒泡排序Bubble Sort
冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
**循环遍历多次每次从前往后把大元素往后调,每次确定一个最大(最小)元素,多次后达到排序序列。**这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
![冒泡排序](images/Algorithm/冒泡排序.jpg)
@ -2779,8 +2779,15 @@ Tips: 基数排序不改变相同元素之间的相对顺序,因此它是稳
### 堆排序Heap Sort
对于堆排序,首先是建立在堆的基础上,堆是一棵完全二叉树,还要先认识下大根堆和小根堆,完全二叉树中所有节点均大于(或小于)它的孩子节点,所以这里就分为两种情况:
- 如果所有节点**「大于」**孩子节点值,那么这个堆叫做**「大根堆」**,堆的最大值在根节点
- 如果所有节点**「小于」**孩子节点值,那么这个堆叫做**「小根堆」**,堆的最小值在根节点
堆排序Heapsort是指利用堆这种数据结构所设计的一种排序算法。堆排序的过程就是将待排序的序列构造成一个堆选出堆中最大的移走再把剩余的元素调整成堆找出最大的再移走重复直至有序。
![大根堆-小根堆](images/Algorithm/大根堆-小根堆.jpg)
**算法描述**
- 将初始待排序关键字序列(R1,R2….Rn)构建成大顶堆,此堆为初始的无序区

@ -608,13 +608,13 @@ WHERE A.EMP_SUPV_ID = B.EMP_ID;
## 行级锁
`InnoDB` 事务中,行锁通过给索引上的索引项加锁来实现。即只有通过索引条件检索数据,`InnoDB` 才使用行级锁,否则将使用表锁。行级锁定同样分为两种类型:`共享锁` 和`排他锁`,以及加锁前需要先获得的 `意向共享锁``意向排他锁`。行锁是在需要的时候才加上的,但并不是不需要了就立刻释放,而是要等到事务结束时才释放。这个就是两阶段锁协议。
## 行锁(Record Locks)
`InnoDB` 事务中,行锁通过给索引上的索引项加锁来实现。即只有通过索引条件检索数据,`InnoDB` 才使用行级锁,否则将使用表锁。行级锁定同样分为两种类型:`共享锁` 和`排他锁`,以及加锁前需要先获得的 `意向共享锁``意向排他锁`
行锁是在需要的时候才加上的,但并不是不需要了就立刻释放,而是要等到事务结束时才释放。这个就是两阶段锁协议
在 InnoDB 存储引擎中SELECT 操作的不可重复读问题通过 MVCC 得到了解决,而 UPDATE、DELETE 的不可重复读问题通过 Record Lock 解决INSERT 的不可重复读问题是通过 Next-Key LockRecord Lock + Gap Lock解决的
@ -636,14 +636,6 @@ WHERE A.EMP_SUPV_ID = B.EMP_ID;
**行锁实现算法**
- Record Lock记录锁
- Gap Lock间隙锁
- Next-Key Lock间隙锁
**MVCC**
**MVCC**主要是通过**版本链**和**ReadView**来实现的。在Mysql的InnoDB引擎中只有**已提交读(READ COMMITTD)**和**可重复读(REPEATABLE READ)**这两种隔离级别下的事务采用了MVCC机制。
@ -675,33 +667,91 @@ WHERE A.EMP_SUPV_ID = B.EMP_ID;
**行锁实现算法**
- Record Lock记录锁
- Gap Lock间隙锁
- Next-Key Lock间隙锁
### Record Lock记录锁
单个行记录上的锁,总是会去锁住索引记录。
记录锁就是为**某行**记录加锁,它封锁该行的索引记录:
行锁,顾名思义,是加在`索引行`(是索引行,不是数据行)上的锁。比如:
```sql
-- id 列为主键列或唯一索引列
SELECT * FROM t_user WHERE id = 1 FOR UPDATE;
```
`select * from user where id=1 and id=10 for update`,就会在`id=1`和`id=10`的索引行上加Record Lock。
id 为 1 的记录行会被锁住。需要注意:
- `id` 列必须为`唯一索引列`或`主键列`,否则上述语句加的锁就会变成`临键锁`
- 同时查询语句必须为`精准匹配``=`),不能为 `>`、`<`、`like`等,否则也会退化成`临键锁`
也可以在通过 `主键索引``唯一索引` 对数据行进行 UPDATE 操作时,也会对该行数据加`记录锁`
```sql
-- id 列为主键列或唯一索引列
UPDATE t_user SET age = 50 WHERE id = 1;
```
### Gap Lock间隙锁
间隙锁,它会锁住两个索引之间的区域。比如:
**间隙锁**基于`非唯一索引`,它`锁定一段范围内的索引记录`。**间隙锁**基于下面将会提到的`Next-Key Locking` 算法,请务必牢记:**使用间隙锁锁住的是一个区间,而不仅仅是这个区间中的每一条数据**。
```sql
SELECT * FROM t_user WHERE id BETWEN 1 AND 10 FOR UPDATE;
-- 或
SELECT * FROM t_user WHERE id > 1 AND id < 10 FOR UPDATE;
```
即所有在`110`区间内的记录行都会被锁住所有id 为 2、3、4、5、6、7、8、9 的数据行的插入会被阻塞,但是 1 和 10 两条记录行并不会被锁住。除了手动加锁外,在执行完某些 `SQL`后,`InnoDB`也会自动加**间隙锁**。
**幻读原因**:因为行锁只能锁住行,但新插入记录这个动作,要更新的是记录之间的“`间隙`”。所以加入间隙锁来解决幻读。
### Next-Key Lock临键锁
临键锁是一种特殊的**间隙锁**,也可以理解为一种特殊的**算法**。通过**临建锁**可以解决`幻读`的问题。每个数据行上的`非唯一索引列`上都会存在一把**临键锁**,当某个事务持有该数据行的**临键锁**时,会锁住一段**左开右闭区间**的数据。需要强调的一点是,`InnoDB` 中`行级锁`是基于索引实现的,**临键锁**只与`非唯一索引列`有关,在`唯一索引列`(包括`主键列`)上不存在**临键锁**。
比如:表信息 `t_user(id PK, age KEY, name)`
![Next-Key-Locks](images/Database/Next-Key-Locks.jpg)
该表中 `age` 列潜在的`临键锁`有:
![Next-Key-Locks-临键锁](images/Database/Next-Key-Locks-临键锁.jpg)
在`事务 A` 中执行如下命令:
`select * from user where id>1 and id<10 for update`就会在id为(1,10)的索引区间上加Gap Lock。
```sql
-- 根据非唯一索引列 UPDATE 某条记录
UPDATE t_user SET name = Vladimir WHERE age = 24;
-- 或根据非唯一索引列 锁住某条记录
SELECT * FROM t_user WHERE age = 24 FOR UPDATE;
```
想一下幻读的原因,其实就是行锁只能锁住行,但新插入记录这个动作,要更新的是记录之间的“`间隙`”。所以加入间隙锁来解决幻读。
不管执行了上述 `SQL` 中的哪一句,之后如果在`事务 B` 中执行以下命令,则该命令会被阻塞:
```sql
INSERT INTO t_user VALUES(100, 26, 'tian');
```
很明显,`事务 A` 在对 `age` 为 24 的列进行 UPDATE 操作的同时,也获取了 `(24, 32]` 这个区间内的临键锁。
### Next-Key Lock间隙锁
不仅如此,在执行以下 SQL 时,也会陷入阻塞等待:
也叫间隙锁,它是 Record Lock + Gap Lock 形成的一个闭区间锁。比如:
```sql
INSERT INTO table VALUES(100, 30, 'zhang');
```
`select * from user where id>=1 and id<=10 for update`就会在id为[1,10]的索引闭区间上加 Next-Key Lock。
那最终我们就可以得知,在根据`非唯一索引` 对记录行进行 `UPDATE \ FOR UPDATE \ LOCK IN SHARE MODE` 操作时InnoDB 会获取该记录行的 `临键锁` ,并同时获取该记录行下一个区间的`间隙锁`
这样组合起来就有:行级共享锁,表级共享锁;行级排它锁,表级排它锁。
即`事务 A`在执行了上述的 SQL 后,最终被锁住的记录区间为 `(10, 32)`
@ -716,23 +766,175 @@ MySQL 里面表级别的锁有这几种:
### 表锁
- 表锁会限制别的线程的读写外
- 表锁也会限制本线程接下来的读写操作
如果我们想对学生表t_student加表锁可以使用下面的命令
```sql
-- 表级别的共享锁,也就是读锁
lock tables t_student read;
-- 表级别的独占锁,也就是写锁
lock tables t_stuent wirte;
```
不过尽量避免在使用 InnoDB 引擎的表使用表锁,因为表锁的颗粒度太大,会影响并发性能,**InnoDB 的优势在于实现了颗粒度更细的行级锁**。要释放表锁,可以使用下面这条命令,会释放当前会话的所有表锁:
```sql
unlock tables
```
### 元数据锁MDL
MDL 是为了保证当用户对表执行 CRUD 操作时,防止其他线程对这个表结构做了变更。不需要显示的使用 MDL因为当我们对数据库表进行操作时会自动给这个表加上 MDL
- 对一张表进行 CRUD 操作时,加的是 **MDL 读锁**
当有线程在执行 select 语句( 加 MDL 读锁)的期间,如果有其他线程要更改该表的结构( 申请 MDL 写锁),那么将会被阻塞,直到执行完 select 语句( 释放 MDL 读锁)。
- 对一张表做结构变更操作的时候,加的是 **MDL 写锁**
当有线程对表结构进行变更( 加 MDL 写锁)的期间,如果有其他线程执行了 CRUD 操作( 申请 MDL 读锁),那么就会被阻塞,直到表结构变更完成( 释放 MDL 写锁)。
MDL 是在事务提交后才会释放,这意味着**事务执行期间MDL 是一直持有的**。申请 MDL 锁的操作会形成一个队列,队列中**写锁获取优先级高于读锁**,一旦出现 MDL 写锁等待,会阻塞后续该表的所有 CRUD 操作。
### 意向锁
- 在使用 InnoDB 引擎的表里对某些记录加上「共享锁」之前,需要先在表级别加上一个「意向共享锁」;
- 在使用 InnoDB 引擎的表里对某些纪录加上「独占锁」之前,需要先在表级别加上一个「意向独占锁」;
也就是,当执行插入、更新、删除操作,需要先对表加上「意向共享锁」,然后对该记录加独占锁。而普通的 select 是不会加行级锁的,普通的 select 语句是利用 MVCC 实现一致性读是无锁的。不过select 也是可以对记录加共享锁和独占锁的,具体方式如下:
```sql
-- 先在表上加上意向共享锁,然后对读取的记录加独占锁
select ... lock in share mode;
-- 先表上加上意向独占锁,然后对读取的记录加独占锁
select ... for update;
```
**意向共享锁和意向独占锁是表级锁,不会和行级的共享锁和独占锁发生冲突,而且意向锁之间也不会发生冲突,只会和共享表锁(\*lock tables … read\*)和独占表锁(\*lock tables … write\*)发生冲突。**
表锁和行锁是满足读读共享、读写互斥、写写互斥的。如果没有「意向锁」,那么加「独占表锁」时,就需要遍历表里所有记录,查看是否有记录存在独占锁,这样效率会很慢。那么有了「意向锁」,由于在对记录加独占锁前,先会加上表级别的意向独占锁,那么在加「独占表锁」时,直接查该表是否有意向独占锁,如果有就意味着表里已经有记录被加了独占锁,这样就不用去遍历表里的记录。所以,**意向锁的目的是为了快速判断表里是否有记录被加锁**。
### AUTO-INC 锁
在为某个字段声明 `AUTO_INCREMENT` 属性时,之后可以在插入数据时,可以不指定该字段的值,数据库会自动给该字段赋值递增的值,这主要是通过 AUTO-INC 锁实现的。
AUTO-INC 锁是特殊的表锁机制,锁**不是再一个事务提交后才释放,而是再执行完插入语句后就会立即释放**。**在插入数据时,会加一个表级别的 AUTO-INC 锁**,然后为被 `AUTO_INCREMENT` 修饰的字段赋值递增的值,等插入语句执行完成后,才会把 AUTO-INC 锁释放掉。
AUTO-INC 锁再对大量数据进行插入的时候,会影响插入性能,因为另一个事务中的插入会被阻塞。因此, 在 MySQL 5.1.22 版本开始InnoDB 存储引擎提供了一种**轻量级的锁**来实现自增。一样也是在插入数据的时候,会为被 `AUTO_INCREMENT` 修饰的字段加上轻量级锁,**然后给该字段赋值一个自增的值,就把这个轻量级锁释放了,而不需要等待整个插入语句执行完后才释放锁**。
InnoDB 存储引擎提供了个 innodb_autoinc_lock_mode 的系统变量,是用来控制选择用 AUTO-INC 锁,还是轻量级的锁。
- 当 innodb_autoinc_lock_mode = 0就采用 AUTO-INC 锁
- 当 innodb_autoinc_lock_mode = 2就采用轻量级锁
- 当 innodb_autoinc_lock_mode = 1这个是默认值两种锁混着用如果能够确定插入记录的数量就采用轻量级锁不确定时就采用 AUTO-INC 锁
不过,当 innodb_autoinc_lock_mode = 2 是性能最高的方式,但是会带来一定的问题。因为并发插入的存在,在每次插入时,自增长的值可能不是连续的,**这在有主从复制的场景中是不安全的**。
## 死锁
当两个及以上的事务,双方都在等待对方释放已经持有的锁或因为加锁顺序不一致造成循环等待锁资源,就会出现“死锁”。常见的报错信息为 ” `Deadlock found when trying to get lock...`”。`MySQL` 出现死锁的几个要素为:
- 两个或者两个以上事务
- 每个事务都已经持有锁并且申请新的锁
- 锁资源同时只能被同一个事务持有或者不兼容
- 事务之间因为持有锁和申请锁导致彼此循环等待
# 事务
### 预防死锁
- `innodb_lock_wait_timeout` **等待锁超时回滚事务**
直观方法是在两个事务相互等待时,当一个等待时间超过设置的某一阀值时,对其中一个事务进行回滚,另一个事务就能继续执行。
- `wait-for graph`**算法来主动进行死锁检测**
每当加锁请求无法立即满足需要并进入等待时wait-for graph算法都会被触发。wait-for graph要求数据库保存以下两种信息
- 锁的信息链表
- 事务等待链表
### 解决死锁
- 等待事务超时,主动回滚
- 进行死锁检查,主动回滚某条事务,让别的事务能继续走下去
下面提供一种方法,解决死锁的状态:
```sql
-- 查看正在被锁的事务
SELECT * FROM INFORMATION_SCHEMA.INNODB_TRX;
```
![解决死锁](images/Database/解决死锁.jpg)
```sql
--上图trx_mysql_thread_id列的值
kill trx_mysql_thread_id;
```
## 锁问题
### 脏读
脏读指的是不同事务下,当前事务可以读取到另外事务未提交的数据。
例如T1 修改一个数据T2 随后读取这个数据。如果 T1 撤销了这次修改,那么 T2 读取的数据是脏数据。
![img](images/Database/007S8ZIlly1gjjfxu6baej30j30kijsr.jpg)
### 不可重复读
不可重复读指的是同一事务内多次读取同一数据集合,读取到的数据是不一样的情况。
例如T2 读取一个数据T1 对该数据做了修改。如果 T2 再次读取这个数据,此时读取的结果和第一次读取的结果不同。
![img](images/Database/007S8ZIlly1gjjfxx1pw3j30i90j0myc.jpg)
在 InnoDB 存储引擎中:
- `SELECT`:操作的不可重复读问题通过 MVCC 得到了解决的
- `UPDATE/DELETE`:操作的不可重复读问题是通过 Record Lock 解决的
- `INSERT`:操作的不可重复读问题是通过 Next-Key LockRecord Lock + Gap Lock解决的
### 幻读
幻读是指在同一事务下,连续执行两次同样的 sql 语句可能返回不同的结果,第二次的 sql 语句可能会返回之前不存在的行。幻影读是一种特殊的不可重复读问题。
### 丢失更新
一个事务的更新操作会被另一个事务的更新操作所覆盖。
例如T1 和 T2 两个事务都对一个数据进行修改T1 先修改T2 随后修改T2 的修改覆盖了 T1 的修改。
![img](images/Database/007S8ZIlly1gjjfxzqa84j30h30eowfd.jpg)
这类型问题可以通过给 SELECT 操作加上排他锁来解决,不过这可能会引入性能问题,具体使用要视业务场景而定。
# 数据库事务
**什么叫事务?**
@ -1102,178 +1304,391 @@ SELECT * FROM products WHERE id LIKE '3' FOR UPDATE;
# 数据库
# InnoDB
## 锁类型
## 数据页
锁是数据库系统区别于文件系统的一个关键特性。锁机制用于管理对共享资源的并发访问
数据页主要是用来存储表中记录的,它在磁盘中是用双向链表相连的,方便查找,能够非常快速得从一个数据页,定位到另一个数据页
**共享锁S Lock**:允许事务读一行数据
- 写操作时,先将数据写到内存的某个批次中,然后再将该批次的数据一次性刷到磁盘上。如下图所示:
**排他锁X Lock**:允许事务删除或者更新一行数据
![InnoDB-数据页-写操作](images/Database/InnoDB-数据页-写操作.jpg)
**意向共享锁IS Lock**:事务想要获得一张表中某几行的共享锁
- 读操作时,从磁盘上一次读一批数据,然后加载到内存当中,以后就在内存中操作。如下图所示:
**意向排他锁**:事务想要获得一张表中某几行的排他锁
![InnoDB-数据页-读操作](images/Database/InnoDB-数据页-读操作.jpg)
磁盘中各数据页的整体结构如下图所示:
![InnoDB-数据页](images/Database/InnoDB-数据页.jpg)
## 锁算法
通常情况下,单个数据页默认的大小是`16kb`。当然,我们也可以通过参数:`innodb_page_size`,来重新设置大小。不过,一般情况下,用它的默认值就够了。单个数据页包含内容如下:
### Record Lock
![InnoDB-单个数据页内容](images/Database/InnoDB-单个数据页内容.jpg)
锁定一个记录上的索引,而不是记录本身。
### 文件头部
如果表没有设置索引InnoDB 会自动在主键上创建隐藏的聚簇索引,因此 Record Locks 依然可以使用
通过前面介绍的行记录中`下一条记录的位置`和`页目录`innodb能非常快速的定位某一条记录。但有个前提条件就是用户记录必须在同一个数据页当中
如果用户记录非常多,在第一个数据页找不到我们想要的数据,需要到另外一页找该怎么办呢?这时就需要使用`文件头部`了。它里面包含了多个信息但我只列出了其中4个最关键的信息
- 页号
- 上一页页号
- 下一页页号
- 页类型
### Gap Lock
顾名思义innodb是通过页号、上一页页号和下一页页号来串联不同数据页的。如下图所示
锁定索引之间的间隙,但是不包含索引本身。例如当一个事务执行以下语句,其它事务就不能在 t.c 中插入 15。
![InnoDB-文件头部](images/Database/InnoDB-文件头部.jpg)
```sql
SELECT c FROM t WHERE c BETWEEN 10 and 20 FOR UPDATE;
```
不同的数据页之间通过上一页页号和下一页页号构成了双向链表。这样就能从前向后一页页查找所有的数据了。此外页类型也是一个非常重要的字段它包含了多种类型其中比较出名的有数据页、索引页目录项页、溢出页、undo日志页等。
### Next-Key Lock
### 页头部
它是 Record Locks 和 Gap Locks 的结合不仅锁定一个记录上的索引也锁定索引之间的间隙。例如一个索引包含以下值10, 11, 13, and 20那么就需要锁定以下区间
比如一页数据到底保存了多条记录,或者页目录到底使用了多个槽等。这些信息是实时统计,还是事先统计好了,保存到某个地方?为了性能考虑,上面的这些统计数据,当然是先统计好,保存到一个地方。后面需要用到该数据时,再读取出来会更好。这个保存统计数据的地方,就是`页头部`。当然页头部不仅仅只保存:槽的数量、记录条数等信息。它还记录了
```
(-∞, 10]
(10, 11]
(11, 13]
(13, 20]
(20, +∞)
```
- 已删除记录所占的字节数
- 最后插入记录的位置
- 最大事务id
- 索引id
- 索引层级
在 InnoDB 存储引擎中SELECT 操作的不可重复读问题通过 MVCC 得到了解决,而 UPDATE、DELETE 的不可重复读问题通过 Record Lock 解决INSERT 的不可重复读问题是通过 Next-Key LockRecord Lock + Gap Lock解决的。
### 最大和最小记录
## 锁问题
在一个数据页当中,如果存在多条用户记录,它们是通过`下一条记录的位置`相连的。不过有个问题如果才能快速找到最大的记录和最小的记录呢这就需要在保存用户记录的同时也保存最大和最小记录了。最大记录保存到Supremum记录中。最小记录保存在Infimum记录中。
### 脏读
在保存用户记录时数据库会自动创建两条额外的记录Supremum 和 Infimum。它们之间的关系如下图所示
脏读指的是不同事务下,当前事务可以读取到另外事务未提交的数据。
![InnoDB-最大和最小记录](images/Database/InnoDB-最大和最小记录.jpg)
例如T1 修改一个数据T2 随后读取这个数据。如果 T1 撤销了这次修改,那么 T2 读取的数据是脏数据
从图中可以看出用户数据是从最小记录开始,通过下一条记录的位置,从小到大,一步步查找,最后找到最大记录为止
![img](images/Database/007S8ZIlly1gjjfxu6baej30j30kijsr.jpg)
### 用户记录
### 不可重复读
对于新申请的数据页用户记录是空的。当插入数据时innodb会将一部分`空闲空间`分配给用户记录。用户记录是innodb的重中之重我们平时保存到数据库中的数据就存储在它里面。其实在innodb支持的数据行格式有四种
不可重复读指的是同一事务内多次读取同一数据集合,读取到的数据是不一样的情况。
- compact行格式
- redundant行格式
- dynamic行格式
- compressed行格式
例如T2 读取一个数据T1 对该数据做了修改。如果 T2 再次读取这个数据,此时读取的结果和第一次读取的结果不同。
![img](images/Database/007S8ZIlly1gjjfxx1pw3j30i90j0myc.jpg)
在 InnoDB 存储引擎中
以compact行格式为例
- `SELECT`:操作的不可重复读问题通过 MVCC 得到了解决的
- `UPDATE/DELETE`:操作的不可重复读问题是通过 Record Lock 解决的
- `INSERT`:操作的不可重复读问题是通过 Next-Key LockRecord Lock + Gap Lock解决的
![InnoDB-compact行格式](images/Database/InnoDB-compact行格式.jpg)
一条用户记录主要包含三部分内容:
- 记录额外信息它包含了变长字段、null值列表和记录头信息
- 隐藏列它包含了行id、事务id和回滚点
- 真正的数据列:包含真正的用户数据,可以有很多列
### 幻读
幻读是指在同一事务下,连续执行两次同样的 sql 语句可能返回不同的结果,第二次的 sql 语句可能会返回之前不存在的行。幻影读是一种特殊的不可重复读问题。
#### 额外信息
额外信息并非真正的用户数据,它是为了辅助存数据用的。
### 丢失更新
- **变长字段列表**
一个事务的更新操作会被另一个事务的更新操作所覆盖
有些数据如果直接存会有问题比如如果某个字段是varchar或text类型它的长度不固定可以根据存入数据的长度不同而随之变化。如果不在一个地方记录数据真正的长度innodb很可能不知道要分配多少空间。假如都按某个固定长度分配空间但实际数据又没占多少空间岂不是会浪费所以需要在变长字段中记录某个变长字段占用的字节数方便按需分配空间
例如T1 和 T2 两个事务都对一个数据进行修改T1 先修改T2 随后修改T2 的修改覆盖了 T1 的修改。
- **null值列表**
![img](images/Database/007S8ZIlly1gjjfxzqa84j30h30eowfd.jpg)
数据库中有些字段的值允许为null如果把每个字段的null值都保存到用户记录中显然有些浪费存储空间。有没有办法只简单的标记一下不存储实际的null值呢答案将为null的字段保存到null值列表。在列表中用二进制的值1表示该字段允许为null用0表示不允许为null。它只占用了1位就能表示某个字符是否为null确实可以节省很多存储空间。
这类型问题可以通过给 SELECT 操作加上排他锁来解决,不过这可能会引入性能问题,具体使用要视业务场景而定。
- **记录头信息**
记录头信息用于描述一些特殊的属性。它主要包含:
- deleted_flag即删除标记用于标记该记录是否被删除了
- min_rec_flag即最小目录标记它是非叶子节点中的最小目录标记
- n_owned即拥有的记录数记录该组索引记录的条数
- heap_no即堆上的位置它表示当前记录在堆上的位置
- record_type即记录类型其中0表示普通记录1表示非叶子节点2表示Infrimum记录 3表示Supremum记录
- next_record即下一条记录的位置
## 数据切分
### 水平切分
水平切分又称为 Sharding它是将同一个表中的记录拆分到多个结构相同的表中。当一个表的数据不断增多时Sharding 是必然的选择,它可以将数据分布到集群的不同节点上,从而缓存单个数据库的压力。
#### 隐藏列
![img](images/Database/007S8ZIlly1gjjfy33yx2j30fm05zwg9.jpg)
数据库在保存一条用户记录时,会自动创建一些隐藏列。如下图所示:
![InnoDB-隐藏列](images/Database/InnoDB-隐藏列.jpg)
目前innodb自动创建的隐藏列有三种
### 垂直切分
- db_row_id即行id它是一条记录的唯一标识。
- db_trx_id即事务id它是事务的唯一标识。
- db_roll_ptr即回滚点它用于事务回滚。
垂直切分是将一张表按列分成多个表,通常是按照列的关系密集程度进行切分,也可以利用垂直气氛将经常被使用的列喝不经常被使用的列切分到不同的表中。在数据库的层面使用垂直切分将按数据库中表的密集程度部署到不通的库中,例如将原来电商数据部署库垂直切分称商品数据库、用户数据库等。
如果表中有主键则用主键做行id无需额外创建。如果表中没有主键假如有不为null的unique唯一键则用它做为行id同样无需额外创建。如果表中既没有主键又没有唯一键则数据库会自动创建行id。也就是说在innodb中隐藏列中`事务id`和`回滚点`是一定会被创建的但行id要根据实际情况决定
![img](images/Database/007S8ZIlly1gjjfy5yoatj30cy09l776.jpg)
#### 真正数据列
### Sharding策略
真正的数据列中存储了用户的真实数据,它可以包含很多列的数据。
- 哈希取模hash(key)%N
- 范围:可以是 ID 范围也可以是时间范围
- 映射表:使用单独的一个数据库来存储映射关系
### 页目录
### Sharding存在的问题
从上面可以看出,如果我们要查询某条记录的话,数据库会从最小记录开始,一条条查找所有记录。如果中途找到了,则直接返回该记录。如果一直找到最大记录,还没有找到想要的记录,则返回空。
- **事务问题**:使用分布式事务来解决,比如 XA 接口
但效率会不会有点低?这不是要对整页用户数据进行扫描吗?
- **连接**:可以将原来的连接分解成多个单表查询,然后在用户程序中进行连接。
这就需要使用`页目录`了。说白了,就是把一页用户记录分为若干组,每一组的最大记录都保存到一个地方,这个地方就是`页目录`。每一组的最大记录叫做`槽`。由此可见,页目录是有多个槽组成的。所下图所示:
- **唯一性**
- 使用全局唯一 ID GUID
- 为每个分片指定一个 ID 范围
- 分布式 ID 生成器(如 Twitter 的 Snowflake 算法)
![InnoDB-页目录](images/Database/InnoDB-页目录.jpg)
假设一页的数据分为4组这样在页目录中就对应了4个槽每个槽中都保存了该组数据的最大值。这样就能通过二分查找比较槽中的记录跟需要找到的记录的大小。如果用户需要查找的记录小于当前槽中的记录则向上查找上一个槽。如果用户需要查找的记录大于当前槽中的记录则向下查找下一个槽。如此一来就能通过二分查找快速的定位需要查找的记录了。
## 复制
### 主从复制
### 文件尾部
主要涉及三个线程:
数据库的数据是以数据页为单位加载到内存中如果数据有更新的话需要刷新到磁盘上。但如果某一天比较倒霉程序在刷新到磁盘的过程中出现了异常比如进程被kill掉了或者服务器被重启了。这时候数据可能只刷新了一部分如何判断上次刷盘的数据是完整的呢这就需要用到`文件尾部`。它里面记录了页面的`校验和`。
- **binlog 线程**负责将主服务器上的数据更改写入二进制日志Binary log
- **I/O 线程**:负责从主服务器上读取- 二进制日志并写入从服务器的中继日志Relay log
- **SQL 线程**负责读取中继日志解析出主服务器已经执行的数据更改并在从服务器中重放Replay
在数据刷新到磁盘之前,会先计算一个页面的校验和。后面如果数据有更新的话,会计算一个新值。文件头部中也会记录这个校验和,由于文件头部在前面,会先被刷新到磁盘上。
![img](images/Database/007S8ZIlly1gjjfy97e83j30jk09ltav.jpg)
接下来,刷新用户记录到磁盘的时候,假设刷新了一部分,恰好程序出现异常了。这时,文件尾部的校验和,还是一个旧值。数据库会去校验,文件尾部的校验和,不等于文件头部的新值,说明该数据页的数据是不完整的。
### 读写分离
## Buffer Pool
主服务器处理写操作以及实时性要求比较高的读操作,而从服务器处理读操作。读写分离能提高性能的原因在于:
`InnoDB`为了解决磁盘`IO`问题,`MySQL`需要申请一块内存空间,这块内存空间称为`Buffer Pool`。
- 主从服务器负责各自的读和写,极大程度缓解了锁的争用
- 从服务器可以使用 MyISAM提升查询性能以及节约系统开销
- 增加冗余,提高可用性
![Buffer-Pool](images/Database/Buffer-Pool.png)
读写分离常用代理方式来实现,代理服务器接收应用层传来的读写请求,然后决定转发到哪个服务器。
### 缓存页
![img](images/Database/007S8ZIlly1gjjfycefayj313k0s20wl.jpg)
`Buffer Pool`申请下来后,`Buffer Pool`里面放什么,要怎么规划?
`MySQL`数据是以页为单位,每页默认`16KB`,称为数据页,在`Buffer Pool`里面会划分出若干**个缓存页**与数据页对应。
![Buffer-Pool-缓存页](images/Database/Buffer-Pool-缓存页.png)
### 描述数据
如何知道缓存页对应那个数据页呢?
所以还需要缓存页的元数据信息,可以称为**描述数据**,它与缓存页一一对应,包含一些所属表空间、数据页的编号、`Buffer Pool`中的地址等等。
![Buffer-Pool-描述数据](images/Database/Buffer-Pool-描述数据.png)
## 索引
后续对数据的增删改查都是在`Buffer Pool`里操作
### 索引的优点
- 查询:从磁盘加载到缓存,后续直接查缓存
- 插入:直接写入缓存
- 更新删除:缓存中存在直接更新,不存在加载数据页到缓存更新
- **大大减少了服务器需要扫描的数据行数**
- **帮助服务器避免进行排序和分组,以及避免创建临时表**B+Tree 索引是有序的,可以用于 ORDER BY 和 GROUP BY 操作。临时表主要是在排序和分组过程中创建,不需要排序和分组,也就不需要创建临时表)。
- **将随机 I/O 变为顺序 I/O**B+Tree 索引是有序的,会将相邻的数据都存储在一起)。
`MySQL`宕机数据不就全丢了吗?
`InnoDB`提供了`WAL`技术Write-Ahead Logging通过`redo log`让`MySQL`拥有了崩溃恢复能力。再配合空闲时,会有异步线程做缓存页刷盘,保证数据的持久性与完整性。
![Buffer-Pool-Write-Ahead-Logging](images/Database/Buffer-Pool-Write-Ahead-Logging.png)
直接更新数据的缓存页称为**脏页**,缓存页刷盘后称为**干净页**。
### Free链表
`MySQL`数据库启动时,按照设置的`Buffer Pool`大小,去找操作系统申请一块内存区域,作为`Buffer Pool`**假设申请了512MB**)。申请完毕后,会按照默认缓存页的`16KB`以及对应的`800Byte`的描述数据,在`Buffer Pool`中划分出来一个一个的缓存页和它们对应的描述数据。
![Buffer-Pool-Free链表](images/Database/Buffer-Pool-Free链表.png)
`MySQL`运行起来后,会不停的执行增删改查,需要从磁盘读取一个一个的数据页放入`Buffer Pool`对应的缓存页里,把数据缓存起来,以后就可以在内存里执行增删改查。
![Buffer-Pool-Free链表-增删改查](images/Database/Buffer-Pool-Free链表-增删改查.png)
但是这个过程必然涉及一个问题,**哪些缓存页是空闲的**
为了解决这个问题,我们使用链表结构,把空闲缓存页的**描述数据**放入链表中,这个链表称为`free`链表。针对`free`链表我们要做如下设计:
![Buffer-Pool-Free链表设计](images/Database/Buffer-Pool-Free链表设计.png)
- 新增`free`基础节点
- 描述数据添加`free`节点指针
最终呈现出来的,是由空闲缓存页的**描述数据**组成的`free`链表。
![Buffer-Pool-Free链表组成](images/Database/Buffer-Pool-Free链表组成.png)
有了`free`链表之后,我们只需要从`free`链表获取一个**描述数据**,就可以获取到对应的缓存页。
![Buffer-Pool-Free链表-获取描述数据](images/Database/Buffer-Pool-Free链表-获取描述数据.png)
往**描述数据**与**缓存页**写入数据后,就将该**描述数据**移出`free`链表。
### 缓存页哈希表
查询数据时,如何在`Buffer Pool`里快速定位到对应的缓存页呢?难道需要一个**非空闲的描述数据**链表,再通过**表空间号+数据页编号**遍历查找吗?这样做也可以实现,但是效率不太高,时间复杂度是`O(N)`。
![Buffer-Pool-缓存页哈希表](images/Database/Buffer-Pool-缓存页哈希表.png)
所以我们可以换一个结构,使用哈希表来缓存它们间的映射关系,时间复杂度是`O(1)`。
![Buffer-Pool-缓存页哈希表-复杂度](images/Database/Buffer-Pool-缓存页哈希表-复杂度.png)
**表空间号+数据页号**,作为一个`key`,然后缓存页的地址作为`value`。每次加载数据页到空闲缓存页时,就写入一条映射关系到**缓存页哈希表**中。
![Buffer-Pool-缓存页哈希表-映射关系](images/Database/Buffer-Pool-缓存页哈希表-映射关系.png)
后续的查询,就可以通过**缓存页哈希表**路由定位了。
### Flush链表
还记得之前有说过「**空闲时会有异步线程做缓存页刷盘,保证数据的持久性与完整性**」吗?
新问题来了,难道每次把`Buffer Pool`里所有的缓存页都刷入磁盘吗?当然不能这样做,磁盘`IO`开销太大了,应该把**脏页**刷入磁盘才对(更新过的缓存页)。可是我们怎么知道,那些缓存页是**脏页**?很简单,参照`free`链表,弄个`flush`链表出来就好了,只要缓存页被更新,就将它的**描述数据**加入`flush`链表。针对`flush`链表我们要做如下设计:
- 新增`flush`基础节点
- 描述数据添加`flush`节点指针
![Buffer-Pool-Flush链表](images/Database/Buffer-Pool-Flush链表.png)
最终呈现出来的,是由更新过数据的缓存页**描述数据**组成的`flush`链表。
![Buffer-Pool-Flush链表-缓存页](images/Database/Buffer-Pool-Flush链表-缓存页.png)
后续异步线程都从`flush`链表刷缓存页,当`Buffer Pool`内存不足时,也会优先刷`flush`链表里的缓存页。
### LRU链表
目前看来`Buffer Pool`的功能已经比较完善了。
![Buffer-Pool-LRU链表](images/Database/Buffer-Pool-LRU链表.png)
但是仔细思考下,发现还有一个问题没处理。`MySQL`数据库随着系统的运行会不停的把磁盘上的数据页加载到空闲的缓存页里去,因此`free`链表中的空闲缓存页会越来越少,直到没有,最后磁盘的数据页无法加载。
![Buffer-Pool-LRU链表-无法加载](images/Database/Buffer-Pool-LRU链表-无法加载.png)
为了解决这个问题,我们需要淘汰缓存页,腾出空闲缓存页。可是我们要优先淘汰哪些缓存页?总不能一股脑直接全部淘汰吧?这里就要借鉴`LRU`算法思想,把最近最少使用的缓存页淘汰(命中率低),提供`LRU`链表出来。针对`LRU`链表我们要做如下设计:
- 新增`LRU`基础节点
- 描述数据添加`LRU`节点指针
![Buffer-Pool-LRU链表-结构](images/Database/Buffer-Pool-LRU链表-结构.png)
实现思路也很简单,只要是查询或修改过缓存页,就把该缓存页的描述数据放入链表头部,也就说近期访问的数据一定在链表头部。
![Buffer-Pool-LRU链表-节点](images/Database/Buffer-Pool-LRU链表-节点.png)
当`free`链表为空的时候,直接淘汰`LRU`链表尾部缓存页即可。
### LRU链表优化
麻雀虽小五脏俱全,基本`Buffer Pool`里与缓存页相关的组件齐全了。
![Buffer-Pool-LRU链表优化](images/Database/Buffer-Pool-LRU链表优化.png)
但是缓存页淘汰这里还有点问题,如果仅仅只是使用`LRU`链表的机制,有两个场景会让**热点数据**被淘汰。
- **预读机制**
InnoDB使用两种预读算法来提高I/O性能线性预读linear read-ahead和随机预读randomread-ahead
- **全表扫描**
预读机制是指`MySQL`加载数据页时,可能会把它相邻的数据页一并加载进来(局部性原理)。这样会带来一个问题,预读进来的数据页,其实我们没有访问,但是它却排在前面。
![Buffer-Pool-LRU链表优化-全表扫描](images/Database/Buffer-Pool-LRU链表优化-全表扫描.png)
正常来说,淘汰缓存页时,应该把这个预读的淘汰,结果却把尾部的淘汰了,这是不合理的。我们接着来看第二个场景全表扫描,如果**表数据量大**,大量的数据页会把空闲缓存页用完。最终`LRU`链表前面都是全表扫描的数据,之前频繁访问的热点数据全部到队尾了,淘汰缓存页时就把**热点数据页**给淘汰了。
![图片](images/Database/819dbbcd31605b3a692576932f25d325.png)
为了解决上述的问题,我们需要给`LRU`链表做冷热数据分离设计,把`LRU`链表按一定比例,分为冷热区域,热区域称为`young`区域,冷区域称为`old`区域。
**以7:3为例young区域70%old区域30%**
![图片](images/Database/0f2c2610773fb9fe304c374fc37af4ac.png)
如上图所示数据页第一次加载进缓存页的时候是先放入冷数据区域的头部如果1秒后再次访问缓存页则会移动到热区域的头部。这样就保证了**预读机制**与**全表扫描**加载的数据都在链表队尾。
- `young`区域其实还可以做一个小优化,为了防止`young`区域节点频繁移动到表头
- `young`区域前面`1/4`被访问不会移动到链表头部,只有后面的`3/4`被访问了才会
记住是按照某个比例将`LRU`链表分成两部分,不是某些节点固定是`young`区域的,某些节点固定是`old`区域的,随着程序的运行,某个节点所属的区域也可能发生变化。
- InnoDB在LRU列表中引入了midpoint参数。新读取的页并不会直接放在LRU列表的首部而是放在LRU列表的midpoint位置即 innodb_old_blocks_pct这个点的设置。默认是37%最小是5最大是95如果内存比较大的话可以将这个数值调低通常会调成20也就是说20%的是冷数据块。目的是为了保护热区数据不被刷出内存。
- InnoDB还引入了innodb_old_blocks_time参数控制成为热数据的所需时间默认是1000ms也就是1s也就是数据在1s内没有被刷走就调入热区。
## InnoDB日志
### Redo Log(重做日志)
### Undo Log
# 数据切分
## 水平切分
水平切分又称为 Sharding它是将同一个表中的记录拆分到多个结构相同的表中。当一个表的数据不断增多时Sharding 是必然的选择,它可以将数据分布到集群的不同节点上,从而缓存单个数据库的压力。
![img](images/Database/007S8ZIlly1gjjfy33yx2j30fm05zwg9.jpg)
## 垂直切分
垂直切分是将一张表按列分成多个表,通常是按照列的关系密集程度进行切分,也可以利用垂直气氛将经常被使用的列喝不经常被使用的列切分到不同的表中。在数据库的层面使用垂直切分将按数据库中表的密集程度部署到不通的库中,例如将原来电商数据部署库垂直切分称商品数据库、用户数据库等。
![img](images/Database/007S8ZIlly1gjjfy5yoatj30cy09l776.jpg)
## Sharding策略
- 哈希取模hash(key)%N
- 范围:可以是 ID 范围也可以是时间范围
- 映射表:使用单独的一个数据库来存储映射关系
## Sharding存在的问题
- **事务问题**:使用分布式事务来解决,比如 XA 接口
- **连接**:可以将原来的连接分解成多个单表查询,然后在用户程序中进行连接。
- **唯一性**
- 使用全局唯一 ID GUID
- 为每个分片指定一个 ID 范围
- 分布式 ID 生成器(如 Twitter 的 Snowflake 算法)
@ -2252,6 +2667,34 @@ MyISAM既不支持事务、也不支持外键、其优势是访问速度快
## 复制
### 主从复制
主要涉及三个线程:
- **binlog 线程**负责将主服务器上的数据更改写入二进制日志Binary log
- **I/O 线程**:负责从主服务器上读取- 二进制日志并写入从服务器的中继日志Relay log
- **SQL 线程**负责读取中继日志解析出主服务器已经执行的数据更改并在从服务器中重放Replay
![img](images/Database/007S8ZIlly1gjjfy97e83j30jk09ltav.jpg)
### 读写分离
主服务器处理写操作以及实时性要求比较高的读操作,而从服务器处理读操作。读写分离能提高性能的原因在于:
- 主从服务器负责各自的读和写,极大程度缓解了锁的争用
- 从服务器可以使用 MyISAM提升查询性能以及节约系统开销
- 增加冗余,提高可用性
读写分离常用代理方式来实现,代理服务器接收应用层传来的读写请求,然后决定转发到哪个服务器。
![img](images/Database/007S8ZIlly1gjjfycefayj313k0s20wl.jpg)
## 日志系统
**生产优化**
@ -2362,71 +2805,6 @@ redo log 实际的触发 fsync 操作写盘包含以下几个场景:
## 全局锁表锁&行锁
### 全局锁
**FTWRL**
全局锁就是对整个数据库实例加锁MySQL 提供了 flush tables with read lock (FTWRL) 的方式去加全局锁。当你需要让整个库处于只读状态的时候,就可以使用这个命令了,之后所有线程的更改操作都会被阻塞。
**mysqldump**
mysqldump 是官方提供的备份工具,可以通过 --single-transaction 参数来启用可重复读隔离级别,从而可以拿到一个一致性视图。
**set global readonly = true**
通过上述命令可以让全库进入只读状态,但是在开发当中,事务框架往往会利用这个参数来处理读写分离。所以通常情况下,还是不建议使用这种方式。
### 表级锁
MySQL 的表级锁有 2 种:表锁和元数据锁。
**表锁**
表锁可以使用 lock tables T read/write , 可以使用 unlock tables 主动释放锁,也可以在客户端断开的时候自动释放锁。
**MDL (metadata lock)**
MDL 没有显示的命令当执行改表语句时MDL 会保证读写的正确性。MySQL 在 5.5 版本以后引入了 MDL 锁,当对一个表做增删改查的时候,加 MDL 读锁;当要多表结构做变更的时候,加 MDL 写锁。
- MDL 读锁之间不互斥,因此可以有多个线程同时对一张表 增删改查
- MDL 读-写、写-写之间是互斥的,因此如果同时有 2 个线程给表加字段,则需要顺序执行
### 行锁
**两阶段锁**
当使用update 更新数据时,会对 where 条件扫描到的行加行锁。在 InnoDB 事务中,行锁是在需要的时候才加上的,并且在事务提交后释放的,这就是两阶段锁协议。所以我们在更新数据时,应尽量把容易产生并发更新的行放在事务末端执行。
**死锁**
在事务对不同行加锁的时候,就很有可能出现死锁的情况。
MySQL 有 2 中策略去解决死锁:
- **超时等待**:可以通过 innodb_lock_wait_timeout 来设置,默认 50S。一般不采用因为 50S 对应用来说是不可接受的,并且这个值的设置也没有合适的估算值)
- **死锁检测**:发现死锁后,主动回滚其中一个事务。可以通过 innodb_deadlock_detect 设置为 on
上面 2 种死锁的解决方法都是MySQL 本身提供的。我们实际开发的过程当中,往往是需要自己从业务的角度去考虑,如何规避死锁和解决死锁的问题:
- **按规则加锁**:如 A 转账给 B同时 B 也转账给 A此时就很可能出现死锁。但是如果我们根据 userId 的升序规则去加锁,就不会产生死锁的问题了
- **控制并发度**:如支付系统中的账户系统,可以将总账户拆分成子账户,然后每个子账户是一个独立的锁实体
## mysql复制原理
### 基于语句的复制
@ -2457,26 +2835,6 @@ MySQL5.1开始支持基于行的复制,这种方式会将实际数据记录在
# InnoDB原理
## Buffer Pool
`InnoDB`为了解决磁盘`IO`问题,`MySQL`需要申请一块内存空间,这块内存空间称为`Buffer Pool`。
![img](images/Database/modb_20210816_ed8bed5e-fe80-11eb-8072-00163e068ecd.png)
### 缓存页
`Buffer Pool`申请下来后,`Buffer Pool`里面放什么,要怎么规划?
`MySQL`数据是以页为单位,每页默认`16KB`,称为数据页,在`Buffer Pool`里面会划分出若干**个缓存页**与数据页对应。
![img](images/Database/modb_20210816_edb843d6-fe80-11eb-8072-00163e068ecd.png)
### 描述数据
@ -2889,6 +3247,10 @@ mysql> show variables like 'slave_parallel%';
# ClickHouse
# 常见问题
## MySQL事务

@ -3841,105 +3841,6 @@ public void vectorTest(){
# Classloader
## JVM类加载机制
JVM类加载机制分为五个部分加载验证准备解析初始化下面我们就分别来看一下这五个过程。
![JVM类加载机制](images/JAVA/JVM类加载机制.png)
### 加载
加载是类加载过程中的一个阶段,这个阶段会在内存中生成一个代表这个类的 java.lang.Class 对 象,作为方法区这个类的各种数据的入口。注意这里不一定非得要从一个 Class 文件获取,这里既可以从 ZIP 包中读取(比如从 jar 包和 war 包中读取),也可以在运行时计算生成(动态代理),也可以由其它文件生成(比如将 JSP 文件转换成对应的 Class 类)。
### 验证
这一阶段的主要目的是为了确保 Class 文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
### 准备
准备阶段是正式为类变量分配内存并设置类变量的初始值阶段,即在方法区中分配这些变量所使用的内存空间。注意这里所说的初始值概念,比如一个类变量定义为:
```java
public static int v = 8080;
```
实际上变量 v 在准备阶段过后的初始值为 0 而不是 8080将 v 赋值为 8080 的 put static 指令是程序被编译后,存放于类构造器<client>方法之中。但是注意如果声明为:
```java
public static final int v = 8080;
```
在编译阶段会为 v 生成 ConstantValue 属性,在准备阶段虚拟机会根据 ConstantValue 属性将 v 赋值为 8080。
### 解析
解析阶段是指虚拟机将常量池中的符号引用替换为直接引用的过程。符号引用就是 class 文件中的:
- CONSTANT_Class_info
- CONSTANT_Field_info
- CONSTANT_Method_info
等类型的常量。
## 类加载器
虚拟机设计团队把加载动作放到 JVM 外部实现以便让应用程序决定如何获取所需的类JVM 提供了 3 种类加载器:
![Classloader](images/JAVA/Classloader.png)
- **启动类加载器(Bootstrap ClassLoader)**
负责加载 JAVA_HOME\lib 目录中的,或通过-Xbootclasspath 参数指定路径中的,且被虚拟机认可(按文件名识别,如 rt.jar的类。
- **扩展类加载器(Extension ClassLoader)**
负责加载 JAVA_HOME\lib\ext 目录中的,或通过 java.ext.dirs 系统变量指定路径中的类库。
- **应用程序类加载器(Application ClassLoader)**
负责加载用户路径classpath上的类库。JVM 通过双亲委派模型进行类的加载当然我们也可以通过继承java.lang.ClassLoader 实现自定义的类加载器。
此外我们比较需要知道的几点:
- 一个类是由 jvm 加载是通过类加载器+全限定类名确定唯一性的
- 双亲委派,众所周知,子加载器会尽量委托给父加载器进行加载,父加载器找不到再自己加载
- 线程上下文类加载,为了满足 spi 等需求突破双亲委派机制,当高层类加载器想加载底层类时通过 Thread.contextClassLoader 来获取当前线程的类加载器(往往是底层类加载器)去加载类
## 双亲委派
当一个.class文件要被加载时不考虑我们自定义类加载器类首先会在AppClassLoader中检查是否加载过如果有那就无需再加载如果没有会交到父加载器然后调用父加载器的loadClass方法。父加载器同样也会先检查自己是否已经加载过如果没有再往上直到到达BootstrapClassLoader之前都是在检查是否加载过并不会选择自己去加载。到了根加载器时才会开始检查是否能够加载当前类能加载就结束使用当前的加载器否则就通知子加载器进行加载子加载器重复该步骤。如果到最底层还不能加载就抛出异常ClassNotFoundException。
**总结**:所有的加载请求都会传送到根加载器去加载,只有当父加载器无法加载时,子类加载器才会去加载
![双亲委派](images/JAVA/双亲委派.png)
**作用**
- 避免类的重复加载
- 保证Java核心类库的安全
**如何打破双亲委派机制?**
# Throwable
![Throwable](images/JAVA/Throwable.png)

159
JVM.md

@ -8,27 +8,92 @@
https://mp.weixin.qq.com/s?__biz=MzI4NjI1OTI4Nw==&mid=2247489183&idx=1&sn=02ab3551c473bd2c8429862e3689a94b&chksm=ebdef7a7dca97eb17194c3d935c86ade240d3d96bbeaf036233a712832fb94af07adeafa098b&mpshare=1&scene=23&srcid=0812OQ78gD47QJEguFGixUVa&sharer_sharetime=1628761112690&sharer_shareid=0f9991a2eb945ab493c13ed9bfb8bf4b#rd
# JVM
## JVM常量池
- **字符串常量池**存放在堆上即执行intern方法后存的地方。class文件的静态常量池如果是字符串则也会被装到字符串常量池中
- **运行时常量池**:存放在方法区,属于元空间,是类加载后的一些存储区域,大多数是类中 constant_pool 的内容
- **类文件常量池**也就是constant_pool这个是概念性的并没有什么实际存储区域
# Classloader
## 类加载机制
JVM把描述类的数据加载到内存里面并对数据进行校验、解析和初始化最终变成可以被虚拟机直接使用的class对象。整个生命周期包括加载Loading、验证Verification、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段。其中准备、验证、解析3个部分统称为连接Linking
## JVM内存布局
![JVM类加载机制](images/JVM/JVM类加载机制.png)
类加载过程如下:
- 加载。加载分为三步:
- 通过类的全限定性类名获取该类的二进制流
- 将该二进制流的静态存储结构转为方法区的运行时数据结构
- 在堆中为该类生成一个class对象
- 验证验证该class文件中的字节流信息复合虚拟机的要求不会威胁到jvm的安全
- 准备为class对象的静态变量分配内存初始化其初始值
- 解析:该阶段主要完成符号引用转化成直接引用
- 初始化到了初始化阶段才开始执行类中定义的java代码初始化阶段是调用类构造器的过程
## 类加载器
虚拟机设计团队把加载动作放到 JVM 外部实现以便让应用程序决定如何获取所需的类JVM 提供了 3 种类加载器:
![Classloader](images/JVM/Classloader.png)
- **启动类加载器(Bootstrap ClassLoader)**
**用来加载java核心类库无法被java程序直接引用**。负责加载 JAVA_HOME\lib 目录中的,或通过-Xbootclasspath 参数指定路径中的,且被虚拟机认可(按文件名识别,如 rt.jar的类。
- **扩展类加载器(Extension ClassLoader)**
**用来加载java的扩展库java的虚拟机实现会提供一个扩展库目录该类加载器在扩展库目录里面查找并加载java类**。负责加载 JAVA_HOME\lib\ext 目录中的,或通过 java.ext.dirs 系统变量指定路径中的类库。
- **应用程序类加载器(Application ClassLoader)**
**它根据java的类路径来加载类一般来说java应用的类都是通过它来加载的**。负责加载用户路径classpath上的类库。JVM 通过双亲委派模型进行类的加载当然我们也可以通过继承java.lang.ClassLoader 实现自定义的类加载器。
- **自定义类加载器(User ClassLoader)**
由JAVA语言实现继承自ClassLoader。
JVM包含**堆**、**元空间**、**Java虚拟机栈**、**本地方法栈**、**程序计数器**等内存区域,其中**堆**是占用内存最大的,如下图所示:
![JVM架构](images/JVM/JVM架构.png)
此外我们比较需要知道的几点:
- 一个类是由 jvm 加载是通过类加载器+全限定类名确定唯一性的
- 双亲委派,众所周知,子加载器会尽量委托给父加载器进行加载,父加载器找不到再自己加载
- 线程上下文类加载,为了满足 spi 等需求突破双亲委派机制,当高层类加载器想加载底层类时通过 Thread.contextClassLoader 来获取当前线程的类加载器(往往是底层类加载器)去加载类
## JAVA内存模型
Java Memory Model (JAVA 内存模型JMM描述线程之间如何通过内存(memory)来进行交互。具体说来JVM中存在一个主存区Main Memory或Java Heap Memory对于所有线程进行共享而每个线程又有自己的工作内存Working Memory实际上是一个虚拟的概念工作内存中保存的是主存中某些变量的拷贝线程对所有变量的操作并非发生在主存区而是发生在工作内存中而线程之间是不能直接相互访问的变量在程序中的传递是依赖主存来完成的。具体的如下图所示
## 双亲委派
当一个类加载器收到一个类加载的请求,他首先不会尝试自己去加载,而是将这个请求委派给父类加载器去加载,只有父类加载器在自己的搜索范围类查找不到给类时,子加载器才会尝试自己去加载该类。
所有的加载请求都会传送到根加载器去加载,只有当父加载器无法加载时,子类加载器才会去加载:
![双亲委派](images/JVM/双亲委派.png)
**为什么需要双亲委派模型?**
为了防止内存中出现多个相同的字节码。因为如果没有双亲委派的话用户就可以自己定义一个java.lang.String类那么就无法保证类的唯一性。
**那怎么打破双亲委派模型?**
自定义类加载器继承ClassLoader类重写loadClass方法和findClass方法。
**双亲委派模型的作用**
- 避免类的重复加载
- 保证Java核心类库的安全
# JVM
## JAVA内存模型(JMM)
JAVA内存模型(Java Memory ModelJMM描述线程之间如何通过内存(Memory)来进行交互。具体说来:
- **主存区Main Memory或Java Heap Memory**对于所有线程进行共享而每个线程又有自己的工作内存Working Memory实际上是一个虚拟的概念
- **工作内存**:工作内存中保存的是主存中某些变量的拷贝,线程对所有变量的操作并非发生在主存区,而是发生在工作内存中,而线程之间是不能直接相互访问的,变量在程序中的传递,是依赖主存来完成的。
![JAVA内存模型](images/JVM/JAVA内存模型.jpg)
@ -39,6 +104,38 @@ JMM描述了Java程序中各种变量线程共享变量的访问规则
**Java 内存模型**(下文简称 **JMM**)就是在底层处理器内存模型的基础上,定义自己的多线程语义。它明确指定了一组排序规则,来保证线程间的可见性。这一组规则被称为 **Happens-Before**, JMM 规定,要想保证 B 操作能够看到 A 操作的结果(无论它们是否在同一个线程),那么 A 和 B 之间必须满足 **Happens-Before 关系**
- **单线程规则**:一个线程中的每个动作都 happens-before 该线程中后续的每个动作
- **监视器锁定规则**:监听器的**解锁**动作 happens-before 后续对这个监听器的**锁定**动作
- **volatile 变量规则**:对 volatile 字段的写入动作 happens-before 后续对这个字段的每个读取动作
- **线程 start 规则**:线程 **start()** 方法的执行 happens-before 一个启动线程内的任意动作
- **线程 join 规则**:一个线程内的所有动作 happens-before 任意其他线程在该线程 **join()** 成功返回之前
- **传递性**:如果 A happens-before B, 且 B happens-before C, 那么 A happens-before C
![JMM](images/JAVA/JMM.jpg)
## JVM内存结构
JVM包含**堆**、**元空间**、**Java虚拟机栈**、**本地方法栈**、**程序计数器**等内存区域,其中**堆**是占用内存最大的,如下图所示:
![JVM架构](images/JVM/JVM架构.png)
**JVM常量池**
JVM常量池主要分为**Class文件常量池、运行时常量池、全局字符串常量池、以及基本类型包装类对象常量池**。
- **Class文件常量池**class文件是一组以字节为单位的二进制数据流在java代码的编译期间我们编写的java文件就被编译为.class文件格式的二进制数据存放在磁盘中其中就包括class文件常量池。
- **运行时常量池**运行时常量池相对于class常量池一大特征就是具有动态性java规范并不要求常量只能在运行时才产生也就是说运行时常量池的内容并不全部来自class常量池在运行时可以通过代码生成常量并将其放入运行时常量池中这种特性被用的最多的就是String.intern()。
- **全局字符串常量池**字符串常量池是JVM所维护的一个字符串实例的引用表在HotSpot VM中它是一个叫做StringTable的全局表。在字符串常量池中维护的是字符串实例的引用底层C++实现就是一个Hashtable。这些被维护的引用所指的字符串实例被称作”被驻留的字符串”或”interned string”或通常所说的”进入了字符串常量池的字符串”。
- **基本类型包装类对象常量池**java中基本类型的包装类的大部分都实现了常量池技术这些类是Byte,Short,Integer,Long,Character,Boolean,另外两种浮点数类型的包装类则没有实现。另外上面这5种整型的包装类也只是在对应值小于等于127时才可使用对象池也即对象不负责创建和管理大于127的这些类的对象。
## JVM内存模型
JVM试图定义一种统一的内存模型能将各种底层硬件以及操作系统的内存访问差异进行封装使Java程序在不同硬件以及操作系统上都能达到相同的并发效果。**它分为工作内存和主内存,线程无法对主存储器直接进行操作,如果一个线程要和另外一个线程通信,那么只能通过主存进行交换**。如下图所示:
@ -637,17 +734,14 @@ GC Roots 是一组必须活跃的引用。用通俗的话来说,就是程序
- **用于同步的监控对象,比如调用了对象的 wait() 方法**
- **JNI handles包括 global handles 和 local handles**
这些 GC Roots 大体可以分为三大类:
GC Roots 大体可以分为三大类:
- **活动线程相关的各种引用**
- **类的静态变量的引用**
- **JNI 引用**
有两个注意点:
- **我们这里说的是活跃的引用,而不是对象,对象是不能作为 GC Roots 的**
- **GC 过程是找出所有活对象,并把其余空间认定为“无用”;而不是找出所有死掉的对象,并回收它们占用的空间。所以,哪怕 JVM 的堆非常的大,基于 tracing 的 GC 方式,回收速度也会非常快**
## 清理垃圾算法
@ -902,11 +996,29 @@ Parallel Old 收集器是 Parallel Scavenge 的老年代版本,追求 CPU 吞
- 并发收集
- 停顿时间最短
**缺点**
- 并发收集**占据一定CPU资源**导致程序GC过程中变慢吞吐量下降
- **无法处理浮动垃圾**可能出现”Concurrent Mode Failure“失败而导致另一次Full GC
- 因为基于”**标记-清除算法**“导致空间碎片过多可能因此在分配对象时引起另一次GC
- **并发回收导致CPU资源紧张**
在并发阶段它虽然不会导致用户线程停顿但却会因为占用了一部分线程而导致应用程序变慢降低程序总吞吐量。CMS默认启动的回收线程数是CPU核数 + 3/ 4当CPU核数不足四个时CMS对用户程序的影响就可能变得很大。
- **无法清理浮动垃圾**
在CMS的并发标记和并发清理阶段用户线程还在继续运行就还会伴随有新的垃圾对象不断产生但这一部分垃圾对象是出现在标记过程结束以后CMS无法在当次收集中处理掉它们只好留到下一次垃圾收集时再清理掉。这一部分垃圾称为“浮动垃圾”。
- **并发失败Concurrent Mode Failure**
由于在垃圾回收阶段用户线程还在并发运行那就还需要预留足够的内存空间提供给用户线程使用因此CMS不能像其他回收器那样等到老年代几乎完全被填满了再进行回收必须预留一部分空间供并发回收时的程序运行使用。默认情况下当老年代使用了 92% 的空间后就会触发 CMS 垃圾回收,这个值可以通过 -XX**:** CMSInitiatingOccupancyFraction 参数来设置。
这里会有一个风险要是CMS运行期间预留的内存无法满足程序分配新对象的需要就会出现一次“并发失败”Concurrent Mode Failure这时候虚拟机将不得不启动后备预案Stop The World临时启用 Serial Old 来重新进行老年代的垃圾回收,这样一来停顿时间就很长了。
- **内存碎片问题**
CMS是一款基于“标记-清除”算法实现的回收器,这意味着回收结束时会有内存碎片产生。内存碎片过多时,将会给大对象分配带来麻烦,往往会出现老年代还有很多剩余空间,但就是无法找到足够大的连续空间来分配当前对象,而不得不提前触发一次 Full GC 的情况。
为了解决这个问题CMS收集器提供了一个 -XX:+UseCMSCompactAtFullCollection 开关参数(默认开启),用于在 Full GC 时开启内存碎片的合并整理过程,由于这个内存整理必须移动存活对象,是无法并发的,这样停顿时间就会变长。还有另外一个参数 -XX:CMSFullGCsBeforeCompaction这个参数的作用是要求CMS在执行过若干次不整理空间的 Full GC 之后,下一次进入 Full GC 前会先进行碎片整理默认值为0表示每次进入 Full GC 时都进行碎片整理)。
@ -922,6 +1034,15 @@ Parallel Old 收集器是 Parallel Scavenge 的老年代版本,追求 CPU 吞
#### G1收集器
G1Garbage First回收器采用面向局部收集的设计思路和基于Region的内存布局形式是一款主要面向服务端应用的垃圾回收器。G1设计初衷就是替换 CMS成为一种全功能收集器。G1 在JDK9 之后成为服务端模式下的默认垃圾回收器,取代了 Parallel Scavenge 加 Parallel Old 的默认组合,而 CMS 被声明为不推荐使用的垃圾回收器。G1从整体来看是基于 标记-整理 算法实现的回收器但从局部两个Region之间上看又是基于 标记-复制 算法实现的。**G1 回收过程**G1 回收器的运作过程大致可分为四个步骤:
- **初始标记会STW**:仅仅只是标记一下 GC Roots 能直接关联到的对象并且修改TAMS指针的值让下一阶段用户线程并发运行时能正确地在可用的Region中分配新对象。这个阶段需要停顿线程但耗时很短而且是借用进行Minor GC的时候同步完成的所以G1收集器在这个阶段实际并没有额外的停顿。
- **并发标记**:从 GC Roots 开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以后,还要重新处理在并发时有引用变动的对象。
- **最终标记会STW**:对用户线程做短暂的暂停,处理并发阶段结束后仍有引用变动的对象。
- **清理阶段会STW**更新Region的统计数据对各个Region的回收价值和成本进行排序根据用户所期望的停顿时间来制定回收计划可以自由选择任意多个Region构成回收集然后把决定回收的那一部分Region的存活对象复制到空的Region中再清理掉整个旧Region的全部空间。这里的操作涉及存活对象的移动必须暂停用户线程由多条回收器线程并行完成的。
G1收集器中的堆内存被划分为多个大小相等的内存块Region每个Region是逻辑连续的一段内存结构如下
![G1-Region](images/JVM/G1-Region.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 476 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 495 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 436 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 245 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 363 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 548 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 484 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 507 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 436 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 425 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 494 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Before

Width:  |  Height:  |  Size: 121 KiB

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 243 KiB

Loading…
Cancel
Save