MySQL

MySQL

1. 执行一条 select 语句,期间发生了什么?

  • 连接器:建立连接,管理连接、校验用户身份;

  • 查询缓存:查询语句如果命中查询缓存则直接返回,否则继续往下执行。MySQL 8.0 已删除该模块;

  • 解析 SQL,通过解析器对 SQL 查询语句进行词法分析、语法分析,然后构建语法树,方便后续模块读取表名、字段、语句类型;

  • 执行 SQL:执行 SQL 共有三个阶段:

    • 预处理阶段:检查表或字段是否存在;将 select _ 中的 _ 符号扩展为表上的所有列。

    • 优化阶段:基于查询成本的考虑, 选择查询成本最小的执行计划;

    • 执行阶段:根据执行计划执行 SQL 查询语句,从存储引擎读取记录,返回给客户端;

2. MySQL 的 NULL 值是怎么存放的?

MySQL 的 Compact 行格式中会用「NULL 值列表」来标记值为 NULL 的列,NULL 值并不会存储在行格式中的真实数据部分。

NULL 值列表会占用 1 字节空间,当表中所有字段都定义成 NOT NULL,行格式中就不会有 NULL 值列表,这样可节省 1 字节的空间。

3. MySQL 怎么知道 varchar(n) 实际占用数据的大小?

MySQL 的 Compact 行格式中会用「变长字段长度列表」存储变长字段实际占用的数据大小。

4. varchar(n) 中 n 最大取值为多少?

一行记录最大能存储 65535 字节的数据,但是这个是包含「变长字段字节数列表所占用的字节数」和「NULL 值列表所占用的字节数」。所以, 我们在算 varchar(n) 中 n 最大值时,需要减去这两个列表所占用的字节数。

如果一张表只有一个 varchar(n) 字段,且允许为 NULL,字符集为 ascii。varchar(n) 中 n 最大取值为 65532。

计算公式:65535 - 变长字段字节数列表所占用的字节数 - NULL 值列表所占用的字节数 = 65535 - 2 - 1 = 65532。

如果有多个字段的话,要保证所有字段的长度 + 变长字段字节数列表所占用的字节数 + NULL 值列表所占用的字节数 <= 65535。

5. 行溢出后,MySQL 是怎么处理的?

如果一个数据页存不了一条记录,InnoDB 存储引擎会自动将溢出的数据存放到「溢出页」中。

Compact 行格式针对行溢出的处理是这样的:当发生行溢出时,在记录的真实数据处只会保存该列的一部分数据,而把剩余的数据放在「溢出页」中,然后真实数据处用 20 字节存储指向溢出页的地址,从而可以找到剩余数据所在的页。

Compressed 和 Dynamic 这两种格式采用完全的行溢出方式,记录的真实数据处不会存储该列的一部分数据,只存储 20 个字节的指针来指向溢出页。而实际的数据都存储在溢出页中。

6. 事务隔离级别是怎么实现的?

事务是在 MySQL 引擎层实现的,我们常见的 InnoDB 引擎是支持事务的,事务的四大特性是原子性、一致性、隔离性、持久性,我们这次主要讲的是隔离性。

当多个事务并发执行的时候,会引发脏读、不可重复读、幻读这些问题,那为了避免这些问题,SQL 提出了四种隔离级别,分别是读未提交、读已提交、可重复读、串行化,从左往右隔离级别顺序递增,隔离级别越高,意味着性能越差,InnoDB 引擎的默认隔离级别是可重复读。

要解决脏读现象,就要将隔离级别升级到读已提交以上的隔离级别,要解决不可重复读现象,就要将隔离级别升级到可重复读以上的隔离级别。

而对于幻读现象,不建议将隔离级别升级为串行化,因为这会导致数据库并发时性能很差。MySQL InnoDB 引擎的默认隔离级别虽然是「可重复读」,但是它很大程度上避免幻读现象(并不是完全解决了),解决的方案有两种:

  • 针对快照读(普通 select 语句),是通过 MVCC 方式解决了幻读,因为可重复读隔离级别下,事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,即使中途有其他事务插入了一条数据,是查询不出来这条数据的,所以就很好了避免幻读问题。

  • 针对当前读(select ... for update 等语句),是通过 next-key lock(记录锁 + 间隙锁)方式解决了幻读,因为当执行 select ... for update 语句的时候,会加上 next-key lock,如果有其他事务在 next-key lock 锁范围内插入了一条记录,那么这个插入语句就会被阻塞,无法成功插入,所以就很好了避免幻读问题。

对于「读提交」和「可重复读」隔离级别的事务来说,它们是通过 Read View 来实现的,它们的区别在于创建 Read View 的时机不同:

  • 「读提交」隔离级别是在每个 select 都会生成一个新的 Read View,也意味着,事务期间的多次读取同一条数据,前后两次读的数据可能会出现不一致,因为可能这期间另外一个事务修改了该记录,并提交了事务。

  • 「可重复读」隔离级别是启动事务时生成一个 Read View,然后整个事务期间都在用这个 Read View,这样就保证了在事务期间读到的数据都是事务启动前的记录。

这两个隔离级别实现是通过「事务的 Read View 里的字段」和「记录中的两个隐藏列」的比对,来控制并发事务访问同一个记录时的行为,这就叫 MVCC(多版本并发控制)。

在可重复读隔离级别中,普通的 select 语句就是基于 MVCC 实现的快照读,也就是不会加锁的。而 select .. for update 语句就不是快照读了,而是当前读了,也就是每次读都是拿到最新版本的数据,但是它会对读到的记录加上 next-key lock 锁。

7. MySQL 可重复读隔离级别,完全解决幻读了吗?

MySQL InnoDB 引擎的可重复读隔离级别(默认隔离级),根据不同的查询方式,分别提出了避免幻读的方案:

  • 针对快照读(普通 select 语句),是通过 MVCC 方式解决了幻读。

  • 针对当前读(select ... for update 等语句),是通过 next-key lock(记录锁 + 间隙锁)方式解决了幻读。

我举例了两个发生幻读场景的例子。

第一个例子:对于快照读, MVCC 并不能完全避免幻读现象。因为当事务 A 更新了一条事务 B 插入的记录,那么事务 A 前后两次查询的记录条目就不一样了,所以就发生幻读。

第二个例子:对于当前读,如果事务开启后,并没有执行当前读,而是先快照读,然后这期间如果其他事务插入了一条记录,那么事务后续使用当前读进行查询的时候,就会发现两次查询的记录条目就不一样了,所以就发生幻读。

所以,MySQL 可重复读隔离级别并没有彻底解决幻读,只是很大程度上避免了幻读现象的发生。

要避免这类特殊场景下发生幻读的现象的话,就是尽量在开启事务之后,马上执行 select ... for update 这类当前读的语句,因为它会对记录加 next-key lock,从而避免其他事务插入一条新记录。

8. 为什么 MySQL InnoDB 选择 B+tree 作为索引的数据结构?

  1. B+Tree vs B Tree

B+Tree 只在叶子节点存储数据,而 B 树 的非叶子节点也要存储数据,所以 B+Tree 的单个节点的数据量更小,在相同的磁盘 I/O 次数下,就能查询更多的节点。

另外,B+Tree 叶子节点采用的是双链表连接,适合 MySQL 中常见的基于范围的顺序查找,而 B 树无法做到这一点。

  1. B+Tree vs 二叉树

对于有 N 个叶子节点的 B+Tree,其搜索复杂度为 O(logdN),其中 d 表示节点允许的最大子节点个数为 d 个。

在实际的应用当中, d 值是大于 100 的,这样就保证了,即使数据达到千万级别时,B+Tree 的高度依然维持在 34 层左右,也就是说一次数据查询操作只需要做 34 次的磁盘 I/O 操作就能查询到目标数据。

而二叉树的每个父节点的儿子节点个数只能是 2 个,意味着其搜索复杂度为 O(logN),这已经比 B+Tree 高出不少,因此二叉树检索到目标数据所经历的磁盘 I/O 次数要更多。

  1. B+Tree vs Hash

Hash 在做等值查询的时候效率贼快,搜索复杂度为 O(1)。

但是 Hash 表不适合做范围查询,它更适合做等值的查询,这也是 B+Tree 索引要比 Hash 表索引有着更广泛的适用场景的原因。

9. 联合索引

联合索引的最左匹配原则,在遇到范围查询(如 >、<)的时候,就会停止匹配,也就是范围查询的字段可以用到联合索引,但是在范围查询字段的后面的字段无法用到联合索引。注意,对于 >=、<=、BETWEEN、like 前缀匹配的范围查询,并不会停止匹配。

10. 索引下推

在 MySQL 5.6 之前,只能从 ID(主键值)开始一个个回表,到「主键索引」上找出数据行,再对比 b 字段值。

而 MySQL 5.6 引入的索引下推优化(index condition pushdown), 可以在联合索引遍历过程中,对联合索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数。

当你的查询语句的执行计划里,出现了 Extra 为 Using index condition,那么说明使用了索引下推的优化。

11. 什么时候需要 / 不需要创建索引?

需要

  • 字段有唯一性限制的,比如商品编码;

  • 经常用于 WHERE 查询条件的字段,这样能够提高整个表的查询速度,如果查询条件不是一个字段,可以建立联合索引。

  • 经常用于 GROUP BY 和 ORDER BY 的字段,这样在查询的时候就不需要再去做一次排序了,因为我们都已经知道了建立索引之后在 B+Tree 中的记录都是排序好的。

不需要

  • WHERE 条件,GROUP BY,ORDER BY 里用不到的字段,索引的价值是快速定位,如果起不到定位的字段通常是不需要创建索引的,因为索引是会占用物理空间的。

  • 字段中存在大量重复数据,不需要创建索引,比如性别字段,只有男女,如果数据库表中,男女的记录分布均匀,那么无论搜索哪个值都可能得到一半的数据。在这些情况下,还不如不要索引,因为 MySQL 还有一个查询优化器,查询优化器发现某个值出现在表的数据行中的百分比很高的时候,它一般会忽略索引,进行全表扫描。

  • 表数据太少的时候,不需要创建索引;

  • 经常更新的字段不用创建索引,比如不要对电商项目的用户余额建立索引,因为索引字段频繁修改,由于要维护 B+Tree 的有序性,那么就需要频繁的重建索引,这个过程是会影响数据库性能的。

12. 有什么优化索引的方法?

  • 前缀索引优化;

使用前缀索引是为了减小索引字段大小,可以增加一个索引页中存储的索引值,有效提高索引的查询速度。在一些大字符串的字段作为索引时,使用前缀索引可以帮助我们减小索引项的大小。

  • 覆盖索引优化;

覆盖索引是指 SQL 中 query 的所有字段,在索引 B+Tree 的叶子节点上都能找得到的那些索引,从二级索引中查询得到记录,而不需要通过聚簇索引查询获得,可以避免回表的操作。

  • 主键索引最好是自增的;

如果我们使用自增主键,那么每次插入的新数据就会按顺序添加到当前索引节点的位置,不需要移动已有的数据,当页面写满,就会自动开辟一个新页面。因为每次插入一条新记录,都是追加操作,不需要重新移动数据,因此这种插入数据的方法效率非常高。

另外,主键字段的长度不要太大,因为主键字段长度越小,意味着二级索引的叶子节点越小(二级索引的叶子节点存放的数据是主键值),这样二级索引占用的空间也就越小。

  • 索引最好设置为 NOT NULL

    • 第一原因:索引列存在 NULL 就会导致优化器在做索引选择的时候更加复杂,更加难以优化,因为可为 NULL 的列会使索引、索引统计和值比较都更复杂,比如进行索引统计时,count 会省略值为 NULL 的行。

    • 第二个原因:NULL 值是一个没意义的值,但是它会占用物理空间,所以会带来的存储空间的问题,因为 InnoDB 存储记录的时候,如果表中存在允许为 NULL 的字段,那么行格式 (opens new window) 中至少会用 1 字节空间存储 NULL 值列表。

  • 防止索引失效;

    • 当我们使用左或者左右模糊匹配的时候,也就是 like %xx 或者 like %xx% 这两种方式都会造成索引失效;

    • 当我们在查询条件中对索引列做了计算、函数、类型转换操作,这些情况下都会造成索引失效;

    • 联合索引要能正确使用需要遵循最左匹配原则,也就是按照最左优先的方式进行索引的匹配,否则就会导致索引失效。

    • 在 WHERE 子句中,如果在 OR 前的条件列是索引列,而在 OR 后的条件列不是索引列,那么索引会失效。

13. 从数据页的角度看 B+ 树

InnoDB 的数据是按「数据页」为单位来读写的,默认数据页大小为 16 KB。每个数据页之间通过双向链表的形式组织起来,物理上不连续,但是逻辑上连续。

数据页内包含用户记录,每个记录之间用单向链表的方式组织起来,为了加快在数据页内高效查询记录,设计了一个页目录,页目录存储各个槽(分组),且主键值是有序的,于是可以通过二分查找法的方式进行检索从而提高效率。

为了高效查询记录所在的数据页,InnoDB 采用 b+ 树作为索引,每个节点都是一个数据页。

如果叶子节点存储的是实际数据的就是聚簇索引,一个表只能有一个聚簇索引;如果叶子节点存储的不是实际数据,而是主键值则就是二级索引,一个表中可以有多个二级索引。

在使用二级索引进行查找数据时,如果查询的数据能在二级索引找到,那么就是「索引覆盖」操作,如果查询的数据不在二级索引里,就需要先在二级索引找到主键值,需要去聚簇索引中获得数据行,这个过程就叫作「回表」。

14. 为什么 MySQL 采用 B+ 树作为索引?

MySQL 是会将数据持久化在硬盘,而存储功能是由 MySQL 存储引擎实现的,所以讨论 MySQL 使用哪种数据结构作为索引,实际上是在讨论存储引使用哪种数据结构作为索引,InnoDB 是 MySQL 默认的存储引擎,它就是采用了 B+ 树作为索引的数据结构。

要设计一个 MySQL 的索引数据结构,不仅仅考虑数据结构增删改的时间复杂度,更重要的是要考虑磁盘 I/0 的操作次数。因为索引和记录都是存放在硬盘,硬盘是一个非常慢的存储设备,我们在查询数据的时候,最好能在尽可能少的磁盘 I/0 的操作次数内完成。

二分查找树虽然是一个天然的二分结构,能很好的利用二分查找快速定位数据,但是它存在一种极端的情况,每当插入的元素都是树内最大的元素,就会导致二分查找树退化成一个链表,此时查询复杂度就会从 O(logn) 降低为 O(n)。

为了解决二分查找树退化成链表的问题,就出现了自平衡二叉树,保证了查询操作的时间复杂度就会一直维持在 O(logn) 。但是它本质上还是一个二叉树,每个节点只能有 2 个子节点,随着元素的增多,树的高度会越来越高。

而树的高度决定于磁盘 I/O 操作的次数,因为树是存储在磁盘中的,访问每个节点,都对应一次磁盘 I/O 操作,也就是说树的高度就等于每次查询数据时磁盘 IO 操作的次数,所以树的高度越高,就会影响查询性能。

B 树和 B+ 都是通过多叉树的方式,会将树的高度变矮,所以这两个数据结构非常适合检索存于磁盘中的数据。

但是 MySQL 默认的存储引擎 InnoDB 采用的是 B+ 作为索引的数据结构,原因有:

  • B+ 树的非叶子节点不存放实际的记录数据,仅存放索引,因此数据量相同的情况下,相比存储即存索引又存记录的 B 树,B+ 树的非叶子节点可以存放更多的索引,因此 B+ 树可以比 B 树更「矮胖」,查询底层节点的磁盘 I/O 次数会更少。

  • B+ 树有大量的冗余节点(所有非叶子节点都是冗余索引),这些冗余索引让 B+ 树在插入、删除的效率都更高,比如删除根节点的时候,不会像 B 树那样会发生复杂的树的变化;

  • B+ 树叶子节点之间用链表连接了起来,有利于范围查询,而 B 树要实现范围查询,因此只能通过树的遍历来完成范围查询,这会涉及多个节点的磁盘 I/O 操作,范围查询效率不如 B+ 树。

15. MySQL 单表不要超过 2000W 行,靠谱吗?

索引结构不会影响单表最大行数,2000W 也只是推荐值,超过了这个值可能会导致 B + 树层级更高,影响查询性能。

16. 索引失效有哪些?

  • 当我们使用左或者左右模糊匹配的时候,也就是 like %xx 或者 like %xx% 这两种方式都会造成索引失效;

  • 当我们在查询条件中对索引列使用函数,就会导致索引失效。

  • 当我们在查询条件中对索引列进行表达式计算,也是无法走索引的。

  • MySQL 在遇到字符串和数字比较的时候,会自动把字符串转为数字,然后再进行比较。如果字符串是索引列,而条件语句中的输入参数是数字的话,那么索引列会发生隐式类型转换,由于隐式类型转换是通过 CAST 函数实现的,等同于对索引列使用了函数,所以就会导致索引失效。

  • 联合索引要能正确使用需要遵循最左匹配原则,也就是按照最左优先的方式进行索引的匹配,否则就会导致索引失效。

  • 在 WHERE 子句中,如果在 OR 前的条件列是索引列,而在 OR 后的条件列不是索引列,那么索引会失效。

17. MySQL 使用 like “%x“,索引一定会失效吗?

如果数据库表中的字段只有主键 + 二级索引,那么即使使用了左模糊匹配,也不会走全表扫描(type=all),而是走全扫描二级索引树 (type=index)。

如果数据库表中的字段都是索引的话,即使查询过程中,没有遵循最左匹配原则,也是走全扫描二级索引树

18. count(*) 和 count(1) 有什么区别?哪个性能最好?

count(1)、 count(*)、 count(主键字段) 在执行的时候,如果表里存在二级索引,优化器就会选择二级索引进行扫描。

所以,如果要执行 count(1)、 count(*)、 count(主键字段) 时,尽量在数据表上建立二级索引,这样优化器会自动采用 key_len 最小的二级索引进行扫描,相比于扫描主键索引效率会高一些。

再来,就是不要使用 count(字段) 来统计记录个数,因为它的效率是最差的,会采用全表扫描的方式来统计。如果你非要统计表中该字段不为 NULL 的记录个数,建议给这个字段建立一个二级索引。

19. 如何优化 count(*)?

  • 近似值

可以使用 show table status 或者 explain 命令来表进行估算。

执行 explain 命令效率是很高的,因为它并不会真正的去查询

  • 额外表保存计数值

当我们在数据表插入一条记录的同时,将计数表中的计数字段 + 1。也就是说,在新增和删除操作时,我们需要额外维护这个计数表。

20. 全局锁是怎么用的?


执行后,整个数据库就处于只读状态了,这时其他线程执行以下操作,都会被阻塞:

对数据的增删改操作,比如 insert、delete、update 等语句; 对表结构的更改操作,比如 alter table、drop table 等语句。 如果要释放全局锁,则要执行这条命令:

unlock tables

当然,当会话断开了,全局锁会被自动释放。

21. 全局锁应用场景是什么?

全局锁主要应用于做全库逻辑备份,这样在备份数据库期间,不会因为数据或表结构的更新,而出现备份文件的数据与预期的不一样。

22. 全局锁又会带来什么缺点呢?

加上全局锁,意味着整个数据库都是只读状态。

那么如果数据库里有很多数据,备份就会花费很多的时间,关键是备份期间,业务只能读数据,而不能更新数据,这样会造成业务停滞。

如何避免

有的,如果数据库的引擎支持的事务支持可重复读的隔离级别,那么在备份数据库之前先开启事务,会先创建 Read View,然后整个事务执行期间都在用这个 Read View,而且由于 MVCC 的支持,备份期间业务依然可以对数据进行更新操作。

因为在可重复读的隔离级别下,即使其他事务更新了表的数据,也不会影响备份数据库时的 Read View,这就是事务四大特性中的隔离性,这样备份期间备份的数据一直是在开启事务时的数据。

23. MySQL 表级锁有哪些?具体怎么用的。

表锁

表锁除了会限制别的线程的读写外,也会限制本线程接下来的读写操作。

也就是说如果本线程对学生表加了「共享表锁」,那么本线程接下来如果要对学生表执行写操作的语句,是会被阻塞的,当然其他线程对学生表进行写操作时也会被阻塞,直到锁被释放。

另外,当会话退出后,也会释放所有表锁。

不过尽量避免在使用 InnoDB 引擎的表使用表锁,因为表锁的颗粒度太大,会影响并发性能,InnoDB 牛逼的地方在于实现了颗粒度更细的行级锁。

元数据锁(MDL)

我们不需要显示的使用 MDL,因为当我们对数据库表进行操作时,会自动给这个表加上 MDL:

  • 对一张表进行 CRUD 操作时,加的是 MDL 读锁;

  • 对一张表做结构变更操作的时候,加的是 MDL 写锁;

MDL 是为了保证当用户对表执行 CRUD 操作时,防止其他线程对这个表结构做了变更。

当有线程在执行 select 语句( 加 MDL 读锁)的期间,如果有其他线程要更改该表的结构( 申请 MDL 写锁),那么将会被阻塞,直到执行完 select 语句( 释放 MDL 读锁)。

反之,当有线程对表结构进行变更( 加 MDL 写锁)的期间,如果有其他线程执行了 CRUD 操作( 申请 MDL 读锁),那么就会被阻塞,直到表结构变更完成( 释放 MDL 写锁)。

意向锁

意向共享锁和意向独占锁是表级锁,不会和行级的共享锁和独占锁发生冲突,而且意向锁之间也不会发生冲突,只会和共享表锁(lock tables ... read)和独占表锁(lock tables ... write)发生冲突。

意向锁的目的是为了快速判断表里是否有记录被加锁。

AUTO-INC 锁

之后可以在插入数据时,可以不指定主键的值,数据库会自动给主键赋值递增的值,这主要是通过 AUTO-INC 锁实现的。

  • 当 innodb_autoinc_lock_mode = 0,就采用 AUTO-INC 锁,语句执行结束后才释放锁;

  • 当 innodb_autoinc_lock_mode = 2,就采用轻量级锁,申请自增主键后就释放锁,并不需要等语句执行后才释放。

  • 当 innodb_autoinc_lock_mode = 1:

    • 普通 insert 语句,自增锁在申请之后就马上释放;

    • 类似 insert … select 这样的批量插入数据的语句,自增锁还是要等语句结束后才被释放;

24. MDL 不需要显示调用,那它是在什么时候释放的?

MDL 是在事务提交后才会释放,这意味着事务执行期间,MDL 是一直持有的。

申请 MDL 锁的操作会形成一个队列,队列中写锁获取优先级高于读锁,一旦出现 MDL 写锁等待,会阻塞后续该表的所有 CRUD 操作。

25. 行级锁

Record Lock

Record Lock 称为记录锁,锁住的是一条记录。而且记录锁是有 S 锁和 X 锁之分的:

  • 当一个事务对一条记录加了 S 型记录锁后,其他事务也可以继续对该记录加 S 型记录锁(S 型与 S 锁兼容),但是不可以对该记录加 X 型记录锁(S 型与 X 锁不兼容);

  • 当一个事务对一条记录加了 X 型记录锁后,其他事务既不可以对该记录加 S 型记录锁(S 型与 X 锁不兼容),也不可以对该记录加 X 型记录锁(X 型与 X 锁不兼容)。

Gap Lock

Gap Lock 称为间隙锁,只存在于可重复读隔离级别,目的是为了解决可重复读隔离级别下幻读的现象。

假设,表中有一个范围 id 为(3,5)间隙锁,那么其他事务就无法插入 id = 4 这条记录了,这样就有效的防止幻读现象的发生。

间隙锁虽然存在 X 型间隙锁和 S 型间隙锁,但是并没有什么区别,间隙锁之间是兼容的,即两个事务可以同时持有包含共同间隙范围的间隙锁,并不存在互斥关系,因为间隙锁的目的是防止插入幻影记录而提出的。

Next-Key Lock

Next-Key Lock 称为临键锁,是 Record Lock + Gap Lock 的组合,锁定一个范围,并且锁定记录本身。

假设,表中有一个范围 id 为(3,5] 的 next-key lock,那么其他事务即不能插入 id = 4 记录,也不能修改 id = 5 这条记录。

next-key lock 是包含间隙锁 + 记录锁的,如果一个事务获取了 X 型的 next-key lock,那么另外一个事务在获取相同范围的 X 型的 next-key lock 时,是会被阻塞的。

插入意向锁

一个事务在插入一条记录的时候,需要判断插入位置是否已被其他事务加了间隙锁(next-key lock 也包含间隙锁)。

如果有的话,插入操作就会发生阻塞,直到拥有间隙锁的那个事务提交为止(释放间隙锁的时刻),在此期间会生成一个插入意向锁,表明有事务想在某个区间插入新记录,但是现在处于等待状态。

插入意向锁名字虽然有意向锁,但是它并不是意向锁,它是一种特殊的间隙锁,属于行级别锁。

插入意向锁与间隙锁的另一个非常重要的差别是:尽管「插入意向锁」也属于间隙锁,但两个事务却不能在同一时间内,一个拥有间隙锁,另一个拥有该间隙区间内的插入意向锁(当然,插入意向锁如果不在间隙锁区间内则是可以的)。

26. MySQL 是怎么加锁的?

唯一索引等值查询:

  • 当查询的记录是「存在」的,在索引树上定位到这一条记录后,将该记录的索引中的 next-key lock 会退化成「记录锁」。

  • 当查询的记录是「不存在」的,在索引树找到第一条大于该查询记录的记录后,将该记录的索引中的 next-key lock 会退化成「间隙锁」。

非唯一索引等值查询:

  • 当查询的记录「存在」时,由于不是唯一索引,所以肯定存在索引值相同的记录,于是非唯一索引等值查询的过程是一个扫描的过程,直到扫描到第一个不符合条件的二级索引记录就停止扫描,然后在扫描的过程中,对扫描到的二级索引记录加的是 next-key 锁,而对于第一个不符合条件的二级索引记录,该二级索引的 next-key 锁会退化成间隙锁。同时,在符合查询条件的记录的主键索引上加记录锁。

  • 当查询的记录「不存在」时,扫描到第一条不符合条件的二级索引记录,该二级索引的 next-key 锁会退化成间隙锁。因为不存在满足查询条件的记录,所以不会对主键索引加锁。

非唯一索引和主键索引的范围查询的加锁规则不同之处在于:

  • 唯一索引在满足一些条件的时候,索引的 next-key lock 退化为间隙锁或者记录锁。

  • 非唯一索引范围查询,索引的 next-key lock 不会退化为间隙锁和记录锁。

其实理解 MySQL 为什么要这样加锁,主要要以避免幻读角度去分析,这样就很容易理解这些加锁的规则了。

还有一件很重要的事情,在线上在执行 update、delete、select ... for update 等具有加锁性质的语句,一定要检查语句是否走了索引,如果是全表扫描的话,会对每一个索引加 next-key 锁,相当于把整个表锁住了,这是挺严重的问题。

27. update 没加索引会锁全表?

当我们要执行 update 语句的时候,确保 where 条件中带上了索引列,并且在测试机确认该语句是否走的是索引扫描,防止因为扫描全表,而对表中的所有记录加上锁。

我们可以打开 MySQL sql_safe_updates 参数,这样可以预防 update 操作时 where 条件没有带上索引列。

如果发现即使在 where 条件中带上了列索引列,优化器走的还是全标扫描,这时我们就要使用 force index([index_name]) 可以告诉优化器使用哪个索引。

28. MySQL 记录锁 + 间隙锁可以防止删除操作而导致的幻读吗?

在 MySQL 的可重复读隔离级别下,针对当前读的语句会对索引加记录锁 + 间隙锁,这样可以避免其他事务执行增、删、改时导致幻读的问题。

29. MySQL 死锁了,怎么办?

死锁的四个必要条件:互斥、占有且等待、不可强占用、循环等待。只要系统发生死锁,这些条件必然成立,但是只要破坏任意一个条件就死锁就不会成立。

在数据库层面,有两种策略通过「打破循环等待条件」来解除死锁状态:

设置事务等待锁的超时时间。当一个事务的等待时间超过该值后,就对这个事务进行回滚,于是锁就释放了,另一个事务就可以继续执行了。在 InnoDB 中,参数 innodb_lock_wait_timeout 是用来设置超时时间的,默认值时 50 秒。

开启主动死锁检测。主动死锁检测在发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。将参数 innodb_deadlock_detect 设置为 on,表示开启这个逻辑,默认就开启。

上面这个两种策略是「当有死锁发生时」的避免方式。

我们可以回归业务的角度来预防死锁,对订单做幂等性校验的目的是为了保证不会出现重复的订单,那我们可以直接将 order_no 字段设置为唯一索引列,利用它的唯一性来保证订单表不会出现重复的订单,不过有一点不好的地方就是在我们插入一个已经存在的订单记录时就会抛出异常。

30. 为什么需要 undo log?

  • 实现事务回滚,保障事务的原子性。事务处理过程中,如果出现了错误或者用户执行了 ROLLBACK 语句,MySQL 可以利用 undo log 中的历史数据将数据恢复到事务开始之前的状态。

  • 实现 MVCC(多版本并发控制)关键因素之一。MVCC 是通过 ReadView + undo log 实现的。undo log 为每条记录保存多份历史数据,MySQL 在执行快照读(普通 select 语句)的时候,会根据事务的 Read View 里的信息,顺着 undo log 的版本链找到满足其可见性的记录。

31. 为什么需要 Buffer Pool?

  • 当读取数据时,如果数据存在于 Buffer Pool 中,客户端就会直接读取 Buffer Pool 中的数据,否则再去磁盘中读取。

  • 当修改数据时,如果数据存在于 Buffer Pool 中,那直接修改 Buffer Pool 中数据所在的页,然后将其页设置为脏页(该页的内存数据和磁盘上的数据已经不一致),为了减少磁盘 I/O,不会立即将脏页写入磁盘,后续由后台线程选择一个合适的时机将脏页写入到磁盘。

32. Buffer Pool 缓存什么?

InnoDB 会为 Buffer Pool 申请一片连续的内存空间,然后按照默认的 16KB 的大小划分出一个个的页, Buffer Pool 中的页就叫做缓存页。

开启事务后,InnoDB 层更新记录前,首先要记录相应的 undo log,如果是更新操作,需要把被更新的列的旧值记下来,也就是要生成一条 undo log,undo log 会写入 Buffer Pool 中的 Undo 页面。

当我们查询一条记录时,InnoDB 是会把整个页的数据加载到 Buffer Pool 中,将页加载到 Buffer Pool 后,再通过页里的「页目录」去定位到某条具体的记录。

33. 为什么需要 redo log?

  • 实现事务的持久性,让 MySQL 有 crash-safe 的能力,能够保证 MySQL 在任何时间段突然崩溃,重启后之前已提交的记录都不会丢失;

  • 将写操作从「随机写」变成了「顺序写」,提升 MySQL 写入磁盘的性能。

34. redo log 和 undo log 区别在哪?

  • redo log 记录了此次事务「完成后」的数据状态,记录的是更新之后的值;

  • undo log 记录了此次事务「开始前」的数据状态,记录的是更新之前的值;

所以有了 redo log,再通过 WAL 技术,InnoDB 就可以保证即使数据库发生异常重启,之前已提交的记录都不会丢失,这个能力称为 crash-safe(崩溃恢复)。可以看出来, redo log 保证了事务四大特性中的持久性。

35. 什么是 WAL

为了防止断电导致数据丢失的问题,当有一条记录需要更新的时候,InnoDB 引擎就会先更新内存(同时标记为脏页),然后将本次对这个页的修改以 redo log 的形式记录下来,这个时候更新就算完成了。

后续,InnoDB 引擎会在适当的时候,由后台线程将缓存在 Buffer Pool 的脏页刷新到磁盘里,这就是 WAL (Write-Ahead Logging)技术。

WAL 技术指的是, MySQL 的写操作并不是立刻写到磁盘上,而是先写日志,然后在合适的时间再写到磁盘上。

36. 产生的 redo log 是直接写入磁盘的吗?

不是的。

实际上, 执行一个事务的过程中,产生的 redo log 也不是直接写入磁盘的,因为这样会产生大量的 I/O 操作,而且磁盘的运行速度远慢于内存。

所以,redo log 也有自己的缓存—— redo log buffer,每当产生一条 redo log 时,会先写入到 redo log buffer

37. redo log 什么时候刷盘?

  • MySQL 正常关闭时;

  • 当 redo log buffer 中记录的写入量大于 redo log buffer 内存空间的一半时,会触发落盘;

  • InnoDB 的后台线程每隔 1 秒,将 redo log buffer 持久化到磁盘。

  • 每次事务提交时都将缓存在 redo log buffer 里的 redo log 直接持久化到磁盘

除此之外,InnoDB 还提供了另外两种策略,由参数 innodb_flush_log_at_trx_commit 参数控制,可取的值有:0、1、2,默认值为 1,这三个值分别代表的策略如下:

  • 当设置该参数为 0 时,表示每次事务提交时 ,还是将 redo log 留在 redo log buffer 中 ,该模式下在事务提交时不会主动触发写入磁盘的操作。

  • 当设置该参数为 1 时,表示每次事务提交时,都将缓存在 redo log buffer 里的 redo log 直接持久化到磁盘,这样可以保证 MySQL 异常重启之后数据不会丢失。

  • 当设置该参数为 2 时,表示每次事务提交时,都只是缓存在 redo log buffer 里的 redo log 写到 redo log 文件,注意写入到「 redo log 文件」并不意味着写入到了磁盘,因为操作系统的文件系统中有个 Page Cache(如果你想了解 Page Cache,可以看这篇 (opens new window)),Page Cache 是专门用来缓存文件数据的,所以写入「 redo log 文件」意味着写入到了操作系统的文件缓存。

38. innodb_flush_log_at_trx_commit 为 0 和 2 的时候,什么时候才将 redo log 写入磁盘?

InnoDB 的后台线程每隔 1 秒:

  • 针对参数 0 :会把缓存在 redo log buffer 中的 redo log ,通过调用 write() 写到操作系统的 Page Cache,然后调用 fsync() 持久化到磁盘。所以参数为 0 的策略,MySQL 进程的崩溃会导致上一秒钟所有事务数据的丢失;

  • 针对参数 2 :调用 fsync,将缓存在操作系统中 Page Cache 里的 redo log 持久化到磁盘。所以参数为 2 的策略,较取值为 0 情况下更安全,因为 MySQL 进程的崩溃并不会丢失数据,只有在操作系统崩溃或者系统断电的情况下,上一秒钟所有事务数据才可能丢失。

39. redo log 文件写满了怎么办?

如果 write pos 追上了 checkpoint,就意味着 redo log 文件满了,这时 MySQL 不能再执行新的更新操作,也就是说 MySQL 会被阻塞(因此所以针对并发量大的系统,适当设置 redo log 的文件大小非常重要),此时会停下来将 Buffer Pool 中的脏页刷新到磁盘中,然后标记 redo log 哪些记录可以被擦除,接着对旧的 redo log 记录进行擦除,等擦除完旧记录腾出了空间,checkpoint 就会往后移动(图中顺时针),然后 MySQL 恢复正常运行,继续执行新的更新操作。

所以,一次 checkpoint 的过程就是脏页刷新到磁盘中变成干净页,然后标记 redo log 哪些记录可以被覆盖的过程。

40. 为什么需要 binlog ?

binlog 用于备份恢复、主从复制;

41. redo log 和 binlog 有什么区别?

  1. 适用对象不同:

  • binlog 是 MySQL 的 Server 层实现的日志,所有存储引擎都可以使用;

  • redo log 是 Innodb 存储引擎实现的日志;

  1. 文件格式不同:

  • binlog 有 3 种格式类型,分别是 STATEMENT(默认格式)、ROW、 MIXED,区别如下:

    • STATEMENT:每一条修改数据的 SQL 都会被记录到 binlog 中(相当于记录了逻辑操作,所以针对这种格式, binlog 可以称为逻辑日志),主从复制中 slave 端再根据 SQL 语句重现。但 STATEMENT 有动态函数的问题,比如你用了 uuid 或者 now 这些函数,你在主库上执行的结果并不是你在从库执行的结果,这种随时在变的函数会导致复制的数据不一致;

    • ROW:记录行数据最终被修改成什么样了(这种格式的日志,就不能称为逻辑日志了),不会出现 STATEMENT 下动态函数的问题。但 ROW 的缺点是每行数据的变化结果都会被记录,比如执行批量 update 语句,更新多少行数据就会产生多少条记录,使 binlog 文件过大,而在 STATEMENT 格式下只会记录一个 update 语句而已;

    • MIXED:包含了 STATEMENT 和 ROW 模式,它会根据不同的情况自动使用 ROW 模式和 STATEMENT 模式;

  • redo log 是物理日志,记录的是在某个数据页做了什么修改,比如对 XXX 表空间中的 YYY 数据页 ZZZ 偏移量的地方做了 AAA 更新;

  1. 写入方式不同:

  • binlog 是追加写,写满一个文件,就创建一个新的文件继续写,不会覆盖以前的日志,保存的是全量的日志。

  • redo log 是循环写,日志空间大小是固定,全部写满就从头开始,保存未被刷入磁盘的脏页日志。

  1. 用途不同:

  • binlog 用于备份恢复、主从复制;

  • redo log 用于掉电等故障恢复。

42. 如果不小心整个数据库的数据被删除了,能使用 redo log 文件恢复数据吗?

不可以使用 redo log 文件恢复,只能使用 binlog 文件恢复。

因为 redo log 文件是循环写,是会边写边擦除日志的,只记录未被刷入磁盘的数据的物理日志,已经刷入磁盘的数据都会从 redo log 文件里擦除。

binlog 文件保存的是全量的日志,也就是保存了所有数据变更的情况,理论上只要记录在 binlog 上的数据,都可以恢复,所以如果不小心整个数据库的数据被删除了,得用 binlog 文件恢复数据。

43. 主从复制是怎么实现?

MySQL 集群的主从复制过程梳理成 3 个阶段:

  • 写入 Binlog:主库写 binlog 日志,提交事务,并更新本地存储数据。

  • 同步 Binlog:把 binlog 复制到所有从库上,每个从库把 binlog 写到暂存日志中。

  • 回放 Binlog:回放 binlog,并更新存储引擎中的数据。

具体详细过程如下:

  • MySQL 主库在收到客户端提交事务的请求之后,会先写入 binlog,再提交事务,更新存储引擎中的数据,事务提交完成后,返回给客户端“操作成功”的响应。

  • 从库会创建一个专门的 I/O 线程,连接主库的 log dump 线程,来接收主库的 binlog 日志,再把 binlog 信息写入 relay log 的中继日志里,再返回给主库“复制成功”的响应。

  • 从库会创建一个用于回放 binlog 的线程,去读 relay log 中继日志,然后回放 binlog 更新存储引擎中的数据,最终实现主从的数据一致性。

44. 从库是不是越多越好?

不是的。

因为从库数量增加,从库连接上来的 I/O 线程也比较多,主库也要创建同样多的 log dump 线程来处理复制的请求,对主库资源消耗比较高,同时还受限于主库的网络带宽。

所以在实际使用中,一个主库一般跟 2 ~ 3 个从库(1 套数据库,1 主 2 从 1 备主),这就是一主多从的 MySQL 集群结构。

45. MySQL 主从复制还有哪些模型?

  • 同步复制:MySQL 主库提交事务的线程要等待所有从库的复制成功响应,才返回客户端结果。这种方式在实际项目中,基本上没法用,原因有两个:一是性能很差,因为要复制到所有节点才返回响应;二是可用性也很差,主库和所有从库任何一个数据库出问题,都会影响业务。

  • 异步复制(默认模型):MySQL 主库提交事务的线程并不会等待 binlog 同步到各从库,就返回客户端结果。这种模式一旦主库宕机,数据就会发生丢失。

  • 半同步复制:MySQL 5.7 版本之后增加的一种复制方式,介于两者之间,事务线程不用等待所有的从库复制成功响应,只要一部分复制成功响应回来就行,比如一主二从的集群,只要数据成功复制到任意一个从库上,主库的事务线程就可以返回给客户端。这种半同步复制的方式,兼顾了异步复制和同步复制的优点,即使出现主库宕机,至少还有一个从库有最新的数据,不存在数据丢失的风险。

46. binlog 什么时候刷盘?

MySQL 给每个线程分配了一片内存用于缓冲 binlog ,该内存叫 binlog cache,参数 binlog_cache_size 用于控制单个线程内 binlog cache 所占内存的大小。如果超过了这个参数规定的大小,就要暂存到磁盘。

47. 什么时候 binlog cache 会写到 binlog 文件?

MySQL 提供一个 sync_binlog 参数来控制数据库的 binlog 刷到磁盘上的频率:

  • sync_binlog = 0 的时候,表示每次提交事务都只 write,不 fsync,后续交由操作系统决定何时将数据持久化到磁盘;

  • sync_binlog = 1 的时候,表示每次提交事务都会 write,然后马上执行 fsync;

  • sync_binlog =N(N>1) 的时候,表示每次提交事务都 write,但累积 N 个事务后才 fsync。

48. 为什么需要两阶段提交?

可以看到,在持久化 redo log 和 binlog 这两份日志的时候,如果出现半成功的状态,就会造成主从环境的数据不一致性。这是因为 redo log 影响主库的数据,binlog 影响从库的数据,所以 redo log 和 binlog 必须保持一致才能保证主从数据一致。

49. 两阶段提交的过程是怎样的?

  • prepare 阶段:将 XID(内部 XA 事务的 ID) 写入到 redo log,同时将 redo log 对应的事务状态设置为 prepare,然后将 redo log 持久化到磁盘(innodb_flush_log_at_trx_commit = 1 的作用);

  • commit 阶段:把 XID 写入到 binlog,然后将 binlog 持久化到磁盘(sync_binlog = 1 的作用),接着调用引擎的提交事务接口,将 redo log 状态设置为 commit,此时该状态并不需要持久化到磁盘,只需要 write 到文件系统的 page cache 中就够了,因为只要 binlog 写磁盘成功,就算 redo log 的状态还是 prepare 也没有关系,一样会被认为事务已经执行成功;

50. 异常重启会出现什么现象?

  • 如果 binlog 中没有当前内部 XA 事务的 XID,说明 redolog 完成刷盘,但是 binlog 还没有刷盘,则回滚事务。对应时刻 A 崩溃恢复的情况。

  • 如果 binlog 中有当前内部 XA 事务的 XID,说明 redolog 和 binlog 都已经完成了刷盘,则提交事务。对应时刻 B 崩溃恢复的情况。

51. 处于 prepare 阶段的 redo log 加上完整 binlog,重启就提交事务,MySQL 为什么要这么设计?

binlog 已经写入了,之后就会被从库(或者用这个 binlog 恢复出来的库)使用。

所以,在主库上也要提交这个事务。采用这个策略,主库和备库的数据就保证了一致性。

52. 事务没提交的时候,redo log 会被持久化到磁盘吗?

会的。

事务执行中间过程的 redo log 也是直接写在 redo log buffer 中的,这些缓存在 redo log buffer 里的 redo log 也会被「后台线程」每隔一秒一起持久化到磁盘。

也就是说,事务没提交的时候,redo log 也是可能被持久化到磁盘的。

有的同学可能会问,如果 mysql 崩溃了,还没提交事务的 redo log 已经被持久化磁盘了,mysql 重启后,数据不就不一致了?

放心,这种情况 mysql 重启会进行回滚操作,因为事务没提交的时候,binlog 是还没持久化到磁盘的。

53. 两阶段提交有什么问题?

  • 磁盘 I/O 次数高:对于“双 1”配置,每个事务提交都会进行两次 fsync(刷盘),一次是 redo log 刷盘,另一次是 binlog 刷盘。

  • 锁竞争激烈:两阶段提交虽然能够保证「单事务」两个日志的内容一致,但在「多事务」的情况下,却不能保证两者的提交顺序一致,因此,在两阶段提交的流程基础上,还需要加一个锁来保证提交的原子性,从而保证多事务的情况下,两个日志的提交顺序一致。

54. 为什么两阶段提交的磁盘 I/O 次数会很高?

binlog 和 redo log 在内存中都对应的缓存空间,binlog 会缓存在 binlog cache,redo log 会缓存在 redo log buffer,它们持久化到磁盘的时机分别由下面这两个参数控制。一般我们为了避免日志丢失的风险,会将这两个参数设置为 1:

  • 当 sync_binlog = 1 的时候,表示每次提交事务都会将 binlog cache 里的 binlog 直接持久到磁盘;

  • 当 innodb_flush_log_at_trx_commit = 1 时,表示每次事务提交时,都将缓存在 redo log buffer 里的 redo log 直接持久化到磁盘;

可以看到,如果 sync_binlog 和 当 innodb_flush_log_at_trx_commit 都设置为 1,那么在每个事务提交过程中, 都会至少调用 2 次刷盘操作,一次是 redo log 刷盘,一次是 binlog 落盘,所以这会成为性能瓶颈。

55. 为什么锁竞争激烈?

在早期的 MySQL 版本中,通过使用 prepare_commit_mutex 锁来保证事务提交的顺序,在一个事务获取到锁时才能进入 prepare 阶段,一直到 commit 阶段结束才能释放锁,下个事务才可以继续进行 prepare 操作。

通过加锁虽然完美地解决了顺序一致性的问题,但在并发量较大的时候,就会导致对锁的争用,性能不佳。

56. binlog 组提交

MySQL 引入了 binlog 组提交(group commit)机制,当有多个事务提交的时候,会将多个 binlog 刷盘操作合并成一个,从而减少磁盘 I/O 的次数,如果说 10 个事务依次排队刷盘的时间成本是 10,那么将这 10 个事务一次性一起刷盘的时间成本则近似于 1。

引入了组提交机制后,prepare 阶段不变,只针对 commit 阶段,将 commit 阶段拆分为三个过程:

  • flush 阶段:多个事务按进入的顺序将 binlog 从 cache 写入文件(不刷盘);

  • sync 阶段:对 binlog 文件做 fsync 操作(多个事务的 binlog 合并一次刷盘);

  • commit 阶段:各个事务按顺序做 InnoDB commit 操作;

上面的每个阶段都有一个队列,每个阶段有锁进行保护,因此保证了事务写入的顺序,第一个进入队列的事务会成为 leader,leader 领导所在队列的所有事务,全权负责整队的操作,完成后通知队内其他事务操作结束。

57. redo log 组提交

在 MySQL 5.7 版本中,做了个改进,在 prepare 阶段不再让事务各自执行 redo log 刷盘操作,而是推迟到组提交的 flush 阶段,也就是说 prepare 阶段融合在了 flush 阶段。

这个优化是将 redo log 的刷盘延迟到了 flush 阶段之中,sync 阶段之前。通过延迟写 redo log 的方式,为 redolog 做了一次组写入,这样 binlog 和 redo log 都进行了优化。

58. MySQL 磁盘 I/O 很高,有什么优化的方法?

我们知道事务在提交的时候,需要将 binlog 和 redo log 持久化到磁盘,那么如果出现 MySQL 磁盘 I/O 很高的现象,我们可以通过控制以下参数,来 “延迟” binlog 和 redo log 刷盘的时机,从而降低磁盘 I/O 的频率:

  • 设置组提交的两个参数: binlog_group_commit_sync_delay 和 binlog_group_commit_sync_no_delay_count 参数,延迟 binlog 刷盘的时机,从而减少 binlog 的刷盘次数。这个方法是基于“额外的故意等待”来实现的,因此可能会增加语句的响应时间,但即使 MySQL 进程中途挂了,也没有丢失数据的风险,因为 binlog 早被写入到 page cache 了,只要系统没有宕机,缓存在 page cache 里的 binlog 就会被持久化到磁盘。

  • 将 sync_binlog 设置为大于 1 的值(比较常见是 100~1000),表示每次提交事务都 write,但累积 N 个事务后才 fsync,相当于延迟了 binlog 刷盘的时机。但是这样做的风险是,主机掉电时会丢 N 个事务的 binlog 日志。

  • 将 innodb_flush_log_at_trx_commit 设置为 2。表示每次事务提交时,都只是缓存在 redo log buffer 里的 redo log 写到 redo log 文件,注意写入到「 redo log 文件」并不意味着写入到了磁盘,因为操作系统的文件系统中有个 Page Cache,专门用来缓存文件数据的,所以写入「 redo log 文件」意味着写入到了操作系统的文件缓存,然后交由操作系统控制持久化到磁盘的时机。但是这样做的风险是,主机掉电的时候会丢数据。

59. 详解更新一条记录的流程

具体更新一条记录 UPDATE t_user SET name = 'xiaolin' WHERE id = 1; 的流程如下:

  1. 执行器负责具体执行,会调用存储引擎的接口,通过主键索引树搜索获取 id = 1 这一行记录:

    • 如果 id=1 这一行所在的数据页本来就在 buffer pool 中,就直接返回给执行器更新;

    • 如果记录不在 buffer pool,将数据页从磁盘读入到 buffer pool,返回记录给执行器。

  2. 执行器得到聚簇索引记录后,会看一下更新前的记录和更新后的记录是否一样:

    • 如果一样的话就不进行后续更新流程;

    • 如果不一样的话就把更新前的记录和更新后的记录都当作参数传给 InnoDB 层,让 InnoDB 真正的执行更新记录的操作;

  3. 开启事务, InnoDB 层更新记录前,首先要记录相应的 undo log,因为这是更新操作,需要把被更新的列的旧值记下来,也就是要生成一条 undo log,undo log 会写入 Buffer Pool 中的 Undo 页面,不过在内存修改该 Undo 页面后,需要记录对应的 redo log。

  4. InnoDB 层开始更新记录,会先更新内存(同时标记为脏页),然后将记录写到 redo log 里面,这个时候更新就算完成了。为了减少磁盘 I/O,不会立即将脏页写入磁盘,后续由后台线程选择一个合适的时机将脏页写入到磁盘。这就是 WAL 技术,MySQL 的写操作并不是立刻写到磁盘上,而是先写 redo 日志,然后在合适的时间再将修改的行数据写到磁盘上。

  5. 至此,一条记录更新完了。

  6. 在一条更新语句执行完成后,然后开始记录该语句对应的 binlog,此时记录的 binlog 会被保存到 binlog cache,并没有刷新到硬盘上的 binlog 文件,在事务提交时才会统一将该事务运行过程中的所有 binlog 刷新到硬盘。

  7. 事务提交(为了方便说明,这里不说组提交的过程,只说两阶段提交):

    • prepare 阶段:将 redo log 对应的事务状态设置为 prepare,然后将 redo log 刷新到硬盘;

    • commit 阶段:将 binlog 刷新到磁盘,接着调用引擎的提交事务接口,将 redo log 状态设置为 commit(将事务设置为 commit 状态后,刷入到磁盘 redo log 文件);

60. 详解 Buffer Pool

Innodb 存储引擎设计了一个缓冲池(Buffer Pool),来提高数据库的读写性能。

Buffer Pool 以页为单位缓冲数据,可以通过 innodb_buffer_pool_size 参数调整缓冲池的大小,默认是 128 M。

Innodb 通过三种链表来管理缓页:

  • Free List (空闲页链表),管理空闲页;

  • Flush List (脏页链表),管理脏页;

  • LRU List,管理脏页 + 干净页,将最近且经常查询的数据缓存在其中,而不常查询的数据就淘汰出去。

InnoDB 对 LRU 做了一些优化,我们熟悉的 LRU 算法通常是将最近查询的数据放到 LRU 链表的头部,而 InnoDB 做 2 点优化:

  • 将 LRU 链表 分为 young 和 old 两个区域,加入缓冲池的页,优先插入 old 区域;页被访问时,才进入 young 区域,目的是为了解决预读失效的问题。

  • 当「页被访问」且「 old 区域停留时间超过 innodb_old_blocks_time 阈值(默认为 1 秒)」时,才会将页插入到 young 区域,否则还是插入到 old 区域,目的是为了解决批量数据访问,大量热数据淘汰的问题。

可以通过调整 innodb_old_blocks_pct 参数,设置 young 区域和 old 区域比例。

在开启了慢 SQL 监控后,如果你发现「偶尔」会出现一些用时稍长的 SQL,这可因为脏页在刷新到磁盘时导致数据库性能抖动。如果在很短的时间出现这种现象,就需要调大 Buffer Pool 空间或 redo log 日志的大小。

61. MySQL 三范式

第一范式确保原子性,表中每一个列数据都必须是不可再分的字段;第二范式确保唯一性,每张表都只描述一种业务属性,一张表只描述一件事;第三范式确保独立性,表中除主键外,每个字段之间不存在任何依赖,都是独立的

62. MySQL 实现分布式锁

MySQL 可以通过在 select 语句后增加 for update 来获取排它锁,从而实现分布式锁。当某条记录被加上排他锁之后,其他线程无法再在该行记录上增加排他锁,因此获得排它锁的线程即可获得分布式锁。

63. 慢 SQL 优化

  • 索引优化:索引是提高查询效率的重要手段,可以通过 explain 命令查看 SQL 语句的执行计划,找到慢查询的原因,然后对相应的字段添加索引。

  • 优化 SQL 语句:常见的 SQL 优化技巧包括避免使用通配符查询、使用 JOIN 代替嵌套 SELECT、减少子查询等。

  • 分析表结构:创建索引、垂直分割表等。创建合适的索引可以加快查询速度,但同时会增加写入数据的时间。垂直分割表可以将表根据不同的功能、访问模式分为多个表,避免查询全部字段和频繁更新次数相同的字段造成的性能问题。

  • 数据库参数优化:可以通过修改 MySQL 配置文件中的参数来优化数据库性能,如 innodb_buffer_pool_size、innodb_log_file_size 等。

  • 代码优化:应用程序中的代码也可能导致慢查询,如连接池设置不合理、代码中存在死循环等。

64. 如何防止 SQL 注入

GORM 使用 database/sql 的参数占位符来构造 SQL 语句,这可以自动转义参数,避免 SQL 注入数据。

65. InnoDB 有 Hash 索引吗

InnoDB 存储引擎 是支持 hash 索引的,不过,我们必须启用,Hash 索引的创建由 InnoDB 存储引擎引擎自动优化创建,我们干预不了。

66. int(1) 和 int(10) 的区别

在 MySQL 中,int(1) 和 int(10) 的区别在于,int(1) 和 int(10) 本身没有区别,但是加上 (M) 值后,会有显示宽度的设置。这个值不是存储数据的大小,而是显示数据的长度。如果你想要存储数据的大小,可以使用 char(10) 或者 varchar(10)。

67. 字符串做主键索引的危害

  • 字符串长度大,占用空间大,不利于查询和排序。

  • 字符串作为主键,会导致索引文件变大,降低查询效率。

68. utf8 varchar 最长是多少

utf8 编码下,MySQL 中的 varchar 最长长度是21844 个字符

69. MySQL 乐观锁实现方式

MySQL 的乐观锁是通过使用版本号或时间戳来实现的。

UPDATE products
SET price = 12.99,
    version = version + 1
WHERE id = 1
  AND version = 0;

在上述 SQL 语句中,我们将 price 更新为 12.99,并将 version 增加 1。同时,我们使用 WHERE 子句来限制更新只能在 id 为 1 且 version 为 0 的记录上进行,这是为了确保在更新期间没有其他并发操作修改了该记录。

如果更新成功,则表示没有其他并发操作修改了该记录。如果更新失败,则表示有其他并发操作修改了该记录,这时可以选择重试操作或执行其他逻辑以适应该冲突。

70. MyISAM 和 InnoDB 的区别有哪些

1. InnoDB 支持事务,MyISAM 不支持事务。这是 MySQL 将默认存储引擎从 MyISAM 变成 InnoDB 的重要原因之一;

2. InnoDB 支持外键,而 MyISAM 不支持。对一个包含外键的 InnoDB 表转为 MYISAM 会失败;

3. InnoDB 是聚集索引,MyISAM 是非聚集索引。聚簇索引的文件存放在主键索引的叶子节点上,因此 InnoDB 必须要有主键,通过主键索引效率很高。但是辅助索引需要两次查询,先查询到主键,然后再通过主键查询到数据。因此,主键不应该过大,因为主键太大,其他索引也都会很大。而 MyISAM 是非聚集索引,数据文件是分离的,索引保存的是数据文件的指针。主键索引和辅助索引是独立的。

4. InnoDB 不保存表的具体行数,执行 select count(*) from table 时需要全表扫描。而 MyISAM 用一个变量保存了整个表的行数,执行上述语句时只需要读出该变量即可,速度很快;

5. InnoDB 最小的锁粒度是行锁,MyISAM 最小的锁粒度是表锁。一个更新语句会锁住整张表,导致其他查询和更新都会被阻塞,因此并发访问受限。这也是 MySQL 将默认存储引擎从 MyISAM 变成 InnoDB 的重要原因之一;

71. 读写 MySQL 超时

  • 调整连接超时设置:MySQL 有两个主要的超时设置,分别是 wait_timeout 和 interactive_timeout。这两个设置决定了连接在多长时间没有活动后会被关闭。

  • 优化查询和事务:如果查询或事务的执行时间超过了连接的超时时间,可以考虑对查询进行优化,例如添加索引、优化查询语句等。另外,可以将长时间运行的查询或事务拆分为多个较小的操作,以减少单个操作的执行时间。

  • 使用合适的连接池:通过使用连接池,可以避免频繁地创建和关闭连接,提高连接的复用率。连接池可以自动管理连接的超时时间,并在需要时重新创建新的连接。

72. 数据库分表

https://zhuanlan.zhihu.com/p/569961814

73. MySQL 索引如何存储?每个索引一个 B+ 树,还是多个索引放一个 B+ 树?

MySQL 使用 B+ 树作为索引的存储结构。每个索引都对应一个独立的 B+ 树。每个索引节点包含多个关键字和对应的指针,关键字按照顺序排列,指针指向下一层的节点或者叶子节点。叶子节点存储了索引的值以及对应的数据行的指针。这种结构可以高效地支持单个值的查找、范围查询和排序操作。

74. MySQL 叶子节点中存的是什么数据?

MySQL 的叶子节点存储的是具体数据或者主键 KEY。在 B+ 树索引中,叶子节点是存储实际数据的节点。每个叶子节点包含一定范围的键值和对应的数据块的指针 (或者是数据本身) 。叶子节点之间通过双向指针连接,形成一个有序的链表。通过这些指针,可以在树中进行快速的搜索和定位,以找到包含特定键值的叶子节点。

75. B+ 树的范围查找怎么做的?

  1. 从根节点开始,按照 B+ 树的搜索算法,找到第一个大于或等于范围的起始值的叶子节点。这可以通过在每个内部节点上进行二分查找来实现。

  2. 从起始叶子节点开始,顺序遍历叶子节点,找到所有在范围内的值。由于 B+ 树的叶子节点是按照关键字顺序链接的,所以可以通过遍历叶子节点来找到范围内的所有值。

  3. 如果范围的结束值大于当前叶子节点的最大关键字,继续遍历下一个叶子节点,重复步骤 2,直到找到所有满足范围条件的值。

B+ 树的范围查找是基于关键字的有序性进行的,因此可以快速定位到范围的起始位置,并按顺序获取范围内的值。这使得 B+ 树在范围查询方面非常高效。

76. 如果只有原子性能保持一致性吗

两个操作是原子操作,即要么全部执行成功,要么全部失败,如果发生故障(如系统挂起、断电等),就不会出现只从 A 账户扣款但没有给 B 账户加款的情况。

然而,仅仅保证了原子性,并不意味着能保证系统的一致性。一致性是指数据要满足预设的一系列约束和规则。在我们的例子中,一致性则要求在转账操作完成后,A 账户和 B 账户的总金额应该和转账操作前保持一致。但是,如果在执行转账操作的同时,同时有其他的操作(如 A 账户有一笔 100 元的存款),那么仅仅依靠原子性,是无法保证数据的一致性的。

77. MySQL 索引类型

MySQL 的索引主要有以下几种类型:

  1. 主键索引 (PRIMARY KEY) :用于唯一标识表中的每一行数据。一个表只能有一个主键索引,且主键索引的值不能为 NULL。

  2. 唯一索引 (UNIQUE) :保证索引列的值在表中是唯一的,可以有多个唯一索引。

  3. 普通索引 (INDEX) :最基本的索引类型,用于加快查询速度。可以在一个表中创建多个普通索引。

  4. 全文索引 (FULLTEXT) :用于在文本字段上进行全文搜索。

除了以上常见的索引类型,MySQL 还支持一些特殊的索引类型:

  1. 哈希索引 (HASH) :将索引列的值通过哈希算法转换为哈希值,然后根据哈希值进行快速查找。哈希索引适用于等值查询,但不支持范围查询。

  2. 前缀索引 (PREFIX) :只索引列值的前缀部分,可以减少索引的存储空间和提高查询效率。

78. undo log 与 redo log 里面记录的是物理数据还是逻辑数据

undo log 记录的内容是逻辑数据。如果某个事务发生了回滚,undo log 就会发生作用,这个过程通常被称为“反向”操作。undo log 记录了一个事务执行所做的每一步修改,这样在需要撤销的时候,就可以利用 undo log 中记录的信息,将数据状态回滚到事务开始之前。

redo log 记录的是物理数据。也就是说,它保存了数据库中的实际更改。当系统崩溃或其他故障发生时,redo log 能够通过重做(redo)之前的操作,将数据恢复到故障发生时的状态。它是数据库恢复的一个重要手段。

79. 单纯的 SELECT 会加锁吗

单纯的 SELECT 语句不会对表进行加锁。在 MySQL 中,SELECT 语句默认使用共享锁 (也称为读锁) ,它允许多个会话同时读取同一张表的数据,不会阻塞其他会话的读取操作。

需要注意的是,如果在 SELECT 语句中使用了 FOR UPDATE 子句,则会对查询的行进行加锁,此时会获取排他锁 (也称为写锁) 。排他锁会阻塞其他会话的读取和写入操作,直到当前会话释放锁为止。

80. MySQL 自增 id 能回滚吗

MySQL 的自增 ID 在事务回滚后是不会回退的。当一个事务中插入了数据并回滚后,虽然插入的数据被删除了,但是自增 ID 的值仍然会自增。

如果您需要连续的自增 ID 序列,建议考虑其他方法来生成,而不是依赖于事务回滚来控制自增 ID 的值。例如,可以使用其他方式生成连续的 ID,比如使用 UUID 或者使用其他表来维护一个递增的序列号。

81. MySQL 内连接和左外连接的区别

  • 内连接 (INNER JOIN) : 内连接返回两个表中满足连接条件的行,即只返回两个表中连接字段相等的行。它只返回符合条件的交集部分。

  • 左外连接 (LEFT JOIN) : 左外连接返回左表中的所有记录和右表中连接字段相等的记录。即左表的所有行都会被返回,而右表中没有匹配的行则会填充为 NULL。

82. 主从数据库在对主库写的时候加锁会不会锁从库

当我们在主库进行写操作(更新,插入,删除等)时,会对相关数据加锁,以保证数据的一致性和完整性。这种锁只在主库生效,并不会影响到从库。

主库完成写操作后,会通过数据库的复制机制将这些变更写入日志文件,然后再将这些日志复制到从库。在从库上重放这些日志,完成与主库的数据同步。

82. 分库分表怎么实现数据的平滑迁移

  1. 设计分库分表方案:根据业务需求和数据特点,设计合适的分库分表方案。常见的策略可以用哈希。

  2. 创建新的数据库和表结构:根据分库分表方案,在新的数据库中创建分片并设计相应的表结构。确保新的数据库和表结构与原始数据库保持一致。

  3. 数据同步:将原始数据库中的数据逐步同步到新的数据库中。可以采用增量同步和全量同步相结合的方式。增量同步可以通过开启增量数据单向同步,将数据从旧库同步到新库。全量同步则是将历史数据从旧库全量同步到新库。

  4. 切换读写操作:当数据同步完成后,可以将读写操作切换到新的数据库上。这可以通过修改应用程序的连接配置或者使用中间件来实现。

  5. 监控和验证:在切换完成后,需要对新的数据库进行监控和验证,确保数据迁移过程中没有出现问题,并且新的数据库能够正常工作。可以通过监控指标、日志和测试来进行验证。

83. 一张表里有班级 学号 分数,用 SQL 求每个班级分数前三的学生

SELECT 班级, 学号, 分数
FROM (
  SELECT 班级, 学号, 分数,
    ROW_NUMBER() OVER(PARTITION BY 班级 ORDER BY 分数 DESC) as rn
  FROM student_info
) t
WHERE rn <= 3

在这个查询中,首先我们在内部查询中使用窗口函数 ROW_NUMBER(),对每个分区(这里的分区就是班级)的记录进行排序(按分数降序)并编号。然后,在外部查询中,我们筛选出每个班级分数前三的学生。

Last updated