pull/1/head
595208882@qq.com 4 years ago
parent 5f8d08119a
commit 3bc172dfe9

@ -561,105 +561,144 @@ public class Stack<E> extends Vector<E> {
### 树(Tree)
**树(Tree)**是一个分层的数据结构,由节点和连接节点的边组成是一种特殊的图,它与图最大的区别是没有循环。树的结构十分直观,而树的很多概念定义都有一个相同的特点:递归。也就是说一棵树要满足某种性质往往要求每个节点都必须满足。而树的考题无非就是要考查树的遍历以及序列化serialization)。常见的树:
**树(Tree)**是一个分层的数据结构,由节点和连接节点的边组成是一种特殊的图,它与图最大的区别是没有循环。树的结构十分直观,而树的很多概念定义都有一个相同的特点:递归。
- **普通二叉树**
- **平衡二叉树**
- **完全二叉树**
- **二叉搜索树**
- **四叉树Quadtree**
- **多叉树N-ary Tree**
- **红黑树Red-Black Tree**
- **自平衡二叉搜索树AVL Tree**
各种树解决的问题以及面临的新问题:
#### 二叉树(Binary Tree)
- **二叉查找树(BST)**:解决了排序的基本问题,但是由于无法保证平衡,可能退化为链表
- **平衡二叉树(AVL)**:通过旋转解决了平衡的问题,但是旋转操作效率太低
- **红黑树**通过舍弃严格的平衡和引入红黑节点解决了AVL旋转效率过低的问题但是在磁盘等场景下树仍然太高IO次数太多
- **B树**:通过将二叉树改为多路平衡查找树,解决了树过高的问题
- **B+树**在B树的基础上将非叶节点改造为不存储数据的纯索引节点进一步降低了树的高度此外将叶节点使用指针连接成链表范围查询更加高效
#### 树的遍历
**① 前序遍历Preorder Traversal**
**实现原理**`先访问根节点,然后访问左子树,最后访问右子树`。
**应用场景**:运用最多的场合包括在树里进行搜索以及创建一棵新的树。
![前序遍历](images/Algorithm/前序遍历.gif)
**② 中序遍历Inorder Traversal**
**实现原理**`先访问左子树,然后访问根节点,最后访问右子树`。
**应用场景**:最常见的是二叉搜索树,由于二叉搜索树的性质就是左孩子小于根节点,根节点小于右孩子,对二叉搜索树进行中序遍历的时候,被访问到的节点大小是按顺序进行的。
**二叉树Binary Tree**是n(n>=0)个结点的有限集合,该集合或者为空集(空二叉树),或者由一个根结点和两棵互不相交的、分别称为根结点的左子树和右子树的二叉树组成(子树也为二叉树)。
![中序遍历](images/Algorithm/中序遍历.gif)
**二叉树特点**
**③ 后序遍历Postorder Traversal**
- 每个结点最多有两棵子树,所以**二叉树中不存在度大于2的结点**
- 左子树和右子树是有顺序的,次序不能任意颠倒
- 即使树中某结点只有一棵子树,也要区分它是左子树还是右子树
**实现原理**`先访问左子树,然后访问右子树,最后访问根节点`。
**应用场景**:在对某个节点进行分析的时候,需要来自左子树和右子树的信息。收集信息的操作是从树的底部不断地往上进行,好比你在修剪一棵树的叶子,修剪的方法是从外面不断地往根部将叶子一片片地修剪掉。
![后序遍历](images/Algorithm/后序遍历.gif)
**二叉树性质**
- **性质1**:在二叉树的第 i 层上**最多**有2^(i-1)个结点i≥1
- **性质2**深度为k的二叉树**最多**有2^k-1个结点k≥1
- **性质3**:对任何一二叉树,如果其终端结点数(叶子节点数)为X,度为2的结点数为Y则X = Y+1
- **性质4**n个结点的完全二叉树的深度为[log2 n ] + 1
- **性质5**:对于任意一个完全二叉树来说,如果将含有的结点按照层次从左到右依次标号,对于任意一个结点 i 完全二叉树还有以下3个结论成立
- 当 i>1 时,父亲结点为结点 [i/2] 。i=1 时,表示的是根结点,无父亲结点)
- 如果 2×i>n总结点的个数 ,则结点 i 肯定没有左孩子(为叶子结点);否则其左孩子是结点 2×i 。
- 如果 2×i+1>n ,则结点 i 肯定没有右孩子;否则右孩子是结点 2×i+1
#### 二叉树(Binary Tree)
**二叉树是每个节点最多有两个子节点的树**。二叉树的叶子节点有0个字节点根节点或内部节点有一个或两个子节点。
**存储结构**
![BinaryTree](images/Algorithm/BinaryTree.png)
其中data是数据域lchild和rchild都是指针域分别指向左孩子和右孩子。
**存储结构**
```java
public class TreeNode {
public Object data;
public TreeNode leftChild;
public TreeNode rightChild;
// 数据域
private Object data;
// 左孩子指针
private TreeNode leftChild;
// 右孩子指针
private TreeNode rightChild;
}
```
#### 红黑树(Red Black Tree)
#### 二叉搜索树(Binary Search Tree)
红黑树全称是Red-Black Tree一种特殊的二叉查找树。红黑树的每个节点上都有存储位表示节点的颜色可以是红(Red)或黑(Black)。
二叉搜索树, 又叫**二叉查找树**,它是一棵空树或是具有下列性质的二叉树:
![红黑树](images/Algorithm/红黑树.jpg)
- **若左子树不空,则左子树上所有结点的值均小于它的根结点的值**
- **若右子树不空,则右子树上所有结点的值均大于它的根结点的值**
- **它的左、右子树也分别为二叉搜索树**
红黑树的特性:
![BinarySearchTree](images/Algorithm/BinarySearchTree.png)
- **每个节点或者是黑色,或者是红色**
- **根节点是黑色**
- **每个叶子节点NIL是黑色。 注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点**
- **如果一个节点是红色的,则它的子节点必须是黑色的**
- **从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点**
**效率总结**
正是红黑树的这5条性质使一棵n个结点的红黑树始终保持了logn的高度从而也就解释了“红黑树的查找、插入、删除的时间复杂度最坏为O(log n)”这一结论成立的原因。
- **访问/查找**:最好时间复杂度 `O(logN)`,最坏时间复杂度 `O(N)`
- **插入/删除**:最好时间复杂度 `O(logN)`,最坏时间复杂度 `O(N)`
**① 左旋**
#### 平衡二叉树(AVL Tree)
逆时针旋转两个节点,让一个节点被其右子节点取代,而该节点成为右子节点的左子节点
二叉查找树在最差情况下竟然和顺序查找效率相当这是无法仍受的。事实也证明当存储数据足够大的时候树的结构对某些关键字的查找效率影响很大。当然造成这种情况的主要原因就是BST不够平衡(左右子树高度差太大)。既然如此那么我们就需要通过一定的算法将不平衡树改变成平衡树。因此AVL树就诞生了
对x进行左旋意味着"将x变成一个左节点"。
**平衡二叉树全称叫做 `平衡二叉搜索(排序)树`,简称 AVL树**。高度为 `logN`。本质是一颗二叉查找树AVL树的特性
![左旋](images/Algorithm/左旋.jpg)
- 它是**一棵空树**或**左右两个子树的高度差**的绝对值不超过 `1`
- 左右两个子树也都是一棵平衡二叉树
如下图根节点左边高度是3因为左边最多有3条边右边高度而2相差1。根节点左边的节点50的左边是1条边高度为1右边有两条边高度为2相差1。
![AVLTree](images/Algorithm/AVLTree.png)
**② 右旋**
**效率总结**
顺时针旋转两个节点,让一个节点被其左子节点取代,而该节点成为左子节点的右子节点。
- 查找:时间复杂度维持在`O(logN)`,不会出现最差情况
- 插入:插入操作时最多需要 `1` 次旋转,其时间复杂度在`O(logN)`左右
- 删除:删除时代价稍大,执行每个删除操作的时间复杂度需要`O(2logN)`
对x进行左旋意味着"将x变成一个左节点"。
![右旋](images/Algorithm/右旋.jpg)
#### 红黑树(Red-Black Tree)
二叉平衡树的严格平衡策略以**牺牲建立查找结构(插入,删除操作)的代**价换来了稳定的O(logN) 的查找时间复杂度。但是这样做是否值得呢? 能不能找一种折中策略,即不牺牲太大的建立查找结构的代价,也能保证稳定高效的查找效率呢? 答案就是:红黑树。
**红黑树是一种含有红、黑结点,并能自平衡的二叉查找树**。高度为 `logN`。其性质如下:
- **每个结点或是红色的,或是黑色的**
- **根节点是黑色的**
- **每个叶子节点NIL是黑色的**
- **如果一个节点是红色的,则它的两个子节点都是黑色的**
- **任意一结点到每个叶子结点的路径都包含数量相同的黑节点**
**③ 变色**
正是红黑树的这5条性质使一棵n个结点的红黑树始终保持了logN的高度从而也就解释了“红黑树的查找、插入、删除的时间复杂度最坏为O(logN)”这一结论成立的原因。
变颜色条件:两个连续红色节点,并且叔叔节点是红色。
![Red-BlackTree](images/Algorithm/Red-BlackTree.jpg)
**效率总结**
- **查找**:最好情况下时间复杂度为`O(logN)`,但在最坏情况下比`AVL`要差一些,但也远远好于`BST`
- **插入/删除**:改变树的平衡性的概率要远远小于`AVL`RBT不是高度平衡的
因此需要旋转操作的可能性要小,且一旦需要旋转,插入一个结点最多只需旋转`2`次,删除最多只需旋转`3`次(小于`AVL`的删除操作所需要的旋转次数)。虽然变色操作的时间复杂度在`O(logN)`,但是实际上,这种操作由于简单所需要的代价很小
##### 左旋
![左旋](images/Algorithm/左旋.jpg)
- **逆时针旋转两个节点,让一个节点被其右子节点取代,而该节点成为右子节点的左子节点**
- **对x进行左旋意味着"将x变成一个左节点"**
- **左旋条件:两个连续红节点,并且叔叔节点是黑色 , 下面的红色节点在右子树**
**④ 左旋条件**
两个连续红节点,并且叔叔节点是黑色 , 下面的红色节点在右子树。
**情况1**:如果当前节点是右子树,并且父节点是左子树。`形如:(900是新插入节点)`
@ -668,9 +707,11 @@ public class TreeNode {
要根据父节点左旋【899】根据谁左旋谁就变成子节点
![左旋条件情况1流程](images/Algorithm/左旋条件情况1流程.gif)
【900】的左子树 挂到 【899】的右子树上 【899】变为子节点 【900】变为父节点`
【900】的左子树挂到 【899】的右子树上 【899】变为子节点 【900】变为父节点`
`此时不变色。
**情况2**:如果当前节点是右子树,并且父节点是右子树。形如:(【100】是当前节点)
![左旋条件情况2](images/Algorithm/左旋条件情况2.png)
@ -683,9 +724,18 @@ public class TreeNode {
**⑤ 右旋条件**
##### 右旋
![右旋](images/Algorithm/右旋.jpg)
- **顺时针旋转两个节点,让一个节点被其左子节点取代,而该节点成为左子节点的右子节点**
- **对x进行左旋意味着"将x变成一个左节点"**
- **右旋条件:两个连续红节点,并且叔叔节点是黑色 , 下面的红色节点在左子树**
两个连续红节点,并且叔叔节点是黑色 , 下面的红色节点在左子树。
**情况1**:如果当前节点是左子树,并且父节点也是右子树。形如:(【8000】是当前节点)
![右旋条件情况1](images/Algorithm/右旋条件情况1.png)
@ -696,6 +746,8 @@ public class TreeNode {
【9000】变为子节点【8000】变为父节点【8000】的右子树 挂到 【9000】的左子树此时不变色。
**情况2**:如果当前节点是左子树,并且父节点也是左子树。形如:(【899】是当前节点)
![右旋条件情况2](images/Algorithm/右旋条件情况2.png)
@ -709,14 +761,20 @@ public class TreeNode {
**⑥ 旋转场景**
##### 变色
如果当前节点的父亲节点和叔叔节点均是红色,那么执行以下变色操作:
父 --> 黑
叔 --> 黑
爷 --> 红
无法通过变色而进行旋转的场景分为以下四种:
**场景一:左左节点旋转**
这种情况下,父节点和插入的节点都是左节点,如下图(旋转原始图1)这种情况下,我们要插入节点 65。
规则如下:以祖父节点【右旋】,搭配【变色】。
**无法通过变色而进行旋转的四种场景**
**场景一:左左节点旋转**
这种情况下,父节点和插入的节点都是左节点,如下图(旋转原始图1)这种情况下,我们要插入节点 65。规则如下以祖父节点【右旋】搭配【变色】。
![左左节点旋转](images/Algorithm/左左节点旋转.png)
@ -725,18 +783,12 @@ public class TreeNode {
![左左节点旋转步骤](images/Algorithm/左左节点旋转步骤.jpeg)
**场景二:左右节点旋转**
这种情况下,父节点是左节点,插入的节点是右节点,在旋转原始图 1 中,我们要插入节点 67。
规则如下:先父节点【左旋】,然后祖父节点【右旋】,搭配【变色】。
按照规则,步骤如下:
这种情况下,父节点是左节点,插入的节点是右节点,在旋转原始图 1 中,我们要插入节点 67。规则如下先父节点【左旋】然后祖父节点【右旋】搭配【变色】。按照规则步骤如下
![左右节点旋转](images/Algorithm/左右节点旋转.png)
**场景三:右左节点旋转**
这种情况下,父节点是右节点,插入的节点是左节点,如下图(旋转原始图 2这种情况我们要插入节点 68。
规则如下:先父节点【右旋】,然后祖父节点【左旋】,搭配【变色】。
这种情况下,父节点是右节点,插入的节点是左节点,如下图(旋转原始图 2这种情况我们要插入节点 68。规则如下先父节点【右旋】然后祖父节点【左旋】搭配【变色】。
![右左节点旋转](images/Algorithm/右左节点旋转.png)
@ -745,63 +797,74 @@ public class TreeNode {
![右左节点旋转步骤](images/Algorithm/右左节点旋转步骤.jpeg)
**场景四:右右节点旋转**
这种情况下,父节点和插入的节点都是右节点,在旋转原始图 2 中,我们要插入节点 70。
这种情况下,父节点和插入的节点都是右节点,在旋转原始图 2 中,我们要插入节点 70。规则如下:以祖父节点【左旋】,搭配【变色】。按照规则,步骤如下:
规则如下:以祖父节点【左旋】,搭配【变色】。
![右右节点旋转](images/Algorithm/右右节点旋转.png)
按照规则,步骤如下:
![右右节点旋转](images/Algorithm/右右节点旋转.png)
#### B树(Balance Tree)
对于在内存中的查找结构而言,红黑树的效率已经非常好了(实际上很多实际应用还对RBT进行了优化)。但是如果是数据量非常大的查找呢将这些数据全部放入内存组织成RBT结构显然是不实际的。实际上像OS中的文件目录存储数据库中的文件索引结构的存储…. 都不可能在内存中建立查找结构。必须在磁盘中建立好这个结构。
在磁盘中组织查找结构从任何一个结点指向其他结点都有可能读取一次磁盘数据再将数据写入内存进行比较。大家都知道频繁的磁盘IO操作效率是很低下的(机械运动比电子运动要慢不知道多少)。显而易见所有的二叉树的查找结构在磁盘中都是低效的。因此B树很好的解决了这一个问题。
#### 树的遍历
**① 前序遍历Preorder Traversal**
**实现原理**`先访问根节点,然后访问左子树,最后访问右子树`。在访问左、右子树的时候,同样,先访问子树的根节点,再访问子树根节点的左子树和右子树,这是一个不断递归的过程。
**B树也称B-树、B-Tree它是一颗多路平衡查找树**。描述一颗B树时需要指定它的阶数阶数表示了一个结点最多有多少个孩子结点一般用字母m表示阶数。当m取2时就是我们常见的二叉搜索树。B树的定义
![前序遍历](images/Algorithm/前序遍历.gif)
- **每个结点最多有m-1个关键字**
- **根结点最少可以只有1个关键字**
- **非根结点至少有Math.ceil(m/2)-1个关键字**
- **每个结点中的关键字都按照从小到大的顺序排列,每个关键字的左子树中的所有关键字都小于它,而右子树中的所有关键字都大于它**
- **所有叶子结点都位于同一层,或者说根结点到每个叶子结点的长度都相同**
**应用场景**:运用最多的场合包括在树里进行搜索以及创建一棵新的树。
**B树的优势**
**② 中序遍历Inorder Traversal**
- **B树的高度远远小于AVL树和红黑树(B树是一颗“矮胖子”)磁盘IO次数大大减少**
- **对访问局部性原理的利用**。指当一个数据被使用时其附近的数据有较大概率在短时间内被使用。当访问其中某个数据时数据库会将该整个节点读到缓存中当它临近的数据紧接着被访问时可以直接在缓存中读取无需进行磁盘IO
**实现原理**`先访问左子树,然后访问根节点,最后访问右子树`。在访问左、右子树的时候,同样,先访问子树的左边,再访问子树的根节点,最后再访问子树的右边。
![中序遍历](images/Algorithm/中序遍历.gif)
**应用场景**:最常见的是二叉搜索树,由于二叉搜索树的性质就是左孩子小于根节点,根节点小于右孩子,对二叉搜索树进行中序遍历的时候,被访问到的节点大小是按顺序进行的。
如下图B树的内部节点可以存放数据类似ZK的中间节点一样。B树不是每个节点都有足够多的子节点
![BalanceTree](images/Algorithm/BalanceTree.png)
上图是一颗阶数为4的B树。在实际应用中的B树的阶数m都非常大通常大于100所以即使存储大量的数据B树的高度仍然比较小。每个结点中存储了关键字key和关键字对应的数据data以及孩子结点的指针。**我们将一个key和其对应的data称为一个记录**。**但为了方便描述除非特别说明后续文中就用key来代替key, value键值对这个整体**。在数据库中我们将B树和B+树作为索引结构可以加快查询速速此时B树中的key就表示键而data表示了这个键对应的条目在硬盘上的逻辑地址。
**③ 后序遍历Postorder Traversal**
**实现原理**`先访问左子树,然后访问右子树,最后访问根节点`。
![后序遍历](images/Algorithm/后序遍历.gif)
#### B+树(B+Tree)
**应用场景**:在对某个节点进行分析的时候,需要来自左子树和右子树的信息。收集信息的操作是从树的底部不断地往上进行,好比你在修剪一棵树的叶子,修剪的方法是从外面不断地往根部将叶子一片片地修剪掉。
**B+树是从B树的变体**。跟B树的不同
- **内部节点不保存数据,只用于索引**
- **B+树的每个叶子节点之间存在指针相连,而且是单链表**,叶子节点本身依关键字的大小自小而大顺序链接
如下图其实B+树上二叉搜索树的扩展二叉搜索树是每次一分为二B树是每次一分为多现代操作系统中磁盘的存储结构使用的是B+树机制mysql的innodb引擎的存储方式也是B+树机制:
**案例一二叉搜索中第K小的元素**
![B+Tree](images/Algorithm/B+Tree.png)
给定一个二叉搜索树,编写一个函数 kthSmallest 来查找其中第 k 个最小的元素。
**B+树与B树相比有以下优势**
**解题思路**
- **更少的IO次数**B+树的非叶节点只包含键而不包含真实数据因此每个节点存储的记录个数比B数多很多即阶m更大因此B+树的高度更低访问时所需要的IO次数更少。此外由于每个节点存储的记录数更多所以对访问局部性原理的利用更好缓存命中率更高
- **更适于范围查询**在B树中进行范围查询时首先找到要查找的下限然后对B树进行中序遍历直到找到查找的上限而B+树的范围查询,只需要对链表进行遍历即可
- **更稳定的查询效率**B树的查询时间复杂度在1到树高之间(分别对应记录在根节点和叶节点)而B+树的查询复杂度则稳定为树高,因为所有数据都在叶节点。
**B+树劣势**
由于键会重复出现,因此**会占用更多的空间**。但是与带来的性能优势相比空间劣势往往可以接受因此B+树的在数据库中的使用比B树更加广泛。
这道题考察了两个知识点:
- **二叉搜索树的性质**:对于每个节点来说,该节点的值比左孩子大,比右孩子小,而且一般来说,二叉搜索树里不出现重复的值。
- **二叉搜索树的遍历**:二叉搜索树的中序遍历是高频考察点,节点被遍历到的顺序是按照节点数值大小的顺序排列好的。即,中序遍历当中遇到的元素都是按照从小到大的顺序出现。
#### B*树
因此,我们只需要对这棵树进行中序遍历的操作,当访问到第 k 个元素的时候返回结果就好。
是B+树的变体,**在B+树的非根和非叶子结点再增加指向兄弟的指针**,且**定义了非叶子结点关键字个数至少为(2/3)×M**即块的最低使用率为2/3代替B+树的1/2
![二叉搜索中第K小的元素](images/Algorithm/二叉搜索中第K小的元素.gif)
![Bx树](images/Algorithm/Bx树.jpg)

@ -923,6 +923,213 @@ SELECT * FROM products WHERE id LIKE '3' FOR UPDATE;
# 数据库
## 存储引擎
InnoDB 和 MyISAM 的比较:
- 事务InnoDB 是事务型的,可以使用 Commit 和 Rollback 语句
- 并发MyISAM 只支持表级锁,而 InnoDB 还支持行级锁
- 外键InnoDB 支持外键
- 备份InnoDB 支持在线热备份
- 崩溃恢复MyISAM 崩溃后发生损坏的概率比 InnoDB 高很多,而且恢复的速度也更慢
- 其它特性MyISAM 支持压缩表和空间数据索引
### InnoDB
- InnoDB 是 MySQL 默认的事务型存储引擎,只要在需要它不支持的特性时,才考虑使用其他存储引擎。
- InnoDB **采用 MVCC 来支持高并发**,并且**实现了四个标准隔离级别**(未提交读、提交读、可重复读、可串行化)。其默认级别时可重复读REPEATABLE READ在可重复读级别下通过 MVCC + Next-Key Locking 防止幻读。
- 主索引时聚簇索引,在索引中保存了数据,从而避免直接读取磁盘,因此对主键查询有很高的性能。
- InnoDB 内部做了很多优化,包括从磁盘读取数据时采用的可预测性读,能够自动在内存中创建 hash 索引以加速读操作的自适应哈希索引,以及能够加速插入操作的插入缓冲区等。
- InnoDB 支持真正的在线热备份MySQL 其他的存储引擎不支持在线热备份,要获取一致性视图需要停止对所有表的写入,而在读写混合的场景中,停止写入可能也意味着停止读取。
### MyISAM
- 设计简单,数据以紧密格式存储。对于只读数据,或者表比较小、可以容忍修复操作,则依然可以使用它。
- 提供了大量的特性,包括压缩表、空间数据索引等。
- 不支持事务。
- 不支持行级锁只能对整张表加锁读取时会对需要读到的所有表加共享锁写入时则对表加排它锁。但在表有读取操作的同时也可以往表中插入新的记录这被称为并发插入CONCURRENT INSERT
- 可以手工或者自动执行检查和修复操作,但是和事务恢复以及崩溃恢复不同,可能导致一些数据丢失,而且修复操作是非常慢的。
- 如果指定了 DELAY_KEY_WRITE 选项,在每次修改执行完成时,不会立即将修改的索引数据写入磁盘,而是会写到内存中的键缓冲区,只有在清理键缓冲区或者关闭表的时候才会将对应的索引块写入磁盘。这种方式可以极大的提升写入性能,但是在数据库或者主机崩溃时会造成索引损坏,需要执行修复操作。
## 锁类型
锁是数据库系统区别于文件系统的一个关键特性。锁机制用于管理对共享资源的并发访问。
**共享锁S Lock**:允许事务读一行数据
**排他锁X Lock**:允许事务删除或者更新一行数据
**意向共享锁IS Lock**:事务想要获得一张表中某几行的共享锁
**意向排他锁**:事务想要获得一张表中某几行的排他锁
## 锁算法
### Record Lock
锁定一个记录上的索引,而不是记录本身。
如果表没有设置索引InnoDB 会自动在主键上创建隐藏的聚簇索引,因此 Record Locks 依然可以使用。
### Gap Lock
锁定索引之间的间隙,但是不包含索引本身。例如当一个事务执行以下语句,其它事务就不能在 t.c 中插入 15。
```sql
SELECT c FROM t WHERE c BETWEEN 10 and 20 FOR UPDATE;
```
### Next-Key Lock
它是 Record Locks 和 Gap Locks 的结合不仅锁定一个记录上的索引也锁定索引之间的间隙。例如一个索引包含以下值10, 11, 13, and 20那么就需要锁定以下区间
```
(-∞, 10]
(10, 11]
(11, 13]
(13, 20]
(20, +∞)
```
在 InnoDB 存储引擎中SELECT 操作的不可重复读问题通过 MVCC 得到了解决,而 UPDATE、DELETE 的不可重复读问题通过 Record Lock 解决INSERT 的不可重复读问题是通过 Next-Key LockRecord Lock + Gap Lock解决的。
## 锁问题
### 脏读
脏读指的是不同事务下,当前事务可以读取到另外事务未提交的数据。
例如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 操作加上排他锁来解决,不过这可能会引入性能问题,具体使用要视业务场景而定。
## 数据切分
### 水平切分
水平切分又称为 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 算法)
## 复制
### 主从复制
主要涉及三个线程:
- **binlog 线程**负责将主服务器上的数据更改写入二进制日志Binary log
- **I/O 线程**:负责从主服务器上读取- 二进制日志并写入从服务器的中继日志Relay log
- **SQL 线程**负责读取中继日志解析出主服务器已经执行的数据更改并在从服务器中重放Replay
![img](images/Database/007S8ZIlly1gjjfy97e83j30jk09ltav.jpg)
### 读写分离
主服务器处理写操作以及实时性要求比较高的读操作,而从服务器处理读操作。读写分离能提高性能的原因在于:
- 主从服务器负责各自的读和写,极大程度缓解了锁的争用
- 从服务器可以使用 MyISAM提升查询性能以及节约系统开销
- 增加冗余,提高可用性
读写分离常用代理方式来实现,代理服务器接收应用层传来的读写请求,然后决定转发到哪个服务器。
![img](images/Database/007S8ZIlly1gjjfycefayj313k0s20wl.jpg)
## 索引
### 索引的优点
- **大大减少了服务器需要扫描的数据行数**
- **帮助服务器避免进行排序和分组,以及避免创建临时表**B+Tree 索引是有序的,可以用于 ORDER BY 和 GROUP BY 操作。临时表主要是在排序和分组过程中创建,不需要排序和分组,也就不需要创建临时表)。
- **将随机 I/O 变为顺序 I/O**B+Tree 索引是有序的,会将相邻的数据都存储在一起)。
# 索引
建立索引的目的是加快对表中记录的查找或排序。索引只是提高效率的一个因素如果你的MySQL有大数据量的表就需要花时间研究建立最优秀的索引或优化查询语句。因此应该只为最经常查询和最经常排序的数据列建立索引。MySQL里同一个数据表里的索引总数限制为16个。
@ -1768,6 +1975,15 @@ desc 和asc混用时会导致索引失效
# 存储引擎
InnoDB 和 MyISAM 的比较
- 事务InnoDB 是事务型的,可以使用 Commit 和 Rollback 语句。
- 并发MyISAM 只支持表级锁,而 InnoDB 还支持行级锁。
- 外键InnoDB 支持外键。
- 备份InnoDB 支持在线热备份。
- 崩溃恢复MyISAM 崩溃后发生损坏的概率比 InnoDB 高很多,而且恢复的速度也更慢。
- 其它特性MyISAM 支持压缩表和空间数据索引。
## InnoDB引擎
InnoDB 是一个事务安全的存储引擎它具备提交、回滚以及崩溃恢复的功能以保护用户数据。InnoDB 的行级别锁定保证数据一致性提升了它的多用户并发数以及性能。InnoDB 将用户数据存储在聚集索引中以减少基于主键的普通查询所带来的 I/O 开销。为了保证数据的完整性InnoDB 还支持外键约束。默认使用B+TREE数据结构存储索引。

@ -24,11 +24,9 @@ JAVA里面进行多线程通信的主要方式就是 `共享内存` 的方式,
**指令重排序分类**
- 编译器重排序
- 编译器优化重排序编译器在不改变单线程程序语义as-if-serial )的前提下,可以重新安排语句的执行顺序
- 处理器重排序
- 内存系统重排序:由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行
- 指令级并行重排序现代处理器采用了指令级并行技术Instruction Level ParallelismILP来将多条指令重叠执行。如果不存在数据依赖性处理器可以改变语句对机器指令的执行顺序
- **编译器优化的重排序**:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序;
- **指令级并行的重排序**:现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序;
- **内存系统的重排序**:由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行的。
**② 顺序一致性**
@ -2847,6 +2845,103 @@ ThreadLocal常用的方法
### 底层结构
![img](images/JAVA/007S8ZIlly1gh4fy6gvw0j30w0093jsu.jpg)
### set流程
![img](images/JAVA/007S8ZIlly1gh4ipc80hfj30w10hugo5.jpg)
然后会判断一下如果当前位置是空的就初始化一个Entry对象放在位置i上
```java
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
// 根据ThreadLocal对象的hash值定位到table中的位置i
int i = key.threadLocalHashCode & (len - 1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// 如果位置i不为空如果这个Entry对象的key正好是即将设置的key那么就刷新Entry中的value
if (k == key) {
e.value = value;
return;
}
// 如果当前位置是空的就初始化一个Entry对象放在位置i上
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
// 如果位置i的不为空而且key不等于entry那就找下一个空位置直到为空为止
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
```
### get流程
在get的时候也会根据ThreadLocal对象的hash值定位到table中的位置然后判断该位置Entry对象中的key是否和get的key一致如果不一致就判断下一个位置set和get如果冲突严重的话效率还是很低的。
```java
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
// get的时候一样是根据ThreadLocal获取到table的i值然后查找数据拿到后会对比key是否相等 if (e != null && e.get() == key)。
while (e != null) {
ThreadLocal<?> k = e.get();
// 相等就直接返回,不相等就继续查找,找到相等位置。
if (k == key)
return e;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
```
**如果想共享线程的ThreadLocal数据怎么办**
使用`InheritableThreadLocal`可以实现多个线程访问ThreadLocal的值我们在主线程中创建一个`InheritableThreadLocal`的实例,然后在子线程中得到这个`InheritableThreadLocal`实例设置的值。
### 内存泄露
![img](images/JAVA/007S8ZIlly1gh4mkx8upjj30jz06m74u.jpg)
ThreadLocal在保存的时候会把自己当做Key存在ThreadLocalMap中正常情况应该是key和value都应该被外界强引用才对但是现在key被设计成WeakReference弱引用了。
![img](images/JAVA/007S8ZIlly1gh4nh8v3haj30w10bbabr.jpg)
**弱引用**:只具有弱引用的对象拥有更短暂的生命周期,在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
这就导致了一个问题ThreadLocal在没有外部强引用时发生GC时会被回收如果创建ThreadLocal的线程一直持续运行那么这个Entry对象中的value就有可能一直得不到回收发生内存泄露。
**解决**在finally中remove即可。
**那为什么ThreadLocalMap的key要设计成弱引用**key不设置成弱引用的话就会造成和entry中value一样内存泄漏的场景。
### InheritableThreadLocal
`InheritableThreadLocal` 是 JDK 本身自带的一种线程传递解决方案。顾名思义,由当前线程创建的线程,将会继承当前线程里 ThreadLocal 保存的值。

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 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: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Loading…
Cancel
Save