Skip to content

MySQL 面试题 13 问

面试题 MySQL

1、myisam 和 innodb 的区别

myisam 引擎是 5.1 版本之前的默认引擎,支持全文检索、压缩、空间函数等,但是不支持事务和行级锁,所以一般用于有大量查询少量插入的场景来使用,而且 myisam 不支持外键,并且索引和数据是分开存储的。

innodb 是基于聚簇索引建立的,和 myisam 相反它支持事务、外键,并且通过 MVCC 来支持高并发,索引和数据存储在一起。

2、mysql 的索引有哪些吧,聚簇和非聚簇索引又是什么?

索引按照数据结构来说主要包含 B+ 树和 Hash 索引。

假设有张表,结构如下:

sql
create table user(
 id int(11) not null,
  age int(11) not null,
  primary key(id),
  key(age)
);

B+ 树是左小右大的顺序存储结构,节点只包含 id 索引列,而叶子节点包含索引列和数据,这种数据和索引在一起存储的索引方式叫做聚簇索引,一张表只能有一个聚簇索引。假设没有定义主键,InnoDB 会选择一个唯一的非空索引代替,如果没有的话则会隐式定义一个主键作为聚簇索引。

1600701126678-7eeb1d96-8ef3-40b3-a4c0-7aa769bbe427.webp

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

1600701126622-6bcc38b6-e12a-48f9-9f5c-2d8498856a0d.webp

最后一张图看看 InnoDB 和 Myisam 聚簇和非聚簇索引的区别

1600701126651-cb0c5e7e-8a5e-484a-8f2b-3df55c4bed70.webp

3、什么是覆盖索引和回表吗?

覆盖索引指的是在一次查询中,如果一个索引包含或者说覆盖所有需要查询的字段的值,称之为覆盖索引,而不再需要回表查询。

而要确定一个查询是否是覆盖索引,只需要 explain sql 语句看 Extra 的结果是否是“Using index”即可。

以上面的 user 表来举例,再增加一个 name 字段,然后做一些查询试试。

sql
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,这就是脏读。

1600701126708-8594d4f4-85ca-4040-84a7-a152a36f637c.webp

read commit 读已提交,两次读取结果不一致,叫做不可重复读。

不可重复读解决了脏读的问题,他只会读取已经提交的事务。

用户开启事务读取 id=1 用户,查询到 age=10,再次读取发现结果=20,在同一个事务里同一个查询读取到不同的结果叫做不可重复读。

1600701126674-bb3e2020-9d6c-4ed3-af67-762b570da5dc.webp

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 表举例子,假设插入两条数据,他们实际上应该长这样。

idnamecreate_versiondelete_version
1张三1
2李四2

这时候假设小明去执行查询,此时 current_version=3

sql
select * from user where id<=3;

同时,小红在这时候开启事务去修改 id=1 的记录,current_version=4

sql
update user set name='张三三' where id=1;

执行成功后的结果是这样的

idnamecreate_versiondelete_version
1张三1
2李四2
1张三三4

如果这时候还有小黑在删除 id=2 的数据,current_version=5,执行后结果是这样的。

idnamecreate_versiondelete_version
1张三1
2李四25
1张三三4

由于 MVCC 的原理是查找创建版本小于或等于当前事务版本,删除版本为空或者大于当前事务版本,小明的真实的查询应该是这样

sql
select * from user where id<=3 and create_version<=3 and (delete_version>3 or delete_version is null);

所以小明最后查询到的 id=1 的名字还是 ' 张三 ',并且 id=2 的记录也能查询到。这样做是为了保证事务读取的数据是在事务开始前就已经存在的,要么是事务自己插入或者修改的。

明白 MVCC 原理,理解幻读就简单多了。举一个常见的场景,用户注册时,先查询用户名是否存在,不存在就插入,假定用户名是唯一索引。

  1. 小明开启事务 current_version=6 查询名字为 ' 王五 ' 的记录,发现不存在。
  2. 小红开启事务 current_version=7 插入一条数据,结果是这样:
idNamecreate_versiondelete_version
1张三1
2李四2
3王五7
  1. 小明执行插入名字 ' 王五 ' 的记录,发现唯一索引冲突,无法插入,这就是幻读。

8、什么是间隙锁吗?

间隙锁是可重复读级别下才会有的锁,结合 MVCC 和间隙锁可以解决幻读的问题。还是以 user 举例,假设现在 user 表有几条记录

idAge
110
220
330

执行事务:

sql
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. 设定步长,比如 1-1024 张表分别设定 1-1024 的基础步长,这样主键落到不同的表就不会冲突了。
  2. 分布式 ID,自己实现一套分布式 ID 生成算法或者使用开源的比如雪花算法这种
  3. 分表后不使用主键作为查询依据,而是每张表单独新增一个字段作为唯一主键使用,比如订单表订单号是唯一的,不管最终落在哪张表都基于订单号作为查询依据,更新也一样。

11、分表后非 sharding_key 的查询怎么处理呢?

  1. 可以做一个 mapping 表,比如这时候商家要查询订单列表怎么办呢?不带 user_id 查询的话总不能扫全表吧?所以可以做一个映射关系表,保存商家和用户的关系,查询的时候先通过商家查询到用户列表,再通过 user_id 去查询。
  2. 打宽表,一般而言,商户端对数据实时性要求并不是很高,比如查询订单列表,可以把订单表同步到离线(实时)数仓,再基于数仓去做成一张宽表,再基于其他如 es 提供查询服务。
  3. 数据量不是很大的话,比如后台的一些查询之类的,也可以通过多线程扫表,然后再聚合结果的方式来做。或者异步的形式也是可以的。
java
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 主从同步的原理

  1. master 提交完事务后,写入 binlog
  2. slave 连接到 master,获取 binlog
  3. master 创建 dump 线程,推送 binglog 到 slave
  4. slave 启动一个 IO 线程读取同步过来的 master 的 binlog,记录到 relay log 中继日志中
  5. slave 再开启一个 sql 线程读取 relay log 事件并在 slave 执行,完成同步
  6. slave 记录自己的 binglog

1600701126707-382d68ec-146b-44eb-854f-545e4bdc316a.webp

由于 mysql 默认的复制方式是异步的,主库把日志发送给从库后不关心从库是否已经处理,这样会产生一个问题就是假设主库挂了,从库处理失败了,这时候从库升为主库后,日志就丢失了。由此产生两个概念。

全同步复制

主库写入 binlog 后强制同步日志到从库,所有的从库都执行完成后才返回给客户端,但是很显然这个方式的话性能会受到严重影响。

半同步复制

和全同步不同的是,半同步复制的逻辑是这样,从库写入日志成功后返回 ACK 确认给主库,主库收到至少一个从库的确认就认为写操作完成。

13、那主从的延迟怎么解决呢?

需要走主库的强制走主库查询。

更新: 2022-08-12 10:50:15
原文: <https://www.yuque.com/fcant/notes/axrvhp&gt;

最近更新