MySQL 基础架构
其实图相对重要一些,一些相关题目可以记忆这个图进行回答
Mysql分为Server层和存储引擎层两部分。
- 连接器: 身份认证和权限相关
- 查询缓存: 执行查询语句的时候,会先查询缓存(命中概率低,8.0移除)
- 分析器: 进行词法分析和语法分析,检查是否正确以及进行鉴权
- 优化器: 决定选择使用最优方案,例如匹配索引,多表关联(join)的连接顺序
- 执行器: 执行语句,然后从存储引擎返回数据。 (会对实际运行的表进行鉴权)
- 插件式存储引擎:主要负责数据的存储和读取,支持 InnoDB、MyISAM、Memory 等多种存储引擎。
1. 查询语句在Mysql 的执行过程
|
|
- 连接数据库
- 在 MySQL8.0 版本以前,会先查询缓存,以这条 SQL 语句为 key 在内存中查询是否有结果。(在工程实现中,在查询缓存返回结果之前,做权限校验)。
- 通过分析器进行词法分析,提取 SQL 语句的关键元素,比如提取上面这个语句是查询 select,提取需要查询的表名为 tb_student,需要查询所有的列,查询条件是这个表的 id=‘1’。然后判断这个 SQL 语句是否有语法错误,比如关键词是否正确等等,如果检查没问题就执行下一步。(注:在词法分析阶段,会进行 precheck 验证权限,判断是否有权限。)
- 优化器进行确定执行方案,上面的 SQL 语句,可以有两种执行方案:a.先查询学生表中姓名为“张三”的学生,然后判断是否年龄是 18。b.先找出学生中年龄 18 岁的学生,然后再查询姓名为“张三”的学生。那么优化器根据自己的优化算法进行选择执行效率最好的一个方案(优化器认为,有时候不一定最好)。那么确认了执行计划后就准备开始执行了。
- 在执行器过程,会先进行权限校验,然后会调用数据库引擎接口,返回引擎的执行结果。
2. 更新语句在Mysql 的执行过程
|
|
更新语句会沿着查询的流程走,与查询语句不同的是执行更新时候,会记录日志。
- 执行器会找到目标行数据。如果目标行所在的数据页在内存(Buffer Pool )中,就会直接返回执行器,否则需要从磁盘读入内存,再返回。
- 执行器拿到引擎给的行数据,修改对应字段值,得到新的一行数据,再调用引擎接口写入这行新数据。
- 引擎将这行新数据更新到内存中,同时将这个更新操作记录到 redo log 里面,此时 redo log 处于 prepare 状态。然后告知执行器执行完成了,随时可以提交事务。
- 执行器生成这个操作的 binlog,并把 binlog 写入磁盘。
- 执行器调用引擎的提交事务接口,引擎把刚刚写入的 redo log 改成提交(commit)状态,更新完成。
5. 什么是数据页的空洞,如何解决?
在InnoDB引擎中,删除某一行数据,会把这个一行数据标记为删除,表明可以复用。行数据的复用,只限于符合范围条件的数据。当你随机删除过多,会造成一个数据页中存在很多可以复用但是没有被使用的地方,称之为空洞。
不止是删除数据会造成空洞,插入数据也会。
当把一个数据页上的所有数据都删除,表明数据页也可以被复用,但是表空间不会回收。
解决方案:使用Online DDL方式重建表
重建表的流程:
- 建立一个临时文件,扫描表 A 主键的所有数据页;
- 用数据页中表 A 的记录生成 B+ 树,存储到临时文件中;
- 生成临时文件的过程中,将所有对 A 的操作记录在一个日志文件(row log)中;
- 临时文件生成后,将日志文件中的操作应用到临时文件,得到一个逻辑数据上与表 A 相同的数据文件;
- 用临时文件替换表 A 的数据文件。
6. 在 select count(?) from t 这样的查询语句里面,count(*)、count(主键 id)、count(字段) 和 count(1) 等不同用法的性能,有哪些差别。
- count(主键 id) :InnoDB 引擎会遍历整张表,把每一行的 id 值都取出来,返回给 server 层。server 层拿到 id 后,判断是不可能为空的,就按行累加。
- count(1):InnoDB 引擎遍历整张表,但不取值。server 层对于返回的每一行,往count函数放一个数字“1”进去,判断是不可能为空的,按行累加。
- count(字段) :【如果没有索引,走主键索引】
- 如果这个“字段”是定义为 not null 的话,一行行地从记录里面读出这个字段,判断不能为 null,按行累加;
- 如果这个“字段”定义允许为 null,那么执行的时候,判断到有可能是 null,还要把值取出来再判断一下,不是 null 才累加。
- count(*):不需要取值,按行累加。【可能走最小的索引】
按照效率排序的话,count(字段)<count(主键id)<count(1) ≈ count(*)
7.order by 中是如何排序的吗?
有这样一个sql语句:
|
|
这个语句执行流程如下所示 :
- 初始化 sort_buffer,确定放入 name、city、age 这三个字段;
- 从索引 city 找到第一个满足 city=‘杭州’条件的主键 id,也就是图中的 ID_X;
- 到主键 id 索引取出整行,取 name、city、age 三个字段的值,存入 sort_buffer 中;
- 从索引 city 取下一个记录的主键 id;
- 重复步骤 3、4 直到 city 的值不满足查询条件为止,对应的主键 id 也就是图中的 ID_Y;
- 对 sort_buffer 中的数据按照字段 name 做快速排序;按照排序结果取前 1000 行返回给客户端。、
MySQL 会给每个线程分配一块内存用于排序,称为 sort_buffer。
也称之为全字段排序(把所需字段全放入 sort_buffer中)。
对于 InnoDB 表来说,执行全字段排序会减少磁盘访问,因此会被优先选择。
8. 全字段排序有什么性能限制吗?
排序,可能在内存中完成,也可能需要使用外部排序,这取决于排序所需的内存和参数 sort_buffer_size。
排序的数据量小于 sort_buffer_size,排序就在内存中完成。但如果排序数据量太大,内存放不下,则需要利用磁盘临时文件辅助排序。
外部排序一般使用多路归并排序算法。
如果查询要返回的字段很多的话,那么 sort_buffer 里面要放的字段数太多,这样内存里能够同时放下的行数很少,要分成很多个临时文件,排序的性能会很差。
9. 如何优化全字段排序带来的性能限制?
增大
sort_buffer_size
:尽量提高内存中的排序缓冲区大小,使得内存能够容纳更多的行,减少生成临时文件的数量。减少查询中返回的字段数量:查询时只返回必要的字段(使用
SELECT
时避免SELECT *
),减少单行数据占用的空间,从而让内存能够存储更多行。构建联合\覆盖索引:让结果是有序的,减少排序过程。
让Mysql 采用另一种排序方法:rowId 排序。
10. rowid 排序是如何执行的?
max_length_for_sort_data,是 MySQL 中专门控制用于排序的行数据的长度的一个参数。如果单行的长度超过这个值,MySQL 就认为单行太大,要换一个算法。
整个执行流程:
- 初始化 sort_buffer,确定放入两个字段,即 name 和 id;
- 从索引 city 找到第一个满足 city=‘杭州’条件的主键 id,也就是图中的 ID_X;
- 到主键 id 索引取出整行,取 name、id 这两个字段,存入 sort_buffer 中;
- 从索引 city 取下一个记录的主键 id;
- 重复步骤 3、4 直到不满足 city=‘杭州’条件为止,也就是图中的 ID_Y;
- 对 sort_buffer 中的数据按照字段 name 进行排序;遍历排序结果,取前 1000 行,并按照 id 的值回到原表中取出 city、name 和 age 三个字段返回给客户端。
与全字段排序的区别:
- sort_buffer 只放入和排序相关的字段
- 多了一次回表的过程,一般优化器会选择全字段排序。
如果 MySQL 认为排序内存太小,会采用 rowid 排序算法,如果 MySQL 认为内存足够大,会优先选择全字段排序。
11.目前你返回的数据行数较多,如果返回行数较少,那么会使用什么排序?
在 MySQL 中,排序会优先尝试使用内存:
- 当需要排序的数据量小于
sort_buffer_size
时,MySQL 会将数据加载到内存中并直接进行排序(通常使用快速排序); - 如果数据量大于
sort_buffer_size
,MySQL 会将数据分批加载到内存中,执行部分排序后写入临时文件,最后通过外部归并排序完成整体排序。 - 如果单行大于
max_length_for_sort_data
,Mysql将会使用rowid的排序。 - 对于
ORDER BY ... LIMIT N
的场景,MySQL 可能使用堆排序(优先队列)来优化性能,动态维护前 N 条记录,从而避免完全排序。 - 临时文件的使用则是基于内存不足时存储中间结果的需要。
12. order by rand() 是如何执行的?
- 读取数据集:MySQL 从存储引擎中读取目标表中的所有符合条件的行(如果没有
WHERE
条件,则读取整个表)。 - 生成随机值:对于每一行,调用
RAND()
函数生成一个随机值,并将随机值与该行的数据关联存储在内存或临时表中。 - 排序:使用内存中的随机值作为排序键,对数据进行排序。如果数据量过大而无法全部放入内存,则使用磁盘上的临时文件进行外部排序。
- 返回结果:根据排序后的数据返回结果。如果指定了
LIMIT
,则只返回前 N 条数据;否则返回排序后的所有行。
注意事项:
ORDER BY RAND()
对所有行生成随机数并排序,导致 CPU 和内存消耗较大。当表很大时,会严重影响性能。
优化建议:
- 预先选取随机主键范围(如
WHERE id >= FLOOR(RAND() * max_id)
)结合LIMIT
。 - 使用应用层随机化,而非在 SQL 层执行。
其他问题
- 两个日志的区别
- 两阶段执行过程
数据库事务
1. 介绍事务的特性
数据库事务可以保证多个对数据库的操作(也就是 SQL 语句)构成一个逻辑上的整体。简单说:要么全部执行成功,要么全部不执行 。
具有ACID特性:
原子性(
Atomicity
):事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;一致性(
Consistency
):执行事务前后,数据保持一致,例如转账业务中,无论事务是否成功,转账者和收款人的总额应该是不变的;隔离性(
Isolation
):并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的;持久性(
Durability
):一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。
注意:只有保证了事务的持久性、原子性、隔离性之后,一致性才能得到保障。也就是说 A、I、D 是手段,C 是目的!
- 持久性是通过 redo log (重做日志)来保证的;
- 原子性是通过 undo log(回滚日志) 来保证的;
- 隔离性是通过 MVCC(多版本并发控制) 或锁机制来保证的;
- 一致性则是通过持久性+原子性+隔离性来保证;
2. 并发事务带来的问题
- 脏读(Dirty read):读取到其他事务未提交的数据。
- 丢失修改(Lost to modify):指两个或多个事务同时对同一数据进行更新操作,其中一个事务的更新被另一个事务覆盖,导致前一个事务的修改丢失。
- 不可重复读(Unrepeatable read):在一个事务中,前后读取的记录内容不一致;
- 幻读(Phantom read):在一个事务中,前后读取的记录数量不一致。
3. 不同事务隔离级别的区别
READ-UNCOMMITTED(读取未提交) :可以读取尚未提交的数据变更。
READ-COMMITTED(读取已提交) :可以读取其他并发事务已经提交的数据。
REPEATABLE-READ(可重复读) :对同一字段的多次读取结果都是一致的。
SERIALIZABLE(可串行化) :加读写锁,保证所有的事务依次逐个执行。
在不同隔离级别下可能发生的问题:
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交 | √ | √ | √ |
读提交 | × | √ | √ |
可重复读 | × | × | √ |
串行化 | × | × | × |
4. 事务隔离的可见性实现
在实现上,数据库里面会创建一个视图,访问的时候以视图的逻辑结果为准。
- 在“可重复读”隔离级别下,这个视图是在事务启动时创建的,整个事务存在期间都用这个视图。
- 在“读提交”隔离级别下,这个视图是在每个 SQL 语句开始执行的时候创建的。
- “读未提交”隔离级别下直接返回记录上的最新值,没有视图概念;
- 而“串行化”隔离级别下直接用加锁的方式来避免并行访问。
5. 长事务的弊病
长事务意味着系统里面会存在很老的事务视图,在这个事务提交之前,回滚记录都要保留,这会导致大量占用存储空间。除此之外,长事务还占用锁资源,可能会拖垮库。
6. MVCC是什么
MVCC 是一种并发控制机制,用于在多个并发事务同时读写数据库时保持数据的一致性和隔离性。
读操作:当一个事务执行读操作时,它会使用快照读取。快照读取是基于事务开始时数据库中的状态创建的,因此事务不会读取其他事务尚未提交的修改。
写操作:当一个事务执行写操作时,它会生成一个新的数据版本,事务提交后将修改后的数据写入数据库。
为了防止数据库中的版本无限增长,MVCC 会定期进行版本的回收。回收机制会删除已经不再需要的旧版本数据,从而释放空间。
7.MVVC中的快照是如何实现的
InnoDB 里面每个事务有一个唯一的事务 ID,叫作 transaction id。它是在事务开始的时候向 InnoDB 的事务系统申请的,是按申请顺序严格递增的。
数据表中的一行记录,有多个版本 (row),每个版本有自己的 row trx_id,是transaction id进行赋值的。
当一个事务修改表中数据的某一行时,将旧版本的数据插入 Undo Log 中,看到的视图不是物理上物理上真实存在的,而是每次需要的时候根据当前版本和 undo log 计算出来的。比如,需要 V2 视图的时候,就是通过 V4 依次执行 U3、U2 算出来。根据 row trx_id、undo log这些信息,实现了数据行的多版本效果。
在 InnoDB
存储引擎中,创建一个新事务后,执行每个 select
语句前,都会创建一个快照(Read View),快照中保存了当前数据库系统中正处于活跃(没有 commit)的事务的 ID 号(即 m_ids)。
Read View 存在两个变量:
- m_up_limit_id:数组里面事务 ID 的最小值
- m_low_limit_id :当前系统里面已经创建过的事务 ID 的最大值
当用户在这个事务中要读取某个记录行的时候,InnoDB
会将该记录行的 row TRX_ID
与 Read View
中的这两个变量 进行比较,判断是否满足可见性条件,不满足就回滚。
一个数据版本的 row trx_id,有以下几种可能:
- 如果落在绿色部分,表示这个版本是已提交的事务或者是当前事务自己生成的,这个数据是可见的;
- 如果落在红色部分,表示这个版本是由将来启动的事务生成的,是肯定不可见的;
- 如果落在黄色部分,那就包括两种情况
- a. 若 row trx_id 在数组中,表示这个版本是由还没提交的事务生成的,不可见;
- b. 若 row trx_id 不在数组中,表示这个版本是已经提交了的事务生成的,可见。
核心简写:
- 版本未提交,不可见;
- 版本已提交,但是是在视图创建后提交的,不可见;
- 版本已提交,而且是在视图创建前提交的,可见。
总结:
- row trx_id 和 transaction id 给每个数据行提供版本号
- undo log 提供版本链,帮助回滚到需要的数据
- 该记录行的
row TRX_ID
与Read View
中的这两个变量 m_up_limit_id 和 m_low_limit_id 进行比较,判断是否满足可见性条件,不满足就回滚。
8. 一致性读和当前读是什么
一致性读是指事务在开启视图时候,直至提交之前,读行数据始终保持一致。主要用到MVCC(多版本并发控制) 技术,在事务中,查询语句不会看到其他事务未提交以及以后事务的更改。
当前读是指读取行数据的最新版本,通过给行数据加Next-key Lock锁来保证的。如果当前的行数据的Next-key Lock锁被其他事务占用的话,就需要进入锁等待。在事务中执行select…for update/lock in share mode、insert、update、delete 等都是当前读。
9.幻读的定义以及幻读有什么问题?
幻读是当一个事务在执行某个范围查询时,比如使用SELECT ... WHERE
语句,第一次查询返回了一些满足条件的行,但在事务继续执行的过程中,另一个事务插入了一些新的符合查询条件的行,导致第二次相同的查询返回的结果集比第一次更多。
关键点在于插入,导致的结果集的不同。
其实会导致数据不一致的问题,实际上,binlog的结果和实际表不一致。一致性指的是数据库内部数据状态在此刻的一致性,还包含了数据和日志在逻辑上的一致性。
10.如何解决幻读
解决幻读的方式有很多,但是它们的核心思想就是一个事务在操作某张表数据的时候,另外一个事务不允许新增或者删除这张表中的数据了。
- 在可重复读隔离级别,引入
Next-key Lock(Record Lock+Gap Lock)
- 在读提交隔离级别,需要把 binlog 格式设置为 row,解决可能出现的数据和日志不一致问题,
- 在可重复读的事务级别下,给事务操作的这张表添加表锁。
其他问题
参考文献:
索引
1. 哈希表、有序数组和搜索树的优缺点
- 哈希表是一种以键值对存储数据的结构。适用于等值查询的场景,例如NoSQL引擎,区间查询的速度很慢。
- 有序数组在等值查询和范围查询的场景中性能优秀,但是在更新数据上,成本太高,只适用于静态存储引擎。
- 搜索树的特点是父节点左子树所有结点的值小于父节点的值,右子树所有结点的值大于父节点的值。二叉树树高过高,访问多个数据块,磁盘随机读取数据块过于耗时,会采用N叉树。N取决于数据块的大小。以InnoDB 的一个整数字段索引为例,在一个节点(页)中,这个 N 差不多是 1200。
2.主键索引和普通索引是什么,在查询中的区别是什么
在 InnoDB 中,表都是根据主键顺序以索引的形式存放的,每一个索引在 InnoDB 里面对应一棵 B+ 树。
主键索引的叶子节点存的是整行数据。在 InnoDB 里,主键索引也被称为聚簇索引(clustered index)。
非主键索引的叶子节点内容是主键的值。在 InnoDB 里,非主键索引也被称为二级索引(secondary index)。
在查询过程中,基于非主键索引的查询需要回表(到主键索引树在搜索一次),相比之下多扫描一颗索引树。
3. 主键不是有序的会带来什么问题,自增主键有什么优势,有没有什么场景适合用业务字段直接做主键的呢?
B+ 树为了维护索引有序性,在插入新值的时候需要做必要的维护。当页满了,根据 B+ 树的算法,需要申请一个新的数据页,然后挪动部分数据过去,这个过程称为页分裂。页分裂操作会影响性能也会影响数据页的利用率。
- 性能角度:自增主键的插入数据模式,正符合了递增插入的场景。每次插入一条新记录,都是追加操作,都不涉及到挪动其他记录,也不会触发叶子节点的分裂。而有业务逻辑的字段做主键,则往往不容易保证有序插入,这样写数据成本相对较高。
- 存储空间:主键长度越小,普通索引的叶子节点就越小,普通索引占用的空间也就越小。
在场景是只有一个索引;该索引必须是唯一索引(KV场景),适合用业务字段直接做主键。
4. 一条Sql查询语句,会扫描多少行以及几次树的搜索操作
|
|
这条 SQL 查询语句的执行流程:
- 在 k 索引树上找到 k=3 的记录,取得 ID = 300;
- 再到 ID 索引树查到 ID=300 对应的 R3;
- 在 k 索引树取下一个值 k=5,取得 ID=500;
- 再回到 ID 索引树查到 ID=500 对应的 R4;
- 在 k 索引树取下一个值 k=6,不满足条件,循环结束。
5. 说一下覆盖索引和联合索引
如果一个索引包含(或者说覆盖)所有需要查询的字段的值,(核心是无需回表查询),就称之为 覆盖索引(Covering Index) 。
由于覆盖索引可以减少树的搜索次数,显著提升查询性能。
使用表中的多个字段创建索引,就是 联合索引。
6. 最左前缀原则详细介绍
最左前缀匹配原则指的是在使用索引时候,查询条件满足左前缀条件,可以利用索引加速检索。最左前缀可以是联合索引的最左 N 个字段,也可以是字符串索引的最左 M 个字符。
对于一个联合索引 (A, B, C)
:
- 索引能被以下查询利用:
WHERE A = ?
WHERE A = ? AND B = ?
WHERE A = ? AND B = ? AND C = ?
最左匹配原则会一直向右匹配,直到遇到范围查询(如 >、<)为止。对于 >=、<=、BETWEEN 以及前缀匹配 LIKE 的范围查询,不会停止匹配。
7. 如何安排索引内的字段顺序
如果通过调整顺序,可以帮助少维护一个索引。
可以将区分度高的字段放在最左边,这也可以过滤更多数据。
考虑空间。
8. 索引下推详细介绍
索引下推优化(index condition pushdown)是指在索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数,提高查询效率。
|
|
9. 普通索引和唯一索引在查询和更新有什么区别
在查询过程中:
- 对于普通索引来说,查找到满足条件的第一个记录 (5,500) 后,需要查找下一个记录,直到碰到第一个不满足 k=5 条件的记录。
- 对于唯一索引来说,由于索引定义了唯一性,查找到第一个满足条件的记录后,就会停止继续检索。
查询带来的性能差距是微乎其微。
在更新过程中:
第一种情况是,这个记录要更新的目标页在内存中。
- 对于唯一索引来说,找到 3 和 5 之间的位置,判断到没有冲突,插入这个值,语句执行结束;
- 对于普通索引来说,找到 3 和 5 之间的位置,插入这个值,语句执行结束。
在该情况下,性能差别很小。
第二种情况是,这个记录要更新的目标页不在内存中。
- 对于唯一索引来说,需要将数据页读入内存,判断到没有冲突,插入这个值,语句执行结束;
- 对于普通索引来说,则是将更新记录在 change buffer,语句执行就结束了。
因为将数据从磁盘读入内存涉及随机 IO 的访问,是成本比较高的操作,对于唯一索引就需要不断地将数据页读入内存,然后change buffer 因为减少了随机磁盘访问,所以对更新性能的提升是会很明显的。
10. change Buffer 是什么?
当某个行数据所在的数据页还没有在内存中的话,在不影响数据一致性的前提下,InnoDB 会将这些更新操作缓存在 change buffer 中,这样就不需要从磁盘中读入这个数据页了,后续会将里面操作应用在原数据页中。
当访问这个数据页(将数据页读入内存中)会触发 merge 操作,在写多读少的业务场景中,在一个数据页做 merge 之前,change buffer 记录的变更越多(也就是这个页面上要更新的次数越多),使用效果越好。但是在写入之后马上会做查询场景中,会触发change buffer的merge 过程,增加了 change buffer 的维护代价。
11. change buffer 和 redo log 都是减少随机读写,那么之间区别是什么
redo log 主要节省的是随机写磁盘的 IO 消耗(转成顺序写)(将内存的数据写入磁盘中)
change buffer 主要节省的则是随机读磁盘的 IO 消耗。(主要是将更新动作变缓,减少读入数据页的过程,)
12. 当机器掉电重启,会不会导致 change buffer 丢失呢,会不会发生数据丢失情况?
不会丢失。
Change Buffer 的内容不仅仅在内存中也会持久化,同时Change Buffer 的修改操作(如插入、更新或删除)也会被记录到 Redo Log 中。
在数据库掉电恢复时会重放 Redo Log,恢复未完成的事务和未刷入磁盘的修改,包括 Change Buffer 的修改。如果 Change Buffer 中有未完成的合并操作,InnoDB 会通过后台线程继续执行这些合并,将修改逐步应用到目标数据页。
13. 前缀索引优缺点
优点:
- 使用前缀索引和字段区分度有很大的关系,区分度越高越好。
- 前缀索引可以对字符串的前缀构建索引,定义好前缀长度,就可以做到既节省空间,又不用额外增加太多的查询成本。
缺点:
- 使用前缀索引就用不上覆盖索引对查询性能的优化
- 前缀索引选取字段长度的越长,占用的磁盘空间就越大,相同的数据页能放下的索引值就越少,搜索的效率也就会越低。
提高前缀索引搜索效率方式:
- 对字符串字段进行处理使其区分度增加,例如倒叙、截断等;
- 使用hash字段;
锁
全局锁是什么,在哪些场景下使用。
全局锁就是对整个数据库实例加锁。全局锁的典型使用场景是,做全库逻辑备份。
使用风险:
- 如果在主库备份,在备份期间不能更新,业务停摆
- 如果在从库备份,备份期间不能执行主库同步的binlog,导致主从延迟
对于InnoDB引擎,可以通过在可重复读隔离级别下开启一个事务,获取数据,在这个过程中数据是可以正常更新的。
官方自带的逻辑备份工具是 mysqldump。当 mysqldump 使用参数–single-transaction 的时候,导数据之前就会启动一个事务,来确保拿到一致性视图。(只适用于支持事务引擎)
表级别的锁有哪些?
MySQL 里面表级别的锁有两种:一种是表锁,一种是元数据锁(meta data lock,MDL)。
表锁会限制别的线程的读写外,也可能限定了本线程的读写操作,一般用的少,影响面太大了。
MDL主要用于隔离DML(Data Manipulation Language,数据操纵语言,如select)和DDL(Data Definition Language,数据定义语言,如改表头新增一列)操作之间的干扰,保证对表数据读写正确。
当对一个表做增删改查操作的时候,加 MDL 读锁;当要对表做结构变更操作的时候,加 MDL 写锁。
元数据锁在什么情况下会发生阻塞?
当有线程在执行 select 语句( 加 MDL 读锁)的期间,如果有其他线程要更改该表的结构( 申请 MDL 写锁),那么将会被阻塞,直到执行完 select 语句( 释放 MDL 读锁)。
反之,当有线程对表结构进行变更( 加 MDL 写锁)的期间,如果有其他线程执行了 CRUD 操作( 申请 MDL 读锁),那么就会被阻塞,直到表结构变更完成( 释放 MDL 写锁)。
MDL 是在事务提交后才会释放,这意味着事务执行期间,MDL 是一直持有的。
如果长事务存在DDL操作,后面有很多DML操作发生阻塞,导致线程爆满。
为了能安全的对表结构进行变更,在对表结构变更前,先要看看数据库中的长事务,是否有事务已经对表加上了 MDL 读锁,如果可以考虑设置超时或者 kill 掉这个长事务,然后再做表结构的变更。
意向锁是什么,干什么用的?
意向锁是一个表级锁,为了支持 InnoDB 的多粒度锁,它解决的是表锁和行锁共存的问题。
- 意向共享锁(Intention Shared Lock,IS 锁):事务有意向对表中的某些记录加共享锁(S 锁),加共享锁前必须先取得该表的 IS 锁。
- 意向排他锁(Intention Exclusive Lock,IX 锁):事务有意向对表中的某些记录加排他锁(X 锁),加排他锁之前必须先取得该表的 IX 锁。
也就是,当执行插入、更新、删除操作,需要先对表加上「意向独占锁」,然后对该记录加独占锁。
当我们需要给一个表加表锁的时候,我们需要根据去判断表中有没有数据行被锁定,以确定是否能加成功。
假如没有意向锁,那么我们就得遍历表中所有数据行来判断有没有行锁;
有了意向锁这个表级锁之后,则我们直接判断一次就知道表中是否有数据行被锁定了。因为意向锁会和表锁互斥。
意向锁之间是互相兼容的。
IS 锁 | IX 锁 | |
---|---|---|
IS 锁 | 兼容 | 兼容 |
IX 锁 | 兼容 | 兼容 |
意向锁和共享锁和排它锁互斥(这里指的是表级别的共享锁和排他锁,意向锁不会与行级的共享锁和排他锁互斥)。
IS 锁 | IX 锁 | |
---|---|---|
S 锁 | 兼容 | 互斥 |
X 锁 | 互斥 | 互斥 |
行锁是什么,有哪些,
行锁就是针对数据表中行数据的锁,主要有三种:
- Record Lock,记录锁,也就是仅仅把一条记录锁上;
- Gap Lock,间隙锁,锁定一个范围,但是不包含记录本身;
- Next-Key Lock:Record Lock + Gap Lock 的组合,锁定一个范围,并且锁定记录本身。
- 插入意向锁:表示了一种插入意图,即当多个不同的事务,同时往同一个索引的同一个间隙中插入数据的时候,它们互相之间无需等待,即不会阻塞。
Record Lock 称为记录锁,锁住的是一条记录。而且记录锁是有 S 锁和 X 锁之分的:
S 锁 | X 锁 | |
---|---|---|
S 锁 | 不冲突 | 冲突 |
X 锁 | 冲突 | 冲突 |
由于 MVCC 的存在,对于一般的 SELECT
语句,InnoDB 不会加任何锁。不过, 你可以通过以下语句显式加共享锁或排他锁。
|
|
Gap Lock:为了解决幻读问题,InnoDB 只好引入新的锁,也就是间隙锁 。间隙锁锁住了两个值的间隙,不包括记录本身,防止进行插入操作。间隙锁之间都不存在冲突关系。
next-key lock:间隙锁+行锁,锁定一个范围,包含记录本身,是左开右闭的区间。
插入意向锁:如果说间隙锁锁住的是一个区间,那么「插入意向锁」锁住的就是一个点。因而从这个角度来说,插入意向锁确实是一种特殊的间隙锁。插入意向锁和间隙锁之间是冲突的。
两阶段协议是什么?
两阶段锁协议:在 InnoDB 事务中,行锁是在需要(更新/插入行数据)的时候才加上的,需要等到事务结束时才释放。
如果你的事务中需要锁多个行,要把最可能造成锁冲突、最可能影响并发度的锁尽量往后放,以减少锁等待的时间,提高并发性能。
select for update / update 等具有加锁性质的语句加锁有什么需要注意的
当我们执行 select for update / update 语句时,实际上是会对记录加锁带基本单位是 next-key 锁,加锁的位置准确的说,锁是加在索引上的而非行上。如果其他事务对持有锁的记录进行修改时是会被阻塞的。另外,这个锁并不是执行完 update 语句就会释放的,而是会等事务结束时才会释放。
在 select for update / update 语句的查询条件没有使用索引,就会全表扫描,于是就会对所有记录加上 next-key 锁(记录锁 + 间隙锁),相当于把整个表锁住了。
核心在于:这条语句在执行过程种,优化器最终选择的是全表扫描,那么就会对全表的记录进行加锁。
如何避免执行加锁性质的语句锁住全表事故的发生?
当 sql_safe_updates 设置为 1 时。update 语句必须满足如下条件之一才能执行成功:
- 使用 where,并且 where 条件中必须有索引列;
- 使用 limit;
- 同时使用 where 和 limit,此时 where 条件中可以没有索引列;
delete 语句必须满足以下条件能执行成功:
- 同时使用 where 和 limit,此时 where 条件中可以没有索引列;
5.死锁是什么,如何解决?
当并发系统中不同线程出现循环资源依赖,涉及的线程都在等待别的线程释放资源时,就会导致这几个线程都进入无限等待的状态,称为死锁。
死锁的四个必要条件:互斥、占有且等待、不可强占用、循环等待。只要系统发生死锁,这些条件必然成立,但是只要破坏任意一个条件就死锁就不会成立。
当出现死锁以后,有两种策略通过「打破循环等待条件」来解除:
- 一种策略是,直接进入等待,直到超时。
- 另一种策略是,发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。
对于业务来说,第一种策略是不可取的,时间太短,会出现很多误伤,时间太长影响体验。
正常情况下采用第二种策略,能够快速发现并进行处理的,但是它也是有额外负担的。当较多线程更新同一行,死锁检测会消耗大量的 CPU 资源。
6.怎么解决由这种热点行更新导致的性能问题呢?
高并发下避免死锁检测带来的负面影响:
- 确保业务上不会产生死锁,直接将死锁检测关闭。(innodb 自带死锁检测)
- 在数据库中间件中统一对更新同一行的请求进行排队,控制并发度。
- 业务逻辑上进行优化,将一行数据分解成多行,降低写入压力。
9. 加锁的原则
原则 1:加锁的基本单位是 next-key lock。next-key lock 是前开后闭区间,具体执行的时候,是要分成间隙锁和行锁两段来执行的。
原则 2:查找过程中访问到的对象才会加锁。加锁是加在索引上的。
优化 1:索引上的等值查询,给唯一索引加锁的时候,next-key lock 退化为行锁。(在唯一的,所以退化成行锁)
优化 2:索引上的等值查询,向右遍历时且最后一个值不满足等值条件的时候,next-key lock 退化为间隙锁。(因为最后一个值不满足,所以可以不加锁)
与锁相关的:06、07、20、21、30、39、40
内存
高可用
Mysql主备的基本流程
主库接收到客户端的更新请求后,执行内部事务的更新逻辑,同时写 binlog。
备库 B 跟主库 A 之间维持了一个长连接。主库 A 内部有一个线程,专门用于服务备库 B 的这个长连接。一个事务日志同步的完整过程是这样的:
- 在备库 B 上通过 change master 命令,设置主库 A 的 IP、端口、用户名、密码,以及要从哪个位置开始请求 binlog,这个位置包含文件名和日志偏移量。
- 在备库 B 上执行 start slave 命令,这时候备库会启动两个线程,就是 io_thread 和 sql_thread。其中 io_thread 负责与主库建立连接。
- 主库 A 校验完用户名、密码后,开始按照备库 B 传过来的位置,从本地读取 binlog,发给 B。
- 备库 B 拿到 binlog 后,写到本地文件,称为中转日志(relay log)。
- sql_thread 读取中转日志,解析出日志里的命令,并执行。
对于主备库为双M结构,互为主备关系,会发生什么问题?怎么解决?
会发生循环复制的问题。
业务逻辑在节点 A 上更新了一条语句,然后再把生成的 binlog 发给节点 B,节点 B 执行完这条更新语句后也会生成 binlog。节点 A 同时是节点 B 的备库,相当于又把节点 B 新生成的 binlog 拿过来执行了一次,然后节点 A 和 B 间,会不断地循环执行这个更新语句,也就是循环复制了。
解决方案:
- 规定两个库的 server id 必须不同,如果相同,则它们之间不能设定为主备关系;
- 一个备库接到 binlog 并在重放的过程中,生成与原 binlog 的 server id 相同的新的 binlog;
- 每个库在收到从自己的主库发过来的日志后,先判断 server id,如果跟自己的相同,表示这个日志是自己生成的,就直接丢弃这个日志。
主备延迟的原因
- 有些部署条件下,备库所在机器的性能要比主库所在的机器性能差。
- 解决方案:主备库选用相同规格的机器,并且做对称部署,
- 备库的压力大。
- 解决方案:一主多从。
- 大事务
- 备库的并行复制能力。
日志篇
redolog
为什么需要 redo log
redo log
是为了保证数据库的 crash-safe
能力。
当数据库发生异常(如宕机或掉电)时,redo log
能够帮助恢复已提交但尚未完全持久化到数据文件的事务,确保数据一致性并减少数据丢失。
什么是 redo log
redo log
是 InnoDB 存储引擎实现的物理日志,用于记录对数据页的修改操作。它的关键特性包括:
记录内容: 记录某个表空间中某数据页特定位置的修改,例如对表空间
XXX
中数据页YYY
偏移量ZZZ
的更新AAA
。固定大小,循环写入: 默认大小为 4 GB,由多个日志文件组成,循环写入。
write pos
(写指针):记录当前写入的位置,随着写入不断推进,循环至日志文件的开头。checkpoint
(检查点指针):记录当前可以被擦除的位置。在擦除之前,日志内容必须已应用到数据文件中。
优点:
- 实现事务的持久性,让 MySQL 有 crash-safe 的能力,能够保证 MySQL 在任何时间段突然崩溃,重启后之前已提交的记录都不会丢失;
- 将写操作从「随机写」变成了「顺序写」,提升 MySQL 写入磁盘的性能。
binlog
是server层实现,是逻辑日志,记录的是语句的原始逻辑。比如“给 ID=2 这一行的 c 字段加 1 ”
是可以追加写入的。“追加写”是指 binlog 文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。
主要用于备份恢复,主从恢复,增量备份。
- 数据恢复:在数据库发生故障时,通过 binlog 可以进行数据的恢复。
- 主从复制:在主从架构中,binlog 用于同步主库的操作到从库。
- 增量备份:binlog 支持记录数据库的增量变化,便于在全量备份的基础上快速恢复最新数据。
undolog
undo log 是一种用于撤销回退的日志。在事务没提交之前,MySQL 会先记录更新前的数据到 undo log 日志文件里面,当事务回滚时,可以利用 undo log 来进行回滚。【可以理解成一种版本链】
它保证了事务的 ACID 特性 (opens new window)中的原子性(Atomicity)。
在发生回滚时,就读取 undo log 里的数据,然后做原先相反操作。比如当 delete 一条记录时,undo log 中会把记录中的内容都记下来,然后执行回滚操作的时候,就读取 undo log 里的数据,然后进行 insert 操作。
针对 delete 操作和 update 操作会有一些特殊的处理:
- delete操作实际上不会立即直接删除,而是将delete对象打上delete flag,标记为删除,最终的删除操作是purge线程完成的。
- update分为两种情况:update的列是否是主键列。
- 如果不是主键列,在undo log中直接反向记录是如何update的。即update是直接进行的。
- 如果是主键列,update分两部执行:先删除该行,再插入一行目标行。
undo log 还有一个作用,通过 ReadView + undo log 实现 MVCC(多版本并发控制)。
对于「读提交」和「可重复读」隔离级别的事务来说,它们的快照读(普通 select 语句)是通过「事务的 Read View 里的字段」和「记录中的两个隐藏列(trx_id 和 roll_pointer)」的比对,如果不满足可见行,就会顺着 undo log 版本链里找到满足其可见性的记录。
因此,undo log 两大作用:
- 实现事务回滚,保障事务的原子性。事务处理过程中,如果出现了错误或者用户执 行了 ROLLBACK 语句,MySQL 可以利用 undo log 中的历史数据将数据恢复到事务开始之前的状态。
- 实现 MVCC(多版本并发控制)关键因素之一。MVCC 是通过 ReadView + undo log 实现的。undo log 为每条记录保存多份历史数据,MySQL 在执行快照读(普通 select 语句)的时候,会根据事务的 Read View 里的信息,顺着 undo log 的版本链找到满足其可见性的记录。
两阶段提交日志具体流程
- 引擎将这行新数据更新到内存中,同时将这个更新操作记录到 redo log 里面,此时 redo log 处于 prepare 状态。然后告知执行器执行完成了,随时可以提交事务。
- 执行器生成这个操作的 binlog,并把 binlog 写入磁盘。
- 执行器调用引擎的提交事务接口,引擎把刚刚写入的 redo log 改成提交(commit)状态,更新完成。
为什么是两阶段提交日志
核心目的:保证两份日志之间的逻辑一致(数据一致性)。
从反证法说明:
先写 redo log 直接提交,然后写 binlog:假设写完 redo log 后,机器挂了,binlog 日志没有被写入,那么机器重启后,这台机器会通过 redo log 恢复数据,但是这个时候 binlog 并没有记录该数据,后续进行机器备份的时候,就会丢失这一条数据,同时主从同步也会丢失这一条数据。
先写 binlog,然后写 redo log,假设写完了 binlog,机器异常重启了,由于没有 redo log,本机是无法恢复这一条记录的,但是 binlog 又有记录,那么和上面同样的道理,就会产生数据不一致的情况。
两阶段提交日志存在什么问题?
两阶段提交虽然保证了两个日志文件的数据一致性,但是性能很差,主要有两个方面的影响:
- 磁盘 I/O 次数高:对于“双1”配置,每个事务提交都会进行两次 fsync(刷盘),一次是 redo log 刷盘,另一次是 binlog 刷盘。
- 锁竞争激烈:两阶段提交虽然能够保证「单事务」两个日志的内容一致,但在「多事务」的情况下,却不能保证两者的提交顺序一致,因此,在两阶段提交的流程基础上,还需要加一个锁来保证提交的原子性,从而保证多事务的情况下,两个日志的提交顺序一致。
什么时候,怎么刷盘binlog
写入的机制:事务执行过程中,先把日志写到 binlog cache,事务提交的时候,再把 binlog cache 写到 binlog 文件中。
一个事务的 binlog 是不能被拆开的,因此不论这个事务多大,也要确保一次性写入。系统给 binlog cache 分配了一片内存,每个线程一个,如果超过了参数规定的大小,就要暂存到磁盘的page cache。
对于持久化也涉及到两步:
- write,指的就是指把日志写入到文件系统的 page cache,并没有把数据持久化到磁盘。
- fsync,才是将数据持久化到磁盘的操作。
MySQL提供一个 sync_binlog 参数来控制数据库的 binlog 刷到磁盘上的频率:
- sync_binlog = 0 的时候,表示每次提交事务都只 write,不 fsync,后续交由操作系统决定何时将数据持久化到磁盘;
- sync_binlog = 1 的时候,表示每次提交事务都会 write,然后马上执行 fsync;
- sync_binlog =N(N>1) 的时候,表示每次提交事务都 write,但累积 N 个事务后才 fsync。
不同的设置会带来不同的结果:
设置是 sync_binlog = 0,这时候的性能是最好的,但是风险也是最大的。因为一旦主机发生异常重启,还没持久化到磁盘的数据就会丢失。
sync_binlog 设置为 1 的时候,是最安全但是性能损耗最大的设置。因为当设置为 1 的时候,即使主机发生异常重启,最多丢失一个事务的 binlog,而已经持久化到磁盘的数据就不会有影响,不过就是对写入性能影响太大。
sync_binlog 设置为 N的时候,需要能容少量事务的 binlog 日志丢失的风险。
什么时候,怎么刷盘redo log
执行一个事务的过程中,产生的 redo log 也不是直接写入磁盘的,会先写入到 redo log buffer,后续在持久化到磁盘。
为了控制 redo log 的写入策略,InnoDB 提供了 innodb_flush_log_at_trx_commit 参数,它有三种可能取值:
- 当设置该参数为 0 时,表示每次事务提交时 ,还是将 redo log 留在 redo log buffer 中 ,该模式下在事务提交时不会主动触发写入磁盘的操作。
- 当设置该参数为 1 时,表示每次事务提交时,都将缓存在 redo log buffer 里的 redo log 直接持久化到磁盘,这样可以保证 MySQL 异常重启之后数据不会丢失。
- 当设置该参数为 2 时,表示每次事务提交时,都只是缓存在 redo log buffer 里的 redo log 写到文件的page cache。
InnoDB 有一个后台线程,每隔 1 秒,就会把 redo log buffer 中的日志,调用 write 写到文件系统的 page cache,然后调用 fsync 持久化到磁盘。
如果 write pos 追上了 checkpoint,就意味着 redo log 文件满了,这时 MySQL 不能再执行新的更新操作,也就是说 MySQL 会被阻塞(因此所以针对并发量大的系统,适当设置 redo log 的文件大小非常重要),此时会停下来将 Buffer Pool 中的脏页刷新到磁盘中,然后标记 redo log 哪些记录可以被擦除,接着对旧的 redo log 记录进行擦除,等擦除完旧记录腾出了空间,checkpoint 就会往后移动(图中顺时针),然后 MySQL 恢复正常运行,继续执行新的更新操作。
binlog的三种格式之间的对比。
binlog 有三种格式,一种是 statement,一种是 row,第三种格式 mixed,是前两种格式的混合。
statement 格式中 binlog文件记录的是真实执行的语句。存在主备不一致的情况,例如在主库执行这条 SQL 语句的时候,用的是索引 a;而在备库执行这条 SQL 语句的时候,却使用了索引 b。
row 格式的binlog记录的是真实数据行的字段的值,不存在主备不一致。
前两种格式各自的优缺点:
- 因为 statement 格式的 binlog 可能会导致主备不一致,row 格式不会发生这个问题。
- 但 row 格式的缺点是,很占空间。比如你用一个 delete 语句删掉 10 万行数据,用 statement 的话就是一个 SQL 语句被记录到 binlog 中,占用几十个字节的空间。但如果用 row 格式的 binlog,就要把这 10 万条记录都写到 binlog 中。这样做,不仅会占用更大的空间,同时写 binlog 也要耗费 IO 资源,影响执行速度。
所以根据这些优缺点,MySQL 就取了个折中方案,也就是有了 mixed 格式的 binlog。MySQL 自己会判断这条 SQL 语句是否可能引起主备不一致,如果有可能,就用 row 格式,否则就用 statement 格式。
在两阶段提交日志,有什么方法可以降低磁盘IO?
依赖于组提交(group commit)机制。
日志逻辑序列号(log sequence number,LSN) 是单调递增的,用来对应 redo log 的一个个写入点。每次写入长度为 length 的 redo log, LSN 的值就会加上 length。
有三组并发事务都要持久化磁盘,LSN都不一样,对应的 LSN 分别是 50、120 和 160。。过程如下:
- trx1 是第一个到达的,会被选为这组的 leader;
- 等 trx1 要开始写盘的时候,这个组里面已经有了三个事务,这时候 LSN 也变成了 160;
- trx1 去写盘的时候,带的就是 LSN=160,因此等 trx1 返回时,所有 LSN 小于等于 160 的 redo log,都已经被持久化到磁盘;
- 这时候 trx2 和 trx3 就可以直接返回了。
一次组提交里面,组员越多,节约磁盘 IOPS 的效果越好。
引入了组提交机制后,两阶段提交日志中的 prepare 阶段不变,对于binlog的wirte 阶段拆分为三个过程:
- flush 阶段:多个事务按进入的顺序将 binlog 从 cache 写入文件(不刷盘);
- sync 阶段:对 binlog 文件做 fsync 操作(多个事务的 binlog 合并一次刷盘);
上面的每个阶段都有一个队列,每个阶段有锁进行保护,因此保证了事务写入的顺序,第一个进入队列的事务会成为 leader,leader领导所在队列的所有事务,全权负责整队的操作,完成后通知队内其他事务操作结束。
为了提高redolog组提交性能,在 prepare 阶段不再让事务各自执行 redo log 刷盘操作,而是推迟到组提交的 flush 阶段之后。(在两个阶段之间)通过延迟写 redo log 的方式,为 redolog 做了一次组写入。
如果想提升 binlog 组提交的效果,可以通过设置下面这两个参数来实现:
binlog_group_commit_sync_delay= N
,表示在等待 N 微妙后,直接调用 fsync,将处于文件系统中 page cache 中的 binlog 刷盘,也就是将「 binlog 文件」持久化到磁盘。binlog_group_commit_sync_no_delay_count = N
,表示如果队列中的事务数达到 N 个,就忽视binlog_group_commit_sync_delay 的设置,直接调用 fsync,将处于文件系统中 page cache 中的 binlog 刷盘。
如果你的 MySQL 现在出现了性能瓶颈,而且瓶颈在 IO 上,可以通过哪些方法来提升性能呢?
- 设置 binlog_group_commit_sync_delay 和 binlog_group_commit_sync_no_delay_count 参数,减少 binlog 的写盘次数。这个方法是基于“额外的故意等待”来实现的,因此可能会增加语句的响应时间,但没有丢失数据的风险。
- 将 sync_binlog 设置为大于 1 的值(比较常见是 100~1000)。这样做的风险是,主机掉电时会丢 binlog 日志。
- 将 innodb_flush_log_at_trx_commit 设置为 2。这样做的风险是,主机掉电的时候会丢数据。
场景题
1. 在什么情况下会出现查一行数据会执行得特别慢的现象?
等锁
- 等表级锁,例如MDL写锁
- 等flush表
- 等行锁
查询慢
- 没有走索引,全表扫描
- 回滚日志过大引起的一致性读慢
2. 业务高峰期,生产环境的 MySQL 压力太大,没法正常响应,有哪些方案可以短期内、临时性地提升一些性能。
第一种情况:短连接风暴
当处于业务高峰期时候,MySQL 建立连接的过程,除了正常的网络连接三次握手外,还需要做登录权限判断和获得这个连接的数据读写权限,成本较高
解决方案:
- 第一种方法:先处理掉那些占着连接但是不工作的线程。
- 可以设置wait_timeout参数,将一个线程空闲 wait_timeout 这么多秒之后,就会被 MySQL 直接断开连接。
- 优先断开事务外的连接
- 第二种方法:减少连接过程的消耗。让数据库跳过权限验证阶段。
第二种情况:慢查询引起的性能问题
存在三种可能
- 索引没有设计好;
- 解决方案:在主从库上紧急添加索引
- SQL 语句没写好;(可能没有用索引等)
- 解决方案:query_rewrite 功能,可以把输入的一种语句改写成另外一种模式。
- MySQL 选错了索引。
- 解决方案:使用查询重写功能,给原来的语句加上 force index,
第三种情况:QPS 突增问题
采用虚拟化、白名单机制、业务账号分离等方法,然后相关服务停掉。
3. 全表扫描,server层的流程
- 获取一行,写到 net_buffer 中。这块内存的大小是由参数 net_buffer_length 定义的,默认是 16k。
- 重复获取行,直到 net_buffer 写满,调用网络接口发出去。
- 如果发送成功,就清空 net_buffer,然后继续取下一行,并写入 net_buffer。
- 如果发送函数返回 EAGAIN 或 WSAEWOULDBLOCK,就表示本地网络栈(socket send buffer)写满了,进入等待。直到网络栈重新可写,再继续发送。