想进大厂,mysql不会那可不行,来接受mysql面试挑战吧,看看你能坚持到哪里?php
1. 能说下myisam 和 innodb的区别吗?
myisam引擎是5.1版本以前的默认引擎,支持全文检索、压缩、空间函数等,可是不支持事务和行级锁,因此通常用于有大量查询少许插入的场景来使用,并且myisam不支持外键,而且索引和数据是分开存储的。mysql
innodb是基于聚簇索引创建的,和myisam相反它支持事务、外键,而且经过MVCC来支持高并发,索引和数据存储在一块儿。程序员
2. 说下mysql的索引有哪些吧,聚簇和非聚簇索引又是什么?
索引按照数据结构来讲主要包含B+树和Hash索引。面试
假设咱们有张表,结构以下:算法
create table user( id int(11) not null, age int(11) not null, primary key(id), key(age) );
B+树是左小右大的顺序存储结构,节点只包含id索引列,而叶子节点包含索引列和数据,这种数据和索引在一块儿存储的索引方式叫作聚簇索引,一张表只能有一个聚簇索引。假设没有定义主键,InnoDB会选择一个惟一的非空索引代替,若是没有的话则会隐式定义一个主键做为聚簇索引。sql

这是主键聚簇索引存储的结构,那么非聚簇索引的结构是什么样子呢?非聚簇索引(二级索引)保存的是主键id值,这一点和myisam保存的是数据地址是不一样的。数据库

最终,咱们一张图看看InnoDB和Myisam聚簇和非聚簇索引的区别数据结构

3. 那你知道什么是覆盖索引和回表吗?
覆盖索引指的是在一次查询中,若是一个索引包含或者说覆盖全部须要查询的字段的值,咱们就称之为覆盖索引,而再也不须要回表查询。多线程
而要肯定一个查询是不是覆盖索引,咱们只须要explain sql语句看Extra的结果是不是“Using index”便可。并发
以上面的user表来举例,咱们再增长一个name字段,而后作一些查询试试。
explain select * from user where age=1; //查询的name没法从索引数据获取 explain select id,age from user where age=1; //能够直接从索引获取
4. 锁的类型有哪些呢
mysql锁分为共享锁和排他锁,也叫作读锁和写锁。
读锁是共享的,能够经过lock in share mode实现,这时候只能读不能写。
写锁是排他的,它会阻塞其余的写锁和读锁。从颗粒度来区分,能够分为表锁和行锁两种。
表锁会锁定整张表而且阻塞其余用户对该表的全部读写操做,好比alter修改表结构的时候会锁表。
行锁又能够分为乐观锁和悲观锁,悲观锁能够经过for update实现,乐观锁则经过版本号实现。
5. 你能说下事务的基本特性和隔离级别吗?
事务基本特性ACID分别是:
原子性指的是一个事务中的操做要么所有成功,要么所有失败。
一致性指的是数据库老是从一个一致性的状态转换到另一个一致性的状态。好比A转帐给B100块钱,假设中间sql执行过程当中系统崩溃A也不会损失100块,由于事务没有提交,修改也就不会保存到数据库。
隔离性指的是一个事务的修改在最终提交前,对其余事务是不可见的。
持久性指的是一旦事务提交,所作的修改就会永久保存到数据库中。
而隔离性有4个隔离级别,分别是:
read uncommit 读未提交,可能会读到其余事务未提交的数据,也叫作脏读。
用户原本应该读取到id=1的用户age应该是10,结果读取到了其余事务尚未提交的事务,结果读取结果age=20,这就是脏读。

read commit 读已提交,两次读取结果不一致,叫作不可重复读。
不可重复读解决了脏读的问题,他只会读取已经提交的事务。
用户开启事务读取id=1用户,查询到age=10,再次读取发现结果=20,在同一个事务里同一个查询读取到不一样的结果叫作不可重复读。

repeatable read 可重复复读,这是mysql的默认级别,就是每次读取结果都同样,可是有可能产生幻读。
serializable 串行,通常是不会使用的,他会给每一行读取的数据加锁,会致使大量超时和锁竞争的问题。
6. 那ACID靠什么保证的呢?
A原子性由undo log日志保证,它记录了须要回滚的日志信息,事务回滚时撤销已经执行成功的sql
C一致性通常由代码层面来保证
I隔离性由MVCC来保证
D持久性由内存+redo log来保证,mysql修改数据同时在内存和redo log记录此次操做,事务提交的时候经过redo log刷盘,宕机的时候能够从redo log恢复
7. 那你说说什么是幻读,什么是MVCC?
要说幻读,首先要了解MVCC,MVCC叫作多版本并发控制,实际上就是保存了数据在某个时间节点的快照。
咱们每行数实际上隐藏了两列,建立时间版本号,过时(删除)时间版本号,每开始一个新的事务,版本号都会自动递增。
仍是拿上面的user表举例子,假设咱们插入两条数据,他们实际上应该长这样。
id | name | create_version | delete_version |
---|---|---|---|
1 | 张三 | 1 | |
2 | 李四 | 2 |
这时候假设小明去执行查询,此时current_version=3
select * from user where id<=3;
同时,小红在这时候开启事务去修改id=1的记录,current_version=4
update user set name='张三三' where id=1;
执行成功后的结果是这样的
id | name | create_version | delete_version |
---|---|---|---|
1 | 张三 | 1 | |
2 | 李四 | 2 | |
1 | 张三三 | 4 |
若是这时候还有小黑在删除id=2的数据,current_version=5,执行后结果是这样的。
id | name | create_version | delete_version |
---|---|---|---|
1 | 张三 | 1 | |
2 | 李四 | 2 | 5 |
1 | 张三三 | 4 |
因为MVCC的原理是查找建立版本小于或等于当前事务版本,删除版本为空或者大于当前事务版本,小明的真实的查询应该是这样
select * from user where id<=3 and create_version<=3 and (delete_version>3 or delete_version is null);
因此小明最后查询到的id=1的名字仍是'张三',而且id=2的记录也能查询到。这样作是为了保证事务读取的数据是在事务开始前就已经存在的,要么是事务本身插入或者修改的。
明白MVCC原理,咱们来讲什么是幻读就简单多了。举一个常见的场景,用户注册时,咱们先查询用户名是否存在,不存在就插入,假定用户名是惟一索引。
小明开启事务current_version=6查询名字为'王五'的记录,发现不存在。
小红开启事务current_version=7插入一条数据,结果是这样:
id | Name | create_version | delete_version |
---|---|---|---|
1 | 张三 | 1 | |
2 | 李四 | 2 | |
3 | 王五 | 7 |
小明执行插入名字'王五'的记录,发现惟一索引冲突,没法插入,这就是幻读。
8. 那你知道什么是间隙锁吗?
间隙锁是可重复读级别下才会有的锁,结合MVCC和间隙锁能够解决幻读的问题。咱们仍是以user举例,假设如今user表有几条记录
id | Age |
---|---|
1 | 10 |
2 | 20 |
3 | 30 |
当咱们执行:
begin; select * from user where age=20 for update; begin; insert into user(age) values(10); #成功 insert into user(age) values(11); #失败 insert into user(age) values(20); #失败 insert into user(age) values(21); #失败 insert into user(age) values(30); #失败
只有10能够插入成功,那么由于表的间隙mysql自动帮咱们生成了区间(左开右闭)
(negative infinity,10],(10,20],(20,30],(30,positive infinity)
因为20存在记录,因此(10,20],(20,30]区间都被锁定了没法插入、删除。
若是查询21呢?就会根据21定位到(20,30)的区间(都是开区间)。
须要注意的是惟一索引是不会有间隙索引的。
9. 大家数据量级多大?分库分表怎么作的?
首先分库分表分为垂直和水平两个方式,通常来讲咱们拆分的顺序是先垂直后水平。
垂直分库
基于如今微服务拆分来讲,都是已经作到了垂直分库了

垂直分表
若是表字段比较多,将不经常使用的、数据较大的等等作拆分

水平分表
首先根据业务场景来决定使用什么字段做为分表字段(sharding_key),好比咱们如今日订单1000万,咱们大部分的场景来源于C端,咱们能够用user_id做为sharding_key,数据查询支持到最近3个月的订单,超过3个月的作归档处理,那么3个月的数据量就是9亿,能够分1024张表,那么每张表的数据大概就在100万左右。
好比用户id为100,那咱们都通过hash(100),而后对1024取模,就能够落到对应的表上了。
10. 那分表后的ID怎么保证惟一性的呢?
由于咱们主键默认都是自增的,那么分表以后的主键在不一样表就确定会有冲突了。有几个办法考虑:
设定步长,好比1-1024张表咱们分别设定1-1024的基础步长,这样主键落到不一样的表就不会冲突了。
分布式ID,本身实现一套分布式ID生成算法或者使用开源的好比雪花算法这种
分表后不使用主键做为查询依据,而是每张表单独新增一个字段做为惟一主键使用,好比订单表订单号是惟一的,无论最终落在哪张表都基于订单号做为查询依据,更新也同样。
11. 分表后非sharding_key的查询怎么处理呢?
能够作一个mapping表,好比这时候商家要查询订单列表怎么办呢?不带user_id查询的话你总不能扫全表吧?因此咱们能够作一个映射关系表,保存商家和用户的关系,查询的时候先经过商家查询到用户列表,再经过user_id去查询。
打宽表,通常而言,商户端对数据实时性要求并非很高,好比查询订单列表,能够把订单表同步到离线(实时)数仓,再基于数仓去作成一张宽表,再基于其余如es提供查询服务。
数据量不是很大的话,好比后台的一些查询之类的,也能够经过多线程扫表,而后再聚合结果的方式来作。或者异步的形式也是能够的。
List<Callable<List<User>>> taskList = Lists.newArrayList(); for (int shardingIndex = 0; shardingIndex < 1024; shardingIndex++) { taskList.add(() -> (userMapper.getProcessingAccountList(shardingIndex))); } List<ThirdAccountInfo> list = null; try { list = taskExecutor.executeTask(taskList); } catch (Exception e) { //do something } public class TaskExecutor { public <T> List<T> executeTask(Collection<? extends Callable<T>> tasks) throws Exception { List<T> result = Lists.newArrayList(); List<Future<T>> futures = ExecutorUtil.invokeAll(tasks); for (Future<T> future : futures) { result.add(future.get()); } return result; } }
12. 说说mysql主从同步怎么作的吧?
首先先了解mysql主从同步的原理
master提交完事务后,写入binlog
slave链接到master,获取binlog
master建立dump线程,推送binglog到slave
slave启动一个IO线程读取同步过来的master的binlog,记录到relay log中继日志中
slave再开启一个sql线程读取relay log事件并在slave执行,完成同步
slave记录本身的binglog

因为mysql默认的复制方式是异步的,主库把日志发送给从库后不关心从库是否已经处理,这样会产生一个问题就是假设主库挂了,从库处理失败了,这时候从库升为主库后,日志就丢失了。由此产生两个概念。
全同步复制
主库写入binlog后强制同步日志到从库,全部的从库都执行完成后才返回给客户端,可是很显然这个方式的话性能会受到严重影响。
半同步复制
和全同步不一样的是,半同步复制的逻辑是这样,从库写入日志成功后返回ACK确认给主库,主库收到至少一个从库的确认就认为写操做完成。
13. 那主从的延迟怎么解决呢?
这个问题貌似真的是个无解的问题,只能是说本身来判断了,须要走主库的强制走主库查询。
—————END—————
喜欢本文的朋友,欢迎关注公众号 程序员小灰,收看更多精彩内容
点个[在看],是对小灰最大的支持!