背景介绍
我的工作是python服务器开发工程师,最近准备看看其他工作机会,也面试了一些公司。MySQL基本是一定会被问到的知识点,所以最近又开始恶补相关的知识点。面试过程中越来越觉得面试官真的很重要,他是面试者了解公司最直接的一个途径,也是公司技术的把关者。所以我也在思考一个问题,假如我是面试官我会怎么考察面试者?
面试的宗旨
首先面试最本质的目的是招收合理的人来工作,所以应该要避免的问题是“面试内容是造火箭,结果工作内容却是搬砖”。所以要问的问题最好是贴近生产环境的。其次面试通常会涉及很多概念,工作比较久的同学通常容易忘记这些东西。所以我觉得没有必要考察概念,而是考察这些概念的本质,这些本质的东西是不容易忘记的。
这里说明一下,通常业务中用的最多的是Innodb,所以以下问题都是基于Innodb的。
问题一:考察数据隔离和事务
第一阶段:
MySQL的隔离级别有如下四种,默认的级别是什么?
READ-UNCOMMITTED
READ-COMMITTED
REPEATABLE-READ
SERIALIZABLE
REPEATABLE-READ。这个应该是常识,大家都知道的。这里需要让面试者解释一下隔离级别的意义,因为后面的问题是基于这个概念的。如果面试者忘了概念建议提示一下,继续下一个问题。
READ-UNCOMMITTED:表示可以读取到未提交的数据
READ-COMMITTED:可以读取到已经提交的数据
REPEATABLE-READ:可重复读,每次读取的结果都是一样的。
SERIALIZABLE 完全串行化的读,每次读都需要获得共享锁,读写相互都会阻塞
这里很多面试官会问一个REPEATABLE-READ怎么解决幻读的问题。当时有点蒙,“幻读”是什么东西。其实本质上很简单,幻读其实就是MySQL怎么确保“可重复读”这个功能的。具体的名词叫数据多版本并发控制(MVCC)。不过这个应该是比较底层的知识,属于Nice to have了。
第二阶段
有一张表user_count,这个表记录的是不同国家用户的个数。现在如下代码会在以上四个级别的隔离中分别执行一次,请问结果分别是什么?注意,以下代码是在5000个线程中执行的,所以是并发执行的。
数据样例:
+——–+——–+
| region | number |
+——–+——–+
| us | 0 |
| cn | 0 |
+——–+——–+
代码:
@dec_multi_thread_run(5000)
def do():
try:
# get old num from table
num = “select number from user_count where region = ‘cn'”
# update to new value
“update user_count set number = %s where region = ‘cn’ ” % (num +1)
except:
rollback()
else:
commit()
首先问这个问题的话,面试者肯定知道答案不是5000,而是小于5000。大部分人会根据隔离级别越来越高判断结果的值会越来越小,而真实结果如下。
read uncommitted,3271
read committed,2638
repeatable read,2551
serializable,3052
对于前三个结果递减比较容易解释,因为随着事物隔离级别变高越不可能读到脏数据。但对于serializable,直观的结果应该是返回5000,因为序列化级别是不会读到脏数据的。但实际上这里考察的是一个会出现死锁的问题。serializable级别会对读取加上一个共享锁,也就是说在多线程下会出现很多事务同时获取到了共享锁。然后当要执行update操作去获取排它锁时就会出现死锁了。也就是说某些更新会失败造成结果小于5000。这里考察到了serializable具体的底层执行方式。
如果问的深入一点,可以问下MySQL检查死锁的方式以及怎么自动解除死锁的。当然,这个也是Nice to have的。在看参考资料1的时候,有提到对高并发业务的优化。具体的方式就是关闭死锁检测,然后用一个较短的timout时间替代。不过我没有亲自验证,有兴趣的同学可以自行验证一下。
第三阶段
怎么优化上面的代码,使结果为5000
这个比较简单,主要是考察一下面试者知不知道锁的概念。 select 语句后面加一个for update就解决问题了。
如果把语句改成update user_count set number = number + 1 where region = ‘cn’ 在四个事物级别下结果分别是多少?
结果都是5000,因为MySQL会默认在insert,update,delete时增加一个排他锁,Nice to have。
问题二:索引
第一阶段:
有一个user表,id是自增主键,记录的是用户的信息。数据量是1000万行,请建立索引并优化用到的查询。
+——–+————–+———-+———-+————–+
| id | country
\ user_
name \ sex \ create_time |+——–+————–+———-+———-+————–+
| 1 | cn | alice | female | 1559476160 |
| 2 | us | bob | male | 1559476170 |
用到的查询语句
— 查找叫Alice的人
select * from user where user_name = “alice”;
— 查找国家是us叫Alice的人
select * from user where country = “us” and user_name = “alice”;
— 查找所有叫Alice的女性
select * from user where
sex = “female” and user_name = “alice”;— 展示最新注册的10个用户
select * from user order by create_time desc limit 10;
最优解是只用一个联合索引(user_name, country)并调整一下语句2和语句3的查询顺序,username放在前面。
针对第一个语句,只查询user_name也能利用到联合索引。针对语句2完美利用了联合索引。针对语句三不用对sex建立索引,因为区分度太低没有意义。针对语句4自增id默认就是按照时间递增的,所以没有必要再增加索引了。
第二阶段:
通常在设计数据库的时候都会用到自增id,具体的原因是什么?
答案是增加数据插入的速度。知道这里就够了。下一个阶段是Nice to Have
第三阶段:
MySQL索引的数据结构是什么?为什么要用这个结构?
因为MySQL存储的数据结构用的是B+树。B+树和B树(多路平衡查找树)的主要区别是B+树所有的数据都是存储在叶子节点的。通常B+树的层级都是很少地,单个非根节点会存储很多的数据,这样可以减少读取索引的次数。因为机械磁盘擅长的是大块地顺序读写,一次性读到越多的数据越有利于快速查找,而且B+树有个好处,所有的叶子节点都是指针相连的,可以快速地进行全表便利。所以是个特别适合数据库的数据结构。
以上是我能想到的两大问题(其实我就准备了这些😹),日常开发中我觉得知道MySQL的这两个问题应该就够了,如果有其他好的问题还请各位不吝赐教~
参考资料: