2万字高频MySQL面试题总结(含答案),金九银十成为offer收割机!建议收藏
Posted yes的练级攻略
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了2万字高频MySQL面试题总结(含答案),金九银十成为offer收割机!建议收藏相关的知识,希望对你有一定的参考价值。
大家好,我是yes。
这篇是mysql 面试题汇总,所有的答案都是我原创的,来来回回差不多整理个一个月左右,如果有什么问题,还请留言区指正!
话不多说,请接招!
你们公司数据库有备份的吧?
我:有的,因为单点故障的情况不可避免,所以我们公司有主从。
面试官:那你知道主备、主从、主主有什么区别?
主备就是:主机和备机。
备机是不干活的,也就是不对外提供服务,只是默默地在同步主机的数据,然后等着某一天主机挂了之后,它取而代之!
至于切换的话主要有两种方式:
- 人工切换,得知主机挂了之后手动把备机切成主机,缺点就是慢。
- 利用 keepalived 或者自己写个脚本来作监控,然后自动切换。
主从就是主机和从机。
从机和备机的区别在于,它是对外提供服务的,一般而言主从就是读写分离,写请求指派到主机,读请求指派到从机。
主主就是两个都是主机
一般情况下都不会有主主的架构。
当同时有两个写请求达到分别打到两个主库同一张表的时候,则会同时创建一条记录,这条记录的 ID 是一样的,这样数据同步之后其中有一条就会被覆盖了,这会出问题的。
为什么要读写分离啊?
读写分离就是读操作和写操作从以前的一台服务器上剥离开来,将主库压力分担一些到从库。
本质上是因为访问量太大,主库的压力过大,单机数据库无法支撑并发读写。
然后一般而言读的次数远高于写,因此将读操作分发到从库上,这就是常见的读写分离。
读写分离还有个操作就是主库不建查询的索引,从库建查询的索引。
因为索引是需要维护的,比如你插入一条数据,不仅要在聚簇索引上面插入,对应的二级索引也得插入,修改也是一样的。
所以将读操作分到从库了之后,可以在主库把查询要用的索引删了,减少写操作对主库的影响。
你们读写分离是用中间件的还是代码封装的?
代码封装。
讲白了就是代码层面抽出一个中间层,由中间层来实现读写分离和数据库连接。
就是搞了个代理类,对外暴露正常的读写接口,里面封装了逻辑,将读操作指向从库的数据源,写操作指向主库的数据源。
- 优点:简单,并且可以根据业务定制化变化,随心所欲。
- 缺点:如果数据库宕机了,发生主从切换了之后,就得修改配置重启。如果系统是多语言的话,需要为每个语言都实现一个中间层代码,重复开发。
中间件
一般而言是独立部署的系统,客户端与这个中间件的交互是通过 SQL 协议的。
所以在客户端看来连接的就是一个数据库,通过 SQL 协议交互也可以屏蔽多语言的差异。
缺点就是整体架构多了一个系统需要维护,并且可能成为性能瓶颈,毕竟交互都需要经过它中转。
常见的开源数据库中间件有:官方的MySQL-Proxy、360的Atlas、Mycat 等。
MySQL 主从同步机制你知道吗?
主从同步主要依赖的就是 binlog,MySQL 默认是异步复制,具体流程如下:
主库:
- 接受到提交事务请求
- 更新数据
- 将数据写到binlog中
- 给客户端响应
- 推送binlog到从库中
从库:
- 由 I/O 线程将同步过来的 binlog 写入到 relay log 中。
- 由 SQL 线程从 relay log 重放事件,更新数据
- 给主库返回响应。
用一句话概括一下:主库提交事务会写binlog,会由一个 dump 线程推送给从库,从库接受之后会有一个I/O线程将其写到 relay log 中,慢慢消化,由 SQL 线程来重放更新数据。
异步复制有数据丢失风险,例如数据还未同步到从库,主库就给客户端响应,然后主库挂了,此时从库晋升为主库的话数据是缺失的。
所以有同步复制,主库需要将 binlog 复制到所有从库,等所有从库响应了之后才会给客户端响应,这样的话性能很差,一般不会选择同步复制。
MySQL 5.7 之后搞了个半同步复制,有个参数可以选择“成功同步几个从库就返回响应。”
比如一共有 3 个从库,我参数配置 1,那么只要有一个从库响应说复制成功了,主库就直接返回响应给客户端,不会等待其他两个从库。
这样的话性能就比较好,并且数据可靠性也增强了,只有当那个从库和主库同时都挂了,才会缺失数据。
主从同步延迟怎么处理啊?
从上图的流程就可以得知,延迟是必然存在的。
延迟过大的话就有可能出现一个用户刚注册,然后登陆报该用户不存在的…
因为数据是写到主库中的,查询走从库有可能还未来同步完毕,导致查不到这个用户。
这就非常不友好了。
常见解决方式有以下几种:
-
二次查询。如果从库查不到数据,则再去主库查一遍,由 API 封装即可,算是一个兜底策略,比较简单。
不过等于读的压力又转移到主库身上了,如果有不法分子估计搞一下必定查不到的查询,这就难受了。 -
强制将写之后立马读的操作转移到主库上。这种属于代码写死了,比如一些写入之后立马查询的操作,就绑定在一起,写死都走主库。不推荐,太僵硬了。
-
关键业务读写都走主库,非关键还是读写分离。比如上面我举例的用户注册这种,可以读写主库,这样就不会有登陆报该用户不存在的问题,这种访问量频次应该也不会很多,所以看业务适当调整此类接口。
说说分库分表?
随着用户量的激增和时间的堆砌,存在数据库里面的数据越来越多,此时的数据库就会产生瓶颈,出现资源报警、查询慢等场景。
首先单机数据库所能承载的连接数、I/O及网络的吞吐等都是有限的,所以当并发量上来了之后,数据库就渐渐顶不住了。
再则,如果单表的数据量过大,查询的性能也会下降。因为数据越多 B+ 树就越高,树越高则查询 I/O 的次数就越多,那么性能也就越差。
因为上述的原因,不得已就得上分库分表了。
把以前存在一个数据库实例里的数据拆分成多个数据库实例,部署在不同的服务器中,这是分库。
把以前存在一张表里面的数据拆分成多张表,这是分表。
一般而言:
- 分表:是为了解决由于单张表数据量多大,而导致查询慢的问题。大致三、四千万行数据就得拆分,不过具体还是得看每一行的数据量大小,有些字段都很小的可能支持更多行数,有些字段大的可能一千万就顶不住了。
- 分库:是为了解决服务器资源受单机限制,顶不住高并发访问的问题,把请求分配到多台服务器上,降低服务器压力。
你们一般怎么分库的?
一般分库都是按照业务划分的,比如订单库、用户库等等。
有时候会针对一些特殊的库再作切分,比如一些活动相关的库都做了拆分。
因为做活动的时候并发可能会比较高,怕影响现有的核心业务,所以即使有关联,也会单独做拆分。
那你觉得分库会带来什么问题呢?
首先是事务的问题。
我们使用关系型数据库,有很大一点在于它保证事务完整性。
而分库之后单机事务就用不上了,必须使用分布式事务来解决,而分布式事务基本的都是残缺的(我之前文章把分布式事务汇总了一波,后台搜索分布式事务就有了)。
这是很重要的一点需要考虑。
连表 JOIN 问题
在一个库中的时候我们还可以利用 JOIN 来连表查询,而跨库了之后就无法使用 JOIN 了。
此时的解决方案就是在业务代码中进行关联,也就是先把一个表的数据查出来,然后通过得到的结果再去查另一张表,然后利用代码来关联得到最终的结果。
这种方式实现起来稍微比较复杂,不过也是可以接受的。
还有可以适当的冗余一些字段。比如以前的表就存储一个关联 ID,但是业务时常要求返回对应的 Name 或者其他字段。这时候就可以把这些字段冗余到当前表中,来去除需要关联的操作。
那你们怎么分表的?
分表其实有两种:
- 垂直分表
- 水平分表
垂直分表,来看个图,很直观:
垂直分表就是把一些不常用的大字段剥离出去。
像上面的例子:用户名是很常见的搜索结果,性别和年龄占用的空间又不大,而地址和个人简介占用的空间相对而言就较大,我们都知道一个数据页的空间是有限的,把一些无用的数据拆分出去,一页就能存放更多行的数据。
内存存放更多有用的数据,就减少了磁盘的访问次数,性能就得到提升。
水平分表,则是因为一张表内的数据太多了,上文也提到了数据越多 B+ 树就越高,访问的性能就差,所以进行水平拆分。
其实不管这些,浅显的理解下,在一百个数据里面找一个数据快,还是在一万个数据里面找一个数据快?
即使有索引,那厚的书目录多,翻目录也慢~
那分表会有什么问题?
垂直分表还好,就是需要关联一下,而水平分表就有点麻烦了。
排序、count、分页问题
如果一个用户的数据被拆分到多个表中,那查询结果分页就不像以前单张表那样直接就能查出来了,像 count 操作也是一样的。
只能由业务代码来实现或者用中间件将各表中的数据汇总、排序、分页然后返回。
像 count 操作的结果其实可以缓存下来,然后每次数据增删都更新计数。
路由问题
分表的路由可以分:
- Hash 路由
- 范围路由
- 路由表
Hash 路由,其实就是选择表中的某一列,然后进行 Hash 运算,将 Hash 运算得到的结果再对子表数进行取模,这样就能均匀的将数据分到不同的子表上。
这跟 HashMap 选哪个桶是一样的原理。
优点就是数据分布均匀。
缺点就是增加子表的时候麻烦,想想 HashMap的扩容,是不是得搬迁数据?这个分表也是一样的,我们可都知道,数据迁移一件麻烦事!
范围路由,其实很简单,可以是时间,也可以是地址,表示一定的范围的即可。
比如本来一张 User 表,我可以分 User_HZ、User_BJ、User_SH,按照地名来划分 User。
再比如 log 表,我可以将表分为 log_202103、 log_202104,把日志按照年月来划分。
优点就是相对而言比较容易扩展,比如现在来个 GZ,那就加个 User_GZ。如果到了 5 月,那就建个 log_202105。
缺点就是数据可能分布不均匀,例如 BJ 的用户特别多或者某个月搞了促销,日志量特别大,等等。
路由表,就是专门搞个表来记录路由信息,来看个图就很清楚了。
从图中我们就能得知,UserID 为 2 的用户数据在要去 User_3 这个用户表查询。
优点就是灵活咯,如果要迁移数据,直接迁移然后路由表一改就完事儿了~
缺点就是得多查一次,每次查询都需要访问路由表,不过这个一般会做缓存的。
全局主键问题
以前单表的时候很简单,就是主键自增,现在分表了之后就有点尴尬了。
所以需要一些手段来保证全局主键唯一。
- 还是自增,只不过自增步长设置一下。比如现在有三张表,步长设置为3,三张表 ID 初始值分别是1、2、3。
这样第一张表的 ID 增长是 1、4、7。第二张表是2、5、8。第三张表是3、6、9,这样就不会重复了。 - UUID,这种最简单,但是不连续的主键插入会导致严重的页分裂,性能比较差。
- 分布式 ID,比较出名的就是 Twitter 开源的 sonwflake 雪花算法,具体就不展开了,不然就又是一篇文章了,简单点利用 redis 来递增也行。
那上面说的路由问题的 Sharding-Key 如何设计呢?
我们分表是按照某个列来拆分的,那个列就是 Sharding-Key,查询的时候必须带上这个列才行。
例如上面提到的 log_202103,那表明查询条件一定得带上日期,这样才能找到正确的表。
所以设计上得考虑查询的条件来作为 Sharding-Key。
举个常常会被问的订单表 Sharding-Key 例子。
你想着查找订单的时候会通过订单号去找,所以应该利用订单 ID 来作为 Sharding-Key。
但是你想想,你打开外卖软件想查找你的历史订单的时候,你是没有订单 ID 的,你只有你的 UserID,那此时只能把所有子表都通过 UserID 遍历一遍,这样效率就很低了!
所以你想着那用 UserID 来作为 Sharding-Key 吧!
但是,商家呢?商家肯定关心自己今天卖了多少单,所以他也要查找订单,但他只有自己的商家 ID,所以如果要查询订单,只能把所有子表都通过商家 ID 遍历一遍,这样效率就很低了!
所以 Sharding-Key 是满足不了所有查询需求的,只能曲线救国。
一般做法就是冗余数据。
将订单同步到另一张表中给商家使用,这个表按商家 ID 来作为 Sharding-Key,也可以将数据同步到 ES 中。一般而言这里的数据同步都是异步处理,不会影响正常流程。
接着的面试题换个口味来,以模拟实战场景来道出相关面试题:
MySQL的事务与MVCC与面试官小战30回合
我,小Y。
此刻,正坐在办公室里等待面试,心情xue微有点忐忑,不知道待会儿老面试官经不经得住我的折磨。
只见一抹光亮闪过,面试官推门而入,我抬头望去,强者的气息铺面而来,没错是那味儿。
看到面试官头上那“傲然矗立”的头发,脑海中止不住幻想他在无数个凌晨于电脑前挑灯夜码的高大形象,一种敬佩感油然而生, 竟忍不住站起来给他敬了个礼。
面试官:有病?
我:没没没,我谢顶反应综合征犯了,面试官好,我是小 Y ,请多多指教。
面试官:哦哦,确实是有病啊,没事,记得吃药就行。我看你简历写你 MySQL 挺懂的,那我先问问你 MySQL 吧。
我:好嘞,您请。
面试官:你知道什么是 MySQL 的酸吗?
这一来就这么猛的吗?脑海中一顿搜索,只能想起张含韵的我喜欢酸的甜这就是真的我之《酸酸甜甜就是我》,算了蒙一个。
我:事务?
面试官:哟,最近好多谐音梗,我特意玩了个英语单词短语梗,脑子转的挺快啊小伙子。
酸,英文 acid,说的就是事务!这都蒙对了,等下就去买彩票!趁这个机会再表现一下!
我:是啊,国外的人就有拼凑单词的习惯,其实事务主要是为了实现 C ,也就是一致性,具体是通过AID,即原子性、隔离性和持久性来达到一致性的目的,所以这四个不应该相提并论,但是他们就想拼成单词,就把它们排好序搞在一起来念。
嘿嘿,这个B装的我有点舒服,果然面试官有点惊讶。
面试官:可以呀,那你知道 MVCC 吧?
我:知道,Multi-Version Concurrency Control (多版本并发控制)。
面试官:能先简短的解释下什么是 MVCC 吗?
我:多版本并发控制,其实指的是一条记录会有多个版本,每次修改记录都会存储这条记录被修改之前的版本,多版本之间串联起来就形成了一条版本链,这样不同时刻启动的事务可以无锁地获得不同版本的数据(普通读)。此时读(普通读)写操作不会阻塞,写操作可以继续写,无非就是多加了一个版本,历史版本记录可供已经启动的事务读取。
(为保持简短,简化了SQL语句,下文也同样简化)
面试官:那你知道事务四种隔离级别吧?
我:读未提交、读已提交、可重复读、可串行化。
面试官:MVCC 用来实现哪几个隔离级别?
我:用来实现读已提交和可重复读。首先隔离级别如果是读未提交的话,直接读最新版本的数据就行了,压根就不需要保存以前的版本。可串行化隔离级别事务都串行执行了,所以也不需要多版本,因此 MVCC 是用来实现读已提交和可重复读的。
面试官:那为什么需要 MVCC ?如果没有 MVCC 会怎样?
我:如果没有 MVCC 读写操作之间就会冲突。想象一下有一个事务1正在执行,此时一个事务2修改了记录A,还未提交,此时事务1要读取记录A,因为事务2还未提交,所以事务1无法读取最新的记录A,不然就是发生脏读的情况,所以应该读记录A被事务2修改之前的数据,但是记录A已经被事务2改了呀,所以事务1咋办?只能用锁阻塞等待事务2的提交,这种实现叫 LBCC(Lock-Based Concurrent Control)。
如果有多版本的话,就不一样了。事务2修改的记录 A,还未提交,但是记录 A 被修改之前的版本还在,此时事务1就可以读取之前的版本数据,这样读写之间就不会阻塞啦,所以说 MVCC 提高了事务的并发度,提升数据库的性能。
面试官:你对这个多版本有没有什么别的理解?
我:(面试官要开始操作我了吗?不过就这,我早有准备!)有点个人的小理解(假装谦虚)。其实这个多版本不是很准确,只是为了便于理解或者说展现出来像多版本的样子而已。
实际上 InnoDB 不会真的存储了多个版本的数据,只是借助 undolog 记录每次写操作的反向操作,所以索引上对应的记录只会有一个版本,即最新版本。只不过可以根据 undolog 中的记录反向操作得到数据的历史版本,所以看起来是多个版本。
面试官:那你能详细的说下 MVCC 是如何实现的吗?
我:您听好啦。
拿上面的insert (1,XX)
这条语句举例,成功插入之后数据页的记录上不仅存储 ID 1,name XX,还有 trx_id 和 roll_pointer 这两个隐藏字段:
- trx_id:当前事务ID。
- roll_pointer:指向 undo log 的指针。
从图中可以得知此时插入的事务ID是1,此时插入会生成一条 undolog ,并且记录上的 roll_pointer 会指向这条 undolog ,而这条 undolog 是一个类型为TRX_UNDO_INSERT_REC
的 log,代表是 insert 生成的,里面存储了主键的长度和值(还有其他值,不提),所以 InnoDB 可以根据 undolog 里的主键的值,找到这条记录,然后把它删除来实现回滚(复原)的效果。因此可以简单地理解 undolog 里面存储的就是当前操作的反向操作,所以认为里面存了个delete 1
就行。
此时事务1提交,然后另一个 ID 为 5 的事务再执行 update NO where id 1
这个语句,此时的记录和 undolog 就如下图所示:
没错,之前 insert 产生的 undolog 没了,insert 的事务提交了之后对应的 undolog 就回收了,因为不可能有别的事务会访问比这还要早的版本了,访问插入之前的版本?访问个寂寞吗?
而 update 产生的 undolog 不一样,它的类型为 TRX_UNDO_UPD_EXIST_REC
。
此时事务 5 提交,然后另一个 ID 为 11 的事务执行update Yes where id 1
这个语句,此时的记录和 undolog 就如下图所示:
没错,update 产生的 undolog 不会马上删除,因为可能有别的事务需要访问之前的版本,所以不能删。这样就串成了一个版本链,可以看到记录本身加上两条 undolog,这条 id 为 1 的记录共有三个版本。
版本链搞清楚了,这时候还需要知道一个概念 readView,这个 readView 就是用来判断哪个版本对当前事务可见的,这里有四个概念:
- creator_trx_id,当前事务ID。
- m_ids,生成 readView 时还活跃的事务ID集合,也就是已经启动但是还未提交的事务ID列表。
- min_trx_id,当前活跃ID之中的最小值。
- max_trx_id,生成 readView 时 InnoDB 将分配给下一个事务的 ID 的值(事务 ID 是递增分配的,越后面申请的事务ID越大)
对于可见版本的判断是从最新版本开始沿着版本链逐渐寻找老的版本,如果遇到符合条件的版本就返回。
判断条件如下:
- 如果当前数据版本的 trx_id == creator_trx_id 说明修改这条数据的事务就是当前事务,所以可见。
- 如果当前数据版本的 trx_id < min_trx_id,说明修改这条数据的事务在当前事务生成 readView 的时候已提交,所以可见。
- 如果当前数据版本的 trx_id 大小在 min_trx_id 和 max_trx_id 之间,此时 trx_id 若在 m_ids 中,说明修改这条数据的事务此时还未提交,所以不可见,若不在 m_ids 中,表明事务已经提交,可见。
- 如果当前数据版本的 trx_id >= max_trx_id,说明修改这条数据的事务在当前事务生成 readView 的时候还未启动,所以不可见(结合事务ID递增来看)。
来看一个简单的案例,练一练上面的规则。
读已提交隔离级别下的MVCC
现在的隔离级别是读已提交。
假设此时上文的事务1已经提交,事务 5 已经执行,但还未提交,此时有另一个事务在执行update YY where id 2
,也未提交,它的事务 ID 为 6,且也是现在最大的事务 ID。
现在有一个查询开启了事务,语句为select name where id 1
,那么这个查询语句:
- 此时 creator_trx_id 为 0,因为一个事务只有当有修改操作的时候才会被分配事务 ID。
- 此时 m_ids 为 [5,6],这两个事务都未提交,为活跃的。
- 此时 min_trx_id,为 5。
- 此时 max_trx_id,为 7,因为最新分配的事务 ID 为 6,那么下一个就是7,事务 ID 是递增分配的。
由于查询的是 ID 为 1 的记录,所以先找到 ID 为 1 的这条记录,此时的版本如下:
此时最新版本的记录上 trx_id 为 5,不比 min_trx_id 小,在 m_ids 之中,表明还是活跃的,未提交,所以不可访问,根据 roll_pointer 找到上一个版本。
于是找到了图上的那条 undolog,这条log上面记录的 trx_id 为 1,比 min_trx_id 还小,说明在生成 readView 的时候已经提交,所以可以访问,因此返回结果 name 为 XX。
然后事务 5 提交。
此时再次查询 select name where id 1
,这时候又会生成新的 readView。
- 此时 creator_trx_id 为 0,因为还是没有修改操作。
- 此时 m_ids 为 [6],因为事务5提交了。
- 此时 min_trx_id,为 6。
- 此时 max_trx_id,为 7,此时没有新的事务申请。
同样还是查询的是 ID 为 1 的记录,所以还是先找到 ID 为 1 的这条记录,此时的版本如下(和上面一样,没变):
此时最新版本的记录上 trx_id 为 5,比 min_trx_id 小,说明事务已经提交了,是可以访问的,因此返回结果 name 为 NO。
这就是读已提交的 MVCC 操作,可以看到一个事务中的两次查询得到了不同的结果,所以也叫不可重复读。
可重复读隔离级别下的MVCC
现在的隔离级别是可重复读。
可重复读和读已提交的 MVCC 判断版本的过程是一模一样的,唯一的差别在生成 readView 上。
上面的读已提交每次查询都会重新生成一个新的 readView ,而可重复读在第一次生成 readView 之后的所有查询都共用同一个 readView 。
也就是说可重复读只会在第一次 select 时候生成一个 readView ,所以一个事务里面不论有几次 select ,其实看到的都是同一个 readView 。
套用上面的情况,差别就在第二次执行select name where id 1
,不会生成新的 readView,而是用之前的 readView,所以第二次查询时:
- m_ids 还是为 [5,6],虽说事务 5 此时已经提交了,但是这个readView是在事务5提交之前生成的,所以当前还是认为这两个事务都未提交,为活跃的。
- 此时 min_trx_id,为 5。
(对于判断过程有点卡顿的同学可以再拉上去看看,判断版本的过程和读已提交一致)。
所以在可重复级别下,两次查询得到的 name 都为 XX,所以叫可重复读。
说完之后,我对面试官挑了挑眉。
面试官瞥了我一眼:可以,那按你这么说其实 undolog 算是热点资源,多个事务不就会争抢 undolog 了吗?
我:对呀,所以为了提高 undolog 的写入性能,每个事务都有属于自己的 undolog 页面链表,这样就提高了写入并发度啦,再细一点就是 insert 类型的 undolog 和 update 类型的 undolog 属于不同的链表。
面试官:还能细吗?
我:再细一点就是普通表和临时表各有一条 insert 类型的 undolog 和 update 类型的 undolog ,所以最多一个事务可以有四条 undolog 页面链表,之所以分普通表和临时表是因为普通表的 undolog 写入是需要记录到redolog 中的需要保证崩溃恢复,而临时表则不需要记录,反正就是临时的。
面试官:对了,你上面说 insert 和 update ,那 delete 呢?
我:delete 其实是属于 update 的,不过分了好几种情况,反正 delete 只会给记录上打个标记,表明这条记录被删除了,不会马上删除这条记录,因为记录还得存着给别的事务作为版本链访问呢。
面试官:那这条被删除的记录就永远存在了?
我:不会的,后台有一个 purge 线程,如果探测出当前没有事务会访问这个记录了,就会把它真正的删除。
面试官:你这么细,应该没有女朋友的吧?
我:(???,不对,面试官应该没有人身攻击我,只是说我天天刻苦学习,没时间找女朋友,但是我还是有点不爽)没呢,面试官您头发这么多,应该也还没找到吧?
“我在仰望,月亮之上…”,此时面试官手机响起。
面试官:“喂,亲爱的,来了来了,马上下班了,待会老地方见哈。”那啥我有点事,你先回去吧。
我:(???小丑竟是我自己)好嘞好嘞。
面试官:对了,这一面还没结束,undolog看你挺熟的,下次详细问你,还有 MySQL 锁啊我都还没问,等通知下次再来吧。
我:(还给我布置家庭作业呢?)我一定回去好好准备准备,等待您的宠幸。
这老面试官可以,竟然没折磨到他,等着,下次 undolog 和 MySQL 锁我一定好好招待他!
MySQL锁与面试官小战30回合
我,小Y。
又来面试了,还是之前那家公司,即将和之前那个老面试官进行第二次 battle,心情还是xue微有点忐忑。
又一抹光亮闪过,面试官推门而入,我抬头望去,没错,还是那味儿。
看到面试官头上那“傲然矗立”的头发,差点又想站起来给他敬了个礼,算了先稳住,低调一点。
面试官瞥了我一眼:来吧,咱们继续面试,上次没办法,女朋友就是粘人,这次问 MySQL InnoDB 的锁喔。
我:…(行,我知道你有女朋友了),好的面试官,您请。
面试官:MySQL InnoDB 的锁 和 MyISAM 的锁有什么区别?
我:MyISAM 只支持表锁,一锁就锁整张表,而 InnoDB 不仅支持表锁,还支持粒度更低的行锁,仅对相关的记录上锁即可,所以对于写入操作来说 InnoDB 的性能更高。
面试官:那不论表锁还是行锁,其实有分为两类的,你知道是哪两类吗?
我:你指的是 shared (S) locks 和 exclusive (X) locks 吗?
-
S锁,称为共享锁,事务在读取记录的时候获取 S 锁,它允许多个事务同时获取 S 锁,互相之间不会冲突。
-
X锁,称为独占锁,事务在修改记录的时候获取 X 锁,且只允许一个事务获取 X 锁,其它事务需要阻塞等待。
所以 S 锁之间不冲突,X 锁则为独占锁,所以 X 之间会冲突, X 和 S 也会冲突。
冲突 | S | X |
---|---|---|
S | 不冲突 | 冲突 |
X | 冲突 | 冲突 |
不论是表级别锁还是行级别锁,S 和 X 的特性都是一样的。
面试官:你说事务在读取记录的时候需要获取 S 锁?这不对吧?
我:确实不准确。益与 MVCC 的功劳,普通的 select 是不需要加锁的,而 SELECT ... LOCK IN SHARE MODE;
这种读取需要对记录上 S 锁。SELECT ... FOR UPDATE;
需要对记录上 X 锁。
面试官:对了,你刚提到表级锁,那你平时用过 InnoDB 的表锁吗?
我:没用过,InnoDB 的表锁很鸡肋,我知道:
LOCK TABLES yes READ
是对 yes 这个表上 S 锁。LOCK TABLES yes WRITE
是对 yes 这个表上 X 锁。
但是基本上没用。
面试官:噢?怎么个鸡肋了?
我:平日的 update 、select 要用也是用行锁了,不可能用粒度粗的表锁。唯一能想到用上表锁的就是 DDL 语句了,比如 ALTER TABLE 的时候,应该锁定整个表,防止查询和修改,但是这个 server 已经提供了一个叫 MDL 的东西,即 Metadata Locks
,所以已经用 MDL 来阻塞了,表锁也就排不上用场了。
真要用表锁,估计也就是数据恢复的时候,手动锁表还原数据了。
面试官摸了摸头上的反光处:可以,但是如果真要到用表锁的时候,那表锁和行锁之间不是会冲突的吗?如果表里面已经加了行锁怎么办?得一条记录一条记录遍历过去找行锁吗?
我:这确实是一种实现方式,但是性能太差了,假设数据库里有上千万的数据,这加个表锁得找死。
所以有了个叫意向锁(Intention Locks)的东西。
- IS(Intention Shared Lock),共享意向锁
- IX(Intention Exclusive Lock),独占意向锁。
这两个锁是表级别的锁,当需要对表中的某条记录上 S 锁的时候,先在表上加个 IS 锁,表明此时表内有 S 锁。当需要对表中的某条记录上 X 锁的时候,先在表上加个 IX 锁,表明此时表内有 X 锁。
这样操作之后,如果要加表锁,就不需要遍历所有记录去找了,直接看看表上面有没有 IS 和 IX 锁。
比如,此时要上表级别的 S 锁,如果表上没有 IX ,说明表中没有记录有独占锁,其实就可以直接上表级 S 锁。
如果此时要上表级别的 X 锁,如果表上没有 IX 和 IS ,说明表中的所有记录都没加锁,其实就可以直接上表级 X 锁。
因此 IS 和 IX 的作用就是在上表级锁的时候,可以快速判断是否可以上锁,而不需要遍历表中的所有记录。
所以 IS 和 IX 互相之间是不会冲突的,因为它们的作用只是打个标记,来丰富一下上面的表格:
冲突 | S | X | IS | IX |
---|---|---|---|---|
S | 不冲突 | 冲突 | 不冲突 | 冲突 |
X | 冲突 | 冲突 | 冲突 | 冲突 |
IS | 不冲突 | 冲突 | 不冲突 | 不冲突 |
IX | 冲突 | 冲突 | 不冲突 | 不冲突 |
面试官:行,那再来说说行锁吧,InnoDB 有几类行锁?
我:有记录锁(Record Locks)、间隙锁(Gap Locks)、Next-Key Locks。
面试官:详细说说看?
我:记录锁顾名思义就是锁住当前的记录,它是作用到索引上的。我们都知道 innodb 是肯定有索引的,即使没有主键也会创建隐藏的聚簇索引,所以记录锁总是锁定索引记录。
比如,此时一个事务 A 执行 SELECT * FROM yes WHERE name = 'xx' FOR UPDATE;
那么 name = xx 这条记录就被锁定了,其他事务无法插入、删除、修改 name = xx 的记录。
此时事务 A 还未提交,另一个事务 B 要执行 insert into yes (name) values ('xx')
,此时会被阻塞,这个很好理解。
但是,如果另一个事务 C 执行了 insert into yes (name) values ('aa')
,这个语句会被阻塞吗?
看情况。
如果 name 没有索引。前面提到记录锁是加到索引上的,但是 name 没索引啊,那只能去找聚簇索引,但聚簇索引上面只有主键啊,它哪知道各自的 name 是什么,所以咋办?都锁了呗!
因此,如果 name 没有索引,那么事务 C 会被阻塞,如果有索引,则不会被阻塞!
所以这里要注意,没索引的列不要轻易的锁,不要以为有行锁就可以为所欲为,并不是这样滴。
面试官:哟,有点东西,继续继续。
我:然后是间隙锁,这个东西它有点东西。
前面说了,记录锁需要加到记录上,但是如果要给此时还未存在的记录加锁怎么办?也就是要预防幻读的出现!
这时候间隙锁就派上用场了,它是给间隙加上锁。
比如此时有 1、3、5、10 这四条记录,之前的文章分析过,数据页中还有两条虚拟的记录,分别是 Infimum
和 Supremum
。
可以看到,记录之前都有间隙,那间隙锁呢,锁的就是这个间隙!
比如我把 3 和 5 之间的间隙锁了,此时要插入 id = 4 的记录,就会被这个间隙锁给阻塞了,这样就避免了幻读的产生!也就实现了锁定未插入的记录的需求!
还有个 Next-Key Locks
就是记录锁+间隙锁,像上面间隙锁的举例,只能锁定(3,5) 这个区间,而 Next-Key Locks
是一个前开后闭的区间(3,5],这样能防止查询 id=5 的这个幻读。
面试官:那间隙锁之间会不会冲突?
我 :不会,间隙锁的唯一目的就是防止其他事务插入数据到间隙中 ,所以即使两个间隙锁要锁住相同的间隙也没有关系,因为它们的目的是一致的,所以不冲突。
面试官:那间隙锁可以显式禁用吗?
我 :可以的。间隙锁是在事务隔离级别为可重复读的时候生效的,如果将事务隔离级别更改为 READ COMMITTED,就会禁用了,此时,间隙锁对于搜索和索引扫描是禁用的,仅用于外键约束检查和重复键检查。
面试官:说到间隙锁,那你知道什么是插入意向锁吗?
我:插入意向锁,即 Insert Intention Locks,它也是一类间隙锁,但是它不是锁定间隙,而是等待某个间隙。比如上面举例的 id = 4 的那个事务 C ,由于被间隙锁给阻塞了,所以事务 C 会生成一个插入意向锁,表明等待这个间隙锁的释放。
并且插入意向锁之间不会阻塞,因为它们的目的也是只等待这个间隙被释放,所以插入意向锁之间没有冲突。
面试官:所以这个插入意向锁其实没什么用的?
我:确实,它的目的不在于锁定资源防止别人访问,我个人觉得更像是为了遵循 MySQL 的锁代码实现而为之。
锁其实就是内存里面的一个结构,每个事务为某个记录或者间隙上锁就是创建一个锁对象来争抢资源。
如果某个事务没有抢到资源,那也会生成一个锁对象,只是状态是等待的,而当拥有资源的事务释放锁之后,就会寻找正在等待当前资源的锁结构,然后选一个让它获得资源并唤醒对应的事务使之得以执行。
所以按照这么个逻辑,那些在等待间隙锁的插入事务,也需要对应的建立一个锁结构,然后锁类型是插入意向锁。
这样一来,间隙锁的事务在释放间隙锁的时候,才能得以找到那些等待插入的事务,然后进行唤醒,而由锁的类型也可以得知是插入意向锁,之间不需要阻塞,所以可以一起执行插入。
面试官:说到插入新记录我问你个问题,如果插入的事务还未提交,现在有另一个事务通过SELECT ... LOCK IN SHARE MODE
或者SELECT ... FOR UPDATE
打算读取这条记录怎么办?此时生效的是什么锁?
我:(我丢,面试官想给我挖坑?哼,但是这难不倒我霸中霸!)
SELECT … LOCK IN SHARE MODE或者
SELECT … FOR UPDATE` 是要获取记录 S 锁和 X 锁的,但是此时事务还未提交,因此这两类 select 会阻塞。
具体是怎么阻塞的呢?因为有事务ID!通过 MVCC 可以利用事务ID 来进行判断当前记录是否可见,这其实相当于一把隐式锁!知道当前记录不可见,于是这个查询事务会为之前未提交的插入的事务生成一个锁结构,然后查询事务自己也生成锁结构,接着等待插入事务的释放,这样就完成了阻塞!
面试官:(这小子,我要压不住他了!)行,那你知道什么是 AUTO-INC Locks 锁吗?
我:知道,Auto-Inc Lock 是一个特殊的表级锁,用于自增列插入数据时使用。 在插入一条数据的时候,需要在表上加个 Auto-Inc Lock,然后为自增列分配递增的值,在语句插入结束之后,再释放 Auto-Inc Lock。
在 MySQL 5.1.22 版本之后,又弄了个互斥量来进行自增减的累加。互斥量的性能高于 Auto-Inc Lock,因为 Auto-Inc Lock是语句插入完毕之后才释放锁,而互斥量是在语句插入的时候,获得递增值之后,就可以释放锁,所以性能更好。
但是我们还需要考虑主从的情况,由于并发插入的情况,基于 statement -based binlog 复制时,自增的值顺序无法把控,可能会导致主从数据不一致。
所以 MySQL 有个 innodb_autoinc_lock_mode 配置,一共有三个值:
- 0,只用 Auto-Inc Lock。
- 1,默认值,对于插入前已知插入行数的插入,用互斥量,对于插入前不知道具体插入数的插入,用 Auto-Inc Lock,这样即使基于 statement -based binlog 复制也是安全的。
- 2,只用互斥量。
面试官:那 MyISAM 有 AUTO-INC Locks 锁吗?
我:没啊,MyISAM 插入本来就用了表锁。
面试官:(这小子行啊,我得找回行子)那你还知道 MySQL 有什么锁吗?
我:(这还没问够??)表锁、IS、IX、MDL、记录锁、间隙锁、Next-key locks、插入意向锁、Auto-Inc Locks,还有啥?
面试官瞥了我一眼:(好小子,总算治了你了)不知道了?
我:(该怂的时候,还是得怂)知识盲区了,请面试官教教我。
面试官: 还有个 Predicate Locks,谓词锁。
我:什么玩意?
面试官:InnoDB 是支持空间数据的,所以有空间索引,为了处理涉及空间索引的操作的锁定,next-key locking 不好使,因为多维数据中没有绝对排序的概念,因此不清楚“下一个” key 在哪。
所以为了支持具有空间索引的表的隔离级别,InnoDB使用谓词锁。
空间索引包含最小边界矩形(MBR)值,因此 InnodB 通过在用于查询的 MBR 值上设置谓词锁定,使得 InnoDB 在索引上执行一致性读, 其他事务无法插入或修改与查询条件匹配的行。
我:(…果然超出了我的知识范围,这个B被他装到了)老面试官您真的是66666。
面试官:行吧,你今天答的马马虎虎还可以,下次再问问你啥 buffer pool、change buffer、doublewrite buffer 啥的。
我:(????还问呢),这是还继续面吗?这算三面吗?
面试官:你管我,我就想多面面你,充分了解一下,我们公司很严格的,人不是随便招的!赶紧回去等通知!
我:行行行,您老等着,就一堆 buffer 是吧,我回去好好准备准备哈~
更新未完,欢迎关注我,后面会补充相关面试题~
更多面试题可以看我汇总的仓库,每个面试题都是含答案的,Java基础已经更新完毕~
面试题仓库,含答案
还有一个个人原创文章汇总仓库,也有很多面试题解析:
原创不易,如果觉得文章不错来个点赞、收藏、评论三连呗!关注我不迷路,我是yes,我们下篇见!
以上是关于2万字高频MySQL面试题总结(含答案),金九银十成为offer收割机!建议收藏的主要内容,如果未能解决你的问题,请参考以下文章