diff --git a/Algorithm.md b/Algorithm.md index 75efdb7..a25fbd6 100644 --- a/Algorithm.md +++ b/Algorithm.md @@ -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)构建成大顶堆,此堆为初始的无序区 diff --git a/Database.md b/Database.md index 21b5928..963b2e5 100644 --- a/Database.md +++ b/Database.md @@ -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 Lock(Record 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; +``` + +即所有在`(1,10)`区间内的记录行都会被锁住,所有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 Lock(Record 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 Lock(Record 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 Lock(Record 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事务 diff --git a/JAVA.md b/JAVA.md index a8bc06b..2d62ca8 100644 --- a/JAVA.md +++ b/JAVA.md @@ -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 指令是程序被编译后,存放于类构造器方法之中。但是注意如果声明为: - -```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) diff --git a/JVM.md b/JVM.md index 83b22b2..b223825 100644 --- a/JVM.md +++ b/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 Model,JMM)描述线程之间如何通过内存(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收集器 +G1(Garbage 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) diff --git a/images/Algorithm/大根堆-小根堆.jpg b/images/Algorithm/大根堆-小根堆.jpg new file mode 100644 index 0000000..fba1133 Binary files /dev/null and b/images/Algorithm/大根堆-小根堆.jpg differ diff --git a/images/Database/0f2c2610773fb9fe304c374fc37af4ac.png b/images/Database/0f2c2610773fb9fe304c374fc37af4ac.png new file mode 100644 index 0000000..15ac0ec Binary files /dev/null and b/images/Database/0f2c2610773fb9fe304c374fc37af4ac.png differ diff --git a/images/Database/819dbbcd31605b3a692576932f25d325.png b/images/Database/819dbbcd31605b3a692576932f25d325.png new file mode 100644 index 0000000..7b6ea69 Binary files /dev/null and b/images/Database/819dbbcd31605b3a692576932f25d325.png differ diff --git a/images/Database/Buffer-Pool-Flush链表-缓存页.png b/images/Database/Buffer-Pool-Flush链表-缓存页.png new file mode 100644 index 0000000..720d2dd Binary files /dev/null and b/images/Database/Buffer-Pool-Flush链表-缓存页.png differ diff --git a/images/Database/Buffer-Pool-Flush链表.png b/images/Database/Buffer-Pool-Flush链表.png new file mode 100644 index 0000000..b521ff9 Binary files /dev/null and b/images/Database/Buffer-Pool-Flush链表.png differ diff --git a/images/Database/Buffer-Pool-Free链表-增删改查.png b/images/Database/Buffer-Pool-Free链表-增删改查.png new file mode 100644 index 0000000..924e711 Binary files /dev/null and b/images/Database/Buffer-Pool-Free链表-增删改查.png differ diff --git a/images/Database/Buffer-Pool-Free链表-获取描述数据.png b/images/Database/Buffer-Pool-Free链表-获取描述数据.png new file mode 100644 index 0000000..0f2481a Binary files /dev/null and b/images/Database/Buffer-Pool-Free链表-获取描述数据.png differ diff --git a/images/Database/Buffer-Pool-Free链表.png b/images/Database/Buffer-Pool-Free链表.png new file mode 100644 index 0000000..07a3298 Binary files /dev/null and b/images/Database/Buffer-Pool-Free链表.png differ diff --git a/images/Database/Buffer-Pool-Free链表组成.png b/images/Database/Buffer-Pool-Free链表组成.png new file mode 100644 index 0000000..05cf1fb Binary files /dev/null and b/images/Database/Buffer-Pool-Free链表组成.png differ diff --git a/images/Database/Buffer-Pool-Free链表设计.png b/images/Database/Buffer-Pool-Free链表设计.png new file mode 100644 index 0000000..75a0b37 Binary files /dev/null and b/images/Database/Buffer-Pool-Free链表设计.png differ diff --git a/images/Database/Buffer-Pool-LRU链表-无法加载.png b/images/Database/Buffer-Pool-LRU链表-无法加载.png new file mode 100644 index 0000000..8590610 Binary files /dev/null and b/images/Database/Buffer-Pool-LRU链表-无法加载.png differ diff --git a/images/Database/Buffer-Pool-LRU链表-结构.png b/images/Database/Buffer-Pool-LRU链表-结构.png new file mode 100644 index 0000000..09fc1ae Binary files /dev/null and b/images/Database/Buffer-Pool-LRU链表-结构.png differ diff --git a/images/Database/Buffer-Pool-LRU链表-节点.png b/images/Database/Buffer-Pool-LRU链表-节点.png new file mode 100644 index 0000000..4922e2a Binary files /dev/null and b/images/Database/Buffer-Pool-LRU链表-节点.png differ diff --git a/images/Database/Buffer-Pool-LRU链表.png b/images/Database/Buffer-Pool-LRU链表.png new file mode 100644 index 0000000..aa966a7 Binary files /dev/null and b/images/Database/Buffer-Pool-LRU链表.png differ diff --git a/images/Database/Buffer-Pool-LRU链表优化-全表扫描.png b/images/Database/Buffer-Pool-LRU链表优化-全表扫描.png new file mode 100644 index 0000000..1ee72cb Binary files /dev/null and b/images/Database/Buffer-Pool-LRU链表优化-全表扫描.png differ diff --git a/images/Database/Buffer-Pool-LRU链表优化.png b/images/Database/Buffer-Pool-LRU链表优化.png new file mode 100644 index 0000000..098008b Binary files /dev/null and b/images/Database/Buffer-Pool-LRU链表优化.png differ diff --git a/images/Database/Buffer-Pool-Write-Ahead-Logging.png b/images/Database/Buffer-Pool-Write-Ahead-Logging.png new file mode 100644 index 0000000..b10ac22 Binary files /dev/null and b/images/Database/Buffer-Pool-Write-Ahead-Logging.png differ diff --git a/images/Database/Buffer-Pool-描述数据.png b/images/Database/Buffer-Pool-描述数据.png new file mode 100644 index 0000000..0601f9b Binary files /dev/null and b/images/Database/Buffer-Pool-描述数据.png differ diff --git a/images/Database/Buffer-Pool-缓存页.png b/images/Database/Buffer-Pool-缓存页.png new file mode 100644 index 0000000..0e212d3 Binary files /dev/null and b/images/Database/Buffer-Pool-缓存页.png differ diff --git a/images/Database/Buffer-Pool-缓存页哈希表-复杂度.png b/images/Database/Buffer-Pool-缓存页哈希表-复杂度.png new file mode 100644 index 0000000..25faac3 Binary files /dev/null and b/images/Database/Buffer-Pool-缓存页哈希表-复杂度.png differ diff --git a/images/Database/Buffer-Pool-缓存页哈希表-映射关系.png b/images/Database/Buffer-Pool-缓存页哈希表-映射关系.png new file mode 100644 index 0000000..3952ec0 Binary files /dev/null and b/images/Database/Buffer-Pool-缓存页哈希表-映射关系.png differ diff --git a/images/Database/Buffer-Pool-缓存页哈希表.png b/images/Database/Buffer-Pool-缓存页哈希表.png new file mode 100644 index 0000000..cfe5bba Binary files /dev/null and b/images/Database/Buffer-Pool-缓存页哈希表.png differ diff --git a/images/Database/640 b/images/Database/Buffer-Pool.png similarity index 100% rename from images/Database/640 rename to images/Database/Buffer-Pool.png diff --git a/images/Database/InnoDB-compact行格式.jpg b/images/Database/InnoDB-compact行格式.jpg new file mode 100644 index 0000000..324e1e0 Binary files /dev/null and b/images/Database/InnoDB-compact行格式.jpg differ diff --git a/images/Database/InnoDB-单个数据页内容.jpg b/images/Database/InnoDB-单个数据页内容.jpg new file mode 100644 index 0000000..5b9966e Binary files /dev/null and b/images/Database/InnoDB-单个数据页内容.jpg differ diff --git a/images/Database/InnoDB-数据页-写操作.jpg b/images/Database/InnoDB-数据页-写操作.jpg new file mode 100644 index 0000000..d252b27 Binary files /dev/null and b/images/Database/InnoDB-数据页-写操作.jpg differ diff --git a/images/Database/InnoDB-数据页-读操作.jpg b/images/Database/InnoDB-数据页-读操作.jpg new file mode 100644 index 0000000..f6c5b42 Binary files /dev/null and b/images/Database/InnoDB-数据页-读操作.jpg differ diff --git a/images/Database/InnoDB-数据页.jpg b/images/Database/InnoDB-数据页.jpg new file mode 100644 index 0000000..47d862c Binary files /dev/null and b/images/Database/InnoDB-数据页.jpg differ diff --git a/images/Database/InnoDB-文件头部.jpg b/images/Database/InnoDB-文件头部.jpg new file mode 100644 index 0000000..08bae08 Binary files /dev/null and b/images/Database/InnoDB-文件头部.jpg differ diff --git a/images/Database/InnoDB-最大和最小记录.jpg b/images/Database/InnoDB-最大和最小记录.jpg new file mode 100644 index 0000000..f361273 Binary files /dev/null and b/images/Database/InnoDB-最大和最小记录.jpg differ diff --git a/images/Database/InnoDB-隐藏列.jpg b/images/Database/InnoDB-隐藏列.jpg new file mode 100644 index 0000000..f64940a Binary files /dev/null and b/images/Database/InnoDB-隐藏列.jpg differ diff --git a/images/Database/InnoDB-页目录.jpg b/images/Database/InnoDB-页目录.jpg new file mode 100644 index 0000000..c31a1c9 Binary files /dev/null and b/images/Database/InnoDB-页目录.jpg differ diff --git a/images/Database/Next-Key-Locks-临键锁.jpg b/images/Database/Next-Key-Locks-临键锁.jpg new file mode 100644 index 0000000..37ec331 Binary files /dev/null and b/images/Database/Next-Key-Locks-临键锁.jpg differ diff --git a/images/Database/Next-Key-Locks.jpg b/images/Database/Next-Key-Locks.jpg new file mode 100644 index 0000000..5029adc Binary files /dev/null and b/images/Database/Next-Key-Locks.jpg differ diff --git a/images/Database/解决死锁.jpg b/images/Database/解决死锁.jpg new file mode 100644 index 0000000..ef8b36d Binary files /dev/null and b/images/Database/解决死锁.jpg differ diff --git a/images/JAVA/JMM.jpg b/images/JAVA/JMM.jpg new file mode 100644 index 0000000..4e9785b Binary files /dev/null and b/images/JAVA/JMM.jpg differ diff --git a/images/JVM/Classloader.png b/images/JVM/Classloader.png new file mode 100644 index 0000000..138e0bd Binary files /dev/null and b/images/JVM/Classloader.png differ diff --git a/images/JVM/JVM类加载机制.png b/images/JVM/JVM类加载机制.png new file mode 100644 index 0000000..6312a67 Binary files /dev/null and b/images/JVM/JVM类加载机制.png differ diff --git a/images/JVM/双亲委派.png b/images/JVM/双亲委派.png new file mode 100644 index 0000000..262210d Binary files /dev/null and b/images/JVM/双亲委派.png differ