diff --git a/基础知识学习笔记/初级算法篇/img_10.png b/基础知识学习笔记/初级算法篇/img_10.png new file mode 100644 index 0000000..78610ac Binary files /dev/null and b/基础知识学习笔记/初级算法篇/img_10.png differ diff --git a/基础知识学习笔记/初级算法篇/img_11.png b/基础知识学习笔记/初级算法篇/img_11.png new file mode 100644 index 0000000..85bcd9a Binary files /dev/null and b/基础知识学习笔记/初级算法篇/img_11.png differ diff --git a/基础知识学习笔记/初级算法篇/img_12.png b/基础知识学习笔记/初级算法篇/img_12.png new file mode 100644 index 0000000..3cb7cd8 Binary files /dev/null and b/基础知识学习笔记/初级算法篇/img_12.png differ diff --git a/基础知识学习笔记/初级算法篇/img_5.png b/基础知识学习笔记/初级算法篇/img_5.png new file mode 100644 index 0000000..638542e Binary files /dev/null and b/基础知识学习笔记/初级算法篇/img_5.png differ diff --git a/基础知识学习笔记/初级算法篇/img_6.png b/基础知识学习笔记/初级算法篇/img_6.png new file mode 100644 index 0000000..3ad4261 Binary files /dev/null and b/基础知识学习笔记/初级算法篇/img_6.png differ diff --git a/基础知识学习笔记/初级算法篇/img_7.png b/基础知识学习笔记/初级算法篇/img_7.png new file mode 100644 index 0000000..59c8662 Binary files /dev/null and b/基础知识学习笔记/初级算法篇/img_7.png differ diff --git a/基础知识学习笔记/初级算法篇/img_8.png b/基础知识学习笔记/初级算法篇/img_8.png new file mode 100644 index 0000000..860e14b Binary files /dev/null and b/基础知识学习笔记/初级算法篇/img_8.png differ diff --git a/基础知识学习笔记/初级算法篇/img_9.png b/基础知识学习笔记/初级算法篇/img_9.png new file mode 100644 index 0000000..459e981 Binary files /dev/null and b/基础知识学习笔记/初级算法篇/img_9.png differ diff --git a/基础知识学习笔记/初级算法篇/算法.md b/基础知识学习笔记/初级算法篇/算法.md index e9b3a03..650bcfa 100644 --- a/基础知识学习笔记/初级算法篇/算法.md +++ b/基础知识学习笔记/初级算法篇/算法.md @@ -11,10 +11,38 @@ 基本运算有一套底层逻辑 >>取相反数a=-b+1 -### 不带符号右移>>> - +### 不带符号右移>>,一般用这个 + +### 除2,除2+1 +>- a>>1 +>- a>>1|1 + +### 几种排序的区别 +>- 冒泡排序:相邻比较 +>- 选择排序: 找到最值,放入位置 +>- 插入排序,在已经排好的基础上继续排序 + +### 数组累加和 +>- 建立二维数组 +>- 将二维数组的值表示对应一维数组的和 +>- ![img_5.png](img_5.png) +> +> -前缀和数组 +> 建立一维数组,后项等于本数组前项+目标同数组下标的值 +> ![img_6.png](img_6.png) +> 1~8的累加和=H[8]-H[0] +>- H[R]-H[L-1] +>- 机器人深度搜索 +![img_7.png](img_7.png) +>- +>- ![img_8.png](img_8.png) +> +> ![img_9.png](img_9.png) + +![img_10.png](img_10.png) +![img_11.png](img_11.png) - +![img_12.png](img_12.png) diff --git a/基础知识学习笔记/初级算法篇/算法2.md b/基础知识学习笔记/初级算法篇/算法2.md new file mode 100644 index 0000000..bfb865b --- /dev/null +++ b/基础知识学习笔记/初级算法篇/算法2.md @@ -0,0 +1,27 @@ +### 蒙特卡洛模拟X平方,x三次方 +-> Math.random函数 +-> math.max(a,b)函数 + +### 随机数的产生 +-> f()等概率生产随机数 (int)Math.random +-> 将函数改造为 0,1 发生器 +-> 采用位运算生成数字的位 +-> 利用左移合并生成的数字 +-> 遇到不合适的数字就重做 + +### 通过不等概率,返回等概率 +-> 两次返回的相同则重做,两次相反则保留 +-> p*(1-p) 返回0 +-> (1-p)*p 返回1 +-> 实现对数器,生成随机样本能够检验数据 + +### 二分法查找数字 +-> 确定循环条件 +-> 调整边界状态 + +### 二分法查找最左数字,即有重复数字出现 +-> 调整二分边界状态,存最小索引 + +### 归并排序 +-> 先左右排序(递归) +-> 后合并merge \ No newline at end of file diff --git a/基础知识学习笔记/编程知识/img_4.png b/基础知识学习笔记/编程知识/img_4.png new file mode 100644 index 0000000..37fbbd8 Binary files /dev/null and b/基础知识学习笔记/编程知识/img_4.png differ diff --git a/基础知识学习笔记/编程知识/语法知识.md b/基础知识学习笔记/编程知识/语法知识.md index 78de14d..8569037 100644 --- a/基础知识学习笔记/编程知识/语法知识.md +++ b/基础知识学习笔记/编程知识/语法知识.md @@ -36,5 +36,15 @@ Integer.MAX_VALUE 2^31-1 ### hashmap扩容 >- ![img_3.png](img_3.png) >- 在扩容时会出现死循环 ->- + +### 接口 +>- 必须实现全部的抽象方法 +>- 接口可以多继承 +>- 接口中直接带有方法体1.8 默认方法、静态方法(必须接口调用)、私有方法 +>- 父类,接口 方法名字一样,优先父类方法 +>- 多继承的方法可以重复,如果有默认方法需要在实现里面重写 + +### 拷贝 +![img_4.png](img_4.png) + diff --git a/基础知识学习笔记/高级算法/img.png b/基础知识学习笔记/高级算法/img.png new file mode 100644 index 0000000..a1028f2 Binary files /dev/null and b/基础知识学习笔记/高级算法/img.png differ diff --git a/基础知识学习笔记/高级算法/img_1.png b/基础知识学习笔记/高级算法/img_1.png new file mode 100644 index 0000000..2306f67 Binary files /dev/null and b/基础知识学习笔记/高级算法/img_1.png differ diff --git a/基础知识学习笔记/高级算法/img_2.png b/基础知识学习笔记/高级算法/img_2.png new file mode 100644 index 0000000..8ccf8d2 Binary files /dev/null and b/基础知识学习笔记/高级算法/img_2.png differ diff --git a/基础知识学习笔记/高级算法/img_3.png b/基础知识学习笔记/高级算法/img_3.png new file mode 100644 index 0000000..65e4eaf Binary files /dev/null and b/基础知识学习笔记/高级算法/img_3.png differ diff --git a/基础知识学习笔记/高级算法/img_4.png b/基础知识学习笔记/高级算法/img_4.png new file mode 100644 index 0000000..7ea0f17 Binary files /dev/null and b/基础知识学习笔记/高级算法/img_4.png differ diff --git a/基础知识学习笔记/高级算法/img_5.png b/基础知识学习笔记/高级算法/img_5.png new file mode 100644 index 0000000..dccfeb1 Binary files /dev/null and b/基础知识学习笔记/高级算法/img_5.png differ diff --git a/基础知识学习笔记/高级算法/img_6.png b/基础知识学习笔记/高级算法/img_6.png new file mode 100644 index 0000000..03c43d2 Binary files /dev/null and b/基础知识学习笔记/高级算法/img_6.png differ diff --git a/基础知识学习笔记/高级算法/img_7.png b/基础知识学习笔记/高级算法/img_7.png new file mode 100644 index 0000000..136ceb4 Binary files /dev/null and b/基础知识学习笔记/高级算法/img_7.png differ diff --git a/基础知识学习笔记/高级算法/img_8.png b/基础知识学习笔记/高级算法/img_8.png new file mode 100644 index 0000000..efd5629 Binary files /dev/null and b/基础知识学习笔记/高级算法/img_8.png differ diff --git a/基础知识学习笔记/高级算法/算法1.md b/基础知识学习笔记/高级算法/算法1.md new file mode 100644 index 0000000..ce0ce4d --- /dev/null +++ b/基础知识学习笔记/高级算法/算法1.md @@ -0,0 +1,24 @@ +### 模拟退火算法 +- >算法简介 +- 通过降温使得晶粒重排有序 +![img.png](img.png) + +- 模拟退火算法的目的 +- ![img_1.png](img_1.png) +- 求解非凸函数的最优解,防止局部最优解 +- 求解大规模组合优化 +- 模拟退火的流程 +- ![img_2.png](img_2.png) +- 在温度高时随机调整,在温度低时有序调整 +- 在温度高时有概率接受更差的解 +- ![img_3.png](img_3.png) +- 候选解的方式,一般采用安装概率目睹函数对解空间进行随机采样 +- 均匀分布,高斯分布,指数分布 +![img_4.png](img_4.png) +- 优点:以一定概率接受恶化解,可能出现更好的最优解 +- ![img_5.png](img_5.png) +- ![img_7.png](img_7.png) +- 缺点: +- ![img_6.png](img_6.png) +- 改进的模拟退火算法 +- ![img_8.png](img_8.png) \ No newline at end of file diff --git a/计算机网络/HTTP.md b/计算机网络/HTTP.md new file mode 100644 index 0000000..afb63b7 --- /dev/null +++ b/计算机网络/HTTP.md @@ -0,0 +1,47 @@ +### 七层模型详解open system internetwork +- ![img.png](img.png) +- 物理层:电器规范、机械规范、网络光纤、铜轴电缆、光模块 +- 数据链路:格式化数据,在物理介质上传输 +- 网络层(地图): 路由数据包、寻址(IP) 、交换机 +- 传输层(TCP): 确保数据安全、端到端连接 +- 会话层:应用程序会话 +- 表述层:格式化数据、提供加密、解析 +- 应用层(TCP): 为应用进程提供网络服务、提供身份验证 + +### 路由 +- ![img_1.png](img_1.png) +- IP地址唯一标识了网络中的一个节点,每个IP地址都拥有自己的网段,各个网段可能分布在网络的不同区域。 + 为实现IP寻址,分布在不同区域的网段之间要能够相互通信。 +- 路由是指导报文转发的路径信息,通过路由可以确认转发IP报文的路径。 +- 路由设备是依据路由转发报文到目的网段的网终设备,最常见的路由设备∶路由器。 +- 路由设备维护着一张路由表,保存着路由信息。 + +### HTTP +- HTTP是要基于TCP连接基础上的 +- HTTP是用来收发数据,即实际应用上来的。 +- 第一:发送一个HTTP请求给主机A,这个请求包括**请求头和请求内容** + - 请求的方法是POST/GET, + - 1.请求的URL,http协议版本; + - 2.请求的数据,和编码方式; + - 3.是否有cookie和cooies,是否缓存等。 +- 第二:主机B收到了HTTP请求,然后根据请求头,返回HTTP响应。 + - cookies或者sessions; + - 状态码; + - 内容大小等; +- 主机A收到了以后,就由浏览器完成一系列的渲染,包括执行JS脚本等。 +- HTTP是应用层协议,定义的是传输数据的内容的规范; +- HTTP协议中的数据是利用TCP协议传输的,所以支持HTTP也就一定支持TCP ; + +### HTTPS工作流程 +- 1、TCP 三次同步握手 +- 2、客户端验证服务器数字证书 CA证书,第三方发布 公钥与私钥 +- 3、DH 算法协商对称加密算法的密钥、hash 算法的密钥 +- 4、SSL 安全加密隧道协商完成 +- 5、网页以加密的方式传输, + +### 对称加密与非对称加密 +- ![img_4.png](img_4.png) +- ![img_5.png](img_5.png) +- 加密与解密的密钥不同 +- ![img_6.png](img_6.png) + diff --git a/计算机网络/TCP.md b/计算机网络/TCP.md new file mode 100644 index 0000000..63d47a6 --- /dev/null +++ b/计算机网络/TCP.md @@ -0,0 +1,26 @@ +### 三HTTP和TCP的关系 +- TCP是传输层协议,而HTTP是应用层协议 +- TCP是底层通讯协议,定义的是数据传输和连接方式的规范; +- TCP/IP实际上是一组协议,它包括上百个各种功能的协议,如:远程登录、文件传输和电子邮件等,而TCP协议和IP协议是保证数据完整传输的两个基本的重要协议。通常说TCP/IP是Internet协议族,而不单单是TCP和IP。 +- TCP就是单纯建立连接,不涉及任何我们需要请求的实际数据,简单的传输。 +- 要和服务端连接TCP连接,需要通过三次连接,包括:请求,确认,建立连接。 + +### 三次握手建立连接 +- ![img_2.png](img_2.png) + +### 单工、全双工、半双工 +- 单工:收音机 +- 半双工:不能同时说话、对讲机 +- 全双工:可以同时收发(双绞线8根线) + +### 三次握手 +- 目的:确保双线畅通 +- syn标识:标志第几次握手 +- 第一次(客户端):序列号(随机数):seq=x;回应序列号ack=0;syn标识,表示探测询问 +- 第二次(服务端):seq=y;ack=x+1(表示收到);ack标识,表示收到;syn标识,表示收到 +- 第三次(客户端):seq=x+1(表示收到);ack=y+1;ack标识,表示收到 +- ![img_3.png](img_3.png) +- 第一次握手:客户端尝试连接服务器,向服务器发送 syn 包(同步序列编号Synchronize Sequence Numbers),syn=j,客户端进入 SYN_SEND 状态等待服务器确认 +- 第二次握手:服务器接收客户端syn包并确认(ack=j+1),同时向客户端发送一个 SYN包(syn=k),即 SYN+ACK 包,此时服务器进入 SYN_RECV 状态 +- 第三次握手:第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手 +- 第三次握手的时候,是可以携带数据的。 但是,第一次、第二次握手不可以携带数据 \ No newline at end of file diff --git a/计算机网络/img.png b/计算机网络/img.png new file mode 100644 index 0000000..0afc577 Binary files /dev/null and b/计算机网络/img.png differ diff --git a/计算机网络/img_1.png b/计算机网络/img_1.png new file mode 100644 index 0000000..c0b84aa Binary files /dev/null and b/计算机网络/img_1.png differ diff --git a/计算机网络/img_2.png b/计算机网络/img_2.png new file mode 100644 index 0000000..3f950b5 Binary files /dev/null and b/计算机网络/img_2.png differ diff --git a/计算机网络/img_3.png b/计算机网络/img_3.png new file mode 100644 index 0000000..81cb911 Binary files /dev/null and b/计算机网络/img_3.png differ diff --git a/计算机网络/img_4.png b/计算机网络/img_4.png new file mode 100644 index 0000000..d5c427f Binary files /dev/null and b/计算机网络/img_4.png differ diff --git a/计算机网络/img_5.png b/计算机网络/img_5.png new file mode 100644 index 0000000..8a00ff1 Binary files /dev/null and b/计算机网络/img_5.png differ diff --git a/计算机网络/img_6.png b/计算机网络/img_6.png new file mode 100644 index 0000000..d4c56df Binary files /dev/null and b/计算机网络/img_6.png differ diff --git a/马士兵直播课程/spring源码/SpringMVC源码.md b/马士兵直播课程/spring源码/SpringMVC源码.md new file mode 100644 index 0000000..2c5ae39 --- /dev/null +++ b/马士兵直播课程/spring源码/SpringMVC源码.md @@ -0,0 +1,6 @@ +### 邓老师springMVC源码 +-> 适配器 +-> servlet +-> 9大核心组件的初始化 +->init方法 +-> service方法 \ No newline at end of file diff --git a/高级知识/MySQL性能调优/MySQL中的锁.md b/高级知识/MySQL性能调优/MySQL中的锁.md new file mode 100644 index 0000000..4b830bc --- /dev/null +++ b/高级知识/MySQL性能调优/MySQL中的锁.md @@ -0,0 +1,463 @@ +## 1.6. MySQL中的锁 + +InnoDB中锁非常多,总的来说,可以如下分类: + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1653286288069/c4dad088c5534409930be499ded99b42.png)![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1653286288069/f72d5746a3464c3dae9e71b1d2b35ccc.png) + +这些锁都是做什么的?具体含义是什么?我们现在来一一学习。 + +### 1.6.1.解决并发事务问题 + +我们已经知道事务并发执行时可能带来的各种问题,最大的一个难点是:一方面要最大程度地利用数据库的并发访问,另外一方面还要确保每个用户能以一致的方式读取和修改数据,尤其是一个事务进行读取操作,另一个同时进行改动操作的情况下。 + +### 1.6.2.并发事务问题 + +一个事务进行读取操作,另一个进行改动操作,我们前边说过,这种情况下可能发生脏读、不可重复读、幻读的问题。 + +怎么解决脏读、不可重复读、幻读这些问题呢?其实有两种可选的解决方案: + +#### 1.6.2.1.方案一:读操作MVCC,写操作进行加锁 + +**事务利用MVCC进行的读取操作称之为一致性读,或者一致性无锁读,也称之为快照读**,但是往往读取的是历史版本数据。所有普通的SELECT语句(plain SELECT)在READ COMMITTED、REPEATABLE READ隔离级别下都算是一致性读。 + +一致性读并不会对表中的任何记录做加锁操作,其他事务可以自由的对表中的记录做改动。 + +很明显,采用MVCC方式的话,**读-写操作彼此并不冲突,性能更高**,**采用加锁方式的话,读-写操作彼此需要排队执行,影响性能**。一般情况下我们当然愿意采用MVCC来解决读-写操作并发执行的问题,但是业务在某些情况下,要求必须采用加锁的方式执行。 + +#### 1.6.2.2方案二:读、写操作都采用加锁的方式 + +**适用场景:** + +业务场景不允许读取记录的旧版本,而是每次都必须去读取记录的最新版本, + +比方在银行存款的事务中,你需要先把账户的余额读出来,然后将其加上本次存款的数额,最后再写到数据库中。在将账户余额读取出来后,就不想让别的事务再访问该余额,直到本次存款事务执行完成,其他事务才可以访问账户的余额。这样在读取记录的时候也就需要对其进行加锁操作,这样也就意味着读操作和写操作也像写-写操作那样排队执行。 + +**脏读**的产生是因为当前事务读取了另一个未提交事务写的一条记录,如果另一个事务在写记录的时候就给这条记录加锁,那么当前事务就无法继续读取该记录了,所以也就不会有脏读问题的产生了。 + +**不可重复读**的产生是因为当前事务先读取一条记录,另外一个事务对该记录做了改动之后并提交之后,当前事务再次读取时会获得不同的值,如果在当前事务读取记录时就给该记录加锁,那么另一个事务就无法修改该记录,自然也不会发生不可重复读了。 + +**幻读问题**的产生是因为当前事务读取了一个范围的记录,然后另外的事务向该范围内插入了新记录,当前事务再次读取该范围的记录时发现了新插入的新记录,我们把新插入的那些记录称之为幻影记录。采用加锁的方式解决幻读问题就有不太容易了,因为当前事务在第一次读取记录时那些幻影记录并不存在,所以读取的时候加锁就有点麻烦—— 因为并不知道给谁加锁。InnoDB中是如何解决的,我们后面会讲到。 + +### 1.6.3锁定读(LockingReads)/LBCC + +也称当前读, 读取的是最新版本, 并且对读取的记录加锁, 阻塞其他事务同时改动相同记录,避免出现安全问题。 + +哪些是当前读呢?select lock in share mode (共享锁)、select for update (排他锁)、update (排他锁)、insert (排他锁/独占锁)、delete (排他锁)、串行化事务隔离级别都是当前读。 + +当前读这种实现方式,也可以称之为LBCC(基于锁的并发控制,Lock-Based Concurrency Control),怎么做到? + +#### 1.6.3.1. 共享锁和独占锁 + +在使用加锁的方式解决问题时,由于既要允许读-读情况不受影响,又要使写-写、读-写或写-读情况中的操作相互阻塞,MySQL中的锁有好几类: + +**共享锁**英文名:Shared Locks,简称S锁。在事务要读取一条记录时,需要先获取该记录的S锁。 + +假如事务E1首先获取了一条记录的S锁之后,事务E2接着也要访问这条记录: + +如果事务E2想要再获取一个记录的S锁,那么事务E2也会获得该锁,也就意味着事务E1和E2在该记录上同时持有S锁。 + +**独占锁,**也常称**排他锁**,英文名:Exclusive Locks,简称X锁。在事务要改动一条记录时,需要先获取该记录的X锁。 + +如果事务E2想要再获取一个记录的X锁,那么此操作会被阻塞,直到事务E1提交之后将S锁释放掉。 + +如果事务E1首先获取了一条记录的X锁之后,那么不管事务E2接着想获取该记录的S锁还是X锁都会被阻塞,直到事务E1提交。 + +所以我们说S锁和S锁是兼容的,S锁和X锁是不兼容的,X锁和X锁也是不兼容的,画个表表示一下就是这样: + +X 不兼容X 不兼容S + +S 不兼容X 兼容S + +#### 1.6.3.2.锁定读的SELECT语句 + +MySQ有两种比较特殊的SELECT语句格式: + +``` +SELECT * from test LOCK IN SHARE MODE; +``` + +一个事务中开启S锁 + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/a2a38c121e4e44e6a2045fec1724254e.png) + +另一个事务中开启S锁,可以读 + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/b647ccce3edf4313b64b844139d7199d.png) + +如果另外一个事务中开启X锁,阻塞! + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/02a271b4b3864be7a8a873888c11fc3d.png) + +也就是在普通的SELECT语句后边加LOCK IN SHARE MODE,如果当前事务执行了该语句,那么它会为读取到的记录加S锁,这样允许别的事务继续获取这些记录的S锁(比方说别的事务也使用SELECT ... LOCK IN SHARE MODE语句来读取这些记录),但是不能获取这些记录的X锁(比方说使用SELECT ... FOR UPDATE语句来读取这些记录,或者直接修改这些记录)。 + +如果别的事务想要获取这些记录的X锁,那么它们会阻塞,直到当前事务提交之后将这些记录上的S锁释放掉。 + +对读取的记录加X锁: + +``` +SELECT * from test FOR UPDATE; +``` + +也就是在普通的SELECT语句后边加FOR UPDATE,如果当前事务执行了该语句,那么它会为读取到的记录加X锁,这样既不允许别的事务获取这些记录的S锁(比方说别的事务使用SELECT ... LOCK IN SHARE MODE语句来读取这些记录),也不允许获取这些记录的X锁(比如说使用SELECT ... FOR UPDATE语句来读取这些记录,或者直接修改这些记录)。 + +一个事务中开启X锁 + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/28116a3e03264a399c6fa5eea78b8497.png) + +另外一个事务中的X锁阻塞 + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/644044fd54f746b29faa4f3580cce5e9.png) + +除非第一个事务提交 + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/6e1d9b6a68c749609dc88477960b6fcb.png) + +另外一个事务才能获得X锁 + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/49662552879242f4b8e34ee640700e93.png) + +同样如果另外一个事务执行X锁,使用S锁也不行 + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/27229c3d97f74246888b858f2d0c2e70.png) + +如果别的事务想要获取这些记录的S锁或者X锁,那么它们会阻塞,直到当前事务提交之后将这些记录上的X锁释放掉。 + +#### 1.2.1.3. 写操作的锁 + +平常所用到的写操作无非是DELETE、UPDATE、INSERT这三种: + +**DELETE:** + +对一条记录做DELETE操作的过程其实是先在B+树中定位到这条记录的位置,然后获取一下这条记录的X锁,然后再执行delete mark操作。我们也可以把这个定位待删除记录在B+树中位置的过程看成是一个获取X锁的锁定读。 + +**INSERT:** + +一般情况下,新插入一条记录的操作并不加锁,InnoDB通过一种称之为隐式锁来保护这条新插入的记录在本事务提交前不被别的事务访问。当然,在一些特殊情况下INSERT操作也是会获取锁的,具体情况我们后边再说。 + +**UPDATE:** + +在对一条记录做UPDATE操作时分为三种情况: + +1、如果未修改该记录的键值并且被更新的列占用的存储空间在**修改前后未发生变化**,则先在B+树中定位到这条记录的位置,然后再获取一下记录的X锁,最后在原记录的位置进行修改操作。其实我们也可以把这个定位待修改记录在B+树中位置的过程看成是一个**获取X锁的锁定读**。 + +2、如果未修改该记录的键值并且至少有一个被更新的列占用的存储空间在**修改前后发生变化**,则先在B+树中定位到这条记录的位置,然后获取一下记录的X锁,将该记录彻底删除掉(就是把记录彻底移入垃圾链表),最后再插入一条新记录。这个定位待修改记录在B+树中位置的过程看成是一个**获取X锁的锁定读**,新插入的记录由INSERT操作提供的**隐式锁进行保护**。 + +3、如果修改了该记录的键值,则相当于在原记录上做DELETE操作之后再来一次INSERT操作,加锁操作就需要按照DELETE和INSERT的规则进行了。 + +### 1.6.4锁的粒度 + +我们前边提到的锁都是针对记录的,也可以被称之为行级锁或者行锁,对一条记录加锁影响的也只是这条记录而已,我们就说这个锁的粒度比较细;其实一个事务也可以在表级别进行加锁,自然就被称之为表级锁或者表锁,对一个表加锁影响整个表中的记录,我们就说这个锁的粒度比较粗。给表加的锁也可以分为共享锁(S锁)和独占锁(X锁) + +#### 1.6.4.1.表锁与行锁的比较 + +**锁定粒度:表锁 > 行锁** + +**加锁效率:表锁 > 行锁** + +**冲突概率:表锁 > 行锁** + +**并发性能:表锁 < 行锁** + +#### 1.6.4.2.给表加S锁 + +**如果一个事务给表加了S锁,那么:** + +别的事务可以继续获得该表的S锁 + +别的事务可以继续获得该表中的某些记录的S锁 + +别的事务不可以继续获得该表的X锁 + +别的事务不可以继续获得该表中的某些记录的X锁 + +#### 1.6.4.3.给表加X锁 + +**如果一个事务给表加了X锁(意味着该事务要独占这个表),那么:** + +别的事务不可以继续获得该表的S锁 + +别的事务不可以继续获得该表中的某些记录的S锁 + +别的事务不可以继续获得该表的X锁 + +别的事务不可以继续获得该表中的某些记录的X锁。 + +为了更好的理解这个表级别的S锁和X锁和后面的意向锁,我们举一个现实生活中的例子。我们用曾经很火爆的互联网风口项目共享Office来说明加锁: + +共享Office有栋大楼,楼自然有很多层。办公室都是共享的,客户可以随便选办公室办公。每层楼可以容纳客户同时办公,每当一个客户进去办公,就相当于在每层的入口处挂了一把S锁,如果很多客户进去办公,相当于每层的入口处挂了很多把S锁(类似行级别的S锁)。 + +有的时候楼层会进行检修,比方说换地板,换天花板,检查水电啥的,这些维修项目并不能同时开展。如果楼层针对某个项目进行检修,就不允许客户来办公,也不允许其他维修项目进行,此时相当于楼层门口会挂一把X锁(类似行级别的X锁)。 + +上边提到的这两种锁都是针对楼层而言的,不过有时候我们会有一些特殊的需求: + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/021b5f9cea2e4bd4a06ec5cca86f440d.png) + +A、有投资人要来考察Office的环境。 + +投资人和公司并不想影响客户进去办公,但是此时不能有楼层进行检修,所以可以在大楼门口放置一把S锁(类似表级别的S锁)。此时: + +来办公的客户们看到大楼门口有S锁,可以继续进入大楼办公。 + +修理工看到大楼门口有S锁,则先在大楼门口等着,啥时候投资人走了,把大楼的S锁撤掉再进入大楼维修。 + +B、公司要和房东谈条件。 + +此时不允许大楼中有正在办公的楼层,也不允许对楼层进行维修。所以可以在大楼门口放置一把X锁(类似表级别的X锁)。此时: + +来办公的客户们看到大楼门口有X锁,则需要在大楼门口等着,啥时候条件谈好,把大楼的X锁撤掉再进入大楼办公。 + +修理工看到大楼门口有X锁,则先在大楼门口等着,啥时候谈判结束,把大楼的X锁撤掉再进入大楼维修。 + +### 1.6.5.意向锁 + +但是在上面的例子这里头有两个问题: + +如果我们想对大楼整体上S锁,首先需要确保大楼中的没有正在维修的楼层,如果有正在维修的楼层,需要等到维修结束才可以对大楼整体上S锁。 + +如果我们想对大楼整体上X锁,首先需要确保大楼中的没有办公的楼层以及正在维修的楼层,如果有办公的楼层或者正在维修的楼层,需要等到全部办公的同学都办公离开,以及维修工维修完楼层离开后才可以对大楼整体上X锁。 + +我们在对大楼整体上锁(表锁)时,怎么知道大楼中有没有楼层已经被上锁(行锁)了呢?依次检查每一楼层门口有没有上锁?那这效率也太慢了吧!于是InnoDB提出了一种意向锁(英文名:Intention Locks): + +**意向共享锁** ,英文名:Intention Shared Lock,简称IS锁。当事务准备在某条记录上加S锁时,需要先在表级别加一个IS锁。 + +**意向独占锁** ,英文名:Intention Exclusive Lock,简称IX锁。当事务准备在某条记录上加X锁时,需要先在表级别加一个IX锁。 + +视角回到大楼和楼层上来: + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/524d4de2c0b94fe9a5daa7618b5d5fe1.png) + +如果有客户到楼层中办公,那么他先在整栋大楼门口放一把IS锁(表级锁),然后再到楼层门口放一把S锁(行锁)。 + +如果有维修工到楼层中维修,那么它先在整栋大楼门口放一把IX锁(表级锁),然后再到楼层门口放一把X锁(行锁)。 + +之后: + +如果有投资人要参观大楼,也就是想在大楼门口前放S锁(表锁)时,首先要看一下大楼门口有没有IX锁,如果有,意味着有楼层在维修,需要等到维修结束把IX锁撤掉后才可以在整栋大楼上加S锁。 + +如果有谈条件要占用大楼,也就是想在大楼门口前放X锁(表锁)时,首先要看一下大楼门口有没有IS锁或IX锁,如果有,意味着有楼层在办公或者维修,需要等到客户们办完公以及维修结束把IS锁和IX锁撤掉后才可以在整栋大楼上加X锁。 + +注意: 客户在大楼门口加IS锁时,是不关心大楼门口是否有IX锁的,维修工在大楼门口加IX锁时,是不关心大楼门口是否有IS锁或者其他IX锁的。IS和IX锁只是为了判断当前时间大楼里有没有被占用的楼层用的,也就是在对大楼加S锁或者X锁时才会用到。 + +**总结一下**:IS、IX锁是表级锁,它们的提出仅仅为了在之后加表级别的S锁和X锁时可以快速判断表中的记录是否被上锁,以避免用遍历的方式来查看表中有没有上锁的记录。就是说其实IS锁和IX锁是兼容的,IX锁和IX锁是兼容的。我们画个表来看一下**表级别**的各种锁的兼容性: + +| 兼容性 | X | IX | S | IS | +| ------ | ------ | ------ | ------ | ------ | +| X | 不兼容 | 不兼容 | 不兼容 | 不兼容 | +| IX | 不兼容 | | 不兼容 | | +| S | 不兼容 | 不兼容 | | | +| IS | 不兼容 | | | | + +锁的组合性:(**意向锁没有行锁**) + +| 组合性 | X | IX | S | IS | +| ------ | -- | -- | -- | -- | +| 表锁 | 有 | 有 | 有 | 有 | +| 行锁 | 有 | | 有 | | + +### 1.6.6.MySQL中的行锁和表锁 + +MySQL支持多种存储引擎,不同存储引擎对锁的支持也是不一样的。当然,我们重点还是讨论InnoDB存储引擎中的锁,其他的存储引擎只是稍微看看。 + +#### 1.6.6.1.其他存储引擎中的锁 + +对于MyISAM、MEMORY、MERGE这些存储引擎来说,它们只支持表级锁,而且这些引擎并不支持事务,所以使用这些存储引擎的锁一般都是针对当前会话来说的。比方说在Session 1中对一个表执行SELECT操作,就相当于为这个表加了一个表级别的S锁,如果在SELECT操作未完成时,Session 2中对这个表执行UPDATE操作,相当于要获取表的X锁,此操作会被阻塞,直到Session 1中的SELECT操作完成,释放掉表级别的S锁后,Session 2中对这个表执行UPDATE操作才能继续获取X锁,然后执行具体的更新语句。 + +因为使用MyISAM、MEMORY、MERGE这些存储引擎的表在同一时刻只允许一个会话对表进行写操作,所以这些存储引擎实际上最好用在只读,或者大部分都是读操作,或者单用户的情景下。 +另外,在MyISAM存储引擎中有一个称之为Concurrent Inserts的特性,支持在对MyISAM表读取时同时插入记录,这样可以提升一些插入速度。关于更多Concurrent Inserts的细节,详情可以参考文档。 + +#### 1.6.6.2.InnoDB存储引擎中的锁 + +InnoDB存储引擎既支持表锁,也支持行锁。表锁实现简单,占用资源较少,不过粒度很粗,有时候你仅仅需要锁住几条记录,但使用表锁的话相当于为表中的所有记录都加锁,所以性能比较差。行锁粒度更细,可以实现更精准的并发控制。下边我们详细看一下。 + +##### 1.6.6.2.1.InnoDB中的表级锁 + +###### 1.6.6.2.1.1.表级别的S锁、X锁、元数据锁 + +在对某个表执行SELECT、INSERT、DELETE、UPDATE语句时,InnoDB存储引擎是不会为这个表添加表级别的S锁或者X锁的。 + +另外,在对某个表执行一些诸如ALTER TABLE、DROP TABLE这类的DDL语句时,其他事务对这个表并发执行诸如SELECT、INSERT、DELETE、UPDATE的语句会发生阻塞,同理,某个事务中对某个表执行SELECT、INSERT、DELETE、UPDATE语句时,在其他会话中对这个表执行DDL语句也会发生阻塞。这个过程其实是通过在server层使用一种称之为元数据锁(英文名:Metadata Locks,简称MDL)来实现的,一般情况下也不会使用InnoDB存储引擎自己提供的表级别的S锁和X锁。 + +其实这个InnoDB存储引擎提供的表级S锁或者X锁是相当鸡肋,只会在一些特殊情况下,比方说崩溃恢复过程中用到。不过我们还是可以手动获取一下的,比方说在系统变量autocommit=0,innodb_table_locks = 1时,手动获取InnoDB存储引擎提供的表t的S锁或者X锁可以这么写: + +LOCK TABLES t +READ:InnoDB存储引擎会对表t加表级别的S锁。 + +LOCK TABLES t +WRITE:InnoDB存储引擎会对表t加表级别的X锁。 + +**请尽量避免在使用InnoDB存储引擎的表上使用LOCK TABLES这样的手动锁表语句,它们并不会提供什么额外的保护,只是会降低并发能力而已。** + +###### 1.6.6.2.1.2.表级别的IS锁、IX锁 + +当我们在对使用InnoDB存储引擎的表的某些记录加S锁之前,那就需要先在表级别加一个IS锁,当我们在对使用InnoDB存储引擎的表的某些记录加X锁之前,那就需要先在表级别加一个IX锁。 + +IS锁和IX锁的使命只是为了后续在加表级别的S锁和X锁时判断表中是否有已经被加锁的记录,以避免用遍历的方式来查看表中有没有上锁的记录。我们并不能手动添加意向锁,只能由InnoDB存储引擎自行添加。 + +###### 1.6.6.2.1.3.表级别的AUTO-INC锁 + +在使用MySQL过程中,我们可以为表的某个列添加AUTO_INCREMENT属性,之后在插入记录时,可以不指定该列的值,系统会自动为它赋上递增的值系统实现这种自动给AUTO_INCREMENT修饰的列递增赋值的原理主要是两个: + +1、采用AUTO-INC锁,也就是在执行插入语句时就在表级别加一个AUTO-INC锁,然后为每条待插入记录的AUTO_INCREMENT修饰的列分配递增的值,在该语句执行结束后,再把AUTO-INC锁释放掉。这样一个事务在持有AUTO-INC锁的过程中,其他事务的插入语句都要被阻塞,可以保证一个语句中分配的递增值是连续的。 + +如果我们的插入语句在执行前不可以确定具体要插入多少条记录(无法预计即将插入记录的数量),比方说使用INSERT ... SELECT、REPLACE ... SELECT或者LOAD DATA这种插入语句,一般是使用AUTO-INC锁为AUTO_INCREMENT修饰的列生成对应的值。 + +2、采用一个轻量级的锁,在为插入语句生成AUTO_INCREMENT修饰的列的值时获取一下这个轻量级锁,然后生成本次插入语句需要用到的AUTO_INCREMENT列的值之后,就把该轻量级锁释放掉,并不需要等到整个插入语句执行完才释放锁。 + +如果我们的插入语句在执行前就可以确定具体要插入多少条记录,比方说我们上边举的关于表t的例子中,在语句执行前就可以确定要插入2条记录,那么一般采用轻量级锁的方式对AUTO_INCREMENT修饰的列进行赋值。这种方式可以避免锁定表,可以提升插入性能。 + +InnoDB提供了一个称之为innodb_autoinc_lock_mode的系统变量来控制到底使用上述两种方式中的哪种来为AUTO_INCREMENT修饰的列进行赋值,当innodb_autoinc_lock_mode值为0时,一律采用AUTO-INC锁;当innodb_autoinc_lock_mode值为2时,一律采用轻量级锁;当innodb_autoinc_lock_mode值为1时,两种方式混着来(也就是在插入记录数量确定时采用轻量级锁,不确定时使用AUTO-INC锁)。 + +**不过当innodb_autoinc_lock_mode值为2时,可能会造成不同事务中的插入语句为AUTO_INCREMENT修饰的列生成的值是交叉的,在有主从复制的场景中是不安全的。** + +``` +show variables like 'innodb_autoinc_lock_mode' ; +``` + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1653286288069/deb3ad3775c149f2943b98331cf9ed59.png) + +MySQL5.7.X中缺省为1。 + +#### 1.6.7.2.InnoDB中的行级锁 + +行锁,也称为记录锁,顾名思义就是在记录上加的锁。但是要注意,这个记录指的是通过给索引上的索引项加锁。InnoDB 这种行锁实现特点意味着:**只有通过索引条件检索数据,InnoDB 才使用行级锁,否则,InnoDB 将使用表锁。** + +不论是使用主键索引、唯一索引或普通索引,InnoDB都会使用行锁来对数据加锁。 + +只有执行计划真正使用了索引,才能使用行锁:**即便在条件中使用了索引字段,但是否使用索引来检索数据是由 MySQL 通过判断不同执行计划的代价来决定的,如果 MySQL认为全表扫描效率更高,比如对一些很小的表,它就不会使用索引,这种情况下 InnoDB** **将使用表锁,而不是行锁。** + +同时当我们用范围条件而不是相等条件检索数据,并请求锁时,InnoDB会给符合条件的已有数据记录的索引项加锁。 + +不过即使是行锁,InnoDB里也是分成了各种类型的。换句话说即使对同一条记录加行锁,如果类型不同,起到的功效也是不同的。我们使用前面的teacher,增加一个索引,并插入几条记录。 + +``` +INDEX `idx_number`(`number`) +``` + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1653286288069/d449e4f972f642c1a4b67c354e9f12b3.png) + +我们来看看都有哪些常用的行锁类型。 + +**Record Locks** + +也叫记录锁,就是仅仅把一条记录锁上,官方的类型名称为:LOCK_REC_NOT_GAP。比方说我们把number值为6的那条记录加一个记录锁的示意图如下: + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1653286288069/36ea705892b34f8ca4542ae51ec34680.png) + +记录锁是有S锁和X锁之分的,当一个事务获取了一条记录的S型记录锁后,其他事务也可以继续获取该记录的S型记录锁,但不可以继续获取X型记录锁;当一个事务获取了一条记录的X型记录锁后,其他事务既不可以继续获取该记录的S型记录锁,也不可以继续获取X型记录锁; + +**Gap Locks** + +我们说MySQL在REPEATABLE READ隔离级别下是可以解决幻读问题的,解决方案有两种,可以使用MVCC方案解决,也可以采用加锁方案解决。但是在使用加锁方案解决时有问题,就是事务在第一次执行读取操作时,那些幻影记录尚不存在,我们无法给这些幻影记录加上记录锁。InnoDB提出了一种称之为Gap Locks的锁,官方的类型名称为:LOCK_GAP,我们也可以简称为gap锁。 + +**间隙锁实质上是对索引前后的间隙上锁,不对索引本身上锁。** + +会话1开启一个事务,执行 + +``` +begin; +update teacher set domain ='JVM' where number='6'; +``` + +会对2~6之间和6到10之间进行上锁。 + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1653286288069/0428f43b0117482387536f42688dda55.png) + +如图中为2~6和 6 ~ 10的记录加了gap锁,意味着不允许别的事务在这条记录前后间隙插入新记录。 + +``` +begin; +insert into teacher value(7,'晁','docker'); +``` + +为什么不能插入?因为记录(7,'晁','docker')要 插入的话,在索引idx_number上,刚好落在6 ~ 10之间,是有锁的,当然不允许插入。 + +但是当SQL语句变为:insert +into teacher value(70,'晁','docker');能插入吗? + +当然能,因为70这条记录不在被锁的区间内。 + +### 1.6.7.死锁 + +#### 1.6.7.1.概念 + +是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁。 + +举个例子:A和B去按摩洗脚,都想在洗脚的时候,同时顺便做个头部按摩,13技师擅长足底按摩,14擅长头部按摩。 + +这个时候A先抢到14,B先抢到13,两个人都想同时洗脚和头部按摩,于是就互不相让,扬言我死也不让你,这样的话,A抢到14,想要13,B抢到13,想要14,在这个想同时洗脚和头部按摩的事情上A和B就产生了死锁。怎么解决这个问题呢? + +第一种,假如这个时候,来了个15,刚好也是擅长头部按摩的,A又没有两个脑袋,自然就归了B,于是B就美滋滋的洗脚和做头部按摩,剩下A在旁边气鼓鼓的,这个时候死锁这种情况就被打破了,不存在了。 + +第二种,C出场了,用武力强迫A和B,必须先做洗脚,再头部按摩,这种情况下,A和B谁先抢到13,谁就可以进行下去,另外一个没抢到的,就等着,这种情况下,也不会产生死锁。 + +所以总结一下: + +死锁是必然发生在多操作者(M>=2个)情况下,争夺多个资源(N>=2个,且N<=M)才会发生这种情况。很明显,单线程自然不会有死锁,只有B一个去,不要2个,打十个都没问题;单资源呢?只有13,A和B也只会产生激烈竞争,打得不可开交,谁抢到就是谁的,但不会产生死锁。同时,死锁还有几个要求,1、争夺资源的顺序不对,如果争夺资源的顺序是一样的,也不会产生死锁; + +2、争夺者拿到资源不放手。 + +#### 1.6.7.2.MySQL中的死锁 + +MySQL中的死锁的成因是一样的。 + +会话1: + +``` +begin; +select * from +teacher where number = 1 for update; +``` + +会话2: + +``` +begin; +select * from +teacher where number = 3 for update; +``` + +会话1 + +``` +select * from teacher where number = 3 for +update; +``` + +**可以看到这个语句的执行将会被阻塞** + +会话2 : + +``` +select * from +teacher where number = 1 for update; +``` + +MySQL检测到了死锁,并结束了会话2中事务的执行,此时,切回会话1,发现原本阻塞的SQL语句执行完成了。 + +同时通过 + +``` +show engine innodb status\G +``` + +可以看见死锁的详细情况: + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1653286288069/8c39265a4c3c4205a40a6c815a73ffbc.png) + +查看事务加锁的情况,不过一般情况下,看不到哪个事务对哪些记录加了那些锁,需要修改系统变量innodb_status_output_locks(MySQL5.6.16引入),缺省是OFF。 + +``` +show +variables like 'innodb_status_output_locks'; +``` + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1653286288069/691dfae0b8554417b5e4b85c18767dfd.png) + +我们需要设置为ON, + +``` +set global +innodb_status_output_locks = ON; +``` + +然后开启事务,并执行语句 diff --git a/高级知识/MySQL性能调优/MySQL性能优化.md b/高级知识/MySQL性能调优/MySQL性能优化.md new file mode 100644 index 0000000..a75cdf4 --- /dev/null +++ b/高级知识/MySQL性能调优/MySQL性能优化.md @@ -0,0 +1,1057 @@ +## 1.3.MySQL调优 + +### 1.3.1.MySQL调优金字塔 + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/098cb1ed7e96428eaea382ab6b69848d.png) + +很明显从图上可以看出,越往上走,难度越来越高,收益却是越来越小的。 + +对于**架构调优**,在系统设计时首先需要充分考虑业务的实际情况,是否可以把不适合数据库做的事情放到数据仓库、搜索引擎或者缓存中去做;然后考虑写的并发量有多大,是否需要采用分布式;最后考虑读的压力是否很大,是否需要读写分离。对于核心应用或者金融类的应用,需要额外考虑数据安全因素,数据是否不允许丢失。所以在进行优化时,首先需要关注和优化的应该是架构,如果架构不合理,即使是DBA能做的事情其实是也是比较有限的。 + +对于**MySQL调优**,需要确认业务表结构设计是否合理,SQL语句优化是否足够,该添加的索引是否都添加了,是否可以剔除多余的索引等等 + +比如**硬件和OS调优**,需要对硬件和OS有着非常深刻的了解,仅仅就磁盘一项来说,一般非DBA能想到的调整就是SSD盘比用机械硬盘更好。DBA级别考虑的至少包括了,使用什么样的磁盘阵列(RAID)级别、是否可以分散磁盘IO、是否使用裸设备存放数据,使用哪种文件系统(目前比较推荐的是XFS),操作系统的磁盘调度算法选择,是否需要调整操作系统文件管理方面比如atime属性等等。 + +所以本章我们重点关注MySQL方面的调优,特别是索引。SQL/索引调优要求对业务和数据流非常清楚。在阿里巴巴内部,有三分之二的DBA是业务DBA,从业务需求讨论到表结构审核、SQL语句审核、上线、索引更新、版本迭代升级,甚至哪些数据应该放到非关系型数据库中,哪些数据放到数据仓库、搜索引擎或者缓存中,都需要这些DBA跟踪和复审。他们甚至可以称为数据架构师(Data Architecher)。 + +### 1.3.2.查询性能优化 + +前面的章节我们知道如何设计最优的库表结构、如何建立最好的索引,这些对于高性能来说是必不可少的。但这些还不够—还需要合理的设计查询。如果查询写得很糟糕,即使库表结构再合理、索引再合适,也无法实现高性能。 + +#### 1.3.2.1.什么是慢查询 + +慢查询日志,顾名思义,就是查询花费大量时间的日志,是指mysql记录所有执行超过long_query_time参数设定的时间阈值的SQL语句的日志。该日志能为SQL语句的优化带来很好的帮助。默认情况下,慢查询日志是关闭的,要使用慢查询日志功能,首先要开启慢查询日志功能。如何开启,我们稍后再说。 + +##### **1.3.2.1.1慢查询基础-优化数据访问** + +查询性能低下最基本的原因是访问的数据太多。大部分性能低下的查询都可以通过减少访问的数据量的方式进行优化。对于低效的查询,一般通过下面两个步骤来分析总是很有效: + +1.确认应用程序是否在检索大量超过需要的数据。这通常意味着访问了太多的行,但有时候也可能是访问了太多的列。 + +2.确认MySQL服务器层是否在分析大量超过需要的数据行。 + +##### **1.3.2.1.2请求了不需要的数据?** + +有些查询会请求超过实际需要的数据,然后这些多余的数据会被应用程序丢弃。这会给MySQL服务器带来额外的负担,并增加网络开销,另外也会消耗应用服务器的CPU和内存资源。比如: + +**查询不需要的记录** + +一个常见的错误是常常会误以为MySQL会只返回需要的数据,实际上MySQL却是先返回全部结果集再进行计算。我们经常会看到一些了解其他数据库系统的人会设计出这类应用程序。 + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/c42215b7f5c442869027906f5aa80f4e.png) + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/8915ae332c7b4dbea0fa3c591e902197.png) + +以上SQL你认为MySQL会执行查询,并只返回他们需要的20条数据,然后停止查询。实际情况是MySQL会查询出全部的结果集,客户端的应用程序会接收全部的结果集数据,然后抛弃其中大部分数据。 + +**总是取出全部列** + +每次看到SELECT*的时候都需要用怀疑的眼光审视,是不是真的需要返回全部的列?很可能不是必需的。取出全部列,会让优化器无法完成索引覆盖扫描这类优化,还会为服务器带来额外的I/O、内存和CPU的消耗。因此,一些DBA是严格禁止SELECT *的写法的,这样做有时候还能避免某些列被修改带来的问题。 + +尤其是使用二级索引,使用*的方式会导致回表,导致性能低下。 + +什么时候可以使用SELECT*如果应用程序使用了某种缓存机制,或者有其他考虑,获取超过需要的数据也可能有其好处,但不要忘记这样做的代价是什么。获取并缓存所有的列的查询,相比多个独立的只获取部分列的查询可能就更有好处。 + +**重复查询相同的数据** + +不断地重复执行相同的查询,然后每次都返回完全相同的数据。比较好的方案是,当初次查询的时候将这个数据缓存起来,需要的时候从缓存中取出,这样性能显然会更好。 + +##### 1.3.2.1.3.是否在扫描额外的记录 + +在确定查询只返回需要的数据以后,接下来应该看看查询为了返回结果是否扫描了过多的数据。对于MySQL,最简单的衡量查询开销的三个指标如下: + +**响应时间、扫描的行数、返回的行数** + +没有哪个指标能够完美地衡量查询的开销,但它们大致反映了MySQL在内部执行查询时需要访问多少数据,并可以大概推算出查询运行的时间。这三个指标都会记录到MySQL的慢日志中,所以检查慢日志记录是找出扫描行数过多的查询的好办法。 + +**响应时间** + +响应时间是两个部分之和:服务时间和排队时间。 + +服务时间是指数据库处理这个查询真正花了多长时间。 + +排队时间是指服务器因为等待某些资源而没有真正执行查询的时间—-可能是等I/O操作完成,也可能是等待行锁,等等。 + +**扫描的行数和返回的行数** + +分析查询时,查看该查询扫描的行数是非常有帮助的。这在一定程度上能够说明该查询找到需要的数据的效率高不高。 + +理想情况下扫描的行数和返回的行数应该是相同的。但实际情况中这种“美事”并不多。例如在做一个关联查询时,服务器必须要扫描多行才能生成结果集中的一行。扫描的行数对返回的行数的比率通常很小,一般在1:1和10:1之间,不过有时候这个值也可能非常非常大。 + +**扫描的行数和访问类型** + +在评估查询开销的时候,需要考虑一下从表中找到某一行数据的成本。MySQL有好几种访问方式可以查找并返回一行结果。有些访问方式可能需要扫描很多行才能返回一行结果,也有些访问方式可能无须扫描就能返回结果。 + +在EXPLAIN语句中的type列反应了访问类型。访问类型有很多种,从全表扫描到索引扫描、范围扫描、唯一索引查询、常数引用等。这里列的这些,速度是从慢到快,扫描的行数也是从小到大。你不需要记住这些访问类型,但需要明白扫描表、扫描索引、范围访问和单值访问的概念。 + +如果查询没有办法找到合适的访问类型,那么解决的最好办法通常就是增加一个合适的索引,为什么索引对于查询优化如此重要了。索引让 MySQL以最高效、扫描行数最少的方式找到需要的记录。 + +一般 MySQL能够使用如下三种方式应用WHERE条件,从好到坏依次为: + +1、在索引中使用WHERE条件来过滤不匹配的记录。这是在存储引擎层完成的。 + +select .... from where a>100 and a <200 + +2、使用覆盖索引扫描来返回记录,直接从索引中过滤不需要的记录并返回命中的结果。这是在 MySQL服务器层完成的,但无须再回表查询记录。 + +3、从数据表中返回数据(存在回表),然后过滤不满足条件的记录。这在 MySQL服务器层完成,MySQL需要先从数据表读出记录然后过滤。 + +好的索引可以让查询使用合适的访问类型,尽可能地只扫描需要的数据行。 + +**如果发现查询需要扫描大量的数据但只返回少数的行,那么通常可以尝试下面的技巧去优化它:** + +1、使用索引覆盖扫描,把所有需要用的列都放到索引中,这样存储引擎无须回表获取对应行就可以返回结果了 + +2、改变库表结构。例如使用单独的汇总表。 + +3、重写这个复杂的查询,让MySQL优化器能够以更优化的方式执行这个查询。 + +### 1.3.3.慢查询 + +#### 1.3.3.1慢查询配置 + +我们已经知道慢查询日志可以帮助定位可能存在问题的SQL语句,从而进行SQL语句层面的优化。但是默认值为关闭的,需要我们手动开启。 + +``` +show VARIABLES like 'slow_query_log'; +``` + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/f72a83180181494fa68d4f014375127b.png) + +``` +set GLOBAL slow_query_log=1; +``` + +开启1,关闭0 + +但是多慢算慢?MySQL中可以设定一个阈值,将运行时间超过该值的所有SQL语句都记录到慢查询日志中。long_query_time参数就是这个阈值。默认值为10,代表10秒。 + +``` +show VARIABLES like '%long_query_time%'; +``` + +当然也可以设置 + +``` +set global long_query_time=0; +``` + +默认10秒,这里为了演示方便设置为0 + +同时对于运行的SQL语句没有使用索引,则MySQL数据库也可以将这条SQL语句记录到慢查询日志文件,控制参数是: + +``` +show VARIABLES like '%log_queries_not_using_indexes%'; +``` + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/19a8d15f0d724fae93946b5bd4f63cc3.png) + +开启1,关闭0(默认) + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/67a74308e0524b459cb30cfa66bdba40.png) + +``` +show VARIABLES like '%slow_query_log_file%'; +``` + +##### 小结 + +l slow_query_log 启动停止慢查询日志 + +l slow_query_log_file 指定慢查询日志得存储路径及文件(默认和数据文件放一起) + +l long_query_time 指定记录慢查询日志SQL执行时间得伐值(单位:秒,默认10秒) + +l log_queries_not_using_indexes 是否记录未使用索引的SQL + +l log_output 日志存放的地方可以是[TABLE][FILE][FILE,TABLE] + +### 1.3.4.Explain执行计划 + +#### 1.3.4.1.什么是执行计划 + +有了慢查询语句后,就要对语句进行分析。一条查询语句在经过MySQL查询优化器的各种基于成本和规则的优化会后生成一个所谓的执行计划,这个执行计划展示了接下来具体执行查询的方式,比如多表连接的顺序是什么,对于每个表采用什么访问方法来具体执行查询等等。EXPLAIN语句来帮助我们查看某个查询语句的具体执行计划,我们需要搞懂EPLATNEXPLAIN的各个输出项都是干嘛使的,从而可以有针对性的提升我们查询语句的性能。 + +通过使用EXPLAIN关键字可以模拟优化器执行SQL查询语句,从而知道MySQL是如何处理你的SQL语句的。分析查询语句或是表结构的性能瓶颈,总的来说通过EXPLAIN我们可以: + +l 表的读取顺序 + +l 数据读取操作的操作类型 + +l 哪些索引可以使用 + +l 哪些索引被实际使用 + +l 表之间的引用 + +l 每张表有多少行被优化器查询 + +#### 1.3.4.2.执行计划的语法 + +执行计划的语法其实非常简单: 在SQL查询的前面加上EXPLAIN关键字就行。比如:EXPLAIN select * from table1 + +重点的就是EXPLAIN后面你要分析的SQL语句 + +除了以SELECT开头的查询语句,其余的DELETE、INSERT、REPLACE以及UPOATE语句前边都可以加上EXPLAIN,用来查看这些语句的执行计划,不过我们这里对SELECT语句更感兴趣,所以后边只会以SELECT语句为例来描述EsxPLAIN语句的用法。 + +#### 1.3.4.3.执行计划详解 + +为了让大家先有一个感性的认识,我们把EXPLAIN语句输出的各个列的作用先大致罗列一下: + +**explain +select * from order_exp;** + +**id** **: ****在一个大的查询语句中每个SELECT****关键字都对应一个唯一的id** + +**select_type** **: SELECT****关键字对应的那个查询的类型** + +**table** **:表名** + +**partitions** **:匹配的分区信息** + +**type** **:针对单表的访问方法** + +**possible_keys** **:可能用到的索引** + +**key** **:实际上使用的索引** + +**key_len** **:实际使用到的索引长度** + +**ref** **:当使用索引列等值查询时,与索引列进行等值匹配的对象信息** + +**rows** **:预估的需要读取的记录条数** + +**filtered** **:某个表经过搜索条件过滤后剩余记录条数的百分比** + +**Extra** **:—些额外的信息** + +##### id + +我们知道我们写的查询语句一般都以SELECT关键字开头,比较简单的查询语句里只有一个SELECT关键字, + +稍微复杂一点的连接查询中也只有一个SELECT关键字,比如: + +```sql +SELECT *FROM s1 +INNER J0IN s2 ON s1.id = s2.id +WHERE s1.order_status = 0 ; +``` + +但是下边两种情况下在一条查询语句中会出现多个SELECT关键字: + +1、查询中包含子查询的情况 + +比如下边这个查询语句中就包含2个SELECT关键字: + +``` +SELECT* FROM s1 WHERE id IN ( SELECT * FROM s2); +``` + +2、查询中包含UNION语句的情况 + +比如下边这个查询语句中也包含2个SELECT关键字: + +``` +SELECT * FROM s1 +UNION SELECT * FROM s2 ; +``` + +查询语句中每出现一个SELECT关键字,MySQL就会为它分配一个唯一的id值。这个id值就是EXPLAIN语句的第一个列。 + +###### 单SELECT关键字 + +比如下边这个查询中只有一个SELECT关键字,所以EXPLAIN的结果中也就只有一条id列为1的记录∶ + +``` +EXPLAIN SELECT * FROM s1 WHERE order_no = 'a'; +``` + +###### 连接查询 + +对于连接查询来说,一个SELEOT关键字后边的FROM子句中可以跟随多个表,所以在连接查询的执行计划中,每个表都会对应一条记录,但是这些记录的id值都是相同的,比如: + +``` +EXPLAIN SELECT * FROM s1 WHERE order_no = 'a'; +``` + +可以看到,上述连接查询中参与连接的s1和s2表分别对应一条记录,但是这两条记录对应的id值都是1。这里需要大家记住的是,在连接查询的执行计划中,每个表都会对应一条记录,这些记录的id列的值是相同的。 + +###### 包含子查询 + +对于包含子查询的查询语句来说,就可能涉及多个SELECT关键字,所以在包含子查询的查询语句的执行计划中,每个SELECT关键字都会对应一个唯一的id值,比如这样: + +``` +EXPLAIN +SELECT * FROM s1 WHERE id IN (SELECT id FROM s2) OR order_no = 'a'; +``` + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/93136f4766424e9e91a0b065908ad1c8.png) + +但是这里大家需要特别注意,查询优化器可能对涉及子查询的查询语句进行重写,从而转换为连接查询。所以如果我们想知道查询优化器对某个包含子查询的语句是否进行了重写,直接查看执行计划就好了,比如说: + +``` +EXPLAIN +SELECT * FROM s1 WHERE id IN (SELECT id FROM s2 WHERE order_no = 'a'); +``` + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/6475b28cd68144b89566254b0891a210.png) + +可以看到,虽然我们的查询语句是一个子查询,但是执行计划中s1和s2表对应的记录的id值全部是1,这就表明了查询优化器将子查询转换为了连接查询, + +###### 包含UNION子句 + +对于包含UNION子句的查询语句来说,每个SELECT关键字对应一个id值也是没错的,不过还是有点儿特别的东西,比方说下边这个查询: + +``` +EXPLAIN +SELECT * FROM s1 UNION SELECT * FROM s2; +``` + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/4907ca3c4cea47a0add9684f8081acc3.png) + +这个语句的执行计划的第三条记录为什么这样?UNION +子句会把多个查询的结果集合并起来并对结果集中的记录进行去重,怎么去重呢? MySQL使用的是内部的临时表。正如上边的查询计划中所示,UNION 子句是为了把id为1的查询和id为2的查询的结果集合并起来并去重,所以在内部创建了一个名为<union1,2>的临时表(就是执行计划第三条记录的table列的名称),id为NULL表明这个临时表是为了合并两个查询的结果集而创建的。 + +跟UNION 对比起来,UNION +ALL就不需要为最终的结果集进行去重,它只是单纯的把多个查询的结果集中的记录合并成一个并返回给用户,所以也就不需要使用临时表。所以在包含UNION ALL子句的查询的执行计划中,就没有那个id为NULL的记录,如下所示: + +``` +EXPLAIN +SELECT * FROM s1 UNION ALL SELECT * FROM s2; +``` + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/f58b6838d5c34edc8580e1309a96cd16.png) + +##### table + +不论我们的查询语句有多复杂,里边包含了多少个表,到最后也是需要对每个表进行单表访问的,MySQL规定EXPLAIN语句输出的每条记录都对应着某个单表的访问方法,该条记录的table列代表着该表的表名。 + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/ad26adcd3f0b45d39ebc12b51de8c74b.png) + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/e41c8c8a619c4781b0fd7293c556ed54.png) + +可以看见,只涉及对s1表的单表查询,所以EXPLAIN输出中只有一条记录,其中的table列的值是s1,而连接查询的执行计划中有两条记录,这两条记录的table列分别是s1和s2. + +##### partitions + +和分区表有关,一般情况下我们的查询语句的执行计划的partitions列的值都是NULL。 + +##### type + +我们前边说过执行计划的一条记录就代表着MySQL对某个表的执行查询时的访问方法/访问类型,其中的type列就表明了这个访问方法/访问类型是个什么东西,是较为重要的一个指标,结果值从最好到最坏依次是: + +出现比较多的是system>const>eq_ref>ref>range>index>ALL + +一般来说,得保证查询至少达到range级别,最好能达到ref。 + +###### system + +当表中只有一条记录并且该表使用的存储引擎的统计数据是精确的,比如MyISAM、Memory,那么对该表的访问方法就是system。 + +``` +explain select * from test_myisam; +``` + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/0cd60414ae624c2aad6b7870d8702116.png) + +当然,如果改成使用InnoDB存储引擎,试试看执行计划的type列的值是什么。 + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/5f326bb8471841da9b8b750877c497b8.png) + +###### const + +就是当我们根据主键或者唯一二级索引列与常数进行等值匹配时,对单表的访问方法就是const。因为只匹配一行数据,所以很快。 + +例如将主键置于where列表中 + +``` +EXPLAIN +SELECT * FROM s1 WHERE id = 716; +``` + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/854f80afc83e4d53b368a29634dea02a.png) + +B+树叶子节点中的记录是按照索引列排序的,对于的聚簇索引来说,它对应的B+树叶子节点中的记录就是按照id列排序的。B+树矮胖,所以这样根据主键值定位一条记录的速度很快。类似的,我们根据唯一二级索引列来定位一条记录的速度也很快的,比如下边这个查询: + +``` +SELECT * FROM +order_exp WHERE insert_time=’’ and order_status=’’ and expire_time=’’ ; +``` + +这个查询的执行分两步,第一步先从u_idx_day_status对应的B+树索引中根据索引列与常数的等值比较条件定位到一条二级索引记录,然后再根据该记录的id值到聚簇索引中获取到完整的用户记录。 + +MySQL把这种通过主键或者唯一二级索引列来定位一条记录的访问方法定义为:const,意思是常数级别的,代价是可以忽略不计的。 + +不过这种const访问方法只能在主键列或者唯一二级索引列和一个常数进行等值比较时才有效,如果主键或者唯一二级索引是由多个列构成的话,组成索引的每一个列都是与常数进行等值比较时,这个const访问方法才有效。 + +对于唯一二级索引来说,查询该列为NULL值的情况比较特殊,因为唯一二级索引列并不限制 NULL 值的数量,所以上述语句可能访问到多条记录,也就是说is null不可以使用const访问方法来执行。 + +###### eq_ref + +在连接查询时,如果被驱动表是通过主键或者唯一二级索引列等值匹配的方式进行访问的〈如果该主键或者唯一二级索引是联合索引的话,所有的索引列都必须进行等值比较),则对该被驱动表的访问方法就是eq_ref。 + +(***驱动表与被驱动表:*** *A**表和B**表join**连接查询,如果通过A**表的结果集作为循环基础数据,然后一条一条地通过该结果集中的数据作为过滤条件到B**表中查询数据,然后合并结果。那么我们称A**表为驱动表,B**表为被驱动表*) + +比方说: + +``` +EXPLAIN +SELECT * FROM s1 INNER JOIN s2 ON s1.id = s2.id; +``` + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/a33c7b8ce8e449d79866fc374baa7739.png) + +从执行计划的结果中可以看出,MySQL打算将s2作为驱动表,s1作为被驱动表,重点关注s1的访问方法是eq_ref,表明在访问s1表的时候可以通过主键的等值匹配来进行访问。 + +###### ref + +当通过普通的二级索引列与常量进行等值匹配时来查询某个表,那么对该表的访问方法就可能是ref。 + +本质上也是一种索引访问,它返回所有匹配某个单独值的行,然而,它可能会找到多个符合条件的行,所以他属于查找和扫描的混合体 + +``` +EXPLAIN +SELECT * FROM s1 WHERE order_no = 'a'; +``` + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/adbe165526944750b5087866b270c858.png) + +对于这个查询,我们当然可以选择全表扫描来逐一对比搜索条件是否满足要求,我们也可以先使用二级索引找到对应记录的id值,然后再回表到聚簇索引中查找完整的用户记录。 + +由于普通二级索引并不限制索引列值的唯一性,所以可能找到多条对应的记录,也就是说使用二级索引来执行查询的代价取决于等值匹配到的二级索引记录条数。如果匹配的记录较少,则回表的代价还是比较低的,所以MySQL可能选择使用索引而不是全表扫描的方式来执行查询。这种搜索条件为二级索引列与常数等值比较,采用二级索引来执行查询的访问方法称为:ref。 + +对于普通的二级索引来说,通过索引列进行等值比较后可能匹配到多条连续的记录,而不是像主键或者唯一二级索引那样最多只能匹配1条记录,所以这种ref访问方法比const要差些,但是在二级索引等值比较时匹配的记录数较少时的效率还是很高的(如果匹配的二级索引记录太多那么回表的成本就太大了)。 + +###### range + +如果使用索引获取某些范围区间的记录,那么就可能使用到range访问方法,一般就是在你的where语句中出现了between、<、>、in等的查询。 + +这种范围扫描索引扫描比全表扫描要好,因为它只需要开始于索引的某一点,而结束语另一点,不用扫描全部索引。 + +``` +EXPLAIN +SELECT * FROM s1 WHERE order_no IN ('a', 'b', 'c'); + +EXPLAIN +SELECT * FROM s1 WHERE order_no > 'a' AND order_no < 'b'; +``` + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/e04fb43a98264bf5850510fdbe1db8d5.png) + +这种利用索引进行范围匹配的访问方法称之为:range。 + +此处所说的使用索引进行范围匹配中的 `索引` 可以是聚簇索引,也可以是二级索引。 + +###### index + +当我们可以使用索引覆盖,但需要扫描全部的索引记录时,该表的访问方法就是index。 + +``` +EXPLAIN +SELECT insert_time FROM s1 WHERE expire_time = '2021-03-22 18:36:47'; +``` + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/a79786e8fe9147eb97696eaa28f1fada.png) + +###### all + +最熟悉的全表扫描,将遍历全表以找到匹配的行 + +``` +EXPLAIN +SELECT * FROM s1; +``` + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/315c3953f3a64e249435e19a5ce2089f.png) + +##### possible_keys与key + +在EXPLAIN 语句输出的执行计划中,possible_keys列表示在某个查询语句中,对某个表执行单表查询时可能用到的索引有哪些,key列表示实际用到的索引有哪些,如果为NULL,则没有使用索引。比方说下边这个查询:。 + +``` +EXPLAIN SELECT order_note FROM s1 WHERE +insert_time = '2021-03-22 18:36:47'; +``` + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/d2f72f3156cd44369ed275e5991dcbb2.png) + +上述执行计划的possible keys列的值表示该查询可能使用到u_idx_day_status,idx_insert_time两个索引,然后key列的值是u_idx_day_status,表示经过查询优化器计算使用不同索引的成本后,最后决定使用u_idx_day_status来执行查询比较划算。 + +##### key_len + +key_len列表示当优化器决定使用某个索引执行查询时,该索引记录的最大长度,计算方式是这样的: + +对于使用固定长度类型的索引列来说,它实际占用的存储空间的最大长度就是该固定值,对于指定字符集的变长类型的索引列来说,比如某个索引列的类型是VARCHAR(100),使用的字符集是utf8,那么该列实际占用的最大存储空间就是100 x 3 = 300个字节。 + +如果该索引列可以存储NULL值,则key_len比不可以存储NULL值时多1个字节。 + +对于变长字段来说,都会有2个字节的空间来存储该变长列的实际长度。 + +⽐如下边这个查询: + +``` +EXPLAIN +SELECT * FROM s1 WHERE id = 718; +``` + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/f898e5b7cfa54e3e9c63cd3b11a3920e.png) + +由于id列的类型是bigint,并且不可以存储NULL值,所以在使用该列的索引时key_len大小就是8。 + +对于可变长度的索引列来说,比如下边这个查询: + +``` +EXPLAIN +SELECT * FROM s1 WHERE order_no = 'a'; +``` + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/74a8a80a304545c69e8fbaf4325908de.png) + +由于order_no列的类型是VARCHAR(50),所以该列实际最多占用的存储空间就是50*3字节,又因为该列是可变长度列,所以key_len需要加2,所以最后ken_len的值就是152。 + +MySQL在执行计划中输出key_len列主要是为了让我们区分某个使用联合索引的查询具体用了几个索引列(复合索引有最左前缀的特性,如果复合索引能全部使用上,则是复合索引字段的索引长度之和,这也可以用来判定复合索引是否部分使用,还是全部使用),而不是为了准确的说明针对某个具体存储引擎存储变长字段的实际长度占用的空间到底是占用1个字节还是2个字节。 + +##### rows + +如果查询优化器决定使用全表扫描的方式对某个表执行查询时,执行计划的rows列就代表预计需要扫描的行数,如果使用索引来执行查询时,执行计划的rows列就代表预计扫描的索引记录行数。比如下边两个个查询: + +``` +EXPLAIN +SELECT * FROM s1 WHERE order_no > 'z'; + +EXPLAIN +SELECT * FROM s1 WHERE order_no > 'a'; +``` + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/b3527b40c9494d17b4e608b3d8a21db6.png) + +我们看到执行计划的rows列的值是分别是1和10573,这意味着查询优化器在经过分析使用idx_order_no进行查询的成本之后,觉得满足order_no> ' a '这个条件的记录只有1条,觉得满足order_no> ' a '这个条件的记录有10573条。 + +##### filtered + +查询优化器预测有多少条记录满⾜其余的搜索条件,什么意思呢?看具体的语句: + +``` +EXPLAIN SELECT * +FROM s1 WHERE id > 5890 AND order_note = 'a'; +``` + +从执行计划的key列中可以看出来,该查询使用 PRIMARY索引来执行查询,从rows列可以看出满足id > 5890的记录有5286条。执行计划的filtered列就代表查询优化器预测在这5286条记录中,有多少条记录满足其余的搜索条件,也就是order_note = 'a'这个条件的百分比。此处filtered列的值是10.0,说明查询优化器预测在5286条记录中有10.00%的记录满足order_note = 'a'这个条件。 + +对于单表查询来说,这个filtered列的值没什么意义,我们更关注在连接查询中驱动表对应的执行计划记录的filtered值,比方说下边这个查询: + +``` +EXPLAIN SELECT * FROM s1 INNER JOIN s2 ON s1.order_no = s2.order_no WHERE s1.order_note > '你好,李焕英'; +``` + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/6ba004e4db004b03b5632d9aa1a327c9.png) + +从执行计划中可以看出来,查询优化器打算把s +1当作驱动表,s2当作被驱动表。我们可以看到驱动表s1表的执行计划的rows列为10573,filtered列为33.33 ,这意味着驱动表s1的扇出值就是10573 x 33.33 % = 3524.3,这说明还要对被驱动表执行大约3524次查询。 + +##### Extra + +顾名思义,Extra列是用来说明一些额外信息的,我们可以通过这些额外信息来更准确的理解MySQL到底将如何执行给定的查询语句 + +### 1.3.5.查询优化器 + +一条SQL语句在MySQL执行的过程如下: + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/b0dc955876354f09938142e60bc1f4ce.png) + +1.如果是查询语句(select语句),首先会查询缓存是否已有相应结果,有则返回结果,无则进行下一步(如果不是查询语句,同样调到下一步) + +2.解析查询,创建一个内部数据结构(解析树),这个解析树主要用来SQL语句的语义与语法解析; + +3.优化:优化SQL语句,例如重写查询,决定表的读取顺序,以及选择需要的索引等。这一阶段用户是可以查询的,查询服务器优化器是如何进行优化的,便于用户重构查询和修改相关配置,达到最优化。这一阶段还涉及到存储引擎,优化器会询问存储引擎,比如某个操作的开销信息、是否对特定索引有查询优化等。 + +### 1.3.6.高性能的索引使用策略 + +#### 1.3.6.1.不在索引列上做任何操作 + +我们通常会看到一些查询不当地使用索引,或者使得MySQL无法使用已有的索引。如果查询中的列不是独立的,则 MySQL就不会使用索引。“独立的列”是指索引列不能是表达式的一部分,也不能是函数的参数。 + +例如,我们假设id上有主键索引,但是下面这个查询无法使用主键索引: + +``` +EXPLAIN SELECT * FROM order_exp WHERE id + 1 = 17; +``` + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/4654e2c912994f258e99f87a7fbf722c.png) + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/8b61405caded4ebb9cf0aa43e97db2ab.png) + +凭肉眼很容易看出 WHERE中的表达式其实等价于id= 16,但是MySQL无法自动解析这个方程式。这完全是用户行为。我们应该养成简化WHERE条件的习惯,始终将索引列单独放在比较符号的一侧。 + +下面是另一个常见的错误: + +在索引列上使用函数,也是无法利用索引的。 + +``` +EXPLAIN SELECT * from order_exp WHERE YEAR(insert_time)=YEAR(DATE_SUB(NOW(),INTERVAL 1 YEAR)); +``` + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/40f54f165b8b45138d0adf947eb013b4.png) + +``` +EXPLAIN SELECT * from order_exp WHERE insert_time BETWEEN str_to_date('01/01/2021', '%m/%d/%Y') and str_to_date('12/31/2021', '%m/%d/%Y'); +``` + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/d8e9925bf3b445a3812aeca5ab3caa9c.png) + +#### 1.3.6.2.尽量全值匹配 + +建立了联合索引列后,如果我们的搜索条件中的列和索引列一致的话,这种情况就称为全值匹配,比方说下边这个查找语句: + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/2ef49edd0615433c8d878da02793f414.png) + +``` + EXPLAIN select * from order_exp where insert_time='2021-03-22 18:34:55' and order_status=0 and expire_time='2021-03-22 18:35:14'; +``` + +我们建立的u_idx_day_statusr索引包含的3个列在这个查询语句中都展现出来了,联合索引中的三个列都可能被用到。 + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/d8ba5a3726cc4fc59bdd677cbfe314b7.png) + +有的同学也许有个疑问,WHERE子句中的几个搜索条件的顺序对查询结果有啥影响么?也就是说如果我们调换 `insert_time`, `order_status`, `expire_time`这几个搜索列的顺序对查询的执行过程有影响么?比方说写成下边这样: + +``` +EXPLAIN select * from order_exp where order_status=0 and insert_time='2021-03-22 18:34:55' and expire_time='2021-03-22 18:35:14'; +``` + +放心,MySQL没这么蠢,查询优化器会分析这些搜索条件并且按照可以使用的索引中列的顺序来决定先使用哪个搜索条件,后使用哪个搜索条件。 + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/5341da5a2f8c4d548496e938571446fe.png) + +所以,当建立了联合索引列后,能在where条件中使用索引的尽量使用。 + +#### 1.3.6.3.最佳左前缀法则 + +建立了联合索引列,如果搜索条件不够全值匹配怎么办?在我们的搜索语句中也可以不用包含全部联合索引中的列,但要遵守最左前缀法则。指的是查询从索引的最左前列开始并且不跳过索引中的列。 + +搜索条件中必须出现左边的列才可以使用到这个B+树索引 + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/6470ee4f75b54fe9a3895a04a285445e.png) + +``` +EXPLAIN select * from order_exp where insert_time='2021-03-22 18:23:42' and order_status=1; +``` + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/d6c0f39567d44f488b33673d7426fae1.png) + +``` +EXPLAIN select * from order_exp where insert_time='2021-03-22 18:23:42' ; +``` + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/2cb52dc41f2d467485a6540a03fdad13.png) + +搜索条件中没有出现左边的列不可以使用到这个B+树索引 + +``` +EXPLAIN SELECT * FROM order_exp WHERE order_status=1; +``` + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/2be581275fda43d9bde9e919ca08edea.png) + +``` +EXPLAIN Select * from s1 where order_status=1 and expire_time='2021-03-22 18:35:14'; +``` + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/6904ca07e50942b9a89624601b7e76d5.png) + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/8c5afbb6141042729c9cb6b62eae9be3.png) + +那为什么搜索条件中必须出现左边的列才可以使用到这个B+树索引呢?比如下边的语句就用不到这个B+树索引么? + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/6d37e1eeeac044c8935919a484dc12f4.png) + +因为B+树的数据页和记录先是按照insert_time列的值排序的,在insert_time列的值相同的情况下才使用order_status列进行排序,也就是说insert_time列的值不同的记录中order_status的值可能是无序的。而现在你跳过insert_time列直接根据order_status的值去查找,怎么可能呢?expire_time也是一样的道理,那如果我就想在只使用expire_time的值去通过B+树索引进行查找咋办呢?这好办,你再对expire_time列建一个B+树索引就行了。 + +但是需要特别注意的一点是,如果我们想使用联合索引中尽可能多的列,搜索条件中的各个列必须是联合索引中从最左边连续的列。比方说联合索引u_idx_day_status中列的定义顺序是 `insert_time`, `order_status`, `expire_time`,如果我们的搜索条件中只有insert_time和expire_time,而没有中间的order_status, + +``` +EXPLAIN select * from order_exp where insert_time='2021-03-22 18:23:42' and expire_time='2021-03-22 18:35:14'; +``` + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/71c66aeee5624538b15c1ac65bfb441c.png) + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/b0c30125c76547549274176558ba4eac.png) + +请注意key_len,只有5,说明只有insert_time用到了,其他的没有用到。 + +#### 1.3.6.4.范围条件放最后 + +这一点,也是针对联合索引来说的,前面我们反复强调过,所有记录都是按照索引列的值从小到大的顺序排好序的,而联合索引则是按创建索引时的顺序进行分组排序。 + +比如: + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/c6343d4b9e404154a5d9542ab0991961.png) + +``` +EXPLAIN select * from order_exp_cut where insert_time>'2021-03-22 18:23:42' and insert_time<'2021-03-22 18:35:00'; +``` + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/288265c4c0ef489eb6fd23f908389ba4.png) + +由于B+树中的数据页和记录是先按insert_time列排序的,所以我们上边的查询过程其实是这样的: + +找到insert_time值为'2021-03-22 18:23:42' 的记录。 + +找到insert_timee值为'2021-03-22 18:35:00'的记录。 + +由于所有记录都是由链表连起来的,所以他们之间的记录都可以很容易的取出来,找到这些记录的主键值,再到聚簇索引中回表查找完整的记录。 + +但是如果对多个列同时进行范围查找的话,只有对索引最左边的那个列进行范围查找的时候才能用到B+树索引: + +``` +select * from order_exp_cut where insert_time>'2021-03-22 18:23:42' and insert_time<'2021-03-22 18:35:00' and order_status > -1; +``` + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/b4ef55a0bbb043aa9fb6ea7aea4014d1.png) + +上边这个查询可以分成两个部分: + +通过条件insert_time>'2021-03-22 18:23:42' and insert_time<'2021-03-22 18:35:00' 来对insert_time进行范围,查找的结果可能有多条insert_time值不同的记录, + +对这些insert_time值不同的记录继续通过order_status>-1条件继续过滤。 + +这样子对于联合索引u_idx_day_status来说,只能用到insert_time列的部分,而用不到order_status列的部分(这里的key_len和之前的SQL的是一样长),因为只有insert_time值相同的情况下才能用order_status列的值进行排序,而这个查询中通过insert_time进行范围查找的记录中可能并不是按照order_status列进行排序的,所以在搜索条件中继续以order_status列进行查找时是用不到这个B+树索引的。 + +**所以对于一个联合索引来说,虽然对多个列都进行范围查找时只能用到最左边那个索引列,但是如果左边的列是精确查找,则右边的列可以进行范围查找:** + +``` +EXPLAIN select * from order_exp_cut +where insert_time='2021-03-22 18:34:55' and order_status=0 and expire_time>'2021-03-22 +18:23:57' and expire_time<'2021-03-22 18:35:00' ; +``` + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/591940f39f1b4b38aa5b32e55202bd60.png) + +**而中间有范围查询会导致后面的列全部失效,无法充分利用这个联合索引:** + +``` +EXPLAIN select * from order_exp_cut +where insert_time='2021-03-22 18:23:42' and order_status>-1 and expire_time='2021-03-22 +18:35:14'; +``` + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/5874bb991dc1459cabd34bfa0bc7299a.png) + +#### 1.3.6.5.覆盖索引尽量用 + +覆盖索引是非常有用的工具,能够极大地提高性能,三星索引里最重要的那颗星就是宽索引星。考虑一下如果查询只需要扫描索引而无须回表,会带来多少好处: + +索引条目通常远小于数据行大小,所以如果只需要读取索引,那 MySQL就会极大地减少数据访问量。这对缓存的负载非常重要,因为这种情况下响应时间大部分花费在数据拷贝上。覆盖索引对于I/O密集型的应用也有帮助,因为索引比数据更小,更容易全部放入内存中。 + +因为索引是按照列值顺序存储的,所以对于I/O密集型的范围查询会比随机从磁盘读取每一行数据的I/O要少得多。 + +由于InnoDB的聚簇索引,覆盖索引对InnoDB表特别有用。InnoDB的二级索引在叶子节点中保存了行的主键值,所以如果二级主键能够覆盖查询,则可以避免对主键索引的二次查询。 + +尽量使用覆盖索引(只访问索引的查询(索引列和查询列一致)),不是必要的情况下减少select*,除非是需要将表中的全部列检索后,进行缓存。 + +``` +EXPLAIN select * from +order_exp_cut where insert_time='2021-03-22 18:34:55' and order_status=0 and +expire_time='2021-03-22 18:35:04' ; +``` + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/9dde17c64e9648cd8c41d8ed8e3b2b65.png) + +使用具体名称取代* + +``` +EXPLAIN select expire_time,id from +order_exp_cut where insert_time='2021-03-22 18:34:55' and order_status=0 and +expire_time='2021-03-22 18:35:04' ; +``` + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/c50ef9c9280f4d8e94cf27cca596f9c1.png) + +**解释一下Extra中的Using index** + +当我们的查询列表以及搜索条件中只包含属于某个索引的列,也就是在可以**使用索引覆盖的情况**下,在Extra列将会提示该额外信息。以上的查询中只需要用到u_idx_day_status而不需要回表操作: + +#### 1.3.6.6.不等于要慎用 + +mysql 在使用不等于(!= 或者<>)的时候无法使用索引会导致全表扫描 + +``` +EXPLAIN SELECT * FROM order_exp WHERE order_no <> 'DD00_6S'; +``` + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/d75a6e6d69f547caa3b8661b0a369973.png) + +**解释一下Extra中的Using where** +当我们使用全表扫描来执行对某个表的查询,并且该语句的WHERE子句中有针对该表的搜索条件时,在Extra列中会提示上述额外信息。 + +#### 1.3.6.7.Null/Not 有影响 + +需要注意null/not null对索引的可能影响 + +**表order_exp的order_no为索引列,同时不允许为null,** + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/8ed4193b0c934d5dbbbc34f8dd581836.png) + +``` +explain SELECT * FROM order_exp WHERE order_no is null; +explain SELECT * FROM order_exp WHERE order_no is not null; +``` + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/aa527c85faf945e6a81faaa7a0025409.png) + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/ec817b8cf6e5488bb6e9382e503828f0.png) + +可以看见,order_no is null的情况下,MySQL直接表示Impossible WHERE(查询语句的WHERE子句永远为FALSE时将会提示该额外信息),对于 is not null直接走的全表扫描。 + +**表order_exp_cut的order_no为索引列,同时允许为null,** + +``` +explain SELECT * FROM order_exp_cut WHERE order_no is null; +explain SELECT * FROM order_exp_cut WHERE order_no is not null; +``` + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/227c7a3ab927457d84739f1c3583ccdd.png) + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/d954036891374b18a6ef0671d793cd86.png) + +is null会走ref类型的索引访问,is not null;依然是全表扫描。所以总结起来: + +is not null容易导致索引失效,is null则会区分被检索的列是否为null,如果是null则会走ref类型的索引访问,如果不为null,也是全表扫描。 + +**但是当联合索引上使用时覆盖索引时,情况会有一些不同(order_exp_cut表的order_no可为空):** + +``` +explain SELECT order_status,expire_time FROM order_exp WHERE insert_time is null; +explain SELECT order_status,expire_time FROM order_exp WHERE insert_time is not null; +``` + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/14d36c91cf0840a2b2be921b057e6f7a.png) + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/42ebdd9dd8774f0d939440cde50512e1.png) + +``` +explain SELECT order_status,expire_time FROM order_exp_cut WHERE insert_time is null; +explain SELECT order_status,expire_time FROM order_exp_cut WHERE insert_time is not null; +``` + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/d2fc5cb60d1c455e92b38dd4f1c87ebe.png) + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/50565fb21ad145d89d0329c126aca8cf.png) + +根据system>const>eq_ref>ref>range>index>ALL 的原则,看起来在联合索引中,is not null的表现会更好(如果列可为null的话),但是key_len的长度增加了1。所以总的来说,在设计表时列尽可能的不要声明为null。 + +#### 1.3.6.8.Like查询要当心 + +like以通配符开头('%abc...'),mysql索引失效会变成全表扫描的操作 + +``` +explain SELECT * FROM order_exp WHERE order_no like '%_6S'; +``` + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/ac911fc11114479caa8ae55ac317bacf.png) + +此时如果使用覆盖索引可以改善这个问题 + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/0dd075168f3e48d69eca71710c72ed9e.png) + +``` +explain SELECT order_status,expire_time FROM order_exp_cut WHERE insert_time like '%18:35:09'; +``` + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/e494db4f81c846db89e74e6d23cdc646.png) + +#### 1.3.6.9.字符类型加引号 + +字符串不加单引号索引失效 + +``` +explain SELECT * FROM order_exp WHERE order_no = 6; +explain SELECT * FROM order_exp WHERE order_no = '6'; +``` + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/511255e7474c4502b15419058913d0cc.png) + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/97f9ff147b5a4954aaf006f00b60660d.png) + +MySQL的查询优化器,会自动的进行类型转换,比如上个语句里会尝试将order_no转换为数字后和6进行比较,自然造成索引失效。 + +#### 1.3.6.10.使用or关键字时要注意 + +``` +explain SELECT * FROM order_exp WHERE order_no = 'DD00_6S' OR order_no = 'DD00_9S'; +explain SELECT * FROM order_exp WHERE expire_time= '2021-03-22 18:35:09' OR order_note = 'abc'; +``` + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/55f8e2400c4845e5b337c1a122ea4737.png) + +表现是不一样的,第一个SQL的or是相同列,相当于产生两个扫描区间,可以使用上索引。 + +第二个SQL中or是不同列,并且order_note不是索引。所以只能全表扫描 + +当然如果两个条件都是索引列,情况会有变化: + +``` +explain SELECT * FROM order_exp WHERE expire_time= '2021-03-22 18:35:09' OR order_no = 'DD00_6S'; +``` + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/deabc3433e2440c5be499b296789d427.png) + +这也给了我们提示,如果我们将 SQL改成union all + +``` +explain SELECT * FROM order_exp WHERE expire_time= '2021-03-22 18:35:09' + union all SELECT * FROM order_exp WHERE order_note = 'abc'; +``` + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/643d0629a531459594e318cc0ea6547b.png) + +当然使用覆盖扫描也可以改善这个问题: + +``` +explain SELECT order_status,id FROM order_exp_cut WHERE insert_time='2021-03-22 18:34:55' or expire_time='2021-03-22 18:28:28'; +``` + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/aa9bc2fae8c54d4b86a3a38720cc42c2.png) + +#### 1.3.6.11.使用索引扫描来做排序和分组 + +MySQL有两种方式可以生成有序的结果﹔通过排序操作﹔或者按索引顺序扫描施﹔如果EXPLAIN出来的type列的值为“index”,则说明MySQL使用了索引扫描来做排序。 + +扫描索引本身是很快的,因为只需要从一条索引记录移动到紧接着的下一条记录。但如果索引不能覆盖查询所需的全部列,那就不得不每扫描一条索引记录就都回表查询一次对应的行。这基本上都是随机I/O,因此按索引顺序读取数据的速度通常要比顺序地全表扫描慢,尤其是在IO密集型的工作负载时。 + +MySQL可以使用同一个索引既满足排序,又用于查找行。因此,如果可能,设计索引时应该尽可能地同时满足这两种任务,这样是最好的。 + +只有当索引的列顺序和ORDER BY子句的顺序完全一致,并且所有列的排序方向(倒序或正序)都一样时,MySQL才能够使用索引来对结果做排序。如果查询需要关联多张表,则只有当0RDER BY子句引用的字段全部为第一个表时,才能使用索引做排序。 + +#### 1.3.6.12.排序要当心 + +**ASC、DESC别混用** + +对于使用联合索引进行排序的场景,我们要求各个排序列的排序顺序是一致的,也就是要么各个列都是ASC规则排序,要么都是DESC规则排序。 + +**排序列包含非同一个索引的列** + +用来排序的多个列不是一个索引里的,这种情况也不能使用索引进行排序 + +``` +explain +SELECT * FROM order_exp order by +order_no,insert_time; +``` + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/52bc3ba85bc1433cbed4510cba638556.png) + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/645c2fa335824da292d3dcfcb32969e8.png) + +#### 1.3.6.13.尽可能按主键顺序插入行 + +最好避免随机的(不连续且值的分布范围非常大)聚簇索引,特别是对于I/O密集型的应用。例如,从性能的角度考虑,使用UUID来作为聚簇索引则会很糟糕,它使得聚簇索引的插入变得完全随机,这是最坏的情况,使得数据没有任何聚集特性。 + +最简单的方法是使用AUTO_INCREMENT自增列。这样可以保证数据行是按顺序写入,对于根据主键做关联操作的性能也会更好。 + +注意到向UUID主键插入行不仅花费的时间更长,而且索引占用的空间也更大。这一方面是由于主键字段更长﹔另一方面毫无疑问是由于页分裂和碎片导致的。 + +因为主键的值是顺序的,所以InnoDB把每一条记录都存储在上一条记录的后面。当达到页的最大填充因子时(InnoDB默认的最大填充因子是页大小的15/16,留出部分空间用于以后修改),下一条记录就会写入新的页中。一旦数据按照这种顺序的方式加载,主键页就会近似于被顺序的记录填满,这也正是所期望的结果。 + +如果新行的主键值不一定比之前插入的大,所以InnoDB无法简单地总是把新行插入到索引的最后,而是需要为新的行寻找合适的位置-—通常是已有数据的中间位置——并且分配空间。这会增加很多的额外工作,并导致数据分布不够优化。下面是总结的一些缺点: + +写入的目标页可能已经刷到磁盘上并从缓存中移除,或者是还没有被加载到缓存中,InnoDB在插入之前不得不先找到并从磁盘读取目标页到内存中。这将导致大量的随机IO。 + +因为写入是乱序的,InnoDB不得不频繁地做页分裂操作,以便为新的行分配空间。页分裂会导致移动大量数据,一次插入最少需要修改三个页而不是一个页。 + +所以使用InnoDB时应该尽可能地按主键顺序插入数据,并且尽可能地使用单调增加的聚簇键的值来插入新行。 + +#### 1.3.6.14.优化Count查询 + +首先要注意,COUNT()是一个特殊的函数,有两种非常不同的作用:它可以统计某个列值的数量,也可以统计行数。 + +在统计列值时要求列值是非空的(不统计NULL)。 + +COUNT()的另一个作用是统计结果集的行数。常用的就是就是当我们使用COUNT(*)。实际上,它会忽略所有的列而直接统计所有的行数。 + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/3264f862df374302bef83c9459ad0a7a.png) + +``` +select count(*) from test; +select count(c1) from test; +``` + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/989e64fc120f45288787f480e1746dc1.png)![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/52fccea24657442db67c3d7b102de0fa.png) + +通常来说,COUNT()都需要扫描大量的行(意味着要访问大量数据)才能获得精确的结果,因此是很难优化的。在MySQL层面能做的基本只有索引覆盖扫描了。如果这还不够,就需要考虑修改应用的架构,可以用估算值取代精确值,可以增加汇总表,或者增加类似Redis这样的外部缓存系统。 + +#### 1.3.6.15.优化limit分页 + +在系统中需要进行分页操作的时候,我们通常会使用LIMIT加上偏移量的办法实现,同时加上合适的ORDER BY子句。 + +一个非常常见又令人头疼的问题就是,在偏移量非常大的时候,例如可能是 + +``` +select * from order_exp limit 10000,10; +``` + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/358846c9b2db4bf79de4e2f200c5e46f.png) + +这样的查询,这时MySQL需要查询10010条记录然后只返回最后10条,前面10 000条记录都将被抛弃,这样的代价非常高。 + +优化此类分页查询的一个最简单的办法是 + +会先查询翻页中需要的N条数据的主键值,然后根据主键值回表查询所需要的N条数据,在此过程中查询N条数据的主键id在索引中完成,所以效率会高一些。 + +``` +EXPLAIN SELECT * FROM (select id from order_exp limit 10000,10) b,order_exp + a where a.id = b.id; +``` + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/b24ab67fa56e4da1b7fa151e0098c004.png) + +从执行计划中可以看出,首先执行子查询中的order_exp表,根据主键做索引全表扫描,然后与a表通过id做主键关联查询,相比传统写法中的全表扫描效率会高一些。 + +从两种写法上能看出性能有一定的差距,虽然并不明显,但是随着数据量的增大,两者执行的效率便会体现出来。 + +上面的写法虽然可以达到一定程度的优化,但还是存在性能问题。最佳的方式是在业务上进行配合修改为以下语句: + +``` +EXPLAIN select * from order_exp where id > 67 order by id limit 10; +``` + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/6efb18da3d19465e8538cb4e042d9e86.png) + +采用这种写法,需要前端通过点击More来获得更多数据,而不是纯粹的翻页,因此,每次查询只需要使用上次查询出的数据中的id来获取接下来的数据即可,但这种写法需要业务配合。 + +#### 1.3.6.16.关于Null的特别说明 + +对于Null到底算什么,存在着分歧: + +1、有的认为NULL值代表一个未确定的值,MySQL认为任何和NULL值做比较的表达式的值都为NULL,包括select +null=null和select null!=null; + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/11ebee5ff9db49b6bef2e80d2b2e46bd.png)![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/3e152ca5c31048a5afca4a7611bc5798.png) + +所以每一个NULL值都是独一无二的。 + +2、有的认为其实NULL值在业务上就是代表没有,所有的NULL值和起来算一份; + +3、有的认为这NULL完全没有意义,所以在统计数量时压根儿不能把它们算进来。 + +假设一个表中某个列c1的记录为(2,1000,null,null),在第一种情况下,表中c1的记录数为4,第二种表中c1的记录数为3,第三种表中c1的记录数为2。 + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/09c50dcca1ab4f33b2247edb054905ae.png) + +在对统计索引列不重复值的数量时如何对待NULL值,MySQL专门提供了一个innodb_stats_method的系统变量, + +https://dev.mysql.com/doc/refman/5.7/en/innodb-parameters.html#sysvar_innodb_stats_method + +**这个系统变量有三个候选值:** + +nulls_equal:认为所有NULL值都是相等的。这个值也是innodb_stats_method的默认值。 + +如果某个索引列中NULL值特别多的话,这种统计方式会让优化器认为某个列中平均一个值重复次数特别多,所以倾向于不使用索引进行访问。 + +nulls_unequal:认为所有NULL值都是不相等的。 + +如果某个索引列中NULL值特别多的话,这种统计方式会让优化器认为某个列中平均一个值重复次数特别少,所以倾向于使用索引进行访问。 + +nulls_ignored:直接把NULL值忽略掉。 + +而且有迹象表明,在MySQL5.7.22以后的版本,对这个innodb_stats_method的修改不起作用,MySQL把这个值在代码里写死为nulls_equal。也就是说MySQL在进行索引列的数据统计行为又把null视为第二种情况(NULL值在业务上就是代表没有,所有的NULL值和起来算一份),看起来,MySQL中对Null值的处理也很分裂。所以总的来说,对于列的声明尽可能的不要允许为null。 diff --git a/高级知识/MySQL性能调优/Mysql基础调优.md b/高级知识/MySQL性能调优/Mysql基础调优.md new file mode 100644 index 0000000..1799f63 --- /dev/null +++ b/高级知识/MySQL性能调优/Mysql基础调优.md @@ -0,0 +1,60 @@ +### Mysql查询优化 +>- 慢查询:减少不需要的数据、不取出全部列、减少相同数据的查询 +> 原因:数据量大 select * (禁止) +>- 使用覆盖索引 +>- 改变数据库表结构(汇总表) +>- 重写复杂查询SQL +> +> mysql记录全部超过 long query time的sql语句的日志 + +### Mysql中的索引:聚族索引、辅助索引,定义索引生产B+树 +>- InnoDB引擎,B+树,哈希索引,全文索引 +>- 聚集索引、聚族索引:将表的主键构造B+树,存放在叶子节点 +>- 如果没有主键,mysql创建rowid(看不见) +>- 索引需要定义辅助索引、id索引、主键索引等等 +>- 辅助索引会存放主键、但是不包含全部数据 +>- 主键索引会包含全部数据 +>- 联合索引:多个列组合起来索引,减少B+数的数量 +>- ![img_5.png](img_5.png) + +#### 优化:减少索引数量、联合索引 +>-回表:通过辅助索引找到主键、再通过主键再找到其他数据(尽量减少) +>- 查询优化器:对比:二级索引+回表,全表扫描主键 + +### 索引(Index) 帮助Mysql高效获取数据的数据结构 +>- 哈希索引,用HashMap实现索引。缺点:范围查找、排序、组合索引、hash冲突 +>- B+树=二叉查找(左右子树的值小于根)-平衡(高度差不超过1)-B树:多叉平衡查找树 +>- B+:所有数据放在叶子结点,(非叶子结点保存索引,不保存数据)(链表指向相邻结点),Mysql +>- ![img_2.png](img_2.png) +>- ![img_3.png](img_3.png) +>- B树非叶子结点也存放数据、不用链表相连叶子结点,查询效率低、范围小 +>- B* 非叶子结点也存放指针Oracle用 +>- ![img_4.png](img_4.png) + +### 平衡二叉树调整 +>- 平衡因子:左右高度差,通过旋转:改变高度差(左旋、右旋) +>- ![img.png](img.png) +>- ![img_1.png](img_1.png) + +### 慢查询优化 +>- 重写复杂的sql查询 +>- 把指向sql按时间排序 + +### 事务和事务隔离级别 +>- 事务:整体的执行单位、事务的特性 +>- 事务并发引起的问题:脏读问题(回滚)、不可重复读(修改)、幻读(增加) +>- READ UNCOMMITTED :未提交读 READ COMMITTED:提交读 REPETABLE READ :可重复读 +>- SERIALIZABLE :可串行化 +>- 修改:SET [GLOBAL|SESSION] TRANSACTION ISOLATION LEVEL level; + +### 事务的操作 +>- 开启,提交,回滚 +>- 保存点:高级回滚,SAVEPOINT 保存点名称;ROLLBACK TO [SAVEPOINT] 保存点名称; +>- 隐式提交:(不同的SQL语言类型)可以不用commit提交 +>- 开启下一个事务,上一个事务提交 + +### mysql中的锁 +>- 锁定读:for update(X锁、独占锁) in share mode +>- 锁释放时间 + + diff --git a/高级知识/MySQL性能调优/img.png b/高级知识/MySQL性能调优/img.png new file mode 100644 index 0000000..4a224f0 Binary files /dev/null and b/高级知识/MySQL性能调优/img.png differ diff --git a/高级知识/MySQL性能调优/img_1.png b/高级知识/MySQL性能调优/img_1.png new file mode 100644 index 0000000..51abe7a Binary files /dev/null and b/高级知识/MySQL性能调优/img_1.png differ diff --git a/高级知识/MySQL性能调优/img_2.png b/高级知识/MySQL性能调优/img_2.png new file mode 100644 index 0000000..32a5052 Binary files /dev/null and b/高级知识/MySQL性能调优/img_2.png differ diff --git a/高级知识/MySQL性能调优/img_3.png b/高级知识/MySQL性能调优/img_3.png new file mode 100644 index 0000000..b912ceb Binary files /dev/null and b/高级知识/MySQL性能调优/img_3.png differ diff --git a/高级知识/MySQL性能调优/img_4.png b/高级知识/MySQL性能调优/img_4.png new file mode 100644 index 0000000..00cee6d Binary files /dev/null and b/高级知识/MySQL性能调优/img_4.png differ diff --git a/高级知识/MySQL性能调优/img_5.png b/高级知识/MySQL性能调优/img_5.png new file mode 100644 index 0000000..e85178f Binary files /dev/null and b/高级知识/MySQL性能调优/img_5.png differ diff --git a/高级知识/MySQL性能调优/事务和事务的隔离级别.md b/高级知识/MySQL性能调优/事务和事务的隔离级别.md new file mode 100644 index 0000000..11a3ef5 --- /dev/null +++ b/高级知识/MySQL性能调优/事务和事务的隔离级别.md @@ -0,0 +1,675 @@ +## 1.4.事务和事务的隔离级别 + +### 1.4.1.为什么需要事务 + +事务是数据库管理系统(DBMS)执行过程中的一个逻辑单位(不可再进行分割),由一个有限的数据库操作序列构成(多个DML语句,select语句不包含事务),要不全部成功,要不全部不成功。 + +A 给B 要划钱,A 的账户-1000元, B 的账户就要+1000元,这两个update 语句必须作为一个整体来执行,不然A 扣钱了,B 没有加钱这种情况就是错误的。那么事务就可以保证A 、B 账户的变动要么全部一起发生,要么全部一起不发生。 + +### 1.4.2.事务特性 + +事务应该具有4个属性:原子性、一致性、隔离性、持久性。这四个属性通常称为ACID特性。 + +**l 原子性(atomicity)** + +**l 一致性(consistency)** + +**l 隔离性(isolation)** + +**l 持久性(durability)** + +#### 1.4.2.1.原子性(atomicity) + +一个事务必须被视为一个不可分割的最小单元,整个事务中的所有操作要么全部提交成功,要么全部失败,对于一个事务来说,不能只执行其中的一部分操作。比如: + +连老师借给李老师1000元: + +1.连老师工资卡扣除1000元 + +2.李老师工资卡增加1000元 + +整个事务的操作要么全部成功,要么全部失败,不能出现连老师工资卡扣除,但是李老师工资卡不增加的情况。如果原子性不能保证,就会很自然的出现一致性问题。 + +#### 1.4.2.2.一致性(consistency) + +一致性是指事务将数据库从一种一致性转换到另外一种一致性状态,在事务开始之前和事务结束之后数据库中数据的完整性没有被破坏。 + +连老师借给李老师1000元: + +1.连老师工资卡扣除1000元 + +2.李老师工资卡增加1000元 + +扣除的钱(-500) 与增加的钱(500) 相加应该为0,或者说连老师和李老师的账户的钱加起来,前后应该不变。 + +#### 1.4.2.3.持久性(durability) + +一旦事务提交,则其所做的修改就会永久保存到数据库中。此时即使系统崩溃,已经提交的修改数据也不会丢失。 + +#### 1.4.2.4.隔离性(isolation) + +一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。 + +如果隔离性不能保证,会导致什么问题? + +连老师借给李老师生活费,借了两次,每次都是1000,连老师的卡里开始有10000,李老师的卡里开始有500,从理论上,借完后,连老师的卡里有8000,李老师的卡里应该有2500。 + +我们将连老师向李老师同时进行的两次转账操作分别称为T1和T2,在现实世界中T1和T2是应该没有关系的,可以先执行完T1,再执行T2,或者先执行完T2,再执行T1,结果都是一样的。但是很不幸,真实的数据库中T1和T2的操作可能交替执行的,执行顺序就有可能是: + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/db81c54724b54e749ec5dd801f29e91b.png) + +如果按照上图中的执行顺序来进行两次转账的话,最终我们看到,连老师的账户里还剩9000元钱,相当于只扣了1000元钱,但是李老师的账户里却成了2500元钱,多了10000元,这银行岂不是要亏死了? + +所以对于现实世界中状态转换对应的某些数据库操作来说,不仅要保证这些操作以原子性的方式执行完成,而且要保证其它的状态转换不会影响到本次状态转换,这个规则被称之为隔离性。 + +### 1.4.3.事务并发引发的问题 + +我们知道MySQL是一个客户端/服务器架构的软件,对于同一个服务器来说,可以有若干个客户端与之连接,每个客户端与服务器连接上之后,就可以称之为一个会话(Session)。每个客户端都可以在自己的会话中向服务器发出请求语句,一个请求语句可能是某个事务的一部分,也就是对于服务器来说可能同时处理多个事务。 + +在上面我们说过事务有一个称之为隔离性的特性,理论上在某个事务对某个数据进行访问时,其他事务应该进行排队,当该事务提交之后,其他事务才可以继续访问这个数据,这样的话并发事务的执行就变成了串行化执行。 + +但是对串行化执行性能影响太大,我们既想保持事务的一定的隔离性,又想让服务器在处理访问同一数据的多个事务时性能尽量高些,当我们舍弃隔离性的时候,可能会带来什么样的数据问题呢? + +#### 1.4.3.1.脏读 + +当一个事务读取到了另外一个事务修改但未提交的数据,被称为脏读。 + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/05e67865973a410e93190c52a74be697.png) + +1、在事务A执⾏过程中,事务A对数据资源进⾏了修改,事务B读取了事务A修改后的数据。 +2、由于某些原因,事务A并没有完成提交,发⽣了RollBack操作,则事务B读取的数据就是脏数据。 +这种读取到另⼀个事务未提交的数据的现象就是脏读(Dirty Read)。 + +#### 1.4.3.2.不可重复读 + +当事务内相同的记录被检索两次,且两次得到的结果不同时,此现象称为不可重复读。 + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/3895ad68683747e88b114bba40f48ad9.png) + +事务B读取了两次数据资源,在这两次读取的过程中事务A修改了数据,导致事务B在这两次读取出来的 +数据不⼀致。 + +#### 1.4.3.3.幻读 + +在事务执行过程中,另一个事务将新记录添加到正在读取的事务中时,会发生幻读。 + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/3a92feb9aa014a7eaaf784d1c7057822.png) + +事务B前后两次读取同⼀个范围的数据,在事务B两次读取的过程中事务A新增了数据,导致事务B后⼀ +次读取到前⼀次查询没有看到的⾏。 +幻读和不可重复读有些类似,但是幻读重点强调了读取到了之前读取没有获取到的记录。 + +### 1.1.4.SQL标准中的四种隔离级别 + +我们上边介绍了几种并发事务执行过程中可能遇到的一些问题,这些问题也有轻重缓急之分,我们给这些问题按照严重性来排一下序: + +脏读 > 不可重复读 > 幻读 + +我们上边所说的舍弃一部分隔离性来换取一部分性能在这里就体现在:设立一些隔离级别,隔离级别越低,越严重的问题就越可能发生。有一帮人(并不是设计MySQL的大叔们)制定了一个所谓的SQL标准,在标准中设立了4个隔离级别: + +**READ UNCOMMITTED:未提交读。** + +**READ COMMITTED:已提交读。** + +**REPEATABLE READ:可重复读。** + +**SERIALIZABLE:可串行化。** + +SQL标准中规定,针对不同的隔离级别,并发事务可以发生不同严重程度的问题,具体情况如下: + +也就是说: + +**READ UNCOMMITTED隔离级别下,可能发生脏读、不可重复读和幻读问题。** + +**READ COMMITTED隔离级别下,可能发生不可重复读和幻读问题,但是不可以发生脏读问题。** + +**REPEATABLE READ隔离级别下,可能发生幻读问题,但是不可以发生脏读和不可重复读的问题。** + +**SERIALIZABLE隔离级别下,各种问题都不可以发生。** + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/21048532674b4486a21b90258b046a15.png) + +### 1.4.5.MySQL中的隔离级别 + +不同的数据库厂商对SQL标准中规定的四种隔离级别支持不一样,比方说Oracle就只支持READ COMMITTED和SERIALIZABLE隔离级别。本书中所讨论的MySQL虽然支持4种隔离级别,但与SQL标准中所规定的各级隔离级别允许发生的问题却有些出入,MySQL在REPEATABLE READ隔离级别下,是可以禁止幻读问题的发生的。 + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/1452d922eb0f4dda803126784436e8b2.png) + +MySQL的默认隔离级别为REPEATABLE READ,我们可以手动修改事务的隔离级别。 + +#### 1.4.5.1.如何设置事务的隔离级别 + +我们可以通过下边的语句修改事务的隔离级别: + +SET [GLOBAL|SESSION] TRANSACTION ISOLATION LEVEL level; + +其中的level可选值有4个: + +``` +level: { + REPEATABLE READ + | READ COMMITTED + | READ UNCOMMITTED + | SERIALIZABLE +} +``` + +设置事务的隔离级别的语句中,在SET关键字后可以放置GLOBAL关键字、SESSION关键字或者什么都不放,这样会对不同范围的事务产生不同的影响,具体如下: + +**使用GLOBAL关键字(在全局范围影响):** + +比方说这样: + +``` +SET GLOBAL TRANSACTION ISOLATION LEVEL SERIALIZABLE; +``` + +则: 只对执行完该语句之后产生的会话起作用。当前已经存在的会话无效。 + +**使用SESSION关键字(在会话范围影响):** + +比方说这样: + +``` +SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE; +``` + +则:对当前会话的所有后续的事务有效 + +该语句可以在已经开启的事务中间执行,但不会影响当前正在执行的事务。 + +如果在事务之间执行,则对后续的事务有效。 + +**上述两个关键字都不用(只对执行语句后的下一个事务产生影响):** + +比方说这样: + +``` +SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; +``` + +则:只对当前会话中下一个即将开启的事务有效。下一个事务执行完后,后续事务将恢复到之前的隔离级别。该语句不能在已经开启的事务中间执行,会报错的。 + +如果我们在服务器启动时想改变事务的默认隔离级别,可以修改启动参数transaction-isolation的值,比方说我们在启动服务器时指定了--transaction-isolation=SERIALIZABLE,那么事务的默认隔离级别就从原来的REPEATABLE READ变成了SERIALIZABLE。 + +想要查看当前会话默认的隔离级别可以通过查看系统变量transaction_isolation的值来确定: + +``` +SHOW VARIABLES LIKE 'transaction_isolation'; +``` + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/a863d53bff964d74a21b52de56bc97df.png) + +或者使用更简便的写法: + +``` +SELECT @@transaction_isolation; +``` + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/390b1f7f9b7947ee97716cdc08d3848a.png) + +注意:transaction_isolation是在MySQL 5.7.20的版本中引入来替换tx_isolation的,如果你使用的是之前版本的MySQL,请将上述用到系统变量transaction_isolation的地方替换为tx_isolation。 + +### 1.4.6.MySQL事务 + +#### 1.4.6.1.事务基本语法 + +**事务开始** + +1、begin + +2、START TRANSACTION(推荐) + +3、begin work + +**事务回滚** + +rollback + +**事务提交** + +commit + +使用事务插入两行数据,commit后数据还在 + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/874c5a5d37c44347827edc01c7646d58.png) + +使用事务插入两行数据,rollback后数据没有了 + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/80e27d57435b4034a20c3ea4566c7674.png) + +#### 1.4.6.2.保存点 + +如果你开启了一个事务,执行了很多语句,忽然发现某条语句有点问题,你只好使用ROLLBACK语句来让数据库状态恢复到事务执行之前的样子,然后一切从头再来,但是可能根据业务和数据的变化,不需要全部回滚。所以MySQL里提出了一个保存点(英文:savepoint)的概念,就是在事务对应的数据库语句中打几个点,我们在调用ROLLBACK语句时可以指定会滚到哪个点,而不是回到最初的原点。定义保存点的语法如下: + +SAVEPOINT 保存点名称; + +当我们想回滚到某个保存点时,可以使用下边这个语句(下边语句中的单词WORK和SAVEPOINT是可有可无的): + +``` +ROLLBACK TO [SAVEPOINT] 保存点名称; +``` + +不过如果ROLLBACK语句后边不跟随保存点名称的话,会直接回滚到事务执行之前的状态。 + +如果我们想删除某个保存点,可以使用这个语句: + +``` +RELEASE SAVEPOINT 保存点名称; +``` + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/3dd600ff2c384d5f8e348c8bc33a6c76.png) + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/2e56bef3f9954c3f943168d546ae662f.png) + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/dbca0f38164045eda2543112de99d69b.png) + +#### 1.4.6.3.隐式提交 + +当我们使用START TRANSACTION或者BEGIN语句开启了一个事务,或者把系统变量autocommit的值设置为OFF时,事务就不会进行自动提交,但是如果我们输入了某些语句之后就会悄悄的提交掉,就像我们输入了COMMIT语句了一样,这种因为某些特殊的语句而导致事务提交的情况称为隐式提交,这些会导致事务隐式提交的语句包括: + +##### 1.4.6.3.1.执行DDL + +定义或修改数据库对象的数据定义语言(Datadefinition language,缩写为:DDL)。 + +所谓的数据库对象,指的就是数据库、表、视图、存储过程等等这些东西。当我们使用CREATE、ALTER、DROP等语句去修改这些所谓的数据库对象时,就会隐式的提交前边语句所属于的事务,就像这样: + +``` +BEGIN; + +SELECT ... # 事务中的一条语句 + +UPDATE ... # 事务中的一条语句 + +... # 事务中的其它语句 + +CREATE TABLE ... +``` + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/34acaa6366574b638f74d96d65f76274.png) + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/9a70501b6a0645f389deae88494426fa.png) + +**此语句会隐式的提交前边语句所属于的事务** + +##### 1.4.6.3.2.隐式使用或修改mysql数据库中的表 + +当我们使用ALTER USER、CREATE USER、DROP USER、GRANT、RENAME USER、REVOKE、SET PASSWORD等语句时也会隐式的提交前边语句所属于的事务。 + +##### 1.4.6.3.3.事务控制或关于锁定的语句 + +当我们在一个会话里,一个事务还没提交或者回滚时就又使用START TRANSACTION或者BEGIN语句开启了另一个事务时,会隐式的提交上一个事务,比如这样: + +``` +BEGIN; + +SELECT ... # 事务中的一条语句 + +UPDATE ... # 事务中的一条语句 + +... # 事务中的其它语句 + +BEGIN; # 此语句会隐式的提交前边语句所属于的事务 +``` + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/6aaf9a1f630c4210996782d1c6fa6a97.png) + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/93a9ef0063704f31b47838b257fb6cf0.png) + +或者当前的autocommit系统变量的值为OFF,我们手动把它调为ON时,也会隐式的提交前边语句所属的事务。 + +或者使用LOCK TABLES、UNLOCK TABLES等关于锁定的语句也会隐式的提交前边语句所属的事务。 + +##### 1.4.6.3.4.加载数据的语句 + +比如我们使用LOAD DATA语句来批量往数据库中导入数据时,也会隐式的提交前边语句所属的事务。 + +##### 1.4.6.3.5.关于MySQL复制的一些语句 + +使用START SLAVE、STOP SLAVE、RESET SLAVE、CHANGE MASTER TO等语句时也会隐式的提交前边语句所属的事务。 + +##### 1.4.6.3.6.其它的一些语句 + +使用ANALYZE TABLE、CACHE INDEX、CHECK TABLE、FLUSH、 LOAD INDEX INTO CACHE、OPTIMIZE TABLE、REPAIR TABLE、RESET等语句也会隐式的提交前边语句所属的事务。 + +## 1.5. MVCC + +全称Multi-Version Concurrency Control,即多版本并发控制,主要是为了提高数据库的并发性能。 +同一行数据平时发生读写请求时,会上锁阻塞住。但MVCC用更好的方式去处理读—写请求,做到在发生读—写请求冲突时不用加锁。 +这个读是指的快照读,而不是当前读,当前读是一种加锁操作,是悲观锁。 +那它到底是怎么做到读—写不用加锁的,快照读和当前读是指什么?我们后面都会学到。 + +### 1.5.1.MVCC原理 + +#### 1.5.1.1.复习事务隔离级别![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/6a7f761e85854fc0a252a476f20f8235.png) + +MySQL在REPEATABLE READ隔离级别下,是可以很大程度避免幻读问题的发生的(**好像解决了,但是又没完全解决**),MySQL是怎么做到的? + +#### 1.5.1.2.版本链 + +**必须要知道的概念(每个版本链针对的一条数据):** + +我们知道,对于使用InnoDB存储引擎的表来说,它的聚簇索引记录中都包含两个必要的隐藏列(row_id并不是必要的,我们创建的表中有主键或者非NULL的UNIQUE键时都不会包含row_id列): +trx_id:每次一个事务对某条聚簇索引记录进行改动时,都会把该事务的事务id赋值给trx_id隐藏列。 +roll_pointer:每次对某条聚簇索引记录进行改动时,都会把旧的版本写入到undo日志中,然后这个隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息。 + +(补充点:undo日志:为了实现事务的原子性,InnoDB存储引擎在实际进行增、删、改一条记录时,都需要先把对应的undo日志记下来。**一般每对一条记录做一次改动,就对应着一条undo日志**,但在某些更新记录的操作中,也可能会对应着2条undo日志。一个事务在执行过程中可能新增、删除、更新若干条记录,也就是说需要记录很多条对应的undo日志,这些undo日志会被从0开始编号,也就是说根据生成的顺序分别被称为第0号undo日志、第1号undo日志、...、第n号undo日志等,这个编号也被称之为undo no。) + +为了说明这个问题,我们创建一个演示表 + +``` +CREATE TABLE teacher ( +number INT, +name VARCHAR(100), +domain varchar(100), +PRIMARY KEY (number) +) Engine=InnoDB CHARSET=utf8; +``` + +然后向这个表里插入一条数据: + +``` +INSERT INTO teacher VALUES(1, '李瑾', 'JVM系列'); +``` + +现在表里的数据就是这样的: + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/2ab2e19d985e462e87c1b6e7c50ebc5a.png) + +假设插入该记录的事务id为60,那么此刻该条记录的示意图如下所示: + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/c7b3d5e8c3bd4d91942b0891b0db0956.png) + +假设之后两个事务id分别为80、120的事务对这条记录进行UPDATE操作,操作流程如下: +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/85471f9eaddf4d42a51259b6878056ed.png) + +每次对记录进行改动,都会记录一条undo日志,每条undo日志也都有一个roll_pointer属性(INSERT操作对应的undo日志没有该属性,因为该记录并没有更早的版本),可以将这些undo日志都连起来,串成一个链表,所以现在的情况就像下图一样: + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/c7dc3b48519b4961b04e03594ee80538.png) + +对该记录每次更新后,都会将旧值放到一条undo日志中,就算是该记录的一个旧版本,随着更新次数的增多,所有的版本都会被roll_pointer属性连接成一个链表,我们把这个链表称之为版本链,版本链的头节点就是当前记录最新的值。另外,每个版本中还包含生成该版本时对应的事务id。**于是可以利用这个记录的版本链来控制并发事务访问相同记录的行为,那么这种机制就被称之为多版本并发控制(Mulit-Version Concurrency Control MVCC)。** + +#### 1.5.1.3.ReadView + +**必须要知道的概念(作用于SQL查询语句)** + +对于使用READ UNCOMMITTED隔离级别的事务来说,由于可以读到未提交事务修改过的记录,所以直接读取记录的最新版本就好了(**所以就会出现脏读、不可重复读、幻读**)。 + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/5e3918aac91a4ee8b6a2de3011021922.png) +对于使用SERIALIZABLE隔离级别的事务来说,InnoDB使用加锁的方式来访问记录(**也就是所有的事务都是串行的,当然不会出现脏读、不可重复读、幻读**)。 + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/82450615bb5b4612aaccdd7008280f5e.png) +对于使用READ COMMITTED和REPEATABLE READ隔离级别的事务来说,都必须保证读到已经提交了的事务修改过的记录,也就是说假如另一个事务已经修改了记录但是尚未提交,是不能直接读取最新版本的记录的,核心问题就是:READ COMMITTED和REPEATABLE READ隔离级别在不可重复读和幻读上的区别是从哪里来的,其实结合前面的知识,这两种隔离级别关键**是需要判断一下版本链中的哪个版本是当前事务可见的**。 +**为此,InnoDB提出了一个ReadView的概念(作用于SQL查询语句),** + +这个ReadView中主要包含4个比较重要的内容: +**m_ids:**表示在生成ReadView时当前系统中活跃的读写事务的事务id列表。 +**min_trx_id:**表示在生成ReadView时当前系统中活跃的读写事务中最小的事务id,也就是m_ids中的最小值。 +**max_trx_id:**表示生成ReadView时系统中应该分配给下一个事务的id值。注意max_trx_id并不是m_ids中的最大值,事务id是递增分配的。比方说现在有id为1,2,3这三个事务,之后id为3的事务提交了。那么一个新的读事务在生成ReadView时,m_ids就包括1和2,min_trx_id的值就是1,max_trx_id的值就是4。 +**creator_trx_id:**表示生成该ReadView的事务的事务id。 + +#### 1.5.1.4.READ COMMITTED + +##### 脏读问题的解决 + +READ COMMITTED隔离级别的事务在每次查询开始时都会生成一个独立的ReadView。 + +在MySQL中,READ COMMITTED和REPEATABLE READ隔离级别的的一个非常大的区别就是它们生成ReadView的时机不同。 +我们还是以表teacher 为例,假设现在表teacher 中只有一条由事务id为60的事务插入的一条记录,接下来看一下READ COMMITTED和REPEATABLE READ所谓的生成ReadView的时机不同到底不同在哪里。 +READ COMMITTED —— 每次读取数据前都生成一个ReadView +比方说现在系统里有两个事务id分别为80、120的事务在执行:Transaction 80 + +``` +UPDATE teacher SET name = '马' WHERE number = 1; +UPDATE teacher SET name = '连' WHERE number = 1; +... +``` + +此刻,表teacher 中number为1的记录得到的版本链表如下所示: + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/bd1d16d019c9405ab12fca214e271053.png) + +假设现在有一个使用READ COMMITTED隔离级别的事务开始执行: + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/ce38f77cc7814319b8b8b8e6bf1c7997.png) + +``` +使用READ COMMITTED隔离级别的事务 + +BEGIN; +SELECE1:Transaction 80、120未提交 + +SELECT * FROM teacher WHERE number = 1; # 得到的列name的值为'李瑾' +``` + +第1次select的时间点 如下图: + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1653285598056/2cb53fc2abf649ab88804b8bc4054127.png) + +这个SELECE1的执行过程如下: +在执行SELECT语句时会先生成一个ReadView: + +ReadView的m_ids列表的内容就是[80, 120],min_trx_id为80,max_trx_id为121,creator_trx_id为0。 + +然后从版本链中挑选可见的记录,从图中可以看出,最新版本的列name的内容是'**连**',该版本的trx_id值为80,在m_ids列表内,所以不符合可见性要求(trx_id属性值在ReadView的min_trx_id和max_trx_id之间说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被访问),根据roll_pointer跳到下一个版本。 +下一个版本的列name的内容是'**马**',该版本的trx_id值也为80,也在m_ids列表内,所以也不符合要求,继续跳到下一个版本。 +下一个版本的列name的内容是'**李瑾**',该版本的trx_id值为60,小于ReadView中的min_trx_id值,所以这个版本是符合要求的,最后返回给用户的版本就是这条列name为'**李瑾**'的记录。 + +**所以有了这种机制,就不会发生脏读问题!因为会去判断活跃版本,必须是不在活跃版本的才能用,不可能读到没有 commit的记录。** + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1653285598056/dd356cf81ec14665b7f11c82ec6021e9.png) + +##### 不可重复读问题 + +然后,我们把事务id为80的事务提交一下,然后再到事务id为120的事务中更新一下表teacher 中number为1的记录: + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/352c5da6030041ef820ba4408c670362.png) + +``` +Transaction120 + +BEGIN; + +更新了一些别的表的记录 + +UPDATE teacher SET name = '严' WHERE number = 1; +UPDATE teacher SET name = '晁' WHERE number = 1; + +``` + +此刻,表teacher 中number为1的记录的版本链就长这样: + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/7202b4c9114b440f9473bcfe9699ab74.png) + +然后再到刚才使用READ COMMITTED隔离级别的事务中继续查找这个number为1的记录,如下: + +使用READ COMMITTED隔离级别的事务 + +``` +BEGIN; + +SELECE1:Transaction 80、120均未提交 + +SELECT * FROM teacher WHERE number = 1; # 得到的列name的值为'李瑾' + +SELECE2:Transaction 80提交,Transaction 120未提交 + +SELECT * FROM teacher WHERE number = 1; # 得到的列name的值为'连' + +``` + +**第2次select的时间点 如下图:** + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1653285598056/076acec8fbe7463cb9f2f709327cdfb5.png) + +这个SELECE2的执行过程如下: + +SELECT * FROM teacher WHERE number = 1; + +在执行SELECT语句时会又会单独生成一个ReadView,该ReadView信息如下: + +m_ids列表的内容就是[120](事务id为80的那个事务已经提交了,所以再次生成快照时就没有它了),min_trx_id为120,max_trx_id为121,creator_trx_id为0。 +然后从版本链中挑选可见的记录,从图中可以看出,最新版本的列name的内容是'**晁**',该版本的trx_id值为120,在m_ids列表内,所以不符合可见性要求,根据roll_pointer跳到下一个版本。 +下一个版本的列name的内容是'**严**',该版本的trx_id值为120,也在m_ids列表内,所以也不符合要求,继续跳到下一个版本。 +下一个版本的列name的内容是'**连**',该版本的trx_id值为80,小于ReadView中的min_trx_id值120,所以这个版本是符合要求的,最后返回给用户的版本就是这条列name为'**连**'的记录。 + +以此类推,如果之后事务id为120的记录也提交了,再次在使用READ COMMITTED隔离级别的事务中查询表teacher 中number值为1的记录时,得到的结果就是'**晁**'了,具体流程我们就不分析了。 + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1653285598056/e183bd95aed6411aba3ded451871dbff.png) + + +##### 但会出现不可重复读问题。 + +明显上面一个事务中两次 + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1653285598056/b1232c1150a646eda0637a914716e0fc.png) + + +#### 1.5.1.5.REPEATABLE READ + +##### REPEATABLE READ解决不可重复读问题 + +REPEATABLE READ —— 在第一次读取数据时生成一个ReadView + +对于使用REPEATABLE READ隔离级别的事务来说,只会在第一次执行查询语句时生成一个ReadView,之后的查询就不会重复生成了。我们还是用例子看一下是什么效果。 + +比方说现在系统里有两个事务id分别为80、120的事务在执行:Transaction 80 + +``` +UPDATE teacher SET name = '马' WHERE number = 1; +UPDATE teacher SET name = '连' WHERE number = 1; +... +``` + +此刻,表teacher 中number为1的记录得到的版本链表如下所示: + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/bd1d16d019c9405ab12fca214e271053.png) + +假设现在有一个使用REPEATABLE READ隔离级别的事务开始执行: + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/ce38f77cc7814319b8b8b8e6bf1c7997.png) + +``` +使用READ COMMITTED隔离级别的事务 + +BEGIN; +SELECE1:Transaction 80、120未提交 + +SELECT * FROM teacher WHERE number = 1; # 得到的列name的值为'李瑾' +``` + +这个SELECE1的执行过程如下: +在执行SELECT语句时会先生成一个ReadView: + +ReadView的m_ids列表的内容就是[80, 120],min_trx_id为80,max_trx_id为121,creator_trx_id为0。 + +然后从版本链中挑选可见的记录,从图中可以看出,最新版本的列name的内容是'**连**',该版本的trx_id值为80,在m_ids列表内,所以不符合可见性要求(trx_id属性值在ReadView的min_trx_id和max_trx_id之间说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被访问),根据roll_pointer跳到下一个版本。 +下一个版本的列name的内容是'**马**',该版本的trx_id值也为80,也在m_ids列表内,所以也不符合要求,继续跳到下一个版本。 +下一个版本的列name的内容是'**李瑾**',该版本的trx_id值为60,小于ReadView中的min_trx_id值,所以这个版本是符合要求的,最后返回给用户的版本就是这条列name为'**李瑾**'的记录。 +之后,我们把事务id为80的事务提交一下,然后再到事务id为120的事务中更新一下表teacher 中number为1的记录: + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/352c5da6030041ef820ba4408c670362.png) + +``` +Transaction120 + +BEGIN; + +更新了一些别的表的记录 + +UPDATE teacher SET name = '严' WHERE number = 1; +UPDATE teacher SET name = '晁' WHERE number = 1; + +``` + +此刻,表teacher 中number为1的记录的版本链就长这样: + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/7202b4c9114b440f9473bcfe9699ab74.png) + +然后再到刚才使用REPEATABLE READ隔离级别的事务中继续查找这个number为1的记录,如下: + +使用READ COMMITTED隔离级别的事务 + +``` +BEGIN; + +SELECE1:Transaction 80、120均未提交 + +SELECT * FROM teacher WHERE number = 1; # 得到的列name的值为'李瑾' + +SELECE2:Transaction 80提交,Transaction 120未提交 + +SELECT * FROM teacher WHERE number = 1; # 得到的列name的值为'李瑾' + +``` + +这个SELECE2的执行过程如下: + +因为当前事务的隔离级别为REPEATABLE READ,而之前在执行SELECE1时已经生成过ReadView了,所以此时直接复用之前的ReadView,之前的ReadView的m_ids列表的内容就是[80, 120],min_trx_id为80,max_trx_id为121,creator_trx_id为0。 + +**根据前面的分析,返回的值还是'李瑾'。** + +**也就是说两次SELECT查询得到的结果是重复的,记录的列name值都是'李瑾',这就是可重复读的含义。** + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/64659425cf56414fba3a80c42c0538b7.png) + +**总结一下就是:** + +**ReadView中的比较规则(前两条)** + +1、如果被访问版本的trx_id属性值与ReadView中的creator_trx_id值相同,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问。 + +2、如果被访问版本的trx_id属性值小于ReadView中的min_trx_id值,表明生成该版本的事务在当前事务生成ReadView前已经提交,所以该版本可以被当前事务访问。 + +#### 1.5.1.6.MVCC下的幻读解决和幻读现象 + +前面我们已经知道了,REPEATABLE READ隔离级别下MVCC可以解决不可重复读问题,那么幻读呢?MVCC是怎么解决的?幻读是一个事务按照某个相同条件多次读取记录时,后读取时读到了之前没有读到的记录,而这个记录来自另一个事务添加的新记录。 +我们可以想想,在REPEATABLE READ隔离级别下的事务T1先根据某个搜索条件读取到多条记录,然后事务T2插入一条符合相应搜索条件的记录并提交,然后事务T1再根据相同搜索条件执行查询。结果会是什么?按照**ReadView中的比较规则(后两条):** +3、如果被访问版本的trx_id属性值大于或等于ReadView中的max_trx_id值,表明生成该版本的事务在当前事务生成ReadView后才开启,所以该版本不可以被当前事务访问。 +4、如果被访问版本的trx_id属性值在ReadView的min_trx_id和max_trx_id之间(min_trx_id < trx_id < max_trx_id),那就需要判断一下trx_id属性值是不是在m_ids列表中,如果在,说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被访问。 + +不管事务T2比事务T1是否先开启,事务T1都是看不到T2的提交的。请自行按照上面介绍的版本链、ReadView以及判断可见性的规则来分析一下。 +但是,在REPEATABLE READ隔离级别下InnoDB中的MVCC 可以很大程度地避免幻读现象,而不是完全禁止幻读。怎么回事呢?我们来看下面的情况: + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/00b3c1b4774c47cb9a2ceb19d921b504.png) + +我们首先在事务T1中: + +``` +select * from teacher where number = 30; +``` + +很明显,这个时候是找不到number = 30的记录的。 +我们在事务T2中,执行: + +``` +insert into teacher values(30,'豹','数据湖'); +``` + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/f1935b415db04ac482e023c6406f8de3.png) + +通过执行insert into teacher values(30,'豹','数据湖');,我们往表中插入了一条number = 30的记录。 +此时回到事务T1,执行: + +``` +update teacher set domain='RocketMQ' where number=30; +select * from teacher where number = 30; +``` + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1651212459071/7ca9e16a31d84d5ca72fdb236a95eb28.png) + +嗯,怎么回事?事务T1很明显出现了幻读现象。 +在REPEATABLE READ隔离级别下,T1第一次执行普通的SELECT 语句时生成了一个ReadView(但是版本链没有),之后T2向teacher 表中新插入一条记录并提交,然后T1也进行了一个update语句。 +ReadView并不能阻止T1执行UPDATE 或者DELETE 语句来改动这个新插入的记录,但是这样一来,这条新记录的trx_id隐藏列的值就变成了T1的事务id。 + +![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/5983/1653285598056/92d29ec669db40b1a07fb0b3cc8cb730.png) + +之后T1再使用普通的SELECT 语句去查询这条记录时就可以看到这条记录了,也就可以把这条记录返回给客户端。因为这个特殊现象的存在,我们也可以认为MVCC 并不能完全禁止幻读(**就是第一次读如果是空的情况,且在自己事务中进行了该条数据的修改**)。 + +#### 1.5.1.7.MVCC小结 + +从上边的描述中我们可以看出来,所谓的MVCC(Multi-Version Concurrency Control ,多版本并发控制)指的就是在使用READ COMMITTD、REPEATABLE READ这两种隔离级别的事务在执行普通的SELECT操作时访问记录的版本链的过程,这样子可以使不同事务的读-写、写-读操作并发执行,从而提升系统性能。 + +READ COMMITTD、REPEATABLE READ这两个隔离级别的一个很大不同就是:生成ReadView的时机不同,READ COMMITTD在每一次进行普通SELECT操作前都会生成一个ReadView,而REPEATABLE READ只在第一次进行普通SELECT操作前生成一个ReadView,之后的查询操作都重复使用这个ReadView就好了,从而基本上可以避免幻读现象(**就是第一次读如果ReadView是空的情况中的某些情况则避免不了**)。 + +另外,所谓的MVCC只是在我们进行普通的SEELCT查询时才生效,截止到目前我们所见的所有SELECT语句都算是普通的查询,至于什么是个不普通的查询,后面马上就会讲到(锁定读)。 diff --git a/高级知识/高级开发面试/img_2.png b/高级知识/高级开发面试/img_2.png new file mode 100644 index 0000000..7885091 Binary files /dev/null and b/高级知识/高级开发面试/img_2.png differ diff --git a/高级知识/高级开发面试/多线程.md b/高级知识/高级开发面试/多线程.md index c171533..317c5d2 100644 --- a/高级知识/高级开发面试/多线程.md +++ b/高级知识/高级开发面试/多线程.md @@ -66,3 +66,34 @@ >- 使用CAS方法获取state ### 多线程wait,notify,notifyALl线程等待 + +### 线程 +>- CPU调度的基本单位 +>- 目的:避免等待(IO、网络等待 )操作,提高效率 +>- 局限:切换上下文时,小号很大资源 +>- 依赖业务场景,不能异构化任务 +>- 线程安全问题 + +### 串行,并行,并发 +>- 并行就是同时处理 +>- 并发:多线程的并发,cpu调度线程,cpu在不同的线程切换<(^-^)> + +### 同步与异步 +>- 被调用者,主动反馈信息, +>- 异步:主动反馈 + +### 阻塞与非阻塞 +>- 调用者是否等待结果 +>- 同步阻塞:用锅烧水,水开不主动通知,需要一直等待着水烧开 +>- 同步非阻塞:不需要等待水烧开,时不时查看 +>- 异步阻塞: 用水壶烧水,水来了,会有通知,一直等着烧开 +>- 异步非阻塞: 用水壶烧水,水来了,会有通知,响应通知 + +### 线程创建 +>- Thread +>- Runnable +>- ![img_2.png](img_2.png) +>- target 就是Runnable一个实现类,传入Thread,作为有参构造<(^-^)> + +### run()和start() 的区别 +>- start()会创建一个新的线程,run()是一个具体的执行方法,没有新的线程创建。 \ No newline at end of file