MySQL 底层原理(面试精华)

目录

  • 1 MySQL架构
    • 1.1 架构图
    • 1.2 SQL语句执行流程
  • 2 存储引擎
    • 2.1 查看存储引擎
    • 2.2 设置存储引擎
    • 2.3 文件存储结构对比
    • 2.4 InnoDB 与 MyISAM 的比较
  • 3 数据类型
    • 3.1 CHAR 和 VARCHAR 的区别
    • 3.2 BLOB和TEXT区别
  • 4 索引
    • 4.1 定义
    • 4.2 索引的基本语法
    • 4.3 索引的优缺点
    • 4.4 MySQL索引分类
    • 4.5 B-Tree 和 B+Tree 的区别
      • 4.5.1 B-Tree
      • 4.5.1 B+Tree
    • 4.6 MySQL索引结构
      • 4.6.1 MyISAM主键索引与辅助索引的结构
      • 4.6.2 InnoDB主键索引与辅助索引的结构
    • 4.7 MySQL索引的常见面试问题
  • 5 MySQL查询
    • 5.1 count(*) 、 count(1)、count(列名) 的区别
    • 5.2 in和 exists 的区别?
    • 5.3 UNION 和 UNION ALL 的区别?
    • 5.4 SQL关键字的编写与执行顺序?
    • 5.5 内连接、左连接、右连接?
  • 6 MySQL 事务
    • 6.1 ACID
    • 6.2 并发事务处理带来的问题
    • 6.3 事务隔离级别
    • 6.4 MVCC 多版本并发控制
    • 6.5 事务日志
    • 6.6 事务的实现 (事务日志)
    • 6.7 MySQL 中有哪些日志?
    • 6.8 MySQL对分布式事务的支持
  • 7 MySQL锁机制
    • 7.1 锁的分类
    • 7.2 MyISAM 表锁
    • 7.3 InnoDB 行锁
    • 7.4 死锁
      • 7.4.1 MyISAM不会死锁:
      • 7.4.2 InnoDB避免死锁:
  • 8 MySQL调优
    • 8.1 性能分析
      • 8.2 索引失效
      • 8.3 查询优化
  • 9 主从复制
    • 9.1 主从同步延迟处理方法

MySQL 底层原理(面试精华)_第1张图片



1 MySQL架构

1.1 架构图

和其它数据库相比,MySQL有点与众不同,它的架构可以在多种不同场景中应用并发挥良好作用。主要体现在存储引擎的架构上,插件式的存储引擎架构将查询处理和其它的系统任务以及数据的存储提取相分离。这种架构可以根据业务的需求和实际需要选择合适的存储引擎。
MySQL 底层原理(面试精华)_第2张图片

连接层:最上层是一些客户端和连接服务。主要完成一些类似于连接处理、授权认证、及相关的安全方案。在该层上引入了线程池的概念,为通过认证安全接入的客户端提供线程。同样在该层上可以实现基于SSL的安全链接。服务器也会为安全接入的每个客户端验证它所具有的操作权限。

服务层:第二层服务层,主要完成大部分的核心服务功能, 包括查询解析、分析、优化、缓存、以及所有的内置函数,所有跨存储引擎的功能也都在这一层实现,包括触发器、存储过程、视图等。

引擎层:第三层存储引擎层,存储引擎真正的负责了MySQL中数据的存储和提取,服务器通过API与存储引擎进行通信。不同的存储引擎具有的功能不同,这样我们可以根据自己的实际需要进行选取

存储层:第四层为数据存储层,主要是将数据存储在运行于该设备的文件系统之上,并完成与存储引擎的交互。


1.2 SQL语句执行流程

MySQL 的查询流程具体是怎样的?or 一条SQL语句在MySQL中如何执行的?

客户端请求
—> 连接器(验证用户身份,给予权限)
—> 查询缓存(存在缓存则直接返回,不存在则执行后续操作) 默认关闭
—> 分析器(对SQL进行词法分析和语法分析操作)
—> 优化器(主要对执行的sql优化选择最优的执行方案方法)
—> 执行器(执行时会先看用户是否有执行权限,有才去使用这个引擎提供的接口)
—> 去引擎层获取数据返回(如果开启查询缓存则会缓存查询结果)

MySQL 底层原理(面试精华)_第3张图片
MySQL 底层原理(面试精华)_第4张图片



2 存储引擎

存储引擎是MySQL的组件,用于处理不同表类型的SQL操作。不同的存储引擎提供不同的存储机制、索引技巧、锁定水平等功能,使用不同的存储引擎,还可以获得特定的功能。使用哪一种引擎可以灵活选择,一个数据库中多个表可以使用不同引擎以满足各种性能和实际需求,使用合适的存储引擎,将会提高整个数据库的性能 。MySQL服务器使用可插拔的存储引擎体系结构,可以从运行中的 MySQL 服务器加载或卸载存储引擎 。

2.1 查看存储引擎

-- 查看支持的存储引擎
SHOW ENGINES

-- 查看默认存储引擎
SHOW VARIABLES LIKE 'storage_engine'

--查看具体某一个表所使用的存储引擎,这个默认存储引擎被修改了!
show create table tablename

--准确查看某个数据库中的某一表所使用的存储引擎
show table status like 'tablename'
show table status from database where name="tablename"

2.2 设置存储引擎

-- 建表时指定存储引擎。默认的就是INNODB,不需要设置
CREATE TABLE t1 (i INT) ENGINE = INNODB;
CREATE TABLE t2 (i INT) ENGINE = CSV;
CREATE TABLE t3 (i INT) ENGINE = MEMORY;

-- 修改存储引擎
ALTER TABLE t ENGINE = InnoDB;

-- 修改默认存储引擎,也可以在配置文件my.cnf中修改默认引擎
SET default_storage_engine=NDBCLUSTER;

默认情况下,每当 CREATE TABLE 或 ALTER TABLE 不能使用默认存储引擎时,都会生成一个警告。为了防止在所需的引擎不可用时出现令人困惑的意外行为,可以启用 NO_ENGINE_SUBSTITUTION SQL 模式。如果所需的引擎不可用,则此设置将产生错误而不是警告,并且不会创建或更改表。

常见的存储引擎有 InnoDB、MyISAM、Memory、NDB

InnoDB 现在是 MySQL 默认的存储引擎,支持事务、行级锁定和外键

2.3 文件存储结构对比

mysql中每个数据库对应一个文件夹

MySQL 底层原理(面试精华)_第5张图片
数据库中的表根据存储引擎不同对应不同的文件

MySQL 底层原理(面试精华)_第6张图片

在 MySQL中建立任何一张数据表,在其对应的数据库目录下都有对应表的 .frm 文件,.frm 文件是用来保存每个数据表的元数据(meta)信息,包括表结构的定义等,与数据库存储引擎无关,也就是任何存储引擎的数据表都必须有.frm文件,命名方式为 数据表名.frm,如 user.frm。

查看MySQL 数据保存在哪里:show variables like ‘data%’

MyISAM 物理文件结构为:

  • .frm文件:与表相关的元数据信息,包括表结构的定义信息等;
  • .MYD (MYData) 文件:MyISAM 存储引擎专用,用于存储MyISAM 表的数据 ;
  • .MYI (MYIndex)文件:MyISAM 存储引擎专用,用于存储MyISAM 表的索引相关信息;

InnoDB 物理文件结构为:

  • .frm 文件:与表相关的元数据信息,包括表结构的定义信息等 ;
  • .ibd 文件(或 .ibdata 文件):这两种文件都是存放 InnoDB 数据的文件;

之所以有两种文件形式存放InnoDB 的数据,是因为 InnoDB 的数据存储方式能够通过配置来决定是使用共享表空间存放存储数据,还是用独享表空间存放存储数据。

独享表空间存储方式使用.ibd文件,并且每个表一个.ibd文件
共享表空间存储方式使用.ibdata文件,所有表共同使用一个.ibdata文件(或多个,可自己配置)

2.4 InnoDB 与 MyISAM 的比较

InnoDB 与 MyISAM 的比较?

对比项 MyISAM InnoDB
主外键 不支持 支持
事务 不支持 支持
行表锁 表锁,即使操作一条记录也会锁住整个表,不适合高并发的操作 行锁,操作时只锁某一行,不对其它行有影响,适合高并发的操作
缓存 只缓存索引,不缓存真实数据 (非聚集索引) 不仅缓存索引还要缓存真实数据(聚集索引),对内存要求较高,而且内存大小对性能有决定性的影响
表空间
关注点 性能 事务
  1. InnoDB 支持事务,MyISAM 不支持事务。这是 MySQL 将默认存储引擎从 MyISAM 变成 InnoDB 的重要原因之一;
  2. InnoDB 支持外键,而 MyISAM 不支持。对一个包含外键的 InnoDB 表转为 MYISAM 会失败;
  3. InnoDB是聚簇索引,MyISAM 是非聚簇索引。聚簇索引的文件存放在主键索引的叶子节点上,因此 InnoDB 必须要有主键,通过主键索引效率很高。但是辅助索引需要两次查询,先查询到主键,然后再通过主键查询到数据。因此,主键不应该过大,因为主键太大,其他索引也都会很大。而 MyISAM 是非聚集索引,数据文件是分离的,索引保存的是数据文件的指针。主键索引和辅助索引是独立的。
  4. InnoDB 不保存表的具体行数,执行select count(*) from table 时需要全表扫描。而 MyISAM 用一个变量保存了整个表的行数,执行上述语句时只需要读出该变量即可,速度很快;
  5. InnoDB 最小的锁粒度是行锁,MyISAM 最小的锁粒度是表锁。一个更新语句会锁住整张表,导致其他查询和更新都会被阻塞,因此并发访问受限。这也是 MySQL 将默认存储引擎从 MyISAM 变成 InnoDB 的重要原因之一;

一张表,里面有ID自增主键,当insert了17条记录之后,删除了第15,16,17条记录,再把Mysql重启,再insert一条记录,这条记录的ID是18还是15 ?

  • 如果表的类型是MyISAM,那么是18。因为MyISAM表会把自增主键的最大 ID 记录到数据文件中,重启MySQL自增主键的最大ID也不会丢失;
  • 如果表的类型是InnoDB,那么是15。因为InnoDB 表只是把自增主键的最大ID记录到内存中,所以重启数据库或对表进行OPTION操作,都会导致最大ID丢失。

哪个存储引擎执行 select count(*) 更快,为什么?

MyISAM更快,因为MyISAM内部维护了一个计数器,可以直接调取。

在 MyISAM 存储引擎中,把表的总行数存储在磁盘上,当执行 select count(*) from t 时,直接返回总数据。

在 InnoDB 存储引擎中,没有将总行数存储在磁盘上,当执行 select count(*) from t 时,会先把数据读出来,一行一行的累加,最后返回总数量。

InnoDB 中 count(*) 语句是在执行的时候,全表扫描统计总数量,所以当数据越来越大时,语句就越来越耗时了,为什么 InnoDB 引擎不像 MyISAM 引擎一样,将总行数存储到磁盘上?这跟 InnoDB 的事务特性有关,由于多版本并发控制(MVCC)的原因,InnoDB 表“应该返回多少行”也是不确定的。



3 数据类型

MySQL 底层原理(面试精华)_第7张图片
MySQL 底层原理(面试精华)_第8张图片
MySQL 底层原理(面试精华)_第9张图片

3.1 CHAR 和 VARCHAR 的区别

char是固定长度,varchar长度可变:

char(n) 和 varchar(n) 中括号中 n 代表字符的个数,并不代表字节个数,比如 CHAR(30) 就可以存储 30 个字符。

char 存储时,不管实际存储数据的长度,直接按 char 规定的长度分配存储空间;
varchar 存储时,会根据实际存储的数据分配最终的存储空间;

相同点:

  • char(n),varchar(n)中的 n 都代表字符的个数
  • 超过char,varchar最大长度 n 的限制后,字符串会被截断。

不同点:

  • char不论实际存储的字符数都会占用n个字符的空间;

  • varchar只会占用实际字符应该占用的字节空间+1(实际长度length,0<=length<255)或加2(length>255)。

    因为varchar保存数据时除了要保存字符串之外还会加一个字节来记录长度(如果列声明长度大于255则使用两个字节来保存长度)。

  • 能存储的最大空间限制不一样:char的存储上限为255字节;varchar可配置。

  • char在存储时会截断尾部的空格,而varchar不会。

    char适合存储很短的、一般固定长度的字符串。例如,char非常适合存储密码的MD5值,因为这是一个定长的值。对于非常短的列,char比varchar在存储空间上也更有效率。

3.2 BLOB和TEXT区别

BLOB是一个二进制对象,可以容纳可变数量的数据。有四种类型的BLOB:TINYBLOB、BLOB、MEDIUMBLO和 LONGBLOB

TEXT是一个不区分大小写的BLOB。四种TEXT类型:TINYTEXT、TEXT、MEDIUMTEXT 和 LONGTEXT。

BLOB 保存二进制数据,TEXT 保存字符数据。



4 索引

4.1 定义

说说你对 MySQL 索引的理解?

MYSQL官方对索引的定义为:索引(Index)是帮助MySQL高效获取数据的数据结构,所以说索引的本质是:数据结构

索引的目的在于提高查询效率,可以类比图书的目录。

可以简单的理解为“排好序的用于快速查找的数据结构”,数据本身之外,数据库还维护者一个满足特定查找算法的数据结构,这些数据结构以某种方式引用(指向)数据,这样就可以在这些数据结构上实现高级查找算法。这种数据结构,就是索引。

索引本身也很大,不可能全部存储在内存中,一般以索引文件的形式存储在磁盘上

平常说的索引,没有特别指明的话,就是B+树(多路搜索树,不一定是二叉树)结构组织的索引。其中聚集索引,次要索引,覆盖索引,复合索引,前缀索引,唯一索引默认都是使用B+树索引,统称索引。此外还有哈希索引等。

4.2 索引的基本语法

创建:

# 创建索引: 如果是CHAR,VARCHAR类型,length可以小于字段实际长度;如果是BLOB和TEXT类型,必须指定 length。
CREATE [UNIQUE] INDEX indexName ON mytable(username(length));

# 修改表结构(添加索引):
ALTER table tableName ADD [UNIQUE] INDEX indexName(columnName)

删除:

DROP INDEX [indexName] ON mytable;

查看:

#可以通过添加 \G 来格式化输出信息。
SHOW INDEX FROM table_name\G

使用ALERT命令

# 该语句添加一个主键,这意味着索引值必须是唯一的,且不能为NULL。
ALTER TABLE tbl_name ADD PRIMARY KEY (column_list);
# 这条语句创建索引的值必须是唯一的(除了NULL外,NULL可能会出现多次)
ALTER TABLE tbl_name ADD UNIQUE index_name (column_list); 
# 添加普通索引,索引值可出现多次。
ALTER TABLE tbl_name ADD INDEX index_name (column_list);
# 指定了索引为 FULLTEXT ,用于全文索引。
ALTER TABLE tbl_name ADD FULLTEXT index_name (column_list)

4.3 索引的优缺点

索引优势

  • 提高数据检索效率,降低数据库IO成本;
  • 降低数据排序的成本,降低CPU的消耗;

索引劣势

  • 索引也是一张表,保存了主键和索引字段,并指向实体表的记录,所以也需要占用内存。
  • 虽然索引大大提高了查询速度,同时却会降低更新表的速度,如对表进行INSERT、UPDATE和DELETE。因为更新表时,MySQL不仅要保存数据,还要保存一下索引文件每次更新添加了索引列的字段,都会调整因为更新所带来的键值变化后的索引信息。

4.4 MySQL索引分类

数据结构角度

  • B+ 树索引
  • Hash 索引
  • Full-Text 全文索引
  • R-Tree 索引

从物理存储角度

  • 聚集索引(clustered index)
  • 非聚集索引(non-clustered index),也叫辅助索引(secondary index)

聚集索引和非聚集索引都是B+树结构

从逻辑角度

  • 主键索引:主键索引是一种特殊的唯一索引,不允许有空值 ;
  • 普通索引(单列索引):每个索引只包含单个列,一个表可以有多个单列索引;
  • 多列索引(复合索引、联合索引):复合索引指多个字段上创建的索引,只有在查询条件中使用了创建索引时的第一个字段,索引才会被使用。使用复合索引时遵循 “最左前缀原则”
  • 唯一索引 或 非唯一索引
  • 空间索引:空间索引是对空间数据类型的字段建立的索引,MYSQL中的空间数据类型有4种,分别是GEOMETRY、POINT、LINESTRING、POLYGON。MYSQL使用SPATIAL关键字进行扩展,使得能够用于创建正规索引类型的语法创建空间索引。创建空间索引的列,必须将其声明为 NOT NULL,空间索引只能在存储引擎为MYISAM的表中创建

4.5 B-Tree 和 B+Tree 的区别

4.5.1 B-Tree

B-Tree是为磁盘等外存储设备设计的一种平衡查找树。

系统从磁盘读取数据到内存时是以磁盘块(block)为基本单位的,位于同一个磁盘块中的数据会被一次性读取出来,而不是需要什么取什么。

InnoDB 存储引擎中有页(Page)的概念,页是其磁盘管理的最小单位。InnoDB 存储引擎中默认每个页的大小为16KB,可通过参数 innodb_page_size 将页的大小设置为 4K、8K、16K,在 MySQL 中可通过如下命令查看页的大小:show variables like ‘innodb_page_size’;

而系统一个磁盘块的存储空间往往没有这么大,因此 InnoDB 每次申请磁盘空间时都会是若干地址连续磁盘块来达到页的大小 16KB。InnoDB 在把磁盘数据读入到磁盘时会以页为基本单位,在查询数据时如果一个页中的每条数据都能有助于定位数据记录的位置,这将会减少磁盘I/O次数,提高查询效率。

B-Tree 结构的数据可以让系统高效的找到数据所在的磁盘块。为了描述 B-Tree,首先定义一条记录为一个二元组 [key, data] ,key为记录的键值,对应表中的主键值,data 为一行记录中除主键外的数据。对于不同的记录,key值互不相同。

B-Tree有如下特性:

  • 每个节点最多有m个孩子
  • 除了根节点和叶子节点外,其它每个节点至少有Ceil(m/2)个孩子。
  • 若根节点不是叶子节点,则至少有2个孩子
  • 所有叶子节点都在同一层,且不包含其它关键字信息 每个非终端节点包含n个关键字信息(P0,P1,…Pn, k1,…kn)
  • 关键字的个数n满足:ceil(m/2)-1 <= n <= m-1 ki(i=1,…n)为关键字,且关键字升序排序
  • Pi(i=1,…n)为指向子树根节点的指针。P(i-1)指向的子树的所有节点关键字均小于ki,但都大于k(i-1)

B-Tree 中的每个节点根据实际情况可以包含大量的关键字信息和分支,下图所示为一个 3 阶的 B-Tree:

MySQL 底层原理(面试精华)_第10张图片
每个节点占用一个盘块的磁盘空间,一个节点上有两个升序排序的关键字和三个指向子树根节点的指针,指针存储的是子节点所在磁盘块的地址。两个关键词划分成的三个范围域对应三个指针指向的子树的数据的范围域。以根节点为例,关键字为17和35,P1指针指向的子树的数据范围为小于17,P2指针指向的子树的数据范围为17~35,P3指针指向的子树的数据范围为大于35。

查找关键字29的过程:

  1. 根据根节点找到磁盘块1,读入内存。【磁盘I/O操作第1次】
  2. 比较关键字29在区间(17,35),找到磁盘块1的指针P2。
  3. 根据P2指针找到磁盘块3,读入内存。【磁盘I/O操作第2次】
  4. 比较关键字29在区间(26,30),找到磁盘块3的指针P2。
  5. 根据P2指针找到磁盘块8,读入内存。【磁盘I/O操作第3次】
  6. 在磁盘块8中的关键字列表中找到关键字29。

分析上面过程,发现需要3次磁盘I/O操作,和3次内存查找操作。由于内存中的关键字是一个有序表结构,可以利用二分法查找提高效率。而3次磁盘I/O操作是影响整个B-Tree查找效率的决定因素。B-Tree相对于 AVL-Tree 缩减了节点个数,使每次磁盘I/O取到内存的数据都发挥了作用,从而提高了查询效率。

4.5.1 B+Tree

B+Tree 是在 B-Tree 基础上的一种优化,使其更适合实现外存储索引结构,InnoDB 存储引擎就是用 B+Tree 实现其索引结构。

B-Tree 结构的缺点:

  • 不支持范围查询的快速查找,需要回到根节点重新遍历查找,需要从根节点进行多次遍历。
  • 每个节点中不仅包含数据的key值,还有data值;每一个页的存储空间是有限的,如果data数据较大时将会导致每个节点(即一个页)能存储的key的数量很小;会导致B-Tree的深度较大,增大查询时的磁盘I/O次数,进而影响查询效率。

在B+Tree中:所有数据记录节点都是按照键值大小顺序存放在同一层的叶子节点上,非叶子节点上只存储key值信息,这样可以大大加大每个节点存储的key值数量,降低 B+Tree 的高度

B+Tree相对于B-Tree有几点不同:

  • 非叶子节点只存储键值信息;(增大存储量,降低树的深度来实现 I/O次数)
  • 所有叶子节点形成了一个双向有序链表; (可以不回溯根节点,实现范围查询)
  • 数据记录都存放在叶子节点中;(全表扫描,只需要遍历叶子节点,无需要遍历整棵 B+Tree)

将上一节中的B-Tree优化,由于B+Tree的非叶子节点只存储键值信息,假设每个磁盘块能存储4个键值及指针信息,则变成B+Tree后其结构如下图所示:

MySQL 底层原理(面试精华)_第11张图片
通常在B+Tree上有两个头指针,一个指向根节点,另一个指向关键字最小的叶子节点,而且所有叶子节点(即数据节点)之间是一种链式环结构。因此可以对B+Tree进行两种查找运算:一种是对于主键的范围查找和分页查找,另一种是从根节点开始,进行随机查找。

可能上面例子中只有22条数据记录,看不出B+Tree的优点,下面做一个推算:

InnoDB存储引擎中页的大小为16KB,一般表的主键类型为INT(占用4个字节)或 BIGINT(占用8个字节),指针类型也一般为4或8个字节,也就是说一个页(B+Tree中的一个节点)中大概存储16KB/(8B+8B)=1K个键值(因为是估值,为方便计算,这里的K取值为103 )。 也就是说一个深度为3的 B+Tree索引理论上可以维护 103 * 103 * 103 = 10亿 条记录。

实际情况中每个节点可能不能填充满,因此在 数据库中 B+Tree的高度一般都在2-4层 每次查询IO 次数相同,查询效率稳定MySQL的InnoDB存储引擎在设计时是将 根节点常驻在内存中(每次都从根节点开始查找,可以减少一次IO操作),也就是说查找某一键值的行记录时最多只需要1~3次磁盘I/O操作。

B+Tree性质

  1. 通过上面的分析,我们知道 IO次数取决于 b+ 数的高度h,假设当前数据表的数据为N,每个磁盘块的数据项的数量是m,则有h=㏒(m+1)N,当数据量N一定的情况下,m越大,h越小;而 m = 磁盘块的大小 / 数据项的大小,磁盘块的大小也就是一个数据页的大小,是固定的,如果数据项占的空间越小,数据项的数量越多,树的高度越低。这就是为什么每个数据项,即索引字段要尽量的小,比如int占4字节,要比bigint8字节少一半。这也是为什么 b+ 树要求把真实的数据放到叶子节点而不是内层节点,一旦放到内层节点,磁盘块的数据项会大幅度下降,导致树增高。当数据项等于1时将会退化成线性表。

  2. 当 b+ 树的数据项是复合的数据结构(复合索引),比如 (name,age,sex) 的时候,b+数是按照从左到右的顺序来建立搜索树的,比如当 (张三,20,F) 这样的数据来检索的时候,b+树会优先比较name来确定下一步的所搜方向,如果name相同再依次比较age和sex,最后得到检索的数据;但当(20,F)这样的没有name的数据来的时候,b+树就不知道下一步该查哪个节点,因为建立搜索树的时候name就是第一个比较因子,必须要先根据name来搜索才能知道下一步去哪里查询。比如当(张三,F)这样的数据来检索时,b+树可以用name来指定搜索方向,但下一个字段age的缺失,所以只能把名字等于张三的数据都找到,然后再匹配性别是F的数据了,这个是非常重要的性质,即索引的最左匹配特性

4.6 MySQL索引结构

首先要明白索引(index)是在存储引擎(storage engine)层面实现的,而不是server层面。不是所有的存储引擎都支持所有的索引类型。即使多个存储引擎支持某一索引类型,它们的实现和行为也可能有所差别。

MyISAM 和 InnoDB 存储引擎,都使用 B+Tree的作为索引的数据结构。

4.6.1 MyISAM主键索引与辅助索引的结构

MyISAM引擎的索引文件和数据文件是分离的 — “非聚簇索引

MyISAM引擎 索引结构的 叶子节点存放的不是实际的数据记录,而是数据记录的地址。

MyISAM的主索引与辅助索引区别并不大,只是主键索引不能有重复的关键字。

MySQL 底层原理(面试精华)_第12张图片
索引(含叶子节点)存放在单独的.myi文件中,叶子节点存放的是数据的物理地址偏移量。

主索引是指主键索引,键值不可能重复;
辅助索引则是普通索引,键值可能重复。

通过索引查找数据的流程:

  • 先从索引文件中查找到数据所在的叶子节点,从中拿到数据的文件指针
  • 再到数据文件中通过文件指针定位了具体的数据。。

4.6.2 InnoDB主键索引与辅助索引的结构

叶子节点存放的是实际的数据记录

  • 对于主索引(主键索引),会存放表中的 主键及其对应的一条记录中所有数据;
  • 对于辅助索引(非主键索引),会存放主索引的引用;(检索时,通过非主键索引找到主索引,再用主键索引找到对应完整数据)

InnoDB的数据文件本身就是主键索引文件 – “聚簇索引”(一个表只能有一个聚簇索引)

主键索引:

InnoDB索引是聚集索引,它的 索引和数据是存入同一个.ibd文件中,因此它的索引结构是在同一个树节点中同时存放索引和数据,如下图中最底层的叶子节点有三行数据,对应于数据表中的 id、stu_id、name 数据项。

在Innodb中,索引分叶子节点和非叶子节点,非叶子节点就像新华字典的目录,单独存放在索引段中,叶子节点则是顺序排列的,在数据段中。

Innodb的数据文件可以按照表来切分(只需要开启innodb_file_per_table),切分后存放在xxx.ibd中,默认不切分,存放在xxx.ibdata中。
MySQL 底层原理(面试精华)_第13张图片
MySQL 底层原理(面试精华)_第14张图片


**辅助(非主键)索引:**

以示例中学生表中的name列建立辅助索引,它的索引结构跟主键索引的结构有很大差别,在最底层的叶子结点有两行数据,第一行的字符串是辅助索引,按照ASCII码进行排序,第二行的整数是主键的值。

MySQL 底层原理(面试精华)_第15张图片


回表查询

对非主键索引 name 进行条件搜索,需要两个步骤:

① 在辅助索引上检索 name,到达其叶子节点获取对应的主键;
② 使用主键在主键索引上再进行对应的检索操作,获取数据记录(多扫描了一棵主键索引树);

MySQL 底层原理(面试精华)_第16张图片


联合索引:

先比较第一个索引字段,如果相同比较第二个索引字段,以此类推。
(b+ trees中:第一个索引是有序的,只有第一个索引相同时第二级索引才是有序的,以此类推。所以,使用联合索引一定要遵守 最左前缀法则)

MySQL 底层原理(面试精华)_第17张图片

InnoDB 索引结构需要注意的点

  1. 数据文件本身就是索引文件 ;
  2. 表数据文件本身就是按 B+Tree 组织的一个索引结构文件 ;
  3. 聚集索引中叶节点包含了完整的数据记录 InnoDB;
  4. 表必须要有主键,并且推荐使用整型自增主键;

InnoDB 存储结构,索引与数据是共同存储的,不管是主键索引还是辅助索引,在查找时都是通过先查找到索引节点才能拿到相对应的数据,如果在设计表结构时没有显式指定索引列的话,MySQL 会从表中选择数据不重复的列建立索引,如果没有符合的列,则 MySQL 自动为 InnoDB 表生成一个隐含字段作为主键,并且这个字段长度为6个字节,类型为整型。

4.7 MySQL索引的常见面试问题

那为什么 推荐使用整型自增主键 而不是选择UUID?

  • UUID是字符串,比整型消耗更多的存储空间;
  • 在B+树中进行查找时需要跟经过的节点值比较大小,整型数据的比较运算比字符串更快速;
  • 自增的整型索引在磁盘中会连续存储,在读取一页数据时也是连续;UUID是随机产生的,读取的上下两行数据存储是分散的,不适合执行 类似 where id > 5 && id < 20 这样的条件查询语句。
  • 在插入或删除数据时,整型自增主键会在叶子结点的末尾建立新的叶子节点,不会破坏左侧子树的结构;UUID主键是无序的,B+树为了维持自身的特性,有可能会进行结构的重构,消耗更多的时间。

为什么非主键索引结构叶子节点存储的是主键值?

为了保证 数据一致性节省存储空间

例如,商城系统订单表会存储一个用户ID作为关联外键,而不推荐存储完整的用户信息,因为当我们用户表中的信息(真实名称、手机号、收货地址···)修改后,不需要再次维护订单表的用户数据,同时也节省了存储空间。


为什么Mysql索引要用B+树不是B树 或 红黑树?

用 B+ 树不用 B树 考虑的是IO对性能的影响:B树在id递增的情况下会单边增长,形成链表的结构

B树 的每个节点都存储数据,而 B+树 只有叶子节点才存储数据,所以查找相同数据量的情况下,B树的高度更高,IO更频繁。
数据库索引是存储在磁盘上的,当数据量大时,就不能把整个索引全部加载到内存了,只能逐一加载每一个磁盘页(对应索引树的节点)。
MySQL 底层原理(面试精华)_第18张图片

同理,红黑树虽然是具有左旋功能的平衡二叉树,但是其深度在数据量大时深度依然很深
MySQL 底层原理(面试精华)_第19张图片
B+树,几千万条的记录深度也只有3,极大地减少了IO读取磁盘的次数,提升性能。
MySQL底层对B+树进一步优化:叶子节点中使用双向链表进行连接,且在链表的头结点和尾节点是循环指向的。

为何不采用Hash方式?

MySQL 底层原理(面试精华)_第20张图片

Hash索引底层用的 哈希表是一种以 key-value 存储数据的结构,用一个哈希函数把key换算成一个确定的位置,然后把value放在数组的这个位置。所以多个数据在存储关系上是完全没有任何顺序关系的,所以,对于区间查询是无法直接通过索引查询的,就需要全表扫描。所以,哈希索引只适用于等值查询的场景。

B+ Tree是一种多路平衡查询树,所以他的节点是天然有序的(左子节点小于父节点、父节点小于右子节点),而且叶子结点还用双向链表对节点进行连接,所以对于范围查询的时候不需要做全表扫描。

哈希索引不支持多列联合索引的最左匹配规则,如果有大量重复键值得情况下,哈希索引的效率会很低,因为存在哈希碰撞问题。

哪些情况需要创建索引?

  1. 主键自动建立唯一索引;
  2. 频繁作为查询条件的字段;
  3. 查询中与其他表关联的字段,外键关系建立索引;
  4. 单键/组合索引的选择问题,高并发下倾向创建组合索引;
  5. 查询中排序的字段,排序字段通过索引访问大幅提高排序速度;
  6. 查询中统计或分组字段;

创建索引注意事项?

索引创建在用于 where 判断 order 排序和 join 的(on)字段上

  1. 表记录太少,不要建索引;
  2. 字段区分度低,建立索引没有太大意义;
  3. 频繁更新的字段不适合创建索引(需要维护索引,会加重IO负担);
  4. where、order 、join 条件里用不到的字段不创建索引;
  5. 组合索引把散列性高(区分度高)的字段放在前面;
  6. MySQL中每次只能使用一个索引,所以经常使用多个条件查询时更适合使用组合索引;
  7. 过长的字段,使用 前缀索引。当字段值比较长的时候,建立索引会消耗很多的空间,搜索起来也会很慢。可以通过截取字段的前面一部分内容建立索引,这个就叫前缀索引。
  8. 不建议用无序的值(例如身份证、UUID )作为索引。当主键具有不确定性,会造成叶子节点频繁分裂,出现磁盘存储的碎片化。

覆盖索引是什么?

覆盖索引(Covering Index)不需要回表操作

  • select 的数据列只用从索引中就能够取得,不必读取数据行,MySQL可以利用索引返回 select 列表中的字段,而不必根据索引再次读取数据文件,换句话说查询列要被所建的索引覆盖。

  • 索引是高效找到行的一个方法,但是一般数据库也能使用索引找到一个列的数据,因此它不必读取整个行。毕竟索引叶子节点存储了它们索引的数据,当能通过读取索引就可以得到想要的数据,那就不需要读取行了。一个索引包含(覆盖)满足查询结果的数据就叫做覆盖索引。

  • 判断标准:使用explain,可以通过输出的extra列来判断,对于一个索引覆盖查询,显示为using index,MySQL查询优化器在执行查询前会决定是否有索引覆盖查询;



5 MySQL查询

5.1 count(*) 、 count(1)、count(列名) 的区别

执行效果上:

  • count(*)包括了所有列,相当于行数,在统计结果的时候,不会忽略列值为NULL;
  • count(1)包括了所有列,用1代表代码行,在统计结果的时候,不会忽略列值为NULL;
  • count(列名)只包括列名那一列,在统计结果的时候,会忽略列值为空(这里的空不是只空字符串或者0,而是表示null)的计数,即某个字段值为NULL时,不统计。

执行效率上:

  • 列名为主键,count(列名)会比count(1)快;
  • 列名不为主键,count(1)会比count(列名) 快;
  • 如果表多个列并且没有主键,则 count(1) 的执行效率优于 count(*) ;
  • 如果有主键,则 select count(主键)的执行效率是最优的;
  • 如果表只有一个字段,则 select count(*) 最优。

5.2 in和 exists 的区别?

  • exists:exists对外表用loop逐条查询,每次查询都会查看exists的条件语句,当exists里的条件语句能够返回记录行时(无论记录行是的多少,只要能返回),条件就为真,返回当前loop到的这条记录;反之,如果exists里的条件语句不能返回记录行,则当前loop到的这条记录被丢弃,exists的条件就像一个bool条件,当能返回结果集则为true,不能返回结果集则为false

  • in:in查询相当于多个 or 条件的叠加

如果查询的两个表大小相当,那么用in和exists差别不大。
如果两个表中一个较小,一个是大表,则子查询表大的用exists,子查询表小的用in。

5.3 UNION 和 UNION ALL 的区别?

UNION和UNION ALL都是将两个结果集合并为一个,两个要联合的SQL语句 字段个数必须一样,而且字段类型要“相容”(一致);

  • UNION 在进行表连接后会筛选掉重复的数据记录(效率较低),而 UNION ALL 则不会去掉重复的数据记录;
  • UNION 会按照字段的顺序进行排序,而 UNION ALL 只是简单的将两个结果合并就返回;

5.4 SQL关键字的编写与执行顺序?

手写

SELECT DISTINCT <select_list>
FROM  <left_table> 
<join_type> JOIN  <right_table> ON <join_condition>

WHERE  <where_condition>
GROUP BY <group_by_list>
HAVING <having_condition>
ORDER BY <order_by_condition>
LIMIT <limit_number>

机读

FROM  <left_table>
ON <join_condition>
<join_type> JOIN  <right_table>
WHERE  <where_condition>
GROUP BY  <group_by_list>
HAVING <having_condition>

SELECT DISTINCT <select_list>
ORDER BY <order_by_condition>
LIMIT <limit_number>

MySQL 底层原理(面试精华)_第21张图片

5.5 内连接、左连接、右连接?

什么是内连接、外连接、交叉连接、笛卡尔积呢?

MySQL 底层原理(面试精华)_第22张图片



6 MySQL 事务

6.1 ACID

事务是由一组SQL语句组成的逻辑处理单元,具有4个属性,通常简称为事务的ACID属性。

  • A (Atomicity) 原子性:整个事务中的所有操作,要么全部完成,要么全部不完成,不可能停滞在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样

  • C (Consistency) 一致性:在事务开始之前和事务结束以后,数据库的完整性约束没有被破坏

  • I (Isolation)隔离性:一个事务的执行不能其它事务干扰。即一个事务内部的操作及使用的数据对其它并发事务是隔离的,并发执行的各个事务之间不能互相干扰

  • D (Durability) 持久性:在事务完成以后,该事务所对数据库所作的更改便持久的保存在数据库文件中;

6.2 并发事务处理带来的问题

  • 脏读(Dirty Reads):事务A读取了事务B中未提交的数据。如果B执行回滚操作,那么A读取到的数据是脏数据。

  • 幻读(Phantom Reads):一个事务A读取了几行数据,接着另一个并发事务B 插入 了一些数据时。在随后的查询中,事务A就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。

  • 不可重复读(Non-Repeatable Reads):事务 A 多次读取同一数据,事务 B 在事务A多次读取的过程中,对数据作了 更新 并提交,导致事务A多次读取同一数据时,结果不一致。

幻读和不可重复读的区别?

  • 不可重复读的重点是修改:在同一事务中,同样的条件,第一次读的数据和第二次读的数据不一样。(因为中间有其他事务提交了修改)

  • 幻读的重点在于新增或者删除:在同一事务中,同样的条件,,第一次和第二次读出来的记录数不一样。(因为中间有其他事务提交了插入/删除)

并发事务处理的解决办法?

“脏读” 、“幻读” 、 “不可重复读”,其实都是数据库读一致性问题,必须由数据库提供的事务隔离机制来解决:

  • 加锁:在读取数据前,对其加锁,阻止其他事务对数据进行修改。

  • 数据多版本并发控制(MultiVersion Concurrency Control,简称 MVCC 或 MCC):不用加锁, 通过一定机制生成一个数据请求时间点的一致性 数据快照 (Snapshot), 并用这个快照来提供一定级别 (语句级或事务级) 的一致性读取。从用户的角度来看,好象是数据库可以提供同一数据的多个版本。

6.3 事务隔离级别

数据库事务的隔离级别有4种,由低到高分别为:

READ-UNCOMMITTED(读未提交): 最低的隔离级别,允许读取尚未提交的数据变更。
READ-COMMITTED(读已提交): 允许读取并发事务已经提交的数据。
REPEATABLE-READ(可重复读): 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改。
SERIALIZABLE(可串行化): 最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰。

隔离级别解决数据库事务并发问题的能力
MySQL 底层原理(面试精华)_第23张图片

Spring 支持的事务隔离级别
MySQL 底层原理(面试精华)_第24张图片

数据库产品对事务隔离级别的支持
MySQL 底层原理(面试精华)_第25张图片

查看当前数据库的事务隔离级别

show variables like 'tx_isolation'

事务隔离级别和数据访问的并发性是对立的,事务隔离级别越高并发性就越差。所以要根据具体的应用来确定合适的事务隔离级别,这个地方没有万能的原则。

6.4 MVCC 多版本并发控制

可以认为 MVCC 是行级锁的一个变种,但它在很多情况下避免了加锁操作,因此开销更低。虽然实现机制有所不同,但大都实现了非阻塞的读操作,写操作也只是锁定必要的行。

MVCC 通过保存数据在某个时间点的快照来实现的并发控制。

典型的MVCC实现方式,分为 乐观并发控制(optimistic)悲观并发控制(pressimistic)

InnoDB 的 MVCC 是通过在每行记录后面保存两个隐藏的列来实现:一个保存了行的创建版本号,一个保存行的过期版本号(删除版本号)。每开始一个新的事务,系统版本号都会自动递增。事务开始时刻的系统版本号会作为事务的版本号,用来和查询到的每行记录的版本号进行比较。

保存这两个额外系统版本号,使大多数操作都不用加锁。使数据操作简单,性能很好,并且也能保证只会读取到符合要求的行。不足之处是每行记录都需要额外的存储空间,需要做更多的行检查工作和一些额外的维护工作。

MVCC 只在 COMMITTED READ(读提交)和 REPEATABLE READ(可重复读)两种隔离级别下工作。

InnoDB 中 REPEATABLE READ(可重读)隔离级别下MVCC如何工作?

SELECT

  • InnoDB只查找版本早于当前事务版本的数据行,这样可以确保事务读取的行,要么是在开始事务之前已经存在要么是事务自身插入或者修改过的;
  • 行的删除版本号要么未定义,要么大于当前事务版本号,这样可以确保事务读取到的行在事务开始之前未被删除;

INSERT

  • InnoDB为新插入的每一行保存当前系统版本号作为行版本号;

DELETE

  • InnoDB为删除的每一行保存当前系统版本号作为行删除标识

UPDATE:

  • InnoDB为更新的一行新纪录保存当前系统版本号作为行版本号,同时保存当前系统版本号到原来的行作为删除标识;

6.5 事务日志

InnoDB 使用日志来减少提交事务时的开销。

因为日志中已经记录了事务,就无须在每个事务提交时把缓冲池的脏块刷新到磁盘中。

事务修改的数据和索引通常会映射到表空间的随机位置,所以刷新这些随机位置的变更到磁盘需要很多随机 IO。随机IO 比 顺序IO 昂贵得多,因为一个IO请求需要时间把磁头移到正确的位置,然后等待磁盘上读出需要的部分,再转到开始位置。

InnoDB 用日志把随机IO变成 顺序IO。

一旦日志安全写到磁盘,事务就持久化了,即使断电了,InnoDB可以重放日志并且恢复已经提交的事务。

InnoDB 使用一个后台线程智能地刷新这些变更到数据文件。这个线程可以批量组合写入,使得数据写入更顺序,以提高效率。

事务日志可以帮助提高事务效率:

  • 使用事务日志,存储引擎在修改表中的数据时只需要修改其内存拷贝,再把该修改行为记录到持久在硬盘上的事务日志中,而不用每次都将修改的数据本身持久到磁盘。
  • 事务日志采用的是追加的方式,因此写日志的操作是磁盘上一小块区域内的顺序I/O,而不像随机I/O需要在磁盘的多个地方移动磁头,所以采用事务日志的方式相对来说要快得多。
  • 事务日志持久以后,内存中被修改的数据在后台可以慢慢刷回到磁盘。
  • 如果数据的修改已经记录到事务日志并持久化,但数据本身没有写回到磁盘,此时系统崩溃,存储引擎在重启时能够自动恢复这一部分修改的数据。

大多数存储引擎都是这样实现的,称之为 预写式日志(Write-Ahead Logging),修改数据需要写两次磁盘。

6.6 事务的实现 (事务日志)

事务的实现是基于数据库的存储引擎。不同的存储引擎对事务的支持程度不一样。MySQL 中支持事务的存储引擎有 InnoDB 和 NDB。事务的实现就是如何实现 ACID 特性。

隔离性 ------------------- 通过锁实现
原子性、持久性-------- 通过 redo log(重做日志)实现 。
一致性 ------------------- 通过 undo log(回滚日志)实现 :

事务日志包括:重做日志 (redo log)、回滚日志 (undo log)

首先看一下InnoDB的修改数据的基本流程,当修改某一条数据记录时,InnoDB是把数据从磁盘读取到内存的缓冲池上进行修改。这个时候数据在内存中被修改,与磁盘中相比就存在了差异,我们称这种有差异的数据为 脏页。InnoDB对脏页的处理不是每次生成脏页就将脏页刷新回磁盘,这样会产生海量的IO操作,严重影响InnoDB的处理性能。对于此,InnoDB有一套完善的处理策略(这里先不深究)。既然脏页与磁盘中的数据存在差异,那么如果在这期间DB出现故障就会造成数据的丢失。为了解决这个问题,redo log就应运而生了。

redo log(重做日志) 实现 原子性、持久性

事务提交之后,先将事务写入redo log日志,后续再将 Buffer Pool 中修改数据慢慢写入到磁盘。

此时如果数据库崩溃或者宕机,Buffer Pool 中的数据丢失,可能有部分已修改数据未能同步到磁盘。

那么,当系统重启进行恢复时,就可以根据redo log中记录的日志,把数据库恢复到崩溃前的一个状态。

undo log(回滚日志)实现 一致性

undo log记录了数据在操作前的状态,如果事务执行过程中需要回滚,就可以根据undo log进行回滚操作。单个事务的回滚,只会回滚当前事务做的操作,并不会影响到其他的事务做的操作。


以一个update语句执行为例:

  1. Innodb引擎在收到一个update语句后,会先根据条件找到数据所在的页,并将该页缓存在 BufferPool 中(也就是内存中)。
  2. 执行update语句,修改 BufferPool 中的数据。
  3. 针对update语句生成一个 RedoLog 对象,并存入 LoggerBuffer 中。
  4. 针对update语句生成 UndoLog 日志,用于事务回滚。
  5. 如果提交事务,那么久将 RedoLog 对象进行持久化,后续还有其他机制将 BufferPool 中修改的数据页持久化到磁盘。
  6. 如果事务回滚,那么则利用 UndoLog 日志进行数据恢复。

6.7 MySQL 中有哪些日志?

  • 错误日志(error log):记录出错信息,也记录一些警告信息或者正确的信息。
  • 查询日志(general log):记录所有对数据库请求的信息,不论这些请求是否得到了正确的执行。
  • 慢查询日志(slow query log):运行时间超过阈值的SQL语句都记录到慢查询的日志文件中。
  • 二进制日志(bin log):记录对数据库执行更改的所有操作。
  • 中继日志(relay log):中继日志也是二进制日志,用来给slave 库恢复。
  • 事务日志:重做日志(redo log) 、 回滚日志(undo log)

6.8 MySQL对分布式事务的支持

分布式事务的实现方式有很多,既可以采用 InnoDB 提供的原生的事务支持,也可以采用消息队列来实现分布式事务的最终一致性。这里主要聊一下 InnoDB 对分布式事务的支持。

MySQL 从 5.0.3 InnoDB 存储引擎开始支持 XA 协议的分布式事务。一个分布式事务会涉及多个行动,这些行动本身是事务性的。所有行动都必须一起成功完成,或者一起被回滚。

在MySQL中,使用分布式事务涉及一个或多个资源管理器和一个事务管理器。
MySQL 底层原理(面试精华)_第26张图片

  • 应用程序(AP):定义了事务的边界,指定需要做哪些事务;
  • 资源管理器(RM):提供了访问事务的方法,通常一个数据库就是一个资源管理器;
  • 事务管理器(TM):协调参与了全局事务中的各个事务。

分布式事务采用 两段式提交(two-phase commit)2PC 的方式:

  • 第一阶段所有的事务节点开始准备,告诉事务管理器ready,完成提交或rollback外的所有工作。
  • 第二阶段事务管理器告诉每个节点是commit还是rollback。如果有一个节点失败,就需要全局的节点全部rollback,以此保障事务的原子性。


7 MySQL锁机制

锁是计算机协调多个进程或线程并发访问某一资源的机制。

在数据库中,除传统的计算资源(如CPU、RAM、I/O等)的争用以外,数据也是一种供许多用户共享的资源。数据库锁定机制简单来说,就是数据库为了保证数据的一致性,而使各种共享资源在被并发访问变得有序所设计的一种规则。

7.1 锁的分类

从对数据操作的类型分类:

  • 读锁(共享锁):针对同一份数据,多个读操作可以同时进行,不会互相影响;
  • 写锁(排他锁):当前写操作没有完成前,它会阻断其他写锁和读锁;

从对数据操作的粒度分类:

为了尽可能提高数据库的并发度,每次锁定的数据范围越小越好,理论上每次只锁定当前操作的数据的方案会得到最大的并发度,但是管理锁是很耗资源的事情(涉及获取,检查,释放锁等动作),因此数据库系统需要在高并发响应和系统性能两方面进行平衡,这样就产生了“锁粒度(Lock granularity)”的概念。

  • 表级锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低(MyISAM 和 MEMORY 存储引擎采用的是表级锁);

  • 行级锁:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高(InnoDB 存储引擎既支持行级锁也支持表级锁,但默认情况下是采用行级锁);

  • 页面锁:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。

表级锁更适合于以查询为主,只有少量按索引条件更新数据的应用,如Web应用;
行级锁则更适合于有大量按索引条件并发更新少量不同数据,同时又有并发查询的应用。

行锁 表锁 页锁
MyISAM
InnoDB
BDB
Memory

7.2 MyISAM 表锁

MyISAM 的表锁有两种模式:

  • 表共享读锁 (Table Read Lock):不会阻塞其他用户对同一表的读请求,但会阻塞对同一表的写请求;
  • 表独占写锁 (Table Write Lock):会阻塞其他用户对同一表的读和写操作;

MyISAM 表的读操作与写操作之间,以及写操作之间是串行的:当一个线程获得对一个表的写锁后, 只有持有锁的线程可以对表进行更新操作。其他线程的读、 写操作都会等待,直到锁被释放为止。

默认情况下,写锁比读锁具有更高的优先级:当一个锁释放时,这个锁会优先给写锁队列中等候的获取锁请求,然后再给读锁队列中等候的获取锁请求。

7.3 InnoDB 行锁

InnoDB 实现了以下两种类型的行锁:

  • 共享锁(S):允许一个事务去读一行,阻止其他事务获得相同数据集的排他锁。
  • 排他锁(X):允许获得排他锁的事务更新数据,阻止其他事务取得相同数据集的共享读锁和排他写锁。

为了允许行锁和表锁共存,实现多粒度锁机制,InnoDB 还有两种内部使用的意向锁(Intention Locks),这两种意向锁都是表锁:

  • 意向共享锁(IS):事务打算给数据行加行共享锁,事务在给一个数据行加共享锁前必须先取得该表的 IS 锁。
  • 意向排他锁(IX):事务打算给数据行加行排他锁,事务在给一个数据行加排他锁前必须先取得该表的 IX 锁。

索引失效会导致行锁变表锁。比如 vchar 查询不写单引号的情况。


加锁机制

乐观锁与悲观锁是两种并发控制的思想,可用于解决丢失更新问题

  • 乐观锁 会“乐观地”假定大概率不会发生并发更新冲突,访问、处理数据过程中不加锁,只在更新数据时再根据版本号或时间戳判断是否有冲突,有则处理,无则提交事务。用数据版本(Version)记录机制实现,这是乐观锁最常用的一种实现方式

  • 悲观锁 会“悲观地”假定大概率会发生并发更新冲突,访问、处理数据前就加排他锁,在整个数据处理过程中锁定数据,事务提交或回滚后才释放锁。另外与乐观锁相对应的,悲观锁是由数据库自己实现了的,要用的时候,我们直接调用数据库的相关语句就可以了。

锁模式(InnoDB有三种行锁的算法)

(1) 记录锁(Record Locks):单个行记录上的锁。对索引项加锁,锁定符合条件的行。其他事务不能修改和删除加锁项;

SELECT * FROM table WHERE id = 1 FOR UPDATE;

它会在 id=1 的记录上加上记录锁,以阻止其他事务插入,更新,删除 id=1 这一行;

在通过 主键索引 与 唯一索引 对数据行进行 UPDATE 操作时,也会对该行数据加记录锁:

-- id 列为主键列或唯一索引列
UPDATE SET age = 50 WHERE id = 1;

(2) 间隙锁(Gap Locks):当使用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据记录的索引项加锁。对于键值在条件范围内但并不存在的记录,叫做“间隙”。

InnoDB 也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁。

对索引项之间的 “间隙” 加锁,锁定记录的范围(对第一条记录前的间隙或最后一条将记录后的间隙加锁),不包含索引项本身。其他事务不能在锁范围内插入数据,这样就防止了别的事务新增幻影行。

间隙锁基于非唯一索引,它锁定一段范围内的索引记录。间隙锁基于下面将会提到的Next-Key Locking 算法,请务必牢记:使用间隙锁锁住的是一个区间,而不仅仅是这个区间中的每一条数据。

SELECT * FROM table WHERE id BETWEN 1 AND 10 FOR UPDATE;

即所有在(1,10)区间内的记录行都会被锁住,所有id 为 2、3、4、5、6、7、8、9 的数据行的插入会被阻塞,但是 1 和 10 两条记录行并不会被锁住。

间隙锁的目的,是为了防止同一事务的两次当前读,出现幻读的情况

(3) 临键锁(Next-key Locks):临键锁,是记录锁与间隙锁的组合,它的封锁范围,既包含索引记录,又包含索引区间。(临键锁的主要目的,也是为了避免幻读(Phantom Read)。如果把事务的隔离级别降级为RC,临键锁则也会失效。)

Next-Key 可以理解为一种特殊的间隙锁,也可以理解为一种特殊的算法。通过临建锁可以解决幻读的问题。每个数据行上的非唯一索引列上都会存在一把临键锁,当某个事务持有该数据行的临键锁时,会锁住一段左开右闭区间的数据。需要强调的一点是,InnoDB 中行级锁是基于索引实现的,临键锁只与非唯一索引列有关,在唯一索引列(包括主键列)上不存在临键锁。

对于行的查询,都是采用该方法,主要目的是解决幻读的问题。


select for update有什么含义,会锁表还是锁行还是其他?

for update 仅适用于InnoDB,且必须在事务块(BEGIN/COMMIT)中才能生效。在进行事务操作时,通过“for update”语句,MySQL会对查询结果集中每行数据都添加排他锁,其他线程对该记录的更新与删除操作都会阻塞。排他锁包含行锁、表锁。

InnoDB这种行锁实现特点意味着:只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁!假设有个表单 products ,里面有 id 跟 name 二个栏位,id是主键。

  • 明确指定主键,并且有此记录,row lock
SELECT * FROM products WHERE id='3' FOR UPDATE;
SELECT * FROM products WHERE id='3' and type=1 FOR UPDATE;
  • 明确指定主键,若查无此记录,无lock
SELECT * FROM products WHERE id='-1' FOR UPDATE;
  • 无主键,table lock
 SELECT * FROM products WHERE name='Mouse' FOR UPDATE;
  • 主键不明确,table lock
SELECT * FROM products WHERE id<>'3' FOR UPDATE;
  • 主键不明确,table lock
SELECT * FROM products WHERE id LIKE '3' FOR UPDATE;

注1: FOR UPDATE仅适用于InnoDB,且必须在交易区块(BEGIN/COMMIT)中才能生效。
注2: 要测试锁定的状况,可以利用MySQL的Command Mode ,开2个视窗来做测试。

MySQL 遇到过死锁问题吗,你是如何解决的?

7.4 死锁

死锁产生:

  • 死锁是指两个或多个事务在同一资源上相互占用,并请求锁定对方占用的资源,从而导致恶性循环
  • 当事务试图以不同的顺序锁定资源时,就可能产生死锁。多个事务同时锁定同一个资源时也可能会产生死锁
  • 锁的行为和顺序和存储引擎相关。以同样的顺序执行语句,有些存储引擎会产生死锁有些不会——死锁有双重原因:真正的数据冲突;存储引擎的实现方式。

检测死锁:

  • 数据库系统实现了各种死锁检测和死锁超时的机制。InnoDB存储引擎能检测到死锁的循环依赖并立即返回一个错误。

死锁恢复:

  • 死锁发生以后,只有部分或完全回滚其中一个事务,才能打破死锁,InnoDB目前处理死锁的方法是,将持有最少行级排他锁的事务进行回滚。所以事务型应用程序在设计时必须考虑如何处理死锁,多数情况下只需要重新执行因死锁回滚的事务即可。

外部锁的死锁检测:

  • 发生死锁后,InnoDB 一般都能自动检测到,并使一个事务释放锁并回退,另一个事务获得锁,继续完成事务。但在涉及外部锁,或涉及表锁的情况下,InnoDB 并不能完全自动检测到死锁, 这需要通过设置锁等待超时参数 innodb_lock_wait_timeout 来解决

死锁影响性能:

死锁会影响性能而不是会产生严重错误,因为InnoDB会自动检测死锁状况并回滚其中一个受影响的事务。在高并发系统上,当许多线程等待同一个锁时,死锁检测可能导致速度变慢。有时当发生死锁时,禁用死锁检测(使用innodb_deadlock_detect配置选项)可能会更有效,这时可以依赖innodb_lock_wait_timeout设置进行事务回滚。

7.4.1 MyISAM不会死锁:

在自动加锁的情况下,MyISAM 总是一次获得 SQL 语句所需要的全部锁,所以 MyISAM 表不会出现死锁。

7.4.2 InnoDB避免死锁:

  • 为了在单个InnoDB表上执行多个并发写入操作时避免死锁,可以在事务开始时通过为预期要修改的每个元祖(行)使用SELECT … FOR UPDATE语句来获取必要的锁,即使这些行的更改语句是在之后才执行的;
  • 在事务中,如果要更新记录,应该直接申请足够级别的锁,即排他锁,而不应先申请共享锁、更新时再申请排他锁,因为这时候当用户再申请排他锁时,其他事务可能又已经获得了相同记录的共享锁,从而造成锁冲突,甚至死锁;
  • 如果事务需要修改或锁定多个表,则应在每个事务中以相同的顺序使用加锁语句。在应用中,如果不同的程序会并发存取多个表,应尽量约定以相同的顺序来访问表,这样可以大大降低产生死锁的机会 通过SELECT … LOCK IN SHARE MODE获取行的读锁后,如果当前事务再需要对该记录进行更新操作,则很有可能造成死锁。
  • 改变事务隔离级别

如果出现死锁,可以用 show engine innodb status;命令来确定最后一个死锁产生的原因。返回结果中包括死锁相关事务的详细信息,如引发死锁的 SQL 语句,事务已经获得的锁,正在等待什么锁,以及被回滚的事务等。据此可以分析死锁产生的原因和改进措施。



8 MySQL调优

8.1 性能分析

MySQL Query Optimizer

MySQL 中有专门负责优化 SELECT 语句的优化器模块,主要功能:通过计算分析系统中收集到的统计信息,为客户端请求的 Query 提供他认为最优的执行计划(他认为最优的数据检索方式,但不见得是 DBA 认为是最优的,这部分最耗费时间)

当客户端向 MySQL 请求一条 Query,命令解析器模块完成请求分类,区别出是 SELECT 并转发给 MySQL Query Optimizer时,MySQL Query Optimizer 首先会对整条 Query 进行优化,处理掉一些常量表达式的预算,直接换算成常量值。并对 Query 中的查询条件进行简化和转换,如去掉一些无用或显而易见的条件、结构调整等。然后分析 Query 中的 Hint 信息(如果有),看显示 Hint 信息是否可以完全确定该 Query 的执行计划。如果没有 Hint 或 Hint 信息还不足以完全确定执行计划,则会读取所涉及对象的统计信息,根据 Query 进行写相应的计算分析,然后再得出最后的执行计划。

MySQL常见瓶颈

  • CPU :CPU在饱和的时候一般发生在数据装入内存或从磁盘上读取数据时候;
  • IO :磁盘I/O瓶颈发生在装入数据远大于内存容量的时候;
  • 服务器硬件的性能瓶颈:top,free,iostat 和 vmstat来查看系统的性能状态;

SQL执行时间长的原因分析

  • 查询语句写的烂 ;
  • 索引失效(单值、复合);
  • 关联查询太多join(设计缺陷或不得已的需求);
  • 服务器调优及各个参数设置(缓冲、线程数等);

MySQL常见性能分析手段

在优化MySQL时,通常需要对数据库进行分析,常见的分析手段有慢查询日志

  • EXPLAIN 分析查询;
  • profiling分析;
  • show命令查询系统状态及系统变量;

通过定位分析性能的瓶颈,才能更好的优化数据库系统的性能。

性能瓶颈定位

可以通过 show 命令查看 MySQL 状态及变量,找到系统的瓶颈:

 -- 显示状态信息(扩展show status like ‘XXX’)
Mysql> show status
-- 显示系统变量(扩展show variables like ‘XXX’)
Mysql> show variables
-- 显示InnoDB存储引擎的状态
Mysql> show innodb status 
-- 查看当前SQL执行,包括执行状态、是否锁表等
Mysql> show processlist
-- 显示系统变量
Shell> mysqladmin variables -u username -p password
-- 显示状态信息
Shell> mysqladmin extended-status -u username -p password

慢查询日志

MySQL 的慢查询日志是 MySQL 提供的一种日志记录,它用来记录在 MySQL 中响应时间超过阈值的语句,具体指运行时间超过 long_query_time 值的 SQL,则会被记录到慢查询日志中。

  • long_query_time 的默认值为10,意思是运行10秒以上的语句
  • 默认情况下,MySQL数据库没有开启慢查询日志,需要手动设置参数开启;

查看开启状态

SHOW VARIABLES LIKE '%slow_query_log%'

开启慢查询日志

(1) 临时配置

如果MySQL重启则会失效

mysql> set global slow_query_log='ON';
mysql> set global slow_query_log_file='/var/lib/mysql/hostname-slow.log';
mysql> set global long_query_time=2;

(2) 永久配置

修改配置文件my.cnf或my.ini,在[mysqld]一行下面加入两个配置参数

[mysqld]
slow_query_log = ON
slow_query_log_file = /var/lib/mysql/hostname-slow.log
long_query_time = 3

可以用 select sleep(4) 验证是否成功开启。

在生产环境中,如果手工分析日志,查找、分析SQL,还是比较费劲的,所以MySQL提供了日志分析工具mysqldumpslow。(mysqldumpslow --help 查看操作帮助信息)

# 得到返回记录集最多的10个SQL
mysqldumpslow -s r -t 10 /var/lib/mysql/hostname-slow.log
# 得到访问次数最多的10个SQL
mysqldumpslow -s c -t 10 /var/lib/mysql/hostname-slow.log
# 得到按照时间排序的前10条里面含有左连接的查询语句
mysqldumpslow -s t -t 10 -g "left join" /var/lib/mysql/hostname-slow.log
# 也可以和管道配合使用
mysqldumpslow -s r -t 10 /var/lib/mysql/hostname-slow.log | more

Explain(执行计划)

使用 Explain 关键字可以模拟优化器执行SQL查询语句,从而知道 MySQL 是如何处理你的 SQL 语句的。分析你的查询语句或是表结构的性能瓶颈

能干吗:

  • 表的读取顺序
  • 数据读取操作的操作类型
  • 哪些索引可以使用
  • 哪些索引被实际使用
  • 表之间的引用
  • 每张表有多少行被优化器查询

怎么玩:

Explain + SQL语句
MySQL 底层原理(面试精华)_第27张图片
详见另一篇博客:链接: MySQL 语句的性能优化.


8.2 索引失效

  1. 全值匹配我最爱;
  2. 最佳左前缀法则,比如建立了一个联合索引(a,b,c),那么其实我们可利用的索引就有(a), (a,b), (a,b,c);
  3. 不在索引列上做任何操作(计算、函数、(自动or手动)类型转换),会导致索引失效而转向全表扫描;
  4. 范围之后的索引字段失效(存储引擎不能使用索引中范围条件右边的列)
  5. 尽量使用覆盖索引(只访问索引的查询(索引列和查询列一致)),减少select;
  6. is null ,is not null 也无法使用索引;
  7. like “xxxx%” 是可以用到索引的,like “%xxxx” 则不行(like “%xxx%” 同理)。like以通配符开头(‘%abc…’)索引失效会变成全表扫描的操作;
  8. 字符串不加单引号索引失效;
  9. 少用or,用它来连接时会索引失效;
  10. <,<=,=,>,>=,BETWEEN,IN 可用到索引,<>,not in ,!= 则不行,会导致全表扫描;

一般性建议

  1. 对于单键索引,尽量选择针对当前query过滤性更好的索引;
  2. 在选择组合索引的时候,当前Query中过滤性最好的字段在索引字段顺序中,位置越靠前越好;
  3. 在选择组合索引的时候,尽量选择可以能够包含当前query中的where字句中更多字段的索引;
  4. 尽可能通过分析统计信息和调整query的写法来达到选择合适索引的目的;
  5. 少用Hint强制索引;

8.3 查询优化

永远小表驱动大表(小的数据集驱动大的数据集)

slect * from A where id in (select id from B)

#等价于
select id from B
select * from A where A.id=B.id

当 B 表的数据集必须小于 A 表的数据集时,用 in 优于 exists;

select * from A where exists (select 1 from B where B.id=A.id)

#等价于
select * from A
select * from B where B.id = A.id`

当 A 表的数据集小于B表的数据集时,用 exists优于用 in

注意:A表与B表的ID字段应建立索引。



9 主从复制


主从同步主要依赖的就是 binlog,MySQL 默认是异步复制,具体流程如下:

主库:

  1. 接受到提交事务请求
  2. 更新数据
  3. 将数据写到binlog中
  4. 给客户端响应
  5. 推送binlog到从库中

从库:

  1. 由 I/O 线程将同步过来的 binlog 写入到 relay log 中。
  2. 由 SQL 线程从 relay log 重放事件,更新数据
  3. 给主库返回响应。

MySQL 底层原理(面试精华)_第28张图片


**复制的基本原则**
  • 每个 slave只有一个 master;
  • 每个 salve只能有一个唯一的服务器 ID;
  • 每个master可以有多个salve;

异步复制的问题

  1. 延时导致的主从数据不一致(写入主库后,数据还未同步到从库,立刻查询从库)。
  2. 异步复制有数据丢失风险(数据还未同步到从库,主库就给客户端响应,然后主库挂了,此时从库晋升为主库的话数据是缺失的)。

所以有同步复制,主库需要将 binlog 复制到所有从库,等所有从库响应了之后才会给客户端响应,这样的话性能很差,一般不会选择同步复制。

MySQL 5.7 之后搞了个半同步复制,有个参数可以选择“成功同步几个从库就返回响应。比如一共有 3 个从库,我门参数配置 1,那么只要有一个从库响应说复制成功了,主库就直接返回响应给客户端,不会等待其他两个从库。

9.1 主从同步延迟处理方法

  1. 二次查询。如果从库查不到数据,则再去主库查一遍,由 API 封装即可,算是一个兜底策略,比较简单。不过等于读的压力又转移到主库身上了,如果有不法分子估计搞一下必定查不到的查询,这就难受了。

  2. 将写之后立马读的操作转移到主库上。这种属于代码写死了,比如一些写入之后立马查询的操作,就绑定在一起,写死都走主库。不推荐,太僵硬了。

  3. 关键业务读写都走主库,非关键还是读写分离。比如上面我举例的用户注册这种,可以读写主库,这样就不会有登陆报该用户不存在的问题,这种访问量频次应该也不会很多,所以看业务适当调整此类接口。

  4. 使用缓存Redis,将主库的写入数据放入缓存,并设置缓存时间(主从同步所需时间)。查询先查缓存,缓存张没有再查从库。

你可能感兴趣的:(MySQL,mysql)