MySQL执行底层原理
查询结构
#方式1:
SELECT ...,....,...
FROM ...,...,....
WHERE 多表的连接条件
AND 不包含组函数的过滤条件
GROUP BY ...,...
HAVING 包含组函数的过滤条件
ORDER BY ... ASC/DESC
LIMIT ...,...
#方式2:
SELECT ...,....,...
FROM ... JOIN ...
ON 多表的连接条件
JOIN ...
ON ...
WHERE 不包含组函数的过滤条件
AND/OR 不包含组函数的过滤条件
GROUP BY ...,...
HAVING 包含组函数的过滤条件
ORDER BY ... ASC/DESC
LIMIT ...,...
#其中:
#(1)from:从哪些表中筛选
#(2)on:关联多表查询时,去除笛卡尔积
#(3)where:从表中筛选的条件
#(4)group by:分组依据
#(5)having:在统计结果中再次筛选
#(6)order by:排序
#(7)limit:分页
SELECT执行顺序
FROM -> WHERE -> GROUP BY -> HAVING -> SELECT 的字段 -> DISTINCT -> ORDER BY -> LIMIT
SELECT DISTINCT player_id, player_name, count(*) as num # 顺序 5
FROM player JOIN team ON player.team_id = team.team_id # 顺序 1
WHERE height > 1.80 # 顺序 2
GROUP BY player.team_id # 顺序 3
HAVING num > 2 # 顺序 4
ORDER BY num DESC # 顺序 6
LIMIT 2 # 顺序 7
在 SELECT 语句执行这些步骤的时候,每个步骤都会产生一个 虚拟表 ,然后将这个虚拟表传入下一个步骤中作为输入
SQL的执行原理
SELECT 是先执行 FROM 这一步的。在这个阶段,如果是多张表联查,还会经历下面的几个步骤
- 1、首先通过 CROSS JOIN 求笛卡尔积,相当于得到虚拟表 vt(virtual table)1-1
- 2、通过 ON 进行筛选,在虚拟表 vt1-1 的基础上进行筛选,得到虚拟表 vt1-2
- 3、添加外部行。如果我们使用的是左连接、右链接或者全连接,就会涉及到外部行,也就是在虚拟 表 vt1-2 的基础上增加外部行,得到虚拟表 vt1-3
拿到查询数据表的原始数据(虚拟表vt1),再此基础上进行WHERE阶段,过滤得到虚拟表vt2
再进行GROUP 和 HAVING 阶段,对vt2进行分组和分组过滤,得到中间虚拟表vt3、vt4
随后进入 SELECT 和 DISTINCT 阶段,分别得到中间的虚拟表 vt5-1、vt5-2
指定字段排序,ORDER BY 阶段,得到虚拟表vt6
取出指定行记录,LIMIT阶段,得到虚拟表 vt7
引擎
InnBD引擎
MySQL5.5之后默认采用的引擎
- 默认事务型引擎,被设计用来处理大量的短期事务,确保事务的完整提交和回滚
- 缓存索引和真实数据,对内存要求较高,内存大小对性能有绝对影响
- 为处理巨大数据量的最大性能而设计
MyISAM引擎
MySQL5.5之前默认的存储引擎
- 提供大量特性,包括全文索引、压缩、空间函数等。但不支持事务、行级锁、崩溃后无法安全恢复
- 访问速度快,对事务完整没有要求或者以SELECT、INSERT为主的应用
- 针对数据统计有额外的常数存储
Archive引擎
用于数据存档
特征 | 支持 |
---|---|
压缩数据 | 支持 |
备份/时间点恢复(在服务器中实现,而不是在存储引擎中) | 支持 |
地理空间数据类型 | 支持 |
加密数据(在服务器中实现) | 支持 |
更新数据字典的统计信息 | 支持 |
锁粒度 | 行锁 |
数据缓存 | 不支持 |
外键 | 不支持 |
全文检索索引 | 不支持 |
聚集索引 | 不支持 |
地理空间索引 | 不支持 |
哈希索引 | 不支持 |
索引缓存 | 不支持 |
B树索引 | 不支持 |
MVCC | 不支持 |
存储限制 | 无限制 |
交易 | 不支持 |
集群数据库 | 不支持 |
Memory引擎
置于内存的表,采用逻辑介质是内存,响应速度快。当mysqld守护进程崩溃时数据会丢失,另外要求存储的数据是数据长度不变的格式
特征:
- 同时支持哈希索引、B+树索引
- 比MyISAM快一个数量级
- 表的大小主要取决于两个参数,max_rows(创建表时指定)、max_heap_table_size(默认16MB)
- 数据文件、索引文件分开存储
- 数据易丢失,生命周期短
使用场景:
- 目标数据较少,频繁的进行访问。在内存中存放数据,若数据太大会造成内初溢出
- 数据是临时的,必须立即可用
- 存储在Memory表中的数据突然间丢失也没有太大关系
其他引擎
- Merge引擎:管理多个MyISAM表构成的表集合
- NDB引擎:MySQL集群专用存储引擎,也叫做 NDB Cluster 存储引擎,主要用于 MySQL Cluster 分布式集群环境
常用引擎对比
特点 | MyISAM | InnoDB | MEMORY | MERGE | NDB |
---|---|---|---|---|---|
存储限制 | 有 | 64TB | 有 | 没有 | 有 |
事务安全性 | 支持 | ||||
锁机制 | 表锁 | 行锁 | 表锁 | 表锁 | 行锁 |
B树索引 | 支持 | 支持 | 支持 | 支持 | 支持 |
哈希索引 | 支持 | 支持 | |||
全文索引 | 支持 | ||||
集群索引 | 支持 | ||||
数据缓存 | 支持 | 支持 | 支持 | ||
索引缓存 | 缓存索引,不缓存数据 | 缓存索引、数据 | 支持 | 支持 | 支持 |
数据可压缩 | 支持 | ||||
空间使用 | 低 | 高 | N/A | 低 | 低 |
内存使用 | 低 | 高 | 中等 | 低 | 高 |
批量插入的速度 | 高 | 低 | 高 | 高 | 高 |
支持外键 | 支持 |
InnoDB表的补充
InnoDB表的优势
- 操作便利、提高数据库的性能、维护成本低
- 由于硬件或软件的原因导致服务器崩溃,那么在重启服务器之后不需要进行额外的操作,InnoDB崩溃 恢复功能自动将之前提交的内容定型,然后撤销没有提交的进程,重启之后继续从崩溃点开始执行
- InnoDB存储引擎在主内存中维护缓冲池,高频率使用的数据将在内存中直接被处理。这种缓存方式应用于多种信息,加速了处理进程
- InnoDB不仅支持当前读写,也会缓冲改变的数据到数据流磁盘
- 在不影响性能和可用性的情况下创建或删除索引
- 当处理大数据量时, InnoDB兼顾CPU,以达到最大性能
InnoDB和ACID模型
1、原子,主要涉及InnoDB事务,与MySQL相关的特性主要包括:
- 自动提交设置
- COMMIT语句
- ROLLBACK语句
- 操作INFORMATION_SCHEMA库中的表数据
2、一致性,主要涉及保护数据不崩溃的内部InnoDB处理过程,与MySQL相关的特性主要包括:
- InnoDB双写缓存
- InnoDB崩溃恢复
3、隔离,应用于事务的级别,与MySQL相关的特性主要包括:
- 自动提交设置
- SET ISOLATION LEVEL语句
- InnoDB锁的低级别信息
4、耐久性, ACID模型的耐久性主要涉及与硬件配置相互影响的MySQL软件特性。由于硬件复杂多样 化,耐久性方面没有具体的规则可循。与MySQL相关的特性有:
- InnoDB双写缓存,通过innodb_doublewrite配置项配置
- 配置项innodb_flush_log_at_trx_commit
- 配置项sync_binlog
- 配置项innodb_file_per_table
- 存储设备的写入缓存
- 存储设备的备用电池缓存
- 运行MySQL的操作系统
- 持续的电力供应
- 备份策略
- 对分布式或托管的应用,最主要的在于硬件设备的地点以及网络情况
InnoDB架构
- 缓冲池:主内存中的一部分空间,用来缓存已使用的表和索引数据。缓冲池使得经常被使用的 数据能够直接在内存中获得,从而提高速度
- 更改缓存:一个特殊的数据结构,当受影响的索引页不在缓存中时,更改缓存会缓存辅助索 引页的更改。索引页被其他读取操作时会加载到缓存池,缓存的更改内容就会被合并
- 自适应哈希索引:将负载和足够的内存结合,使得InnoDB像内存数据库一样运行, 不需要降低事务上的性能或可靠性
- 重做日志缓存:存放要放入重做日志的数据,定期地将日志文件刷入磁盘,大型的重做日志缓存 使得大型事务能够正常运行而不需要写入磁盘
- 系统表空间:包括InnoDB数据字典、双写缓存、更新缓存和撤销日志,同时也包括表和索引 数据。多表共享,系统表空间被视为共享表空间
- 双写缓存:位于系统表空间中,用于写入从缓存池刷新的数据页。只有在刷新并写入双写缓存 后,InnoDB才会将数据页写入合适的位置
- 撤销日志:一系列与事务相关的撤销记录的集合,包含如何撤销事务最近的更改
- 每个表一个文件的表空间:每个单独的表空间创建在自身的数据文件中, 而不是系统表空间中,每个表空间由一个单独的.ibd数 据文件代表,该文件默认被创建在数据库目录中
- 通用表空间: 使用CREATE TABLESPACE语法创建共享的InnoDB表空间。通用表空间可以创建在MySQL数 据目录之外能够管理多个表并支持所有行格式的表
- 撤销表空间:由一个或多个包含撤销日志的文件组成
- 临时表空间:用户创建的临时表空间和基于磁盘的内部临时表都创建于临时表空间
- 重做日志:基于磁盘的数据结构,在崩溃恢复期间使用,用来纠正数据。正常操作期间, 重做日志会将请求数据进行编码,这些请求会改变InnoDB表数据。遇到意外崩溃后,未完成的更改会自 动在初始化期间重新进行
事务
事务:一组逻辑操作单元,使数据从一种状态变换到另一种状态
事务处理原则:保证所有事务都作为一个工作单元来执行
- 当一个事务执行多个操作时,要么所有事务都被提交(commit),这些修改永久保存
- 要么数据库管理系统(DBMS)放弃所有修改,事务回滚到初始状态
事务的ACID特性
- 原子性(atomicity):事务是一个 不可分割 的工作单位,要么修改成功,要么失败全部回滚
- undo log(回滚日志)保证
- 一致性(consistency):事务执行前后,数据从一个 合法性状态 变换到另一个 合法性状态
- 持久性 + 原子性 + 隔离性 保证
- 隔离性(isolation):一个事务的执行不能被其他事务干扰。一个事务内部的操作及使用的数据对 并发 的其他事务是隔离的
- MVCC(多版本并发控制)或锁机制保证
- 持久性(durability):事务一旦被提交,对数据库中的数据改变是 永久性 的,接下来其他操作和数据库故障不应该对其有任何影响
- redo log(重做日志) 保证
事务的状态
- 活动(acitvie):事务对应的数据库操作正在执行过程中
- 部分提交(partially committed):当事务中的最后一个操作执行完成,由于操作都在内存中执行,所造成的影响并 没有刷新到磁盘
- 失败(failed):当事务处于 活动 或 部分提交 状态时,遇到某些错误(数据库自身错误、操作系统错误、断电)而无法继续执行,或者认为停止当前事务的执行
- 中止(aborted):事务执行一部分而变为 失败 状态,就需要把已经修改的事务中的操作还原到事务执行前的状态,撤销失败事务对当前数据库造成的影响。撤销过程称之为 回滚,当回滚执行完毕,数据库恢复到执行之前的状态,事务处于 中止 状态
- 提交(commited):当一个处于 部分提交 状态的事务将修改过的数据都 同步到磁盘 上之后,该事务处于 提交 状态
并行事务引起的问题
MySQL 服务端是允许多个客户端连接的,这意味着 MySQL 会出现同时处理多个事务的情况
在同时处理多个事务时,会可能出现脏读(dirty read)、不可重复读(non-repeatable read)、幻读(phantom read)
- 脏读:都到其他事务未提交的数据
- 不可重复读:前后读取的数据不一致
- 幻读:前后读取的记录数量不一致
严重性排序:脏读 > 不可排序 > 幻读
事务的隔离级别
SQL标准提出了四种隔离级别来规避这些现象,隔离级别越高,效率越低
- 读未提交(read uncommitted):一个事务未提交时,它所做变更能被其他事务看到
- 读提交(read committed):一个事务提交后,它所做变更才能被其他事务看到
- 可重复读(repeatable read):一个事务执行过程中看到的数据,一直跟这个事务启动时看到的数据一致,InnoDB默认隔离级别
- 串行化(serializable):对记录加上读写锁,在多个事务对这条记录进行读写操作时,若发生读写冲突,后访问的事务必须等待前一个事务执行完成,才能继续执行
针对不同的隔离级别,并发事务可能发生的现象如下
![](https://img-blog.csdnimg.cn/img_convert/4e98ea2e60923b969790898565b4d643.png)
这四种隔离级别是如何实现的呢?
-
读未提交:直接读取最新的数据
-
读提交、可重复读:通过Read View实现,可将Read View理解为一个快照
- 读提交 在 每个语句执行前 都会重新生成一个Read View
- 可重复读 在 启动事务时 生成一个Read View,整个事务期间用这个 Read View
-
串行化:加读写锁
Read View 在MVCC里的工作
Read View有四个重要字段:
- 创建该 Read View事务的事务id
- m_ids:创建Read View时,当前数据库中 活跃事务 的事务id列表
- min_trx_id:创建Read View时,当前数据库中 活跃事务 中事务id最小的事务
- max_trx_id:不是m_ids里的最大值,而是创建Read View时当前数据库中应该给下一个事务的id值,也是全局事务中最大事务id值+1
![](https://img-blog.csdnimg.cn/img_convert/f595d13450878acd04affa82731f76c5.png)
对于使用InnoDB存储引擎的数据库表,它的聚簇索引记录都包含下面两个隐藏列:
- trx_id:当一个事务对某条聚簇索引记录进行改动时,会把该事务的事务id记录在 trx_id 隐藏列里
- roll_pointer:每次对某条聚簇索引记录进行改动时,会把旧版本的记录写入到undo日志中,该隐藏列是个指针,指向每一个旧版本记录,通过它可以找到修改前的记录
在创建 Read View 后,可将记录中 trx_id 划分为三种情况:
- trx_id < min_trx_id:表示该版本的记录在创建 Read View 前已经提交的事务生成的,故该版本的记录对当前事务可见
- trx_id > max_trx_id:表示该版本的记录在创建 Read View 后已经提交的事务生成的,故该版本的记录对当前事务不可见
- min_trx <= trx_id <= max_trx:需要判断是否在创建的的 Read View 里的 m_ids列表中
- 在列表中,表示生成该版本记录的活跃事务依然活跃着(还没提交事务),该版本记录对当前事务不可见
- 不在列表中,表示生成该版本记录的活跃事务已经被提交,该版本的记录对当前事务可见
这种通过版本链来控制并发事务访问同一个记录的行为被称为 MVCC(多版本并发控制)
索引
![](https://yingziimage.oss-cn-beijing.aliyuncs.com/img/image-20220530192545404.png)
MySQL官方定义:帮助MySQL高效获取数据的数据结构
下图是MySQL的结构图,索引和数据位于存储引擎中
索引的优缺点
优点
- 提高数据检索效率,降低数据库的IO成本,创建索引的主要原因
- 创建唯一索引,保证数据库表中每一行数据的唯一性
- 在实现数据的参考完整性方面,加速表和表直接的连接
- 显著减少查询中分组和排序的时间,降低CPU消耗
缺点
- 创建索引和维护索引需要耗费时间
- 索引需要占据磁盘空间。除了数据表占数据空间外,每一个索引还要占用一定的物理空间,存储在磁盘上
- 降低更新表的速度,当对表中的数据进行增加、删除、修改时,索引也需要动态维护
什么时候(不)需要索引
需要索引
- 字段有唯一性限制,如商品编码
- 经常用于
where
查询条件的字段,若查询调节不是一个字段,可建立联合索引 - 经常用于
GROUP BY
和ORDER BY
的字段,这样在查询的时候就不需要再去做一次排序了,因为在建立索引之后 B+Tree中的记录都是排序好的
不需要索引
where
、group by
、order by
里用到的字段,索引的价值是快速定位,若起不到定位的字段通常不需要创建索引- 字段中存在大量重复数据,如性别字段,男女。MySQL还有一个查询优化器,查询优化器发现某个值出现在表的数据行中的百分比很高时,它一般会忽略索引,进行全表扫描
- 表数据太少时
- 经常更新的字段,由于要维护 B+Tree的有序性,那么久需要频繁的重建索引,这个过程是会影响数据库性能
索引的分类
可以按照四个角度来分类索引
- 数据结构:B+tree索引、Hash索引、Full-text索引
- 物理存储:聚簇索引(主键索引)、二级索引(辅助索引)
- 字段特性:主键索引、唯一索引、普通索引、前缀索引
- 字段个数:单列索引、联合索引
数据结构分类
从数据结构的角度来看,MySQL常见的索引有B+Tree索引、Hash索引、Full-Text索引
每一种存储引擎支持的索引类型不一定相同,下面是MySQL中常见的存储引擎所支持的索引类型
![](https://yingziimage.oss-cn-beijing.aliyuncs.com/img/image-20220530194507365.png)
InnoDB 是在 MySQL 5.5 之后成为默认的 MySQL 存储引擎,B+Tree 索引类型也是 MySQL 存储引擎采用最多的索引类型
在创建表时,InnoDB存储引擎会根据不同的场景选择不同的列作为索引:
- 如果有主键,默认使用主键作为聚簇索引的索引键(key)
- 如果没有主键,选择第一个不包含NULL值的唯一列作为聚簇索引的索引键(key)
- 在上面两个都没有的情况下,InnoDB将自动生成一个隐式自增id列作为聚簇索引的索引键(key)
其他索引都属于辅助索引,也称为二级索引或非聚簇索引,创建的主键索引和二级索引默认使用B+Tree索引
主键索引的叶子节点存放实际数据;二级索引的叶子节点存放主键值
回表:先检查二级索引找那个的 B+Tree 的索引值,找到对应的叶子节点,然后获取主键值,再由主键索引中的 B+Tree 树查询到对应的叶子节点,然后获取整行数据,这个过程叫做回表,也就是说要查两个 B+Tree才能查到数据
覆盖索引:当要查询的数据能在二级索引的 B+Tree的叶子节点里能查询到时(查主键值),就不需要再查主键索引查,这个过程叫做覆盖索引,也就是只需要一个 B+Tree就能找到数据
为什么MySQL InnoDB选择 B+Tree作为索引的数据结构?
B+Tree vs B Tree
B+Tree 只在叶子节点存储数据,而 B 树 的非叶子节点也要存储数据,所以 B+Tree 的单个节点的数据量更小,在相同的磁盘 I/O 次数下,就能查询更多的节点;另外,B+Tree 叶子节点采用的是双链表连接,适合 MySQL 中常见的基于范围的顺序查找,而 B 树无法做到这一点
B+Tree vs 二叉树
对于有 N 个 叶子节点的 B+Tree,其搜索复杂度为O(logdN),其中d表示节点允许的最大子节点个数为d个
在实际应用中,d值是大于100的,这样就保证了即使数据达到千万级别,B+Tree的高度依然维持在 3~4 层左右,一次数据查询操作只需要做3~4此的磁盘I/O操作就能查询到目标数据
二叉树的每个父节点的儿子节点个数只能是2个,意味着其搜索复杂度为O(logN),着比B+Tree高出不少,因此二叉树检索的目标数据所经历的磁盘I/O次数要更多
B+Tree vs Hash
Hash在做等值查询时效率很快,搜索复杂度为O(1),但是不适合做范围查询。这也是B+Tree索引要比Hash表索引有着更广泛的适用场景的原因
按物理存储分类
按物理存储的角度来看,索引分为:聚簇索引(主键索引)、二级索引(辅助索引)
这两者的区别也就是:
- 主键索引的B+Tree 的叶子节点存放的是实际数据,所有完整的用户记录都存放在主键索引的B+Tree 的叶子节点里
- 二级索引的B+ Tree 的叶子节点存放的是主键值,而不是实际数据
在查询时使用了二级索引,若查询的数据能在二级索引里查询的到,就不需要回表,这个过程为覆盖索引;若查询的数据不再二级索引里,那么会先检索二级索引,找到对应的叶子节点,获取到主键值,然后再检索主键索引,就能查询到数据了,这个过程为回表
按字段特性分类
按字段特性的角度来看,索引分为:主键索引、唯一索引、普通索引、前缀索引
- 主键索引:建立在主键字段上的索引,通常在创建表的时候一起创建,一张表最多只有一个主键索引,索引列的值不允许有空值
- 唯一索引:建立在 UNIQUE 字段上的索引,一张表可以有多个唯一索引,索引列的值必须唯一,但允许有空值
- 普通索引:建立在普通字段上的索引,既不要求字段为主键,也不要求字段为 UNIQUE
- 前缀索引:对字符类型字段的前几个字符建立的索引,而不是在整个字段上建立索引,前缀索引可以建立在字段类型为 char、varchar、binary、varbinary的列上。使用前缀索引的目的是为了减少索引占用的存储空间,提升查询效率
按字段个数分类
按字段个数的角度来看,索引分为:单列索引、联合索引(复合索引)
单列索引:建立在单列上的索引称为单列索引,如主键索引
联合索引:建立在多列上的索引称为联合索引
最左匹配原则:按照最左优先的方式进索引的匹配
利用索引的前提是索引里的 key 是有序的
联合索引的最左匹配原则失效:在遇到范围查询时(>、<、between、like),会停止匹配,范围列可以用到联合索引,但是范围列后面的列无法用到联合索引
索引下推优化:在联合索引遍历过程中,对联合索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数
提高索引效率:建立联合索引时,把区分度大的字段排在前面,这样区分度大的字段越有可能被更多的 SQL 使用到
区分度为某个字段 column 不同值的个数除以表的总行数
区分度
=
d
i
s
t
t
i
n
c
t
(
c
o
l
u
m
n
)
c
o
u
n
t
(
∗
)
区分度 = \frac{disttinct(column)}{count(*)}
区分度=count(∗)disttinct(column)
索引优化
常见的优化索引方法:前缀索引优化、覆盖索引优化、主键索引最好是自增、防止索引失效
前缀索引优化
使用某个字段中字符串的前几个字符建立索引,减小索引字段大小,可以增加一个索引页来存储索引值,有效提高索引的查询速度
前缀索引的局限性:
- orber by 无法使用前缀索引
- 无法把前缀索引用作覆盖索引
覆盖索引优化
覆盖索引:SQL中查询的所有字段,在索引 B+Tree 的叶子节点上都能找到那些索引,从二级索引中查询得到记录,而不需要通过聚簇索引查询获得,可以避免回表操作
覆盖索引的好处:不需要查询出包含整行记录的所有信息,也就减少了大量的 I/O 操作
主键索引最好是自增的
建表时,默认将主键索引设置为自增
InnoDB创建主键索引默认为聚簇索引,数据被存放在 B+Tree的叶子节点上。同一个叶子节点内的各个数据时按主键顺序存放的,每当一条新的数据插入时,数据库会根据主键值将其插入到对应的叶子节点中
使用自增主键:每次插入的新数据就会拿顺序添加到当前索引节点的位置,不需要移动已有的数据,当页面写满,就会自动开辟一个新页面。每次插入一条新记录,都是追加操作,不需要重新移动数据,一次这种插入数据的方法效率非常高
使用非自增主键:每次插入主键的索引值都是随机的,因此每次插入新的数据时,就可能会插入到现有数据页中间的某个位置,这将不得不移到其他数据来满足新数据的插入,甚至需要从一个页面复制数据到另一个页面,我们通常将这个情况称为页分裂。页分裂可能会造成大量的内存碎片,导致索引结构不紧凑,从而影响查询效率
索引设置最好为 NOT NULL
- 索引列存在 NULL 就会导致优化器在做索引选择的时候更加复杂,更加难以优化。比如进行索引统计时,count会省略为NULL的行
- NULL值是一个没意义的值,但是会占用物理空间,会带来存储空间的问题,InnoDB默认行存储格式COMPACT,会用1字节空间存储NULL值列表
防止索引失效
用上了索引并不意味着查询的时候回使用到索引,所以我们心里清楚有哪些情况会导致索引失效,从而避免写出索引失效的查询语句
常见发生索引失效的情况:
- 使用左或者左右模糊匹配时,也就是
like %xx
或者like %xx%
这两种方式都会造成索引失效 - 在查询条件中对索引列做了计算、函数、类型转换操作,这些情况下都会造成索引失效
- 联合索引要能正确使用需要遵循最左匹配原则,否则会造成索引失效
- 在WHERE子句中,如果在OR前的条件列是索引列,而在OR后的条件列不是索引列,那么索引会失效
锁
锁的种类
根据加锁范围,可分为:全局锁、表级锁、行锁
全局锁
使用全局锁 -> flush tables with read lock
执行后,整个数据库就只处于只读状态,其他线程执行以下操作,都会被阻塞:
-对数据的增删改操作,如insert、delete、update语句
-对标结构的更改操作,如alter table、drop table语句
释放全局锁 -> unlock tables
全局锁应用场景?
主要应用于全库逻辑备份,这样在备份数据库期间,不会因为数据或表结构的更新,而出现备份文件的数据与预期不一样
加全局锁的缺点?
整个数据库为只读状态。若数据库中有很多数据,备份将花费很多时间,这期间业务只能读数据,而不能更新数据,会造成业务停滞
使用全局锁会影响业务,如何避免?
若数据库引擎支持的事务支持 可重复读的隔离级别,那么在备份数据库之前开启事务,会先创建 Read View,然后整个事务执行期间都在用这个 Read View,并且由于 MVCC 的支持,备份期间业务依然可以对数据进行更新操作
表级锁
MySQL里面表级别的锁有:表锁;元数据锁(MDL);意向锁;AUTO-INC锁
表锁:
表级别的共享锁(读锁) -> lock tables t_student read;
表级别的独占锁(写锁) -> lock tables t_stuent wirte;
表锁除了会限制别的线程的读写外,还会限制本线程接下来的读写操作
尽量避免在使用InnoDB引擎表时使用表锁,表锁颗粒度太大,会影响并非性能
元数据锁(MDL):
不需要显示使用MDL,当我们对数据库表进行操作时,会自动给表加上MDL
-对一张表进行 CRUD 操作时,加的是MDL读锁
-对一张表做结构变更操作时,加的是MDL写锁
MDL是为了保证当用户对表执行 CRUD 操作时,防止其他线程对表结构做变更
-当有线程执行 select 语句(加MDL读锁)的期间,若有其他线程要更改该表结构(申请MDL写锁),那么将会被阻塞,直到执行完 select 语句(释放MDL读锁)
-当有线程对表结构进行变更(加MDL写锁)的期间,若有其他线程执行 CRUD 操作(申请MDL读锁),那么就会被阻塞,直到表结构变更完成(释放MDL写锁)
MDL不需要显示调用,那么它是什么时候释放的? -> MDL在事务提交后才会释放,事务执行期间,MDL一直持有
为什么线程因为申请不到MDL写锁,会导致后续的申请读锁的查询操作会被阻塞? -> 申请MDL锁的操作会形成一个队列,队列中写锁获取优先级高于读锁
意向锁:
意向锁的目的:快速判断表里是否有记录被加锁
-在使用InnoDB引擎的表里对某些记录加上 共享锁 之前,需要先在表级别加上一个 意向共享锁
-在使用InnoDB引擎的表里对某些记录加上 独占锁 之前,需要先在表级别加上一个 意向独占锁
-若没有意向锁,那么在加独占锁时,就需要遍历所有记录,查看记录是否存在独占锁,效率很慢
-有意向锁,那么在加独占锁时,直接查看该表是否有意向独占锁,不用遍历表里的记录
意向锁是表级锁,意向锁之间不会发生冲突;会和表锁发生冲突;不会和行级锁发生冲突
AUTO-INC锁:
特殊的表锁机制,锁不再是一个事务提交后才释放,而是再执行完插入语句后就会立即释放
一个事务在持有 AUTO-INC 锁的过程中,其他事务向该表插入语句都会被阻塞,从而保证插入数据时,被AUTO_INCREMENT修饰的字段是递增的
MySQL 5.1.22 版本开始,InnoDB 存储引擎提供了一种轻量级的锁来实现自增
-当 innodb_autoinc_lock_mode = 0,就采用 AUTO-INC 锁
-当 innodb_autoinc_lock_mode = 2,就采用轻量级锁
-当 innodb_autoinc_lock_mode = 1,两锁混用,确定插入记录的数量就采用轻量级锁,不确定时就采用 AUTO-INC 锁
行级锁
行级锁分为三类:
-Record Lock(记录锁):也就是仅仅把一条记录锁上
-Gap Lock(间隙锁):锁定一个范围,不包含记录本身
-Next-Key Lock:Record Lock + Gap Lock的组合,锁定一个范围,包含记录本身
加锁规则
行级锁加锁规则较为复杂,不同场景加锁形式不同,next-key lock在一些场景下会退化为记录锁或间隙锁
不同版本的加锁规则可能不同,以下MySQL版本为8.0.26
- 唯一索引等值查询
- 查询记录存在,退化为 记录锁
- 查询记录不存在,退化为 间隙锁
- 非唯一索引等值查询
- 查询记录存在,额外加 间隙锁
- 查询记录不存在,退化为间隙锁
唯一索引和非唯一索引的范围查询的加锁规则不同之处在于
- 唯一索引在满足一些条件时,退化为 间隙锁 和 记录锁
- 非唯一索引不会退化
日志
日志的分类
MySQL有不同类型的日志,用来存储不同类型的日志:慢查询日志、通用查询日志、错误日志、二进制日志。在MySQL8之后又新增了两种日志:中继日志、数据定义语句日志
- 慢查询日志:记录所有执行时间超过long_query_time的所有查询,方便对查询进行优化
- 通用查询日志:记录索引连接的起始时间和终止时间,以及连接发送给数据库服务的所有指令,对复原操作的实际场景、发现问题、数据库操作的审计都有帮助
- 错误日志:记录MySQL服务的启动、运行或停止MySQL服务时出现的问题,方便了解服务器的状态,从而对服务器进行维护
- 二进制日志:记录所有更改数据的语句,用于主从服务器之间的数据同步、服务器遇到故障时数据的无损恢复
- 中继日志:用于主从服务器架构,从服务器用来存放主服务器二进制日志内容的一个中间件文件。从服务器通过读取中继日志的内容,来同步主服务器上的操作
- 数据定义语句日志:记录数据定义语句执行的元数据操作
除了二进制日志外,其他日志均为文本文件。默认情况下,所有日志均创建于 MySQL数据目录中
二进制日志
三种格式
binlog的三种格式:Row、Statement、Mixed
- Row:默认格式,不记录sql语句上下文相关信息,仅保存哪条记录别修改;
- 优点:row level的日志内容会非常清楚的记录下每一行数据修改的细节。而且不会出现某些特定情况下的存储过程,或function、trigger的调用和触发无法被正确复制的问题
- Statement:每条会修改数据的sql都会记录在binlog中
- 优点:不需要记录每一行的变化,减少了binlog日志量,节约IO,提高性能
- Mixed:自5.1.8版本开始被提供
写入机制
事务执行过程中,先把日志写入binlog cache,事务提交时,再把binlog cache写到binlog文件中。一个事务的binlog不能被拆开,确保一次性写入,系统将给每个线程分配一块内存作为binlog cache
write和fsync的时机,由参数sync_binlog控制,默认为0
- 0:每次提交事务都只write,由系统自行判断什么时候执行fsync。虽然性能得到提升,但是机器宕机,page cache里的binlog会丢失
- 1:每次提交事务都会执行fsync,如同redo log刷盘流程一样
- N:每次提交事务都write,但累计N个事务后才fsync
在出现I0瓶颈时,将sync_binlog设置成一个较大的值,能提升性能。同样的,若机器宕机会丢失最近N个事务的binlog日志
binlog与reolog对比
- redolog:物理日志,InnoDB存储引擎层产生,记录内容是“在某个数据页上做了什么修改”
- binlog:逻辑日志,MySQL Server层,记录内容是“语句的原始逻辑”
两阶段提交
在执行更新语句过程中,会记录redolog、binlog两块日志,以基本的事务为单位。
redolog在事务执行过程中可以不断写入,binlog只在提交事务时写入
为了解决日志之间的逻辑一致问题,InnoDB存储引擎使用两阶段提交方案
主从复制
作用
- 读写分离
- 数据备份
- 高可用性
原理
实际上主从同步的原理是基于binlog进行数据同步的。在主从复制过程中,会基于3个线程来操作,一个主库线程,两个从库线程
- 二进制日志转储线程(Binglog dump thread)是一个主库线程,当从库线程连接时,主库可以将二进制日志发送给从库,当主库读取事件时,会在Binlog上加锁,读取完成后释放锁
- 从库I/O线程连接主库,向主库发送请求更新Binlog。这时从库的I/O线程就可以读取主库的二进制日志转储线程的Binlog更新部分,并且拷贝到本地的中继日志
- 从库SQL线程会读取从库中的中继日志,并且执行日志中的事件,将从库中的数据与主库保持同步
简单来说为以下三步:
- Master将写操作记录到binlog
- Slave将Master的binlog拷贝到它的中继日志
- Slave重做中继日志中的事件,将改变应用到自己的数据库中。MySQL复制是异步且串行化的,重启后从接入点开始复制
同步数据一致性
主从同步要求
- 读库、写库的数据一致
- 写数据必须写到读库
- 读数据必须到读库
主从延迟原因
网络正常情况下,主从延迟的主要来源是备库接收完binlog和执行完这个事务之间的时间差
- 从库的机器性能比主库差
- 从库压力大
- 大事务的执行
主从延迟的直接表现:从库消费中继日志的速度比主库生产binlog的速度慢
减少主从延迟的方案
- 降低多线程大事务并发的概率,优化业务逻辑
- 优化SQL,避免慢SQL,减少批量操作,建议写脚本以update-sleep这样的形式完成
- 提高从库机器配置,减少主库写binlog和从库读binlog的效率差
- 尽量采用短的链路,也就是主库和从库服务器的距离尽量短,提升端口带宽,减少binlog传输的网络延时
- 实时性要求的业务强制走主库,从库只做灾备、备份
数据一致性问题的解决
若操作的数据存储在同一个数据库中,那么对数据进行更新时,可对记录加写锁,这样在读取时就不会发生数据不一致的情况,但从库的作用仅为备份,未起到读写分离、分担主库读压力的作用
读写分离情况下,解决主从同步中数据不一致的问题,就是解决主从之间数据复制方式的问题。若按照数据一致性的从若到强划分,有3中复制方式:异步复制、半同步复制、组复制
异步复制
半同步复制
组复制
异步复制、半同步复制都无法最终保证数据一致性问题
组复制技术,MRG(MySQL Group Replication),于MySQL在5.7.17推出的一种新的数据复制技术,基于Paxos协议的状态机复制
MGR如何工作?
将多个节点共同组成一个复制组,在执行读写(RW)事务时,需要通过一致性协议层同样,当同意节点数量大于(N/2+1)时才可进行提交,针对只读(RO)事务则不需要组内同意,直接COMMIT即可
在一个复制组内有多个节点组成,它们各自维护了自己的数据副本,并且在一致性协议层实现了原子消息和全局有序消息,从而保证组内数据一致性
参考资料
MySQL数据库教程天花板,mysql安装到mysql高级,强!硬!_哔哩哔哩_bilibili