MySQL
# 基础
# 数据库三大范式
# varchar 与 char的区别
# count(1)、count(*) 与 count(列名) 的区别?
count(*) = count(1) > count(主键) > count(列名)
# 存储引擎
# InnoDB 和 MylSAM 主要有什么区别?
- 事务支持:MyISAM 不提供事务支持;InnoDB 提供事务支持事务,具有事务(commit)、回滚(rollback)和崩溃修复能力(crash recovery capabilities)的事务安全特性。
- 最小锁粒度:MyISAM 只支持表级锁,更新时会锁住整张表,导致其它查询和更新都会被阻塞 InnoDB 支持行级锁。
- 索引类型:MyISAM 的索引为非聚簇索引,数据结构是 B 树;InnoDB 的索引是聚簇索引,数据结构是 B+树。
- 外键支持:MyISAM 不支持外键;InnoDB 支持外键。
# 日志
# undo log , binlog , redo log
binlog、redo log 和 undo log 都是 MySQL 数据库中的日志文件,但它们记录的内容和用途不同。
binlog:它用于记录数据库执行的写入性操作(不包括查询)信息,以二进制的形式保存在磁盘中。它是 MySQL 的逻辑日志,并且由 Server 层进行记录,使用任何存储引擎的 MySQL 数据库都会记录 binlog 日志。它主要用于主从复制和数据恢复。
redo log:它是 InnoDB 存储引擎层维护的,用于记录事务对数据页做了哪些修改。这样就能完美地解决性能问题了(相对而言文件更小并且是顺序IO)。它包括两部分:一个是内存中的日志缓冲(redo log buffer),另一个是磁盘上的日志文件(redo log file)。MySQL 每执行一条 DML 语句,先将记录写入 redo log buffer,后续某个时间点再一次性将多个操作记录写到 redo log file。这种先写日志,再写磁盘的技术就是 MySQL 里经常说到的 WAL(Write-Ahead Logging)技术。
undo log:它也是 InnoDB 存储引擎层维护的,用于在事务回滚时撤销已经完成的修改。它记录了事务执行前数据的原始值,当事务需要回滚时,可以利用 undo log 中的信息将数据恢复到事务执行前的状态
# 什么叫DML语句
DML 是 Data Manipulation Language(数据操纵语言)的缩写。它是 SQL 语言的一个子集,用于检索、插入、更新和删除数据库中的数据。常见的 DML 语句包括 SELECT、INSERT、UPDATE 和 DELETE。
# 为什么需要 undo log?
- 实现事务回滚,保障事务的原子性。事务处理过程中,如果出现了错误或者用户执 行了 ROLLBACK 语句,MySQL 可以利用 undo log 中的历史数据将数据恢复到事务开始之前的状态。
- 实现 MVCC(多版本并发控制)关键因素之一。MVCC 是通过 ReadView + undo log 实现的。undo log 为每条记录保存多份历史数据,MySQL 在执行快照读(普通 select 语句)的时候,会根据事务的 Read View 里的信息,顺着 undo log 的版本链找到满足其可见性的记录。
# ReadView
Read View是一个数据库的内部快照,该快照被用于InnoDB存储引擎中的MVCC机制。Read View记录了数据库在某个时刻的数据信息,用于实现快照读³。
Read View会根据事务的隔离级别决定在某个事务开始时,该事务能看到什么信息。Read View包含了以下几个重要的信息:
m_low_limit_id
:该事务不应该看到任何大于等于这个值的事务ID所做的修改。m_up_limit_id
:该事务应该看到所有小于这个值的事务ID所做的修改。m_ids
:该事务开始时,活跃的读写事务ID的集合。m_creator_trx_id
:创建这个Read View的事务ID。
Read View通过比较记录中的事务ID和自身的信息,来判断记录是否对自己可见。具体的规则如下:
- 如果记录的事务ID小于
m_up_limit_id
,则说明记录是在该事务之前就已经提交了,对该事务可见。 - 如果记录的事务ID大于等于
m_low_limit_id
,则说明记录是在该事务之后才提交了,对该事务不可见。 - 如果记录的事务ID在
m_up_limit_id
和m_low_limit_id
之间,但不在m_ids
中,则说明记录是在该事务之前就已经提交了,但是在该事务开始时还没有提交,对该事务可见。 - 如果记录的事务ID在
m_up_limit_id
和m_low_limit_id
之间,并且在m_ids
中,则说明记录是在该事务开始后才提交了,对该事务不可见。
举个例子,假设有以下几个并发执行的事务:
事务ID | 隔离级别 | 操作 |
---|---|---|
1 | 可重复读 | 更新表A中id为1的记录 |
2 | 可重复读 | 查询表A中id为1的记录 |
3 | 可重复读 | 查询表A中id为1的记录 |
假设表A中id为1的记录最初是由事务0创建并提交的。那么,在不同的时间点,这些事务和记录会有如下情况:
时间点 | 记录中的事务ID | 1 的 Read View | 2 的 Read View | 3 的 Read View |
---|---|---|---|---|
t0 | 0 | - | - | - |
t1(1 开始) | 0 | m_low_limit_id = 2, m_up_limit_id = 2, m_ids = {1} | - | - |
t2(2 开始) | 0 | m_low_limit_id = 2, m_up_limit_id = 2, m_ids = {1} | m_low_limit_id = 3, m_up_limit_id = 3, m_ids = {1,2} | - |
t3(3 开始) | 0 | m_low_limit_id = 2, m_up_limit_id = 2, m_ids = {1} | m_low_limit_id = 3, m_up_limit_id = 3, m_ids = {1,2} | m_low_limit_id = 4, m_up_limit_id = 4, m_ids = {1,2,3} |
t4(1 提交) | 1 | m_low_limit_id = 2, m_up_limit_id = 2, m_ids = {1} | m_low_limit_id = 3, m_up_limit_id = 3, m_ids = {1,2} | m_low_limit_id = 4, m_up_limit_id = 4, m_ids = {1,2,3} |
根据上表,我们可以看到,在t4时刻,对于不同的Read View,记录是否可见的判断如下:
- 对于1的Read View,记录的事务ID(1)大于等于
m_low_limit_id
(2),对1不可见。 - 对于2的Read View,记录的事务ID(1)在
m_up_limit_id
(3)和m_low_limit_id
(3)之间,并且在m_ids
中,对2不可见。 - 对于3的Read View,记录的事务ID(1)在
m_up_limit_id
(4)和m_low_limit_id
(4)之间,但不在m_ids
中,对3可见。
# undo log 是如何刷盘(持久化到磁盘)的?
buffer pool 中有 undo 页,对 undo 页的修改也都会记录到 redo log。redo log 会每秒刷盘,提交事务时也会刷盘,数据页和 undo 页都是靠这个机制保证持久化的。
# 为什么需要 Buffer Pool?
- 当读取数据时,如果数据存在于 Buffer Pool 中,客户端就会直接读取 Buffer Pool 中的数据,否则再去磁盘中读取。
- 当修改数据时,如果数据存在于 Buffer Pool 中,那直接修改 Buffer Pool 中数据所在的页,然后将其页设置为脏页(该页的内存数据和磁盘上的数据已经不一致),为了减少磁盘I/O,不会立即将脏页写入磁盘,后续由后台线程选择一个合适的时机将脏页写入到磁盘。
# 为什么需要 redo log ?
先写入redo log日志的,并且还没有将脏页数据更新到磁盘,但是redo log日志已经持久化,所以可以用redo log日志的内容在MySQL重启的时候,进行故障恢复,将数据恢复到最新的状态。
# redo log 文件写满了怎么办?
重做日志里面有两个大小相等的日志文件,以循环写的方式进行工作。先将文件日志的内容刷新到数据库,再redo log日志进行覆盖写。
# 为什么需要 binlog ?
binlog 文件是记录了所有数据库表结构变更和表数据修改的日志,不会记录查询类的操作,比如 SELECT 和 SHOW 操作。 而 InnoDB 是另一个公司以插件形式引入 MySQL 的,既然只依靠 binlog 是没有 crash-safe 能力的,所以 InnoDB 使用 redo log 来实现 crash-safe 能力。
# 主从复制是怎么实现?
MySQL 主从复制是通过 binlog 来实现的。在主库上,所有的增删改操作都会被记录在 binlog 中。当从库连接到主库时,它会主动从主库获取最新的 binlog 文件。然后把 binlog 存储到本地的 relay log 中,再执行 relay log 中的更新内容²。 MySQL 集群的主从复制过程梳理成 3 个阶段:
- 写入 Binlog:主库写 binlog 日志,提交事务,并更新本地存储数据。
- 同步 Binlog:把 binlog 复制到所有从库上,每个从库把 binlog 写到暂存日志中。
- 回放 Binlog:回放 binlog,并更新存储引擎中的数据。
# 为什么需要两阶段提交?
如果binlog日志 和 redo log日志不同时刷盘的话,MySQL奔溃重启恢复的时候,就会出现主从数据不一致的情况。
MySQL 为了避免出现两份日志之间的逻辑不一致的问题,使用了「两阶段提交」来解决,两阶段提交其实是分布式事务一致性协议,它可以保证多个逻辑操作要不全部成功,要不全部失败,不会出现半成功的状态。
两阶段提交把单个事务的提交拆分成了 2 个阶段,分别是「准备(Prepare)阶段」和「提交(Commit)阶段」:
- 准备阶段:协调者会问多个逻辑参与者,是否都准备,都准备好了的话,就可以做后面的事情了。
- 提交阶段:如果多个逻辑参与者都准备好了,就开始做提交操作;如果哪一方没准备好,就要进行回退操作
比如说redo log 与 binlog之间的两阶段提交
- 第一阶段,InnoDB redo log 写盘,InnoDB 事务进入 prepare 状态。
- 第二阶段,如果前面 prepare 成功,那么再继续将事务日志持久化到 binlog。如果持久化成功,那么 InnoDB 事务则进入 commit 状态,在 redo log 里面写一个 commit 记录。
# 索引
# explain的时候,各个字段的含义
EXPLAIN
语句用于显示 MySQL 如何执行 SELECT
语句。它返回一张表,其中每一行表示查询执行计划中的一个步骤。下面是一些常见的 EXPLAIN
输出列及其含义:
- id:查询的标识符,相同的
id
表示同一个查询块。 - select_type:查询的类型,如 SIMPLE(简单查询)、PRIMARY(主查询)、SUBQUERY(子查询)等。
- table:查询所涉及的表。
- partitions:匹配的分区。
- type:连接类型,表示 MySQL 如何查找表中的行。常见的类型有 ALL(全表扫描)、index(索引扫描)、range(范围扫描)、ref(非唯一索引查找)等。
- possible_keys:可能用到的索引。
- key:实际使用的索引。
- key_len:使用的索引的长度。
- ref:哪些列或常量与
key
一起被用于查找索引。 - rows:MySQL 认为需要检查的行数。
- filtered:按表条件过滤后剩余的行数百分比。
- Extra:额外信息,如 Using index(使用覆盖索引)、Using where(使用 WHERE 过滤)等。
在查看 EXPLAIN
输出时,您应该关注以下几个参数:
type:连接类型应尽可能靠前,避免使用 ALL 和 index 类型。 其中,all表示全表扫描,MySQL会扫描整个表来找到匹配的行。而index则表示全索引扫描,MySQL会扫描整个索引来找到匹配的行。这两种类型都是效率较低的连接类型,应尽量避免使用。
rows:检查的行数应尽量少。
Extra:避免出现 Using temporary 和 Using filesort。出现这两种意味着没有使用到索引。
# 连接mySQL的协议
连接 MySQL 是基于 TCP/IP 协议的,这是一种在互联网上广泛使用的通信协议,可以实现不同主机之间的数据传输。MySQL 客户端和服务器之间的通信协议是有状态的,也就是说,它们会维护一个连接状态,并根据不同的状态进行不同的操作。
为什么要使用 TCP/IP 协议呢?主要有以下几个原因:
- TCP/IP 协议是一种可靠的协议,它可以保证数据包的有序、完整和正确地到达目的地。
- TCP/IP 协议是一种通用的协议,它可以支持不同的操作系统、网络设备和应用程序之间的通信。
- TCP/IP 协议是一种灵活的协议,它可以支持不同的网络拓扑结构和传输媒介,如以太网、无线网等。
- TCP/IP 协议是一种可扩展的协议,它可以支持不断增长的网络规模和需求。
# select_type 列参数详解
select_type 列表示查询的类型,主要用来区分简单查询、联合查询、子查询等复杂查询。select_type 列的常见值有以下几种:
- SIMPLE:简单的 select 查询,查询中不包含子查询或者 UNION。
- PRIMARY:查询中若包含任何复杂的子部分,最外层的查询则被标记为 PRIMARY。
- SUBQUERY:在 select 或者 where 列表中包含了子查询。
- DERIVED:在 from 列表中包含的子查询会被标记为 DERIVED,MySQL 会递归地执行这些子查询,然后把结果放到临时表中。
- UNION:若第二个 select 语句出现在 UNION 之后,则被标记为 UNION。若 UNION 包含在 from 子句的子查询中,外层 select 则被标记为 DERIVED。
- UNION RESULT:从 UNION 表获取结果的 select。
举例说明:
- 简单查询:
explain select * from student;
select_type 为 SIMPLE。
- 联合查询:
explain select * from student
union all
select * from teacher;
2
3
select_type 为 DERIVED 和 UNION。
- 子查询:
explain select * from student
where school_id = (select id from school where name = 'A');
2
select_type 为 PRIMARY 和 SUBQUERY。
# type参数
type 列表示 MySQL 在表中查找行时使用的访问类型。它是 EXPLAIN 输出中最重要的列之一,因为它可以帮助您了解查询的性能瓶颈。type 列的常见值有以下几种,按性能从好到差排列:
- system:表只有一行记录(等于系统表)。这是 const 类型的特殊情况。
- const:表最多有一个匹配行,它在查询执行时就被读取。因为只有一行,所以速度非常快。
- eq_ref:对于每个来自前面表的行组合,从该表中读取一行。这可能是最好的联接类型,除了 const 类型。
- ref:对于每个来自前面表的行组合,所有匹配的行将从该表中读取。ref 是使用非唯一索引或唯一索引的前缀进行查找的。
- range:只检索给定范围内的行,使用索引来选择行。
- index:全索引扫描,与 ALL 类型类似,只是按索引次序进行扫描,而不是按行。如果额外显示 Using index,则说明使用了覆盖索引。
- ALL:全表扫描,MySQL 需要从头到尾读取整张表来找到相关的行。
举例说明:
- const 类型:
explain select * from student where id = 1;
type 为 const。
- eq_ref 类型:
explain select * from student, school where student.school_id = school.id;
type 为 ALL 和 eq_ref。
- ref 类型:
explain select * from student where school_id = 1;
type 为 ref。
- range 类型:
explain select * from student where id between 1 and 10;
type 为 range。
# 索引为什么会加快查询速度
索引可以加快查询速度,因为它可以帮助数据库快速定位到符合查询条件的记录。没有索引时,数据库需要进行全表扫描,即逐行检查每条记录,以确定哪些记录符合查询条件。这种方式非常耗时,特别是当表中的数据量很大时。
而有了索引后,数据库可以使用索引来快速定位到符合查询条件的记录。索引通常使用 B-Tree 数据结构来存储数据,这种数据结构可以让数据库快速找到指定范围内的数据。因此,使用索引可以大大减少数据库需要检查的记录数,从而加快查询速度。
举个例子,假设我们有一个学生表,其中包含学生的姓名、年龄和学校。如果我们要查询来自 A 学校的所有学生,可以执行以下 SQL 语句:
SELECT * FROM student WHERE school = 'A';
如果没有在 school 列上创建索引,那么数据库需要进行全表扫描,逐行检查每条记录,以确定哪些学生来自 A 学校。这种方式非常耗时。
但是如果我们在 school 列上创建了索引,那么数据库就可以使用这个索引来快速定位到来自 A 学校的所有学生。这样就不需要进行全表扫描了,查询速度会大大提高。
# 单列索引 与 多列索引
唯一索引和非唯一索引是根据索引列的唯一性来分类的。
- 唯一索引:唯一索引是指索引列中的值不能重复。这意味着每行记录都必须拥有唯一的索引值。唯一索引可以加快数据检索速度,并强制数据的唯一性。
例如,假设我们有一个学生表,其中包含学生的学号、姓名和年龄。我们可以在学号列上创建一个唯一索引,因为每个学生都应该拥有唯一的学号。
CREATE UNIQUE INDEX idx_student_id ON student (id);
这样,在执行查询时,如果我们需要根据学号查找学生,MySQL 可以使用这个唯一索引来加快查询速度。
SELECT * FROM student WHERE id = 1;
- 非唯一索引:非唯一索引,也称为普通索引,是指索引列中的值可以重复。这意味着多行记录可以拥有相同的索引值。非唯一索引可以加快数据检索速度,但不会强制数据的唯一性。
例如,假设我们有一个学生表,其中包含学生的姓名、年龄和学校。我们可以在学校列上创建一个非唯一索引,因为多个学生可能来自同一所学校。
CREATE INDEX idx_school ON student (school);
这样,在执行查询时,如果我们需要根据学校筛选学生,MySQL 可以使用这个非唯一索引来加快查询速度。
SELECT * FROM student WHERE school = 'A';
# 唯一索引 与 非唯一索引
唯一索引和非唯一索引是根据索引列的唯一性来分类的。
- 唯一索引:唯一索引是指索引列中的值不能重复。这意味着每行记录都必须拥有唯一的索引值。唯一索引可以加快数据检索速度,并强制数据的唯一性。
例如,假设我们有一个学生表,其中包含学生的学号、姓名和年龄。我们可以在学号列上创建一个唯一索引,因为每个学生都应该拥有唯一的学号。
CREATE UNIQUE INDEX idx_student_id ON student (id);
这样,在执行查询时,如果我们需要根据学号查找学生,MySQL 可以使用这个唯一索引来加快查询速度。
SELECT * FROM student WHERE id = 1;
- 非唯一索引:非唯一索引,也称为普通索引,是指索引列中的值可以重复。这意味着多行记录可以拥有相同的索引值。非唯一索引可以加快数据检索速度,但不会强制数据的唯一性。
例如,假设我们有一个学生表,其中包含学生的姓名、年龄和学校。我们可以在学校列上创建一个非唯一索引,因为多个学生可能来自同一所学校。
CREATE INDEX idx_school ON student (school);
这样,在执行查询时,如果我们需要根据学校筛选学生,MySQL 可以使用这个非唯一索引来加快查询速度。
SELECT * FROM student WHERE school = 'A';
# 覆盖索引
覆盖索引是指索引包含了查询所需的所有数据列,而不需要再回表查询原始数据。覆盖索引可以大大提高查询效率,因为它可以减少磁盘 IO 操作,只需要访问索引即可。
例如,假设我们有一个学生表,其中包含学生的姓名、年龄和学校。我们可以在姓名和年龄两列上创建一个多列索引,以加快基于姓名和年龄的查询速度。
CREATE INDEX idx_name_age ON student (name, age);
这样,在执行查询时,如果我们只需要查询姓名和年龄两列,MySQL 可以使用这个多列索引作为覆盖索引来加快查询速度。
SELECT name, age FROM student WHERE name = 'A';
在 EXPLAIN 输出中,如果显示了 Using index,说明使用了覆盖索引。
# 聚簇索引和非聚簇索引
聚簇索引和非聚簇索引是根据数据的物理存储方式来分类的。
- 聚簇索引:聚簇索引是指数据行的物理顺序与索引的逻辑顺序相同。也就是说,索引和数据存储在一起,按照索引的顺序存储。聚簇索引可以加快范围查询的速度,因为相关的数据行在磁盘上是相邻的。
例如,在 InnoDB 存储引擎中,主键索引就是聚簇索引。假设我们有一个学生表,其中包含学生的学号、姓名和年龄。学号列是主键,因此它是聚簇索引。
CREATE TABLE student (
id INT PRIMARY KEY,
name VARCHAR(255),
age INT
) ENGINE=InnoDB;
2
3
4
5
这样,在执行查询时,如果我们需要根据学号范围筛选学生,MySQL 可以使用这个聚簇索引来加快查询速度。
SELECT * FROM student WHERE id BETWEEN 1 AND 10;
- 非聚簇索引:非聚簇索引是指数据行的物理顺序与索引的逻辑顺序不同。也就是说,索引和数据分开存储,索引中只包含数据行的指针。非聚簇索引可以加快等值查询的速度,但范围查询可能会比较慢。
例如,在 MyISAM 存储引擎中,所有索引都是非聚簇索引。假设我们有一个学生表,其中包含学生的学号、姓名和年龄。我们可以在姓名列上创建一个非聚簇索引,以加快基于姓名的查询速度。
CREATE TABLE student (
id INT,
name VARCHAR(255),
age INT,
INDEX idx_name (name)
) ENGINE=MyISAM;
2
3
4
5
6
这样,在执行查询时,如果我们需要根据姓名查找学生,MySQL 可以使用这个非聚簇索引来加快查询速度。
SELECT * FROM student WHERE name = 'A';
# B+树
B+树是一种多路平衡查找树,它的每个节点可以有多个子节点。在B+树中,所有的数据都存储在叶子节点中,而非叶子节点只存储键值和指向子节点的指针。
键值是用来索引数据的,它们将数据分成不同的区间。每个非叶子节点中的键值都对应着一个子节点,指示了该子节点中所有数据的范围。例如,假设我们有一个非叶子节点,其中包含两个键值10和20,以及三个指针分别指向三个子节点。那么第一个子节点中所有数据都小于10,第二个子节点中所有数据都在10和20之间,第三个子节点中所有数据都大于20。
数据是实际存储在叶子节点中的记录。每个叶子节点包含一定数量的数据记录,这些记录按照键值排序。例如,在上面的例子中,第一个叶子节点可能包含键值为1、3、5、7、9的记录,第二个叶子节点可能包含键值为11、13、15、17、19的记录,第三个叶子节点可能包含键值为21、23、25、27、29的记录。
这种结构使得B+树能够快速定位数据。当我们查找一个特定键值的记录时,我们从根节点开始,通过比较键值和指针来确定应该进入哪个子节点。然后重复这个过程直到到达叶子节点,在叶子节点中找到对应的数据记录。
# MySQL 8.0会将查询缓存删掉
MySQL 8.0 版本中删除了查询缓存功能。这是因为查询缓存的效果取决于缓存的命中率,只有命中缓存的查询效果才能有改善,因此无法预测其性能。此外,查询缓存的另一个大问题是它受到单个互斥锁的保护。在具有多个内核的服务器上,大量查询会导致大量的互斥锁争用。通过基准测试发现,大多数工作负载最好禁用查询缓存。
# 索引
索引是一种数据结构,它可以帮助我们快速地查找、排序、分组或者聚合数据。索引的原理类似于书籍的目录,它可以让我们根据关键字或者范围来定位数据的位置,而不需要扫描整个数据集。
举个例子,假设我们有一个学生表,包含学号、姓名、年龄、性别等字段。如果我们想要查询年龄为18岁的学生,我们可以在年龄字段上创建一个索引。这样,当我们执行查询时,就可以直接在索引中找到年龄为18岁的学生的位置,然后根据位置去读取对应的记录。这样就避免了扫描整个表,提高了查询效率。
# 最左匹配原则
最左匹配原则,也称为最左前缀原则,是指在使用联合索引进行查询时,必须满足索引的最左列的查询条件才能使用索引。
举个例子,假设我们有一个学生表,包含学号、姓名、年龄、性别等字段。我们在姓名和年龄两个字段上创建了一个联合索引。那么,当我们执行以下查询时,就可以使用索引:
SELECT * FROM students WHERE name='张三' AND age=18;
这是因为查询条件中包含了索引的最左列(姓名)的查询条件。
但是,如果我们执行以下查询,则无法使用索引:
SELECT * FROM students WHERE age=18;
这是因为查询条件中没有包含索引的最左列(姓名)的查询条件。
总之,最左匹配原则是指在使用联合索引进行查询时,必须满足索引的最左列的查询条件才能使用索引。
# SQL 优化
- 看是否走了索引吧,避免全表扫描
- 加适当的索引,比如利用覆盖索引等等
- 分页优化
# 索引
# 索引分类
# 为什么使用索引会加快查询?
# 创建索引的注意的地方
- 索引应该建在查询应用频繁的字段
- 索引的个数应该适量
- 区分度低的字段,例如性别,不要建索引。
- 频繁更新的值,不要作为主键或者索引
# 索引哪些情况下会失效呢?
- like 通配符可能导致索引失效。
- 联合索引,查询时的条件列不是联合索引中的第一个列,索引失效。
- 对索引列运算(如,+、-、*、/),索引失效。
# 索引是不是建的越多越好呢?
- 索引会占据磁盘空间
- 索引虽然会提高查询效率,但是会降低更新表的效率。比如每次对表进行增删改操作,MySQL 不仅要保存数据,还有保存或者更新对应的索引文件。
# 为什么用B+树,而不用B树,二叉树,哈希
# 锁
# MySQL 有哪些锁
- 表级锁
- 表锁
- 表级别共享锁就是读锁
- 表级别独占锁就是写锁
- 元数据锁(MDL)
- 对一张表进行 CRUD 操作时,加的是 MDL 读锁;
- 对一张表做结构变更操作的时候,加的是 MDL 写锁;
- 意向锁
- 在使用 InnoDB 引擎的表里对某些记录加上「共享锁」之前,需要先在表级别加上一个「意向共享锁」;
- 在使用 InnoDB 引擎的表里对某些纪录加上「独占锁」之前,需要先在表级别加上一个「意向独占锁」;
- 意向锁的目的是为了快速判断表里是否有记录被加锁。
- AUTO-INC 锁
- 插入数据时,可以不指定主键的值,数据库会自动给主键赋值递增的值,这主要是通过 AUTO-INC 锁实现的。
- 表锁
- 行级锁
- Record Lock
- 有了记录锁,事务就可以只锁定需要修改的行,而不是整张表。这样,其他事务就可以继续访问该表中未被锁定的行。这大大提高了数据库系统的并发性能。
- Gap Lock
- 为了防止幻读的发生,数据库系统可以使用 Gap Lock 对索引记录之间的间隙进行锁定。在上面的例子中,如果数据库系统使用了 Gap Lock,那么当事务 2 尝试插入新记录时,它将被阻塞,直到事务 1 提交或回滚。
- Next-Key Lock
- 记录锁 + 间隙锁
- 插入意向锁
- 如果一个区间gap里面被上了间隙锁,当有事务想在这个gap里面插入一条记录的话,就会获取一个插入意向锁,并等待加入gap锁的事务提交
- Record Lock
# MySQL乐观锁和悲观锁
- 悲观锁 数据库中的行锁,表锁,读锁,写锁均为悲观锁。
- 乐观锁 乐观锁通常是通过在表中增加一个版本(version)或时间戳(timestamp)来实现,其中,版本最为常用。
# 事务
# MySQL中的ACID通过什么来保证的呢
MySQL数据库的ACID是通过多种方式来保证的。ACID是指原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability)。
- 原子性:MySQL通过使用undo log来保证原子性。当事务对数据库进行修改时,InnoDB会生成对应的undo log。如果事务执行失败或调用了rollback,导致事务需要回滚,InnoDB引擎会根据undo log中的记录,将数据回滚到之前的样子。
- 一致性:一致性是事务的最终目的,它需要数据库层面和应用层面共同保证。MySQL底层通过两阶段提交事务保证了事务持久化时的一致性。
- 隔离性:MySQL通过使用锁机制和MVCC机制来保证隔离性。锁机制可以保证事务间的读写隔离,而MVCC机制可以保证事务间的写操作隔离。
- 持久性:MySQL通过使用redo log来保证持久性。当事务提交时,redo log会被刷盘。这样,即使MySQL出现宕机,也可以从磁盘中读取redo log进行数据恢复,从而保证了事务的持久性。
# MySQL中的事务隔离级别如何保证
- 事务隔离级别是指在并发访问数据库时,一个事务对数据所做的修改,是否能够被其他事务看到。SQL标准定义了四种隔离级别,分别是读未提交(Read Uncommitted)、读已提交(Read Committed)、可重复读(Repeatable Read)和串行化(Serializable)。
- 不同的隔离级别有不同的实现方式,主要涉及到锁和多版本并发控制(MVCC)两种机制。锁是一种悲观并发控制的方法,通过对数据加上共享锁或排他锁来防止其他事务的干扰。MVCC是一种乐观并发控制的方法,通过为每个数据行保存多个版本来实现一致性非锁定读。
- 在MySQL中,InnoDB存储引擎使用锁和MVCC来实现不同的隔离级别。具体来说:
- 读未提交:这是最低的隔离级别,不加任何锁,也不使用MVCC,直接读取最新的数据行,可能会出现脏读、不可重复读和幻读等问题。
- 读已提交:这个隔离级别使用MVCC来实现一致性非锁定读,每次查询都会生成一个新的快照,只能看到已经提交的数据修改。这样可以避免脏读,但是可能会出现不可重复读和幻读等问题。
- 可重复读:这是MySQL的默认隔离级别,也使用MVCC来实现一致性非锁定读,但是每个事务只会生成一个快照,多次查询都会看到同样的数据。这样可以避免脏读和不可重复读,但是可能会出现幻读等问题。为了解决幻读问题,InnoDB还引入了间隙锁(Gap Lock),即对查询范围内的空间加上共享锁或排他锁,防止其他事务插入新的数据行³。
- 串行化:这是最高的隔离级别,对所有的查询都加上排他锁,使得所有的事务都必须串行执行。这样可以避免所有的并发问题,但是性能会受到很大影响。
# MVCC是什么,解决了什么问题?如何实现?
- MVCC是一种并发控制的方法,全称是Multi-Version Concurrency Control,即多版本并发控制。它主要是为了提高数据库的并发性能,解决事务之间的读写冲突问题³⁴。
- MVCC的基本思想是,每个数据行都有多个版本,每个版本都有一个时间戳或者事务ID来标识。当一个事务要读取一个数据行时,它会根据自己的时间戳或者事务ID来选择合适的版本,而不会被其他事务的修改所影响。当一个事务要修改一个数据行时,它会生成一个新的版本,并更新时间戳或者事务ID,而不会覆盖原来的版本。
- MVCC的实现方式有不同的变体,但一般都包括以下几个步骤:
- 为每个数据行添加两个隐藏字段,分别记录该行创建时的时间戳或者事务ID(简称创建ID),以及该行删除时的时间戳或者事务ID(简称删除ID)。初始时,创建ID为当前事务的ID,删除ID为空。
- 当一个事务要读取一个数据行时,它会检查该行的创建ID和删除ID,如果满足以下条件,则认为该行对该事务可见,否则认为该行对该事务不可见:
- 创建ID小于或等于该事务的ID,并且删除ID为空或者大于该事务的ID。
- 创建ID是已提交事务的ID,并且删除ID为空或者是未提交事务的ID。
- 当一个事务要修改一个数据行时,它会先复制该行,并生成一个新的版本。然后将新版本的创建ID设为当前事务的ID,并将旧版本的删除ID设为当前事务的ID。这样就保证了旧版本对其他事务不可见,而新版本只对当前事务可见。
- 当一个事务要删除一个数据行时,它只需要将该行的删除ID设为当前事务的ID即可。这样就保证了该行对其他事务不可见,而对当前事务仍然可见。
- 当一个事务提交或回滚时,它会根据自己的操作类型和结果来更新数据行的创建ID和删除ID。如果是提交操作,则将创建ID和删除ID都设为已提交状态;如果是回滚操作,则将创建ID和删除ID都设为空状态。
# 高可用/性能
# 为什么要读写分离
通过使用读写分离,可以将读操作分发到多个从服务器上执行,而写操作仍然在主服务器上执行。这样,主服务器只需要处理写操作,而从服务器只需要处理读操作。这可以有效减轻主服务器的压力,提高数据库系统的性能。