Java 面试八股文之数据库篇(二)

245 阅读22分钟

前言

这是系列文章【 Java 面试八股文】数据库篇的第二期。

【 Java 面试八股文】系列会陆续更新 Java 面试中的高频问题,旨在从问题出发,理解 Java 基础,数据结构与算法,数据库,常用框架等。该系列前几期文章可以通过点击文末给出的链接进行查看~

按照惯例——首先要做几点说明:

  1. 【 Java 面试八股文】中的面试题来源于社区论坛,书籍等资源;感谢使我读到这些宝贵面经的作者们。
  2. 对于【 Java 面试八股文】中的每个问题,我都会尽可能地写出我自己认为的“完美解答”。但是毕竟我的身份不是一个“真理持有者”,只是一个秉承着开源分享精神的 “knowledge transmitter” & 菜鸡,所以,如果这些答案出现了错误,可以留言写出你认为更好的解答,并指正我。非常感谢您的分享。
  3. 知识在于“融释贯通”,而非“死记硬背”;现在市面上固然有很多类似于“Java 面试必考 300 题” 这类的文章,但是普遍上都是糟粕,仅讲述其果,而不追其源;希望我的【 Java 面试八股文】可以让你知其然,且知其所以然~

那么,废话不多说,我们正式开始吧!

往期文章

数据库篇(二)

1、如何定位并优化慢查询 sql?


我们从定位到慢查询 sql,再通过分析并进行优化的顺序如下:

  1. 通过开启慢日志定位到慢查询的 sql
  2. 使用 explain 工具分析 sql
  3. 修改并优化 sql

接下来我们就依次通过以上三个步骤来看一下,一条慢查询 sql 是如何被找到,并优化的。

1. 通过开启慢日志定位到慢查询 sql

首先,我们进入到客户端,输入命令:

show variables like '%query%';

命令返回结果如下:

mysql> show variables like '%query%';
+------------------------------+--------------------------------------+
| Variable_name                | Value                                |
+------------------------------+--------------------------------------+
| binlog_rows_query_log_events | OFF                                  |
| ft_query_expansion_limit     | 20                                   |
| have_query_cache             | YES                                  |
| long_query_time              | 10.000000                            |
| query_alloc_block_size       | 8192                                 |
| query_cache_limit            | 1048576                              |
| query_cache_min_res_unit     | 4096                                 |
| query_cache_size             | 1048576                              |
| query_cache_type             | OFF                                  |
| query_cache_wlock_invalidate | OFF                                  |
| query_prealloc_size          | 8192                                 |
| slow_query_log               | OFF                                  |
| slow_query_log_file          | /var/lib/mysql/23f9bc5132dc-slow.log |
+------------------------------+--------------------------------------+
13 rows in set (0.01 sec)

在这里面,我们需要关注三个变量,分别是:slow_query_logslow_query_log_file 以及 long-query_time

slow_query_log 目前对应的 Value 值为 OFF,代表慢日志并未开启;slow_query_log_file 是记录慢 sql 的文件,当一条查询 sql 的时间超过 long_query_time 时,就会被记录到慢日志文件中,我们看到 long_query_time 的默认值为 10 s。

首先,我们需要开启慢日志,使用命令:

set global slow_query_log = on;

并且,通常我们会修改 long_query_time 的值。因为如果一条查询 sql 的执行时间超过 10 s 才被定义为慢查询的话,一般是不能被接受的。我们可以按照自己的业务需求,设定相应的值,譬如将其设置为 1 s:

set global long_query_time = 1;

重新连接数据库后,就可以看到刚刚设置的值已经生效了:

mysql> show variables like '%query%';
+------------------------------+--------------------------------------+
| Variable_name                | Value                                |
+------------------------------+--------------------------------------+
| binlog_rows_query_log_events | OFF                                  |
| ft_query_expansion_limit     | 20                                   |
| have_query_cache             | YES                                  |
| long_query_time              | 1.000000                             |
| query_alloc_block_size       | 8192                                 |
| query_cache_limit            | 1048576                              |
| query_cache_min_res_unit     | 4096                                 |
| query_cache_size             | 1048576                              |
| query_cache_type             | OFF                                  |
| query_cache_wlock_invalidate | OFF                                  |
| query_prealloc_size          | 8192                                 |
| slow_query_log               | ON                                   |
| slow_query_log_file          | /var/lib/mysql/23f9bc5132dc-slow.log |
+------------------------------+--------------------------------------+
13 rows in set (0.01 sec)

除了使用命令进行修改,我们也可以通过修改配置文件(my.cnf)对这些变量进行设置,修改配置文件这种方式会使得这些改动永久保存,不会因为重启数据库服务而失效。

这样,我们就可以通过开启慢日志定位到所有超时的慢查询 sql 语句了。

2. 使用 explain 工具分析 sql

explain 是 MySQL 内置的一个命令,可以获取一条语句的执行计划。语法为 explain + 要分析的语句。通过 explain 的分析我们可以知道表的读取顺序,数据读取操作的类型,是否有使用到索引,有无做全表扫描等信息。

目前我有一张学生表:

create table student (
    id int(11) not null auto_increment,
    name varchar(10) default null,
    motto varchar(50) default null,
    primary key(id)
)engine=InnoDB default charset=utf8;

在慢查询日志文件中,有一条超时的查询语句:

select name from student order by name desc;

我们可以使用 explain 工具对这条查询语句进行解析:

explain select name from student order by name desc;

返回结果如下:

+----+-------------+---------+------------+------+---------------+------+---------+------+--------+----------+----------------+
| id | select_type | table   | partitions | type | possible_keys | key  | key_len | ref  | rows   | filtered | Extra          |
+----+-------------+---------+------------+------+---------------+------+---------+------+--------+----------+----------------+
|  1 | SIMPLE      | student | NULL       | ALL  | NULL          | NULL | NULL    | NULL | 102102 |   100.00 | Using filesort |
+----+-------------+---------+------------+------+---------------+------+---------+------+--------+----------+----------------+

我来解释一下其中一些重点的列分别代表什么含义。

id 代表 select 查询语句的序列号。select_type 表示对应行是简单查询还是复杂查询,譬如,我们这条语句既不包含子查询也没有联合查询,所以被定义为简单查询,显示的信息为 SIMPLE。table 列的含义很简单了,表示我们当前解析的这条 sql 语句访问的是哪一张表。type 列表示 MySQL 查询数据行的方式,从最优到最差分别为:system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL。ALL 表示走全表扫描,就意味着 MySQL 需要从头到尾遍历整张表去寻找我们所需要的数据行。所以当我们使用 explain 分析出的结果中,type 列显示的信息为 ALL 时,就需要留意这条查询语句是否有可以优化的空间。possible_keys 列显示查询可能使用哪些索引来查找。key 列显示 MySQL 实际采用哪个索引来优化对该表的访问,如果我们没有使用索引,那么该列的信息则为 NULL。rows 列对应的信息是 MySQL 估计要读取并检测的行数。Extra 列展示的是额外信息,Extra 列如果对应的是以下的两个值则说明我们的语句无法使用索引,效率会受重大影响:第一个是 Using filesort,第二个是 Using temporary。在出现这两个字段时,往往我们都会考虑对 sql 进行优化。

当 Extra 列为 Using filesort 时,表示 MySQL 会对结果使用一个外部索引排序,而不是从表里按照索引次序读到相关内容。可能是在内存或者磁盘上进行排序,MySQL 中无法利用索引完成的排序操作称为 “文件排序”。

而当 Extra 列为 Using temporary 时,表示 MySQL 创建了一张临时表来处理查询。Using teporary 常见于排序操作 order by 和分组查询 group by,当值为 Using temporary 时,sql 查询一般都是需要进行优化的。

3. 修改并优化 sql

解释了这些列表示的含义后,我们再回到 explain 解析的结果:

+----+-------------+---------+------------+------+---------------+------+---------+------+--------+----------+----------------+
| id | select_type | table   | partitions | type | possible_keys | key  | key_len | ref  | rows   | filtered | Extra          |
+----+-------------+---------+------------+------+---------------+------+---------+------+--------+----------+----------------+
|  1 | SIMPLE      | student | NULL       | ALL  | NULL          | NULL | NULL    | NULL | 102102 |   100.00 | Using filesort |
+----+-------------+---------+------------+------+---------------+------+---------+------+--------+----------+----------------+

可以看到,我们的 type 列对应的值为 ALL,说明该查询语句走了全表扫描。

这条 sql 查询优化的方式很简单,我们可以为这张表的 name 字段加上一个索引:

alter table student add index idx_name (name);

我们再来执行一遍 explain 解析命令:

+----+-------------+---------+------------+-------+---------------+----------+---------+------+--------+----------+-------------+
| id | select_type | table   | partitions | type  | possible_keys | key      | key_len | ref  | rows   | filtered | Extra       |
+----+-------------+---------+------------+-------+---------------+----------+---------+------+--------+----------+-------------+
|  1 | SIMPLE      | student | NULL       | index | NULL          | idx_name | 33      | NULL | 102102 |   100.00 | Using index |
+----+-------------+---------+------------+-------+---------------+----------+---------+------+--------+----------+-------------+

可以看到,此时的 type 列变为了 index,表示 MySQL 获取数据行的方式是扫描索引树,并且 Extra 列的值也变成了 Using index,表示我们使用了索引的方式来获取结果。

这样我们的慢查询 sql 就得到了优化。

总结

本题的解答中,这个示例非常简单,目的就是为了给大家提供一个思路。首先,我们需要开启慢日志定位并获取慢查询的 sql 语句,然后我们可以使用 explain 命令来对这条 sql 语句进行分析,看这条查询 sql 是否使用到索引,是否创建了临时表等。最后我们通过和业务场景进行结合,就可以分析得到优化 sql 的具体思路。

2、索引创建的越多越好吗?


当然不是。

索引可以比作是一本书的目录,当我们想要找到书本中的某些内容时,我们可以先通过目录快速定位到相应的章节,这便大大提升了我们的查找效率。

不过试想一下,如果我们的这本书只有两三页,那就大可不必再为这本书新增一页纸当作目录了。所以,对于一些小型表来说,创建索引是浪费存储空间的。小型表在大部分情况下即便是全表扫描效率也不会太低,所以这种情况下就没必要创建索引。

对于中型表,大型表来说,正确地创建索引的确会提升查询效率,但也绝不是越多越好。我们要知道,数据变更需要维护索引,因此更多的索引就意味着更多的维护成本,同时更多的索引也意味着需要更多的存储空间。

而对于特大型表来说,我们更要谨慎地使用索引,因为使用索引越多维护起来的代价也会越大。实际上,对于特大型表,我们应该考虑的是分库分表的策略,可以使用一些分库分表工具,譬如 sharding-sphere,TDDL,Mycat 等,分散并减轻库与表的压力。

总结

索引越多越好?答案必然是否定的。面试者应该从索引的使用场景,索引的利弊来回答本问题。

3、请谈一下 MySQL 的锁机制?


锁机制非常重要,在这个问题下涵盖了许多经常被考察到的面试题,所以接下来,我会用较长的篇幅来回答这个问题并详细解释其中的每个知识点。

数据库锁按照不同的分类可以做如下划分:

  • 按照锁的粒度划分,可分为表级锁,行级锁,页级锁
  • 按照锁的级别划分,可分为共享锁与排它锁
  • 按照加锁的方式划分,可分为自动锁与显示锁
  • 按照使用方式划分,可分为乐观锁,悲观锁

我们都知道 MySQL 的存储引擎是插件式的,不同的存储引擎有不同的锁机制,其中我们最常用到的两个存储引擎为 MyISAM 与 InnoDB,MyISAM 存储引擎采用的是表级锁(table-level-locking),InnoDB 存储引擎既支持行级锁(row-level-locking)也支持表级锁,但是默认的情况下采用的是行级锁。而我们不常使用到的一种存储引擎—— BDB 采用的是页级锁(page-level-locking),同时 BDB 也支持表级锁,不过,因为现在我们使用到的主流存储引擎几乎都是 MyISAM 与 InnoDB,而且 BDB 现在已经完全被 InnoDB 取代,所以我们对页级锁就不再介绍了,对此感兴趣的童鞋可以自行了解。

那么先来看一下 MyISAM 存储引擎采用的锁机制。

首先,我们要了解共享锁和排它锁的概念。为了不给大家带来太复杂的概念,你可以先这样认为:共享锁等价于读锁(Read Lock),排它锁等价于写锁(Write Lock)。当我们对一张表进行查询操作(select)时,MyISAM 会为这张表自动加上一个读锁;当我们对一张表进行更新操作(insert,update,delete)时,MyISAM 则在这张表上自动加上一个写锁。

共享锁(读锁)与排它锁(写锁)的兼容性如下表所示:

啥意思呢?在有多个用户(线程)对 MyISAM 表进行读操作时,并不会阻塞其他用户对同一表的读请求。所以,我们说读锁和读锁之间是相互兼容的,而其他的方式则是不兼容的。

举个例子🌰:现在我有一张 test 表,指定使用的存储引擎为 MyISAM :

create table test
(
    id        int(11) not null auto_increment,
    test_no   varchar(20) default null,
    test_desc varchar(50) default null,
    primary key (id),
    key (test_no)
) engine = MyISAM
  default charset = utf8;

假设,有两个用户正在对表进行查询(读)操作。

用户 A 输入了一条查询 sql :

select * from test where id between 1 and 1000000;

在用户 A 还没有拿到结果时,用户 B 也输入了一条查询 sql :

select * from test where id between 500000 and 1000000;

此时,用户 B 并不会因为用户 A 还没有拿到结果而进入到线程阻塞的等待状态。原因就是,读锁和读锁是相互兼容的。

如果用户 A 输入了一条查询 sql:

select * from test where id between 1 and 1000000;

在用户 A 还没有拿到查询结果时,用户 B 输入了一条更新 sql:

update test set test_no = null where id = 1000000;

而这个时候,用户 B 输入的 update 语句会被阻塞,需要等待 test 表的读锁释放后才可以进行操作,原因就在于写锁是一种排它锁,并不会和其他线程共享。

以上的方式均为 MyISAM 自动加锁完成的,所以这种方式叫做自动锁。

我们也可以手动加锁。譬如有这样的一个场景:

有一个订单表 orders,其中记录着各订单的总金额 total;同时还有一个订单明细表 order_detail,记录着各订单每一类产品的金额小记 subtotal。我们现在有一个需求,就是检查这两个表的金额合计是否相同,那么就需要执行这样两条 sql 语句:

select sum(total) from orders;
select sum(subtotal) from order_detail;

如果我们不先为这两个表加锁,就很有可能出现错误。比如第一个查询语句执行完毕并返回了结果,然后再对 orders 表做一个更新,那么两次查询的结果就不一致了。

为了避免这种情况的发生,我们需要在这两条查询语句的前后为这两个表手动加读锁,然后再释放:

lock tables orders read,order_detail read;
select sum(total) from order;
select sum(subtotal) from order_detail;
unlock tables;

对于 MyISAM 存储引擎,手动加锁的方式就是:

lock tables T read; # 写锁为 write
...
unlock tables;

而锁的粒度是在整张表上的。

我们再来看一下 InnoDB 存储引擎的锁机制。

首先,我们创建一张使用 InnoDB 引擎的测试表 test2:

create table test2
(
    id        int(11) not null auto_increment,
    test_no   varchar(20) default null,
    test_desc varchar(50) default null,
    primary key (id),
    key (test_no)
) engine = InnoDB
  default charset = utf8;

InnoDB 存储引擎支持事务,并且在默认的情况下是自动提交事务的,它的机制是为同一批事务提交之前加锁,然后 commit 后再一起释放。这里面我为了复现 InnoDB 锁的机制,对当前的几个 MySQL 客户端窗口进行了设置:

set autocommit = 0;

我们将 autocommit 设置为 0,这样就可以关闭事务的自动提交。

现在假设,用户 A 执行了一条 sql 查询语句:

select * from test2 where id = 3 lock in share mode ;

InnoDB 默认的锁是行级锁,lock in share mode 表示我们对 test2 表中 id = 3 的行加了一个共享锁,如果要加排它锁,我们可以使用 for update。现在用户 A 还没有执行 commit;用户 B 执行了一条语句:

update test2 set test_no = null where id = 4;

此时,我们发现,这条语句是可以执行成功的,并没有发生阻塞:

mysql> update test2 set test_no = null where id = 4;
Query OK, 0 rows affected (0.00 sec)
Rows matched: 1  Changed: 0  Warnings: 0

如果用户 B 执行的语句为:

update test2 set test_no = null where id = 3;

那么,用户 B 将进入阻塞等待状态。

这便验证了 InnoDB 默认加锁的粒度是行级锁——不过,有个前提条件,那就是查询的字段上必须有索引,如果没有索引 InnoDB 还是会对整个表加锁。

再举个例子:

test2 表的数据内容如下:

+----+---------+-----------+
| id | test_no | test_desc |
+----+---------+-----------+
|  1 | NULL    | NULL      |
|  2 | NULL    | NULL      |
|  3 | NULL    | test1     |
|  4 | NULL    | NULL      |
|  5 | NULL    | NULL      |
|  6 | NULL    | NULL      |
|  7 | NULL    | NULL      |
|  8 | NULL    | NULL      |
|  9 | NULL    | test      |
| 10 | NULL    | NULL      |
+----+---------+-----------+

假如用户 A 执行了一条 sql 语句:

select * from test2 where test_desc = 'test' lock in share mode;

还未 commit,用户 B 执行了一条 sql 语句:

update test2 set test_desc = null where test_desc = 'test1';

此时,用户 B 被阻塞。

我们看到,用户 A 在查询 id 为 9 的行,而用户 B 则在更新 id 为 3 的行,如果 InnoDB 走的是行级锁,那么用户 A 对第九行加了一个共享锁,用户 B 对第三行加了一个排它锁,理应是不会互相阻塞的,可是用户 B 仍然被阻塞了。这说明,当我们的 sql 语句操作字段没有走索引时,InnoDB 还是会在整个表这个粒度上加锁。所以说,InnoDB 既支持行级锁,也支持表级锁。

那么行级锁一定要比表级锁好么?

其实不然,行级锁的并发度虽然比表级锁要高,但是表级锁的开销比较小,加锁的速度很快;行级锁的开销则比较大,并且加锁的速度慢,最重要的是行级锁可能会出现死锁现象

什么是死锁(Dead Lock)?

死锁是指两个或两个以上的进程或线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。

如上图中所示,线程 A 持有锁 A,线程 B 持有锁 B。两个线程处于争抢状态,线程 A 等待线程 B 释放锁,线程 B 又等待线程 A 释放它的资源,这样相互等待就形成了死锁。

形成死锁必须满足四个必要条件:

  1. 互斥条件
  2. 请求与保持条件
  3. 不剥夺条件
  4. 环路等待条件

我们接下来对这些名字逐一进行解释。

互斥条件是指进程对所分配到的资源进行排它性使用。说白了就是在一段时间内,某个资源每次只能被一个进程所占用。

请求与保持条件是指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放,说人话就是“有了碗里的饼,还嚷嚷着要吃锅里的汤”。

不剥夺条件是指进程已获得的资源在未使用完之前,不能被剥夺,只能由自己释放,说人话就是“自己拿到了就不给别人用,自己用完了才给别人”。

环路等待条件是指当发生死锁时,必然存在着一个资源的请求环形链,若干进程在这个环形链中循环等待。

死锁形成的条件以上四点缺一不可!接下来,我们尝试依据这些条件写一个死锁的案例:

创建一张表 dead_lock_test,建表语句如下:

create table dead_lock_test
(
    id   int(11)     not null auto_increment,
    name varchar(20) not null,
    primary key (id)
) engine = InnoDB
  default charset = utf8;

Session1 我们输入如下 sql:

-- T1 
begin;

select * from dead_lock_test where id = 1 for update;

-- T3
update dead_lock_test set id = id where id = 2;

Session2 我们输入如下 sql:

-- T2
begin;

select * from dead_lock_test where id = 2 for update;

-- T4
update dead_lock_test set id = id where id = 1;

我们在 Session1 和 Session2 上按照 T1 ~ T4 的顺序执行上面的 sql,Session2 客户端最后显示的报错信息为:

mysql> update dead_lock_test set id = id where id = 1;
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction

可以看到,InnoDB 行级锁有可能导致死锁现象的发生,不过 MyISAM 则不会出现死锁,因为 MyISAM 只有表锁,并不满足死锁出现的四个必要条件。

避免死锁有以下几种常见的策略:

  • 设置获得锁的超时时间
  • 避免长事务
  • 避免事务中的用户交互
  • 降低隔离级别
  • ... ...

讲完了什么是死锁后,我们再来看一下什么是乐观锁,什么是悲观锁?

锁按照使用的方式划分即可分为乐观锁与悲观锁。

先讲一下什么是悲观锁。悲观锁指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。

悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。

乐观锁则相比于悲观锁来说使用了更加宽松的机制,它往往不依赖于数据库提供的锁机制,而是使用程序人为地实现。我来举个例子🌰:

我们有一个表:

create table test_optimistic_lock
(
    id      int(11) not null auto_increment,
    money   int(11)          default null,
    version int(11) not null default '0',
    primary key (id)
) engine = InnoDB
  default charset = utf8;

在这个表中有一个 version 字段,version 字段维护的是一个数据版本信息,我们每一次写数据时,都会更新 version 字段;每一次读数据时,都会查看当前的 version 是否已经更新。

test_optimistic_lock 表中现有数据如下:

mysql> select * from test_optimistic_lock;
+----+-------+---------+
| id | money | version |
+----+-------+---------+
|  1 |  1000 |       0 |
|  2 |  NULL |       0 |
|  3 |  NULL |       0 |
|  4 |  NULL |       0 |
+----+-------+---------+
4 rows in set (0.00 sec)

现在有一个进程 A 要对 id 为 2 的行的 money 做更新,将其值设置为 1000,他便会使用这样的语句:

update test_optimistic_lock set money = 1000,version = 0 + 1 where version = 0 and id = 2;

可以看到,我们的策略是在每次对该表写时,为了防止发生冲突,都会先去检查 version 再做更新操作,如果更新成功的话,便让 version + 1。

目前表中数据更新如下:

mysql> select * from test_optimistic_lock;
+----+-------+---------+
| id | money | version |
+----+-------+---------+
|  1 |  1000 |       0 |
|  2 |  1000 |       1 |
|  3 |  NULL |       0 |
|  4 |  NULL |       0 |
+----+-------+---------+
4 rows in set (0.00 sec)

如果进程 B 也要对 id 为 2 的行的 money 做更新,将其值设置为 2000,执行 sql 如下:

update test_optimistic_lock set money = 2000,version = 0 + 1 where version = 0 and id = 2;

很显然,因为第二行的 version 值已经更新到了 1 ,这条语句是无法执行成功的。

我们使用了一种宽松的机制,改变了锁的方式。悲观锁“悲观地”认为数据访问一定会被外界所修改,所以在 commit 之前就加好了锁。这样虽然安全得到了保障,但是也带来了许多问题,譬如数据库性能会造成很大的开销(尤其在长事务),有可能出现死锁等。而乐观锁则采用了一种“乐观”的方式,它只有在数据 commit 时,才会进行排它性的检查。不过乐观锁也不是没有缺点,乐观锁适用于写操作比较少的情况,即冲突很少发生的情况。如果经常发生冲突的话,使用乐观锁反而会影响性能,降低系统的吞吐量。

总结

本题又是一个涵盖了巨多知识点的面试题,大家可以从我的解答中找到很多面试题的 answer,譬如:

  • MyISAM 与 InnoDB 的锁机制有什么区别?
  • 什么是共享锁?什么是排它锁?
  • InnoDB 在什么时候使用行级锁?什么时候使用表级锁?
  • 什么是死锁?死锁的四个必要条件是什么?
  • 请写出一个死锁的案例?如何避免死锁?
  • 什么是乐观锁,什么是悲观锁?
  • 等等...

如果你仔细阅读了本文,相信你一定可以从文章中找到这些问题的所有答案~

4、MyISAM 与 InnoDB 存储引擎有什么不同?如何选择?


其实我们从数据库篇开始,就一直在总结 MyISAM 与 InnoDB 这两个存储引擎的不同了,这里我来给大家总结一下:

除了以上的不同之处外,还有一点需要特殊说明一下。那就是 InnoDB 存储引擎不会保存表的具体行数,而 MyISAM 则使用了一个变量来保存整张表有多少行。

所以,当我们执行:

select count(*) from T;

时,该语句没有任何的 where 条件。InnoDB 执行的速度要比 MyISAM 慢很多,因为 InnoDB 需要走全表扫描来计算数据共有多少行~

那么对于 MyISAM 与 InnoDB 这两种存储引擎,我们该如何选择呢?

MyISAM 适合的场景为:

  • 需要频繁执行全表 count 语句
  • 对数据进行增删改的频率不高,查询非常频繁
  • 没有事务

第一点我们已经解释过了;第二点是因为 MyISAM 使用的是表级锁,如果增删改操作频繁,那么就要频繁地对整张表加锁,这个性能开销无疑是巨大的。而如果更新操作频率不高,MyISAM 相比于 InnoDB 还有一个优势,那就是 MyISAM 使用的索引是非聚簇索引,不用像 InnoDB 的普通索引查询那样需要进行会回表。第三点也无需解释,MyISAM 不支持事务,如果你的业务需求需要事务,就应该使用 InnoDB 存储引擎。

InnoDB 适合的场景为:

  • 数据增删改查都比较频繁
  • 可靠性要求高,需要支持事务

这两点我就不再赘述了,相信大家都能理解:-)

总结

这是一道很好的前菜,面试官可以通过 MyISAM 与 InnoDB 的这些不同之处,向面试者继续深入地提问二者索引的区别,锁的区别等。

总结

又是不知不觉写了快 7000 多字了......

本来想在这一篇文章中将 MySQL 事务相关的面试问题全部写完的,不过这样写下去可能就要 2 万字了......

总之,后续内容我会抓紧更新,感谢您的阅读!

好啦,至此为止,这篇文章就到这里了~欢迎大家关注我的公众号,在这里希望你可以收获更多的知识,我们下一期再见!