• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

MySQL基础知识

武飞扬头像
技术烧烤屋
帮助1

1. MySQL基础

1.1 什么是关系型数据库?

顾名思义,关系型数据库(RDB,Relational Database)就是一种建立在关系模型的基础上的数据库。关系模型表明了数据库中所存储的数据之间的联系(一对一、一对多、多对多)。关系型数据库中,我们的数据都被存放在了各种表中(比如用户表),表中的每一行就存放着一条数据(比如一个用户的信息)。 学新通 大部分关系型数据库都使用 SQL 来操作数据库中的数据。并且,大部分关系型数据库都支持事务的四大特性(ACID)。

有哪些常见的关系型数据库呢? MySQL、PostgreSQL、Oracle、SQL Server、SQLite(微信本地的聊天记录的存储就是用的 SQLite) 等等

1.2 什么是SQL?

SQL 是一种结构化查询语言(Structured Query Language),专门用来与数据库打交道,目的是提供一种从数据库中读写数据的简单有效的方法。

几乎所有的主流关系数据库都支持 SQL ,适用性非常强。并且,一些非关系型数据库也兼容 SQL 或者使用的是类似于 SQL 的查询语言。

SQL 可以帮助我们:

  • 新建数据库、数据表、字段;
  • 在数据库中增加,删除,修改,查询数据;
  • 新建视图、函数、存储过程;
  • 对数据库中的数据进行简单的数据分析;
  • 搭配 Hive,Spark SQL 做大数据;
  • .....

1.3 什么是MySQL?

学新通

MySQL 是一种关系型数据库,主要用于持久化存储我们的系统中的一些数据比如用户信息。 由于 MySQL 是开源免费并且比较成熟的数据库,因此,MySQL 被大量使用在各种系统中。任何人都可以在 GPL(General Public License) 的许可下下载并根据个性化的需要对其进行修改。MySQL 的默认端口号是3306。

MySQL 有什么优点? 这个问题本质上是在问 MySQL 如此流行的原因。MySQL 主要具有下面这些优点:

  • 成熟稳定,功能完善。
  • 开源免费。
  • 文档丰富,既有详细的官方文档,又有非常多优质文章可供参考学习。
  • 开箱即用,操作简单,维护成本低。
  • 兼容性好,支持常见的操作系统,支持多种开发语言。
  • 社区活跃,生态完善。
  • 事务支持优秀, InnoDB 存储引擎默认使用 REPEATABLE-READ 并不会有任何性能损失,并且,InnoDB 实现的 REPEATABLE-READ 隔离级别其实是可以解决幻读问题发生的。
  • 支持分库分表、读写分离、高可用。

1.4 MySQL字段类型

MySQL 字段类型可以简单分为三大类:

  • 数值类型:整型(tinyint、smallint、mediumint、int 和 bigint)、浮点型(float 和 double)、定点型(decimal)
  • 字符串类型:char、varchar、tinytext、text、mediumtext、longtext、tinyblob、blob、mediumblob 和 longblob 等,最常用的是 char 和 varchar 。
  • 日期时间类型:year、time、date、datetime 和 timestamp 等。 学新通

char和varchar的区别是什么?

char 和 varchar 是最常用到的字符串类型,两者的主要区别在于:char 是定长字符串,varchar 是变长字符串。

  • char 在存储时会在右边填充空格以达到指定的长度,检索时会去掉空格;

  • varchar 在存储时需要使用 1 或 2 个额外字节记录字符串的长度,检索时不需要处理。

  • char 更适合存储长度较短或者长度都差不多的字符串,例如 Bcrypt 算法、MD5 算法加密后的密码、身份证号码。

  • varchar 类型适合存储长度不确定或者差异较大的字符串,例如用户昵称、文章标题等。char(M) 和 varchar(M) 的 M 都代表能够保存的字符数的最大值,无论是字母、数字还是中文,每个都只占用一个字符。

decimal 和 float/double 的区别是什么?

decimal 和 float 的区别是:decimal 是定点数,float/double 是浮点数。decimal 可以存储精确的小数值,float/double 只能存储近似的小数值

decimal 用于存储有精度要求的小数比如与金钱相关的数据,可以避免浮点数带来的精度损失。

在 Java 中,MySQL 的 decimal 类型对应的是 Java 类 java.math.BigDecimal

为什么不推荐使用 text 和 blob?

text 类型类似于 char(0 - 255 字节)、varchar(0 - 65 535 字节),不过其可以存储更长的字符串,也就是长文本数据,比如一篇博客的内容。

类型 可存储大小 用途
TINYTEXT 0 - 255 字节 一般文本字符串
TEXT 0 - 65 535 字节 长文本字符串
MEDIUTEXT 0 - 16772 150 字节 较大文本数据
LONGTEXT 0 - 4 294 967 295 字节 极大文本数据

blob 类型主要用于存储二进制大对象,例如图片,音视频等文件。

类型 可存储大小 用途
TINYBLOB 0 - 255 字节 短文本二进制字符串
BLOB 0 - 65KB 二进制字符串
MEDIUMBLOB 0 - 16MB 二进制形式的长文本数据
LONGBLOB 0 - 4GB 二进制形式的极大文本数据

日常开发中,text 类型用的很少,但偶尔会用,blob 类型就属于是基本不用。如果预期长度范围 varchar 就满足,就避免使用 text。

数据库规范中一般不推荐使用 blob 及 text 类型,二者的部分缺点和限制如下:

  • 不能有默认值。
  • 在遇到使用临时表的情况时,无法使用内存临时表,只能在磁盘上创建临时表(《高性能 MySQL》这本书有提到)。
  • 检索效率比 char 和 varchar 低。
  • 不能直接创建索引,需要指定前缀长度。
  • 会消耗大量的网络和 IO 带宽。
  • 可能会导致表上的 DML 操作都变得较慢。
  • ......

datetime-和-timestamp-的区别是什么

DateTime 类型没有时区信息,Timestamp 和时区有关。

Timestamp 只需要使用 4 个字节的存储空间,但是 DateTime 需要耗费 8 个字节的存储空间。但是,这样同样造成了一个问题,Timestamp 表示的时间范围更小。

  • DateTime:1000-01-01 00:00:00 ~ 9999-12-31 23:59:59
  • Timestamp:1970-01-01 00:00:01 ~ 2037-12-31 23:59:59

NULL 和 '' 的区别是什么?

NULL''(空字符串)是两个完全不一样的值,区别如下:

  • NULL 代表一个不确定的值,就算是两个 NULL,它俩也不一定相等。例如,SELECT NULL=NULL的结果为 false,但是在我们使用DISTINCT,GROUP BY,ORDER BY时,NULL又被认为是相等的。
  • ''的长度是 0,是不占用空间的,而NULL 是需要占用空间的。
  • NULL 会影响聚合函数的结果。例如,SUMAVGMINMAX 等聚合函数会忽略 NULL 值。 COUNT 的处理方式取决于参数的类型。如果参数是 *(COUNT(*)),则会统计所有的记录数,包括 NULL 值;如果参数是某个字段名(COUNT(列名)),则会忽略 NULL 值,只统计非空值的个数。
  • 查询 NULL 值时,必须使用 IS NULLIS NOT NULLl 来判断,而不能使用 =、!=、 <、> 之类的比较运算符。而''是可以使用这些比较运算符的。

1.5 MySQL 基础架构

下图是 MySQL 的一个简要架构图,从下图你可以很清晰的看到客户端的一条 SQL 语句在 MySQL 内部是如何执行的

学新通

从上图可以看出, MySQL 主要由下面几部分构成:

  • 连接器: 身份认证和权限相关(登录 MySQL 的时候)。
  • 查询缓存: 执行查询语句的时候,会先查询缓存(MySQL 8.0 版本后移除,因为这个功能不太实用)。
  • 分析器: 没有命中缓存的话,SQL 语句就会经过分析器,分析器说白了就是要先看你的 SQL 语句要干嘛,再检查你的 SQL 语句语法是否正确。
  • 优化器: 按照 MySQL 认为最优的方案去执行。
  • 执行器: 执行语句,然后从存储引擎返回数据。 执行语句之前会先判断是否有权限,如果没有权限的话,就会报错。
  • 插件式存储引擎:主要负责数据的存储和读取,采用的是插件式架构,支持 InnoDB、MyISAM、Memory 等多种存储引擎。

2. MySQL存储引擎

MySQL 核心在于存储引擎,想要深入学习 MySQL,必定要深入研究 MySQL 存储引擎。

2.1 MySQL存储引擎类型

MySQL 支持多种存储引擎,你可以通过 SHOW ENGINES 命令来查看 MySQL 支持的所有存储引擎。

学新通

从上图我们可以查看出, MySQL 当前默认的存储引擎是 InnoDB。并且,所有的存储引擎中只有 InnoDB 是事务性存储引擎,也就是说只有 InnoDB 支持事务。

我这里使用的 MySQL 版本是 8.x,不同的 MySQL 版本之间可能会有差别。

MySQL 5.5.5 之前,MyISAM 是 MySQL 的默认存储引擎。5.5.5 版本之后,InnoDB 是 MySQL 的默认存储引擎。

你可以通过 SELECT VERSION() 命令查看你的 MySQL 版本。

mysql> SELECT VERSION();
 ----------- 
| VERSION() |
 ----------- 
| 8.0.27    |
 ----------- 
1 row in set (0.00 sec)

你也可以通过 SHOW VARIABLES LIKE '%storage_engine%' 命令直接查看 MySQL 当前默认的存储引擎。

mysql> SHOW VARIABLES  LIKE '%storage_engine%';
 --------------------------------- ----------- 
| Variable_name                   | Value     |
 --------------------------------- ----------- 
| default_storage_engine          | InnoDB    |
| default_tmp_storage_engine      | InnoDB    |
| disabled_storage_engines        |           |
| internal_tmp_mem_storage_engine | TempTable |
 --------------------------------- ----------- 
4 rows in set (0.00 sec)

2.2 MySQL存储引擎架构

MySQL 存储引擎采用的是 插件式架构 ,支持多种存储引擎,我们甚至可以为不同的数据库表设置不同的存储引擎以适应不同场景的需要。存储引擎是基于表的,而不是数据库。

并且,你还可以根据 MySQL 定义的存储引擎实现标准接口来编写一个属于自己的存储引擎。这些非官方提供的存储引擎可以称为第三方存储引擎,区别于官方存储引擎。像目前最常用的 InnoDB 其实刚开始就是一个第三方存储引擎,后面由于过于优秀,其被 Oracle 直接收购了。

MySQL 官方文档也有介绍到如何编写一个自定义存储引擎,地址:dev.mysql.com/doc/interna…

2.3 MyISAM和InnoDB的区别

MyISAM:

  • 不⽀持事务,但是每次查询都是原⼦的;
  • ⽀持表级锁,即每次操作是对整个表加锁;
  • 存储表的总⾏数;
  • ⼀个MYISAM表有三个⽂件:索引⽂件、表结构⽂件、数据⽂件;
  • 采⽤⾮聚集索引,索引⽂件的数据域存储指向数据⽂件的指针。辅索引与主索引基本⼀致,但是辅 索引不⽤保证唯⼀性。

InnoDB:

  • ⽀持ACID的事务,⽀持事务的四种隔离级别;
  • ⽀持⾏级锁及外键约束:因此可以⽀持写并发;
  • 不存储总⾏数;
  • ⼀个InnoDb引擎存储在⼀个⽂件空间(共享表空间,表⼤⼩不受操作系统控制,⼀个表可能分布在 多个⽂件⾥),也有可能为多个(设置为独⽴表空,表⼤⼩受操作系统⽂件⼤⼩限制,⼀般为 2G),受操作系统⽂件⼤⼩的限制;
  • 主键索引采⽤聚集索引(索引的数据域存储数据⽂件本身),辅索引的数据域存储主键的值;因此 从辅索引查找数据,需要先通过辅索引找到主键值,再访问辅索引;最好使⽤⾃增主键,防⽌插⼊ 数据时,为维持B 树结构,⽂件的⼤调整。

3. MySQL索引

2.1 索引概述

2.1.1 介绍

索引(index) 是帮助MySQL高效获取数据的数据结构(有序)。在数据之外,数据库系统还维护着满足 特定查找算法的数据结构,这些数据结构以某种方式引用(指向)数据, 这样就可以在这些数据结构上实现高级查找算法,这种数据结构就是索引。

学新通

2.1.2 示例

表结构及其数据如下:

学新通

假如我们要执行的SQL语句为 : select * from user where age = 45;

  1. 无索引情况

学新通

在无索引情况下,就需要从第一行开始扫描,一直扫描到最后一行,我们称之为全表扫描,性能很低。

  1. 有索引情况 如果我们针对于这张表建立了索引,假设索引结构就是二叉树,那么也就意味着,会对age这个字段建立一个二叉树的索引结构。

学新通

此时我们在进行查询时,只需要扫描三次就可以找到数据了,极大的提高的查询的效率。

备注: 这里我们只是假设索引的结构是二叉树,介绍一下索引的大概原理,只是一个示意图,并不是索引的真实结构,索引的真实结构,后面会详细介绍。

2.1.3 特点

优势 劣势
提高数据检索的效率,降低数据库的IO成本 索引列也是要占用空间的。
通过索引列对数据进行排序,降低数据排序的成本,降低CPU的消耗。 索引大大提高了查询效率,同时却也降低更新表的速度,如对表进行INSERT、UPDATE、DELETE时,效率降低。

2.2 索引结构

2.2.1 概述

MySQL的索引是在存储引擎层实现的,不同的存储引擎有不同的索引结构,主要包含以下几种:

索引结构 描述
B Tree索引 最常见的索引类型,大部分引擎都支持 B 树索引
Hash索引 底层数据结构是用哈希表实现的, 只有精确匹配索引列的查询才有效, 不支持范围查询
R-tree(空间索引) 空间索引是MyISAM引擎的一个特殊索引类型,主要用于地理空间数据类型,通常使用较少
Full-text(全文索引) 是一种通过建立倒排索引,快速匹配文档的方式。类似于Lucene,Solr,ES

上述是MySQL中所支持的所有的索引结构,接下来,我们再来看看不同的存储引擎对于索引结构的支持情况。

索引 InnoDB MyISAM Memory
B tree索引 支持 支持 支持
Hash 索引 不支持 不支持 支持
R-tree 索引 不支持 支持 不支持
Full-text 5.6版本之后支持 支持 不支持

注意: 我们平常所说的索引,如果没有特别指明,都是指B 树结构组织的索引

2.2.2 二叉树

假如说MySQL的索引结构采用二叉树的数据结构,比较理想的结构如下:

学新通

如果主键是顺序插入的,则会形成一个单向链表,结构如下:

学新通

所以,如果选择二叉树作为索引结构,会存在以下缺点:

  • 顺序插入时,会形成一个链表,查询性能大大降低。
  • 大数据量情况下,层级较深,检索速度慢。

此时大家可能会想到,我们可以选择红黑树,红黑树是一颗自平衡二叉树,那这样即使是顺序插入数据,最终形成的数据结构也是一颗平衡的二叉树,结构如下:

学新通

但是,即使如此,由于红黑树也是一颗二叉树,所以也会存在一个缺点:

  • 大数据量情况下,层级较深,检索速度慢。

所以,在MySQL的索引结构中,并没有选择二叉树或者红黑树,而选择的是B Tree,那么什么是B Tree呢?在详解B Tree之前,先来介绍一个B-Tree。

2.2.3 B-Tree

B-Tree,B树是一种多叉路衡查找树,相对于二叉树,B树每个节点可以有多个分支,即多叉。以一颗最大度数(max-degree)为5(5阶)的b-tree为例,那这个B树每个节点最多存储4个key,5个指针:

学新通

知识小贴士: 树的度数指的是一个节点的子节点个数。

2.2.4 B Tree

B Tree是B-Tree的变种,我们以一颗最大度数(max-degree)为4(4阶)的b tree为例,来看一下其结构示意图:

学新通

我们可以看到,两部分:

  • 绿色框框起来的部分,是索引部分,仅仅起到索引数据的作用,不存储数据。
  • 红色框框起来的部分,是数据存储部分,在其叶子节点中要存储具体的数据。

最终我们看到,B Tree 与 B-Tree相比,主要有以下三点区别:

  • 所有的数据都会出现在叶子节点。
  • 叶子节点形成一个单向链表。
  • 非叶子节点仅仅起到索引数据作用,具体的数据都是在叶子节点存放的。

上述我们所看到的结构是标准的B Tree的数据结构,接下来,我们再来看看MySQL中优化之后的B Tree。

MySQL索引数据结构对经典的B Tree进行了优化。在原B Tree的基础上,增加一个指向相邻叶子节点的链表指针,就形成了带有顺序指针的B Tree,提高区间访问的性能,利于排序。

学新通

2.2.5 Hash

MySQL中除了支持B Tree索引,还支持一种索引类型---Hash索引。

1. 结构

哈希索引就是采用一定的hash算法,将键值换算成新的hash值,映射到对应的槽位上,然后存储在hash表中。

学新通

如果两个(或多个)键值,映射到一个相同的槽位上,他们就产生了hash冲突(也称为hash碰撞),可以通过链表来解决。

学新通

2. 特点

  • Hash索引只能用于对等比较(=,in),不支持范围查询(between,>,< ,...)
  • 无法利用索引完成排序操作
  • 查询效率高,通常(不存在hash冲突的情况)只需要一次检索就可以了,效率通常要高于B tree索引

3. 存储引擎支持

在MySQL中,支持hash索引的是Memory存储引擎。 而InnoDB中具有自适应hash功能,hash索引是InnoDB存储引擎根据B Tree索引在指定条件下自动构建的。

思考题: 为什么InnoDB存储引擎选择使用B tree索引结构?

  1. 相对于二叉树,层级更少,搜索效率高;
  2. 对于B-tree,无论是叶子节点还是非叶子节点,都会保存数据,这样导致一页中存储的键值减少,指针跟着减少,要同样保存大量数据,只能增加树的高度,导致性能降低;
  3. 相对Hash索引,B tree支持范围匹配及排序操作;

2.3 索引分类

2.3.1 索引分类

在MySQL数据库,将索引的具体类型主要分为以下几类:主键索引、唯一索引、常规索引、全文索引。

分类 含义 特点 关键字
主键索引 针对于表中主键创建的索引 默认自动创建, 只能有一个 PRIMARY
唯一索引 避免同一个表中某数据列中的值重复 可以有多个 UNIQUE
常规索引 快速定位特定数据 可以有多个  
全文索引 全文索引查找的是文本中的关键词,而不是比较索引中的值 可以有多个 FULLTEXT

2.3.2 聚集索引&二级索引

在InnoDB存储引擎中,根据索引的存储形式,又可以分为以下两种:

分类 含义 特点
聚集索引(Clustered Index) 将数据存储与索引放到了一块,索引结构的叶子节点保存了行数据 必须有,而且只有一个
二级索引 将数据与索引分开存储,索引结构的叶子节点关联的是对应的主键 可以存在多个

聚集索引选取规则:

  • 如果存在主键,主键索引就是聚集索引。
  • 如果不存在主键,将使用第一个唯一(UNIQUE)索引作为聚集索引。
  • 如果表没有主键,或没有合适的唯一索引,则InnoDB会自动生成一个rowid作为隐藏的聚集索引。

聚集索引和二级索引的具体结构如下:

学新通

  • 聚集索引的叶子节点下挂的是这一行的数据 。
  • 二级索引的叶子节点下挂的是该字段值对应的主键值。

接下来,我们来分析一下,当我们执行如下的SQL语句时,具体的查找过程是什么样子的。 学新通

具体过程如下:

  1. 由于是根据name字段进行查询,所以先根据name='Arm'到name字段的二级索引中进行匹配查找。但是在二级索引中只能查找到 Arm 对应的主键值 10。
  2. 由于查询返回的数据是*,所以此时,还需要根据主键值10,到聚集索引中查找10对应的记录,最终找到10对应的行row。
  3. 最终拿到这一行的数据,直接返回即可。

回表查询: 这种先到二级索引中查找数据,找到主键值,然后再到聚集索引中根据主键值,获取数据的方式,就称之为回表查询。

思考题: 以下两条SQL语句,那个执行效率高? 为什么?

A. select * from user where id = 10 ;

B. select * from user where name = 'Arm' ;

备注: id为主键,name字段创建的有索引;

解答:

A 语句的执行性能要高于B 语句。 因为A语句直接走聚集索引,直接返回数据。 而B语句需要先查询name字段的二级索引,然 后再查询聚集索引,也就是需要进行回表查询。

InnoDB主键索引的B Tree高度为多高呢?

学新通

假设:

一行数据大小为1k,一页中可以存储16行这样的数据。InnoDB的指针占用6个字节的空间,主键即使为bigint,占用字节数为8。

高度为2:

n * 8 (n 1) * 6 = 16*1024 , 算出n约为 1170

1171* 16 = 18736

也就是说,如果树的高度为2,则可以存储 18000 多条记录。

高度为3:

1171 * 1171 * 16 = 21939856

也就是说,如果树的高度为3,则可以存储 2200w 左右的记录。

2.3.3 非聚簇索引(非聚集索引)

非聚簇索引(Non-Clustered Index)即索引结构和数据分开存放的索引,并不是一种单独的索引类型。二级索引(辅助索引)就属于非聚簇索引。MySQL 的 MyISAM 引擎,不管主键还是非主键,使用的都是非聚簇索引。

非聚簇索引的叶子节点并不一定存放数据的指针,因为二级索引的叶子节点就存放的是主键,根据主键再回表查数据。

非聚簇索引的优缺点

优点

更新代价比聚簇索引要小 。非聚簇索引的更新代价就没有聚簇索引那么大了,非聚簇索引的叶子节点是不存放数据的

缺点

  • 依赖于有序的数据:跟聚簇索引一样,非聚簇索引也依赖于有序的数据
  • 可能会二次查询(回表) :这应该是非聚簇索引最大的缺点了。 当查到索引对应的指针或主键后,可能还需要根据指针或主键再到数据文件或表中查询。

这是 MySQL 的表的文件截图:

学新通

聚簇索引和非聚簇索引:

学新通

非聚簇索引一定回表查询吗(覆盖索引)?

非聚簇索引不一定回表查询。

试想一种情况,用户准备使用 SQL 查询用户名,而用户名字段正好建立了索引。

 SELECT name FROM table WHERE name='guang19';

那么这个索引的 key 本身就是 name,查到对应的 name 直接返回就行了,无需回表查询。

即使是 MYISAM 也是这样,虽然 MYISAM 的主键索引确实需要回表,因为它的主键索引的叶子节点存放的是指针。但是!如果 SQL 查的就是主键呢?

SELECT id FROM table WHERE id=1;

主键索引本身的 key 就是主键,查到返回就行了。这种情况就称之为覆盖索引了

2.3.4 覆盖索引

如果一个索引包含(或者说覆盖)所有需要查询的字段的值,我们就称之为 覆盖索引(Covering Index) 。我们知道在 InnoDB 存储引擎中,如果不是主键索引,叶子节点存储的是主键 列值。最终还是要“回表”,也就是要通过主键再查找一次,这样就会比较慢。而覆盖索引就是把要查询出的列和索引是对应的,不做回表操作!

覆盖索引即需要查询的字段正好是索引的字段,那么直接根据该索引,就可以查到数据了,而无需回表查询。

如主键索引,如果一条 SQL 需要查询主键,那么正好根据主键索引就可以查到主键。再如普通索引,如果一条 SQL 需要查询 name,name 字段正好有索引, 那么直接根据这个索引就可以查到数据,也无需回表。

学新通

我们这里简单演示一下覆盖索引的效果。

1、创建一个名为 cus_order 的表,来实际测试一下这种排序方式。为了测试方便, cus_order 这张表只有 idscorename这 3 个字段。

CREATE TABLE `cus_order` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `score` int(11) NOT NULL,
  `name` varchar(11) NOT NULL DEFAULT '',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=100000 DEFAULT CHARSET=utf8mb4;

2、定义一个简单的存储过程(PROCEDURE)来插入 100w 测试数据。

DELIMITER ;;
CREATE DEFINER=`root`@`%` PROCEDURE `BatchinsertDataToCusOder`(IN start_num INT,IN max_num INT)
BEGIN
      DECLARE i INT default start_num;
      WHILE i < max_num DO
          insert into `cus_order`(`id`, `score`, `name`)
          values (i,RAND() * 1000000,CONCAT('user', i));
          SET i = i   1;
      END WHILE;
  END;;
DELIMITER ;

存储过程定义完成之后,我们执行存储过程即可!

CALL BatchinsertDataToCusOder(1, 1000000); # 插入100w 的随机数据

等待一会,100w 的测试数据就插入完成了!

3、创建覆盖索引并使用 EXPLAIN 命令分析。

为了能够对这 100w 数据按照 score 进行排序,我们需要执行下面的 SQL 语句。

SELECT `score`,`name` FROM `cus_order` ORDER BY `score` DESC;#降序排序

使用 EXPLAIN 命令分析这条 SQL 语句,通过 Extra 这一列的 Using filesort ,我们发现是没有用到覆盖索引的。

学新通

不过这也是理所应当,毕竟我们现在还没有创建索引呢!

我们这里以 scorename 两个字段建立联合索引:

ALTER TABLE `cus_order` ADD INDEX id_score_name(score, name);

创建完成之后,再用 EXPLAIN 命令分析再次分析这条 SQL 语句。

学新通 通过 Extra 这一列的 Using index ,说明这条 SQL 语句成功使用了覆盖索引。

2.3.5 联合索引

使用表中的多个字段创建索引,就是 联合索引,也叫 组合索引复合索引

scorename 两个字段建立联合索引:

ALTER TABLE `cus_order` ADD INDEX id_score_name(score, name);

2.3.6 最左前缀匹配原则

最左前缀匹配原则指的是,在使用联合索引时,MySQL 会根据联合索引中的字段顺序,从左到右依次到查询条件中去匹配,如果查询条件中存在与联合索引中最左侧字段相匹配的字段,则就会使用该字段过滤一批数据,直至联合索引中全部字段匹配完成,或者在执行过程中遇到范围查询(如 >< )才会停止匹配。对于 >=<=BETWEENlike 前缀匹配的范围查询,并不会停止匹配。所以,我们在使用联合索引时,可以将区分度高的字段放在最左边,这也可以过滤更多数据。

2.3.7 使用索引的一些建议

  1. 选择合适的字段创建索引

    • 不为 NULL 的字段:索引字段的数据应该尽量不为 NULL,因为对于数据为 NULL 的字段,数据库较难优化。如果字段频繁被查询,但又避免不了为 NULL,建议使用 0,1,true,false 这样语义较为清晰的短值或短字符作为替代。
    • 被频繁查询的字段:我们创建索引的字段应该是查询操作非常频繁的字段。
    • 被作为条件查询的字段:被作为 WHERE 条件查询的字段,应该被考虑建立索引。
    • 频繁需要排序的字段:索引已经排序,这样查询可以利用索引的排序,加快排序查询时间。
    • 被经常频繁用于连接的字段:经常用于连接的字段可能是一些外键列,对于外键列并不一定要建立外键,只是说该列涉及到表与表的关系。对于频繁被连接查询的字段,可以考虑建立索引,提高多表连接查询的效率。
  2. 被频繁更新的字段应该慎重建立索引

    • 虽然索引能带来查询上的效率,但是维护索引的成本也是不小的。 如果一个字段不被经常查询,反而被经常修改,那么就更不应该在这种字段上建立索引了。
  3. 限制每张表上的索引数量

    • 索引并不是越多越好,建议单张表索引不超过 5 个!索引可以提高效率同样可以降低效率。
    • 索引可以增加查询效率,但同样也会降低插入和更新的效率,甚至有些情况下会降低查询效率。 因为 MySQL 优化器在选择如何优化查询时,会根据统一信息,对每一个可以用到的索引来进行评估,以生成出一个最好的执行计划,如果同时有很多个索引都可以用于查询,就会增加 MySQL 优化器生成执行计划的时间,同样会降低查询性能。
  4. 尽可能的考虑建立联合索引而不是单列索引

    • 因为索引是需要占用磁盘空间的,可以简单理解为每个索引都对应着一颗 B 树。如果一个表的字段过多,索引过多,那么当这个表的数据达到一个体量后,索引占用的空间也是很多的,且修改索引时,耗费的时间也是较多的。如果是联合索引,多个字段在一个索引上,那么将会节约很大磁盘空间,且修改数据的操作效率也会提升。
  5. 注意避免冗余索引

    • 冗余索引指的是索引的功能相同,能够命中索引(a, b)就肯定能命中索引(a) ,那么索引(a)就是冗余索引。如(name,city )和(name )这两个索引就是冗余索引,能够命中前者的查询肯定是能够命中后者的 在大多数情况下,都应该尽量扩展已有的索引而不是创建新索引。
  6. 字符串类型的字段使用前缀索引代替普通索引

    • 前缀索引仅限于字符串类型,较普通索引会占用更小的空间,所以可以考虑使用前缀索引带替普通索引。
  7. 避免索引失效

    • SELECT * 不会直接导致索引失效(如果不走索引大概率是因为 where 查询范围过大导致的),但它可能会带来一些其他的性能问题比如造成网络传输和数据处理的浪费、无法使用索引覆盖;
    • 创建了组合索引,但查询条件未遵守最左匹配原则;
    • 在索引列上进行计算、函数、类型转换等操作;
    • % 开头的 LIKE 查询比如 like 'c';
    • 查询条件中使用 or,且 or 的前后条件中有一个列没有索引,涉及的索引都不会被使用到;
    • .....
  8. 删除长期未使用的索引

    • 删除长期未使用的索引,不用的索引的存在会造成不必要的性能损耗。
    • MySQL 5.7 可以通过查询 sys 库的 schema_unused_indexes 视图来查询哪些索引从未被使用
  9. 分析语句是否走索引查询 我们可以使用 EXPLAIN 命令来分析 SQL 的 执行计划 ,这样就知道语句是否命中索引了。执行计划是指一条 SQL 语句在经过 MySQL 查询优化器的优化会后,具体的执行方式。

EXPLAIN 并不会真的去执行相关的语句,而是通过 查询优化器 对语句进行分析,找出最优的查询方案,并显示对应的信息。

EXPLAIN 的输出格式如下:

mysql> EXPLAIN SELECT `score`,`name` FROM `cus_order` ORDER BY `score` DESC;
 ---- ------------- ----------- ------------ ------ --------------- ------ --------- ------ -------- ---------- ---------------- 
| id | select_type | table     | partitions | type | possible_keys | key  | key_len | ref  | rows   | filtered | Extra          |
 ---- ------------- ----------- ------------ ------ --------------- ------ --------- ------ -------- ---------- ---------------- 
|  1 | SIMPLE      | cus_order | NULL       | ALL  | NULL          | NULL | NULL    | NULL | 997572 |   100.00 | Using filesort |
 ---- ------------- ----------- ------------ ------ --------------- ------ --------- ------ -------- ---------- ---------------- 
1 row in set, 1 warning (0.00 sec)

各个字段的含义如下:

列名 含义
id SELECT 查询的序列标识符
select_type SELECT 关键字对应的查询类型
table 用到的表名
partitions 匹配的分区,对于未分区的表,值为 NULL
type 表的访问方法
possible_keys 可能用到的索引
key 实际用到的索引
key_len 所选索引的长度
ref 当使用索引等值查询时,与索引作比较的列或常量
rows 预计要读取的行数
filtered 按表条件过滤后,留存的记录数的百分比
Extra 附加信息

2.4 MySQL查询缓存

执行查询语句的时候,会先查询缓存。不过,MySQL 8.0 版本后移除,因为这个功能不太实用

my.cnf 加入以下配置,重启 MySQL 开启查询缓存

query_cache_type=1
query_cache_size=600000

MySQL 执行以下命令也可以开启查询缓存

set global  query_cache_type=1;
set global  query_cache_size=600000;

如上,开启查询缓存后在同样的查询条件以及数据情况下,会直接在缓存中返回结果。这里的查询条件包括查询本身、当前要查询的数据库、客户端协议版本号等一些可能影响结果的信息。

查询缓存不命中的情况:

  1. 任何两个查询在任何字符上的不同都会导致缓存不命中。
  2. 如果查询中包含任何用户自定义函数、存储函数、用户变量、临时表、MySQL 库中的系统表,其查询结果也不会被缓存。
  3. 缓存建立之后,MySQL 的查询缓存系统会跟踪查询中涉及的每张表,如果这些表(数据或结构)发生变化,那么和这张表相关的所有缓存数据都将失效。

缓存虽然能够提升数据库的查询性能,但是缓存同时也带来了额外的开销,每次查询后都要做一次缓存操作,失效后还要销毁。 因此,开启查询缓存要谨慎,尤其对于写密集的应用来说更是如此。如果开启,要注意合理控制缓存空间大小,一般来说其大小设置为几十 MB 比较合适。此外,还可以通过 sql_cachesql_no_cache 来控制某个查询语句是否需要缓存:

SELECT sql_no_cache COUNT(*) FROM usr;

3. MySQL事务

3.1 什么是事务

我们设想一个场景,这个场景中我们需要插入多条相关联的数据到数据库,不幸的是,这个过程可能会遇到下面这些问题:

  • 数据库中途突然因为某些原因挂掉了。
  • 客户端突然因为网络原因连接不上数据库了。
  • 并发访问数据库时,多个线程同时写入数据库,覆盖了彼此的更改。
  • ......

上面的任何一个问题都可能会导致数据的不一致性。为了保证数据的一致性,系统必须能够处理这些问题。事务就是我们抽象出来简化这些问题的首选机制。事务的概念起源于数据库,目前,已经成为一个比较广泛的概念。

何为事务? 一言蔽之,事务是逻辑上的一组操作,要么都执行,要么都不执行。

大多数情况下,我们在谈论事务的时候,如果没有特指分布式事务,往往指的就是数据库事务

数据库事务在我们日常开发中接触的最多了。如果你的项目属于单体架构的话,你接触到的往往就是数据库事务了。

那数据库事务有什么作用呢?

简单来说,数据库事务可以保证多个对数据库的操作(也就是 SQL 语句)构成一个逻辑上的整体。构成这个逻辑上的整体的这些数据库操作遵循:要么全部执行成功,要么全部不执行

# 开启一个事务
START TRANSACTION;
# 多条 SQL 语句
SQL1,SQL2...
## 提交事务
COMMIT;

学新通

另外,关系型数据库(例如:MySQLSQL ServerOracle 等)事务都有 ACID 特性:

学新通

  • 原子性Atomicity):事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;

  • 一致性Consistency):执行事务前后,数据保持一致,例如转账业务中,无论事务是否成功,转账者和收款人的总额应该是不变的;

  • 隔离性Isolation):并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的;

  • 持久性Durability):一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。

这里要额外补充一点:只有保证了事务的持久性、原子性、隔离性之后,一致性才能得到保障。也就是说 A、I、D 是手段,C 是目的!

3.2 并发事务带来的问题

在典型的应用程序中,多个事务并发运行,经常会操作相同的数据来完成各自的任务(多个用户对同一数据进行操作)。并发虽然是必须的,但可能会导致以下的问题。

3.2.1 脏读

脏读(Dirty read)

一个事务读取数据并且对数据进行了修改,这个修改对其他事务来说是可见的,即使当前事务没有提交。这时另外一个事务读取了这个还未提交的数据,但第一个事务突然回滚,导致数据并没有被提交到数据库,那第二个事务读取到的就是脏数据,这也就是脏读的由来。

例如:事务 1 读取某表中的数据 A=20,事务 1 修改 A=A-1,事务 2 读取到 A = 19,事务 1 回滚导致对 A 的修改并未提交到数据库, A 的值还是 20。

学新通

3.2.2 丢失修改

丢失修改(Lost to modify)

在一个事务读取一个数据时,另外一个事务也访问了该数据,那么在第一个事务中修改了这个数据后,第二个事务也修改了这个数据。这样第一个事务内的修改结果就被丢失,因此称为丢失修改。

例如:事务 1 读取某表中的数据 A=20,事务 2 也读取 A=20,事务 1 先修改 A=A-1,事务 2 后来也修改 A=A-1,最终结果 A=19,事务 1 的修改被丢失。

学新通

3.2.3 不可重复读

不可重复读(Unrepeatable read)

指在一个事务内多次读同一数据。在这个事务还没有结束时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样的情况,因此称为不可重复读。

例如:事务 1 读取某表中的数据 A=20,事务 2 也读取 A=20,事务 1 修改 A=A-1,事务 2 再次读取 A =19,此时读取的结果和第一次读取的结果不同。

学新通

3.2.4 幻读

幻读(Phantom read)

幻读与不可重复读类似。它发生在一个事务读取了几行数据,接着另一个并发事务插入了一些数据时。在随后的查询中,第一个事务就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。

例如:事务 2 读取某个范围的数据,事务 1 在这个范围插入了新的数据,事务 2 再次读取这个范围的数据发现相比于第一次读取的结果多了新的数据。

学新通

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

  • 不可重复读是针对已经存在的数据行,同一事务内多次读取数据结果不一致。
  • 幻读是针对查询结果集合,同一事务内多次查询同样的条件,结果集合不一致。

3.3 SQL的事务隔离级别

SQL 标准定义了四个隔离级别:

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

4.MySQL锁

锁是一种常见的并发事务的控制方式。MySQL中的锁,按照锁的粒度分为三种:

  • 全局锁:锁定数据库中的所有表
  • 表级锁:每次操作锁住整张表
  • 行级锁:每次操作锁住对应的行数据

4.1 全局锁

全局锁就是对整个数据库实例加锁,加锁后整个实例就处于只读状态,后续的DML的写语句,DDL语句,已经更新操作的事务提交语句都将被阻塞。

其典型的使用场景是做全库的逻辑备份,对所有的表进行锁定,从而获取一致性视图,保证数据的完整性。

为什么全库逻辑备份,就需要加全局锁呢?

A. 我们一起先来分析一下不加全局锁,可能存在的问题。

假设在数据库中存在这样三张表: tb_stock 库存表,tb_order 订单表,tb_orderlog 订单日志表。

学新通

此时备份出来的数据,是存在问题的。因为备份出来的数据,tb_stock表与tb_order表的数据不一致(有最新操作的订单信息,但是库存数没减)。

那如何来规避这种问题呢? 此时就可以借助于MySQL的全局锁来解决。

B. 再来分析一下加了全局锁后的情况

学新通

对数据库进行进行逻辑备份之前,先对整个数据库加上全局锁,一旦加了全局锁之后,其他的DDL、DML全部都处于阻塞状态,但是可以执行DQL语句,也就是处于只读状态,而数据备份就是查询操作。那么数据在进行逻辑备份的过程中,数据库中的数据就是不会发生变化的,这样就保证了数据的一致性和完整性

1. 语法

  1. 加全局锁:flush tables with read lock ;
  2. 数据备份:mysqldump -uroot –p123456 database > database.sql
  3. 释放锁:unlock tables ;

2. 特点

数据库中加全局锁,是一个比较重的操作,存在以下问题:

  • 如果在主库上备份,那么在备份期间都不能执行更新,业务基本上就得停摆。
  • 如果在从库上备份,那么在备份期间从库不能执行主库同步过来的二进制日志(binlog),会导致主从延迟。

在InnoDB引擎中,我们可以在备份时加上参数 --single-transaction 参数来完成不加锁的一致性数据备份。

mysqldump --single-transaction -uroot –p123456 database > database.sql

4.2 表级锁

表级锁: MySQL 中锁定粒度最大的一种锁(全局锁除外),是针对非索引字段加的锁,对当前操作的整张表加锁,实现简单,资源消耗也比较少,加锁快,不会出现死锁。不过,触发锁冲突的概率最高,高并发下效率极低。表级锁和存储引擎无关,MyISAM 和 InnoDB 引擎都支持表级锁。

4.3 行级锁

行级锁: MySQL 中锁定粒度最小的一种锁,是 针对索引字段加的锁 ,只针对当前操作的行记录进行加锁。 行级锁能大大减少数据库操作的冲突。其加锁粒度最小,并发度高,但加锁的开销也最大,加锁慢,会出现死锁。行级锁和存储引擎有关,是在存储引擎层面实现的。

行级锁的使用有什么注意事项? nnoDB 的行锁是针对索引字段加的锁,表级锁是针对非索引字段加的锁。当我们执行 UPDATEDELETE 语句时,如果 WHERE条件中字段没有命中唯一索引或者索引失效的话,就会导致扫描全表对表中的所有行记录进行加锁。这个在我们日常工作开发中经常会遇到,一定要多多注意!

4.4 InnoDB有哪几类锁?

InnoDB 行锁是通过对索引数据页上的记录加锁实现的,MySQL InnoDB 支持三种行锁定方式:

  • 记录锁(Record Lock) :锁定单个行记录的锁,防止其他事务对此行进行update和delete。在RC、RR隔离级别下都支持。
  • 间隙锁(Gap Lock) :锁定索引记录间隙(不含该记录),确保索引记录间隙不变,防止其他事务在这个间隙进行insert,产生幻读。在RR隔离级别下都支持。
  • 临键锁(Next-Key Lock) :Record Lock Gap Lock,锁定一个范围,包含记录本身,主要目的是为了解决幻读问题。记录锁只能锁住已经存在的记录,为了避免插入新记录,需要依赖间隙锁。

在 InnoDB 默认的隔离级别 REPEATABLE-READ 下,行锁默认使用的是 Next-Key Lock。但是,如果操作的索引是唯一索引或主键,InnoDB 会对 Next-Key Lock 进行优化,将其降级为 Record Lock,即仅锁住索引本身,而不是范围。

4.5 共享锁和排他锁

不论是表级锁还是行级锁,都存在共享锁(Share Lock,S 锁)和排他锁(Exclusive Lock,X 锁)这两类:

  • 共享锁(S 锁) :又称读锁,事务在读取记录的时候获取共享锁,允许多个事务同时获取(锁兼容)。
  • 排他锁(X 锁) :又称写锁/独占锁,事务在修改记录的时候获取排他锁,不允许多个事务同时获取。如果一个记录已经被加了排他锁,那其他事务不能再对这条事务加任何类型的锁(锁不兼容)。

排他锁与任何的锁都不兼容,共享锁仅和共享锁兼容。

  S 锁 X 锁
S 锁 不冲突 冲突
X 锁 冲突 冲突

由于 MVCC 的存在,对于一般的 SELECT 语句,InnoDB 不会加任何锁。不过, 你可以通过以下语句显式加共享锁或排他锁。

# 共享锁 可以在 MySQL 5.7 和 MySQL 8.0 中使用
SELECT ... LOCK IN SHARE MODE;
# 共享锁 可以在 MySQL 8.0 中使用
SELECT ... FOR SHARE;
# 排他锁
SELECT ... FOR UPDATE;

4.6 意向锁

如果需要用到表锁的话,如何判断表中的记录没有行锁呢,一行一行遍历肯定是不行,性能太差。我们需要用到一个叫做意向锁的东东来快速判断是否可以对某个表使用表锁。

意向锁是表级锁,共有两种:

  • 意向共享锁(Intention Shared Lock,IS 锁) :事务有意向对表中的某些记录加共享锁(S 锁),加共享锁前必须先取得该表的 IS 锁。
  • 意向排他锁(Intention Exclusive Lock,IX 锁) :事务有意向对表中的某些记录加排他锁(X 锁),加排他锁之前必须先取得该表的 IX 锁。

意向锁是由数据引擎自己维护的,用户无法手动操作意向锁,在为数据行加共享/排他锁之前,InooDB 会先获取该数据行所在在数据表的对应意向锁。

意向锁之间是互相兼容的。

  IS 锁 IX 锁
IS 锁 兼容 兼容
IX 锁 兼容 兼容

意向锁和共享锁和排它锁互斥(这里指的是表级别的共享锁和排他锁,意向锁不会与行级的共享锁和排他锁互斥)。

  IS 锁 IX 锁
S 锁 兼容 互斥
X 锁 互斥 互斥

5. InnoDB引擎

5.1 逻辑存储结构

InnoDB的逻辑存储结构如下:

学新通

1.表空间

表空间是InnoDB存储引擎逻辑结构的最高层, 如果用户启用了参数 innodb_file_per_table(在8.0版本中默认开启) ,则每张表都会有一个表空间(xxx.ibd),一个mysql实例可以对应多个表空间,用于存储记录、索引等数据。

2. 段

段,分为数据段(Leaf node segment)、索引段(Non-leaf node segment)、回滚段(Rollback segment),InnoDB是索引组织表,数据段就是B 树的叶子节点, 索引段即为B 树的非叶子节点。段用来管理多个Extent(区)。

3. 区

区,表空间的单元结构,每个区的大小为1M。 默认情况下, InnoDB存储引擎页大小为16K, 即一个区中一共有64个连续的页。

4. 页

页,是InnoDB 存储引擎磁盘管理的最小单元,每个页的大小默认为 16KB。为了保证页的连续性,InnoDB 存储引擎每次从磁盘申请 4-5 个区。

5. 行

行,InnoDB 存储引擎数据是按行进行存放的。 在行中,默认有两个隐藏字段:

  • Trx_id:每次对某条记录进行改动时,都会把对应的事务id赋值给trx_id隐藏列。
  • Roll_pointer:每次对某条引记录进行改动时,都会把旧的版本写入到undo日志中,然后这个隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息。

5.2 架构概述

MySQL5.5 版本开始,默认使用InnoDB存储引擎,它擅长事务处理,具有崩溃恢复特性,在日常开发中使用非常广泛。下面是InnoDB架构图,左侧为内存结构,右侧为磁盘结构。

学新通

5.2.1 内存结构

学新通

在左侧的内存结构中,主要分为这么四大块儿: Buffer PoolChange BufferAdaptiveHash IndexLog Buffer。 接下来介绍一下这四个部分。

1) Buffer Pool

缓冲池 Buffer Pool,是主内存中的一个区域,里面可以缓存磁盘上经常操作的真实数据,在执行增删改查操作时,先操作缓冲池中的数据(若缓冲池没有数据,则从磁盘加载并缓存),然后再以一定频率刷新到磁盘,从而减少磁盘IO,加快处理速度。

缓冲池以Page页为单位,底层采用链表数据结构管理Page。根据状态,将Page分为三种类型:

  • free page:空闲page,未被使用。
  • clean page:被使用page,数据没有被修改过。
  • dirty page:脏页,被使用page,数据被修改过,也中数据与磁盘的数据产生了不一致。

2) Change Buffer

Change Buffer,更改缓冲区(针对于非唯一二级索引页),在执行DML语句时,如果这些数据Page没有在Buffer Pool中,不会直接操作磁盘,而会将数据变更存在更改缓冲区 Change Buffer中,在未来数据被读取时,再将数据合并恢复到Buffer Pool中,再将合并后的数据刷新到磁盘中。

Change Buffer的意义是什么呢?

先来看一幅图,这个是二级索引的结构图:

学新通

与聚集索引不同,二级索引通常是非唯一的,并且以相对随机的顺序插入二级索引。同样,删除和更新可能会影响索引树中不相邻的二级索引页,如果每一次都操作磁盘,会造成大量的磁盘IO。有了ChangeBuffer之后,我们可以在缓冲池中进行合并处理,减少磁盘IO。

3) Adaptive Hash Index

自适应hash索引,用于优化对Buffer Pool数据的查询。MySQL的innoDB引擎中虽然没有直接支持hash索引,但是给我们提供了一个功能就是这个自适应hash索引。因为前面我们讲到过,hash索引在进行等值匹配时,一般性能是要高于B 树的,因为hash索引一般只需要一次IO即可,而B 树,可能需要几次匹配,所以hash索引的效率要高,但是hash索引又不适合做范围查询、模糊匹配等。

InnoDB存储引擎会监控对表上各索引页的查询,如果观察到在特定的条件下hash索引可以提升速度,则建立hash索引,称之为自适应hash索引。

自适应哈希索引,无需人工干预,是系统根据情况自动完成。

4) Log Buffer

Log Buffer:日志缓冲区,用来保存要写入到磁盘中的log日志数据(redo log 、undo log),默认大小为 16MB,日志缓冲区的日志会定期刷新到磁盘中。如果需要更新、插入或删除许多行的事务,增加日志缓冲区的大小可以节省磁盘 I/O。

参数:

innodb_log_buffer_size:缓冲区大小

innodb_flush_log_at_trx_commit:日志刷新到磁盘时机,取值主要包含以下三个:

  • 1: 日志在每次事务提交时写入并刷新到磁盘,默认值。
  • 0: 每秒将日志写入并刷新到磁盘一次。2: 日志在每次事务提交后写入,并每秒刷新到磁盘一次。

学新通

5.2.2 磁盘结构

接下来,再来看看InnoDB体系结构的右边部分,也就是磁盘结构:

学新通

1) System Tablespace

系统表空间是更改缓冲区的存储区域。如果表是在系统表空间而不是每个表文件或通用表空间中创建的,它也可能包含表和索引数据。(在MySQL5.x版本中还包含InnoDB数据字典、undolog等)

参数:innodb_data_file_path 学新通

系统表空间,默认的文件名叫 ibdata1

2) File-Per-Table Tablespaces

如果开启了innodb_file_per_table开关 ,则每个表的文件表空间包含单个InnoDB表的数据和索引 ,并存储在文件系统上的单个数据文件中。

开关参数:innodb_file_per_table ,该参数默认开启。

学新通

3) General Tablespaces

通用表空间,需要通过 CREATE TABLESPACE 语法创建通用表空间,在创建表时,可以指定该表空间。

A. 创建表空间

CREATE TABLESPACE ts_name ADD DATAFILE 'file_name' ENGINE = engine_name;

B. 创建表时指定表空间

CREATE TABLE xxx ... TABLESPACE ts_name;

4) Undo Tablespaces

撤销表空间,MySQL实例在初始化时会自动创建两个默认的undo表空间(初始大小16M),用于存储undo log日志。

5) Temporary Tablespaces

InnoDB 使用会话临时表空间和全局临时表空间。存储用户创建的临时表等数据。

6) Doublewrite Buffer Files

双写缓冲区,innoDB引擎将数据页从Buffer Pool刷新到磁盘前,先将数据页写入双写缓冲区文件中,便于系统异常时恢复数据。

Redo Log

Redo Log(重做日志)是 MySQL 数据库中的一种重要机制,用于保障数据库的持久性和数据一致性。它记录了数据的变更操作,以便在数据库崩溃或故障时,能够恢复未提交的事务,确保数据的完整性。

Redo Log 的主要作用如下:

  1. 持久性保证: Redo Log 保证了数据的持久性。当事务进行数据修改操作时,实际的数据修改会先记录在 Redo Log 中,然后再更新到实际数据文件中。这样即使数据库崩溃,未提交的数据变更也不会丢失。
  2. 故障恢复: 在数据库重启或崩溃恢复时,通过 Redo Log 中的记录,可以重放事务的数据修改,使数据库从崩溃时的状态恢复到最后一次提交的状态。
  3. 数据复制: 在数据库复制、主从复制等场景中,通过传输 Redo Log 中的变更操作,可以将主数据库的操作同步到从数据库上。

操作 Redo Log 主要是数据库自动进行的,一般情况下不需要手动操作。数据库会根据事务的提交情况,将数据修改操作记录到 Redo Log 中。具体操作步骤如下:

  1. 写入 Redo Log: 在事务进行数据修改操作时,MySQL 会将数据变更的信息(如插入、更新、删除操作)记录到 Redo Log 中。
  2. 刷写到磁盘: Redo Log 中的记录可能会先存储在内存中,然后定期或在事务提交时,会被刷写到磁盘上的 Redo Log 文件中。
  3. 崩溃恢复: 在数据库重启或崩溃后,MySQL 会根据 Redo Log 中的记录来重放未提交的事务操作,将数据库恢复到一致的状态。

Redo Log 恢复数据的基本流程:

  1. 崩溃检测: 当数据库崩溃或意外终止时,MySQL 在下一次启动时会进行崩溃检测,确定最后一次成功关闭数据库的时间点。
  2. 查找 Redo Log: MySQL 根据崩溃检测得到的信息,找到最后一次成功关闭数据库时生成的 Redo Log 文件。
  3. 恢复过程: MySQL 会从找到的 Redo Log 文件中读取未提交的事务数据变更操作,然后将这些操作重新应用到数据库中。这个过程称为“重做”操作。
  4. 事务一致性: 在进行重做操作时,MySQL 确保只会重新应用已经提交的事务的数据变更。这样可以保证数据的一致性。
  5. 更新数据文件: 重做操作将数据变更操作重新应用到数据库中,包括数据页的修改。这样,数据库就会重新恢复到崩溃之前的一致状态。

5.2.3 后台线程

学新通

在InnoDB的后台线程中,分为4类,分别是:Master ThreadIO ThreadPurge Thread

Page Cleaner Thread

1). Master Thread

核心后台线程,负责调度其他线程,还负责将缓冲池中的数据异步刷新到磁盘中, 保持数据的一致性,还包括脏页的刷新、合并插入缓存、undo页的回收 。

2). IO Thread

在InnoDB存储引擎中大量使用了AIO来处理IO请求, 这样可以极大地提高数据库的性能,而IO Thread主要负责这些IO请求的回调。

线程类型 默认个数 职责
Read Thread 4 负责读操作
Write Thread 4 负责写操作
Log thread 1 负责将日志缓冲区刷新到磁盘
Insert buffer thread 1 负责将写缓冲区内容刷新到磁盘

我们可以通过以下的这条指令,查看到InnoDB的状态信息,其中就包含IO Thread信息。

show engine innodb status \G;

学新通

3). Purge Thread

主要用于回收事务已经提交了的undo log,在事务提交之后,undo log可能不用了,就用它来回收。

4). Page Cleaner Thread

协助 Master Thread 刷新脏页到磁盘的线程,它可以减轻 Master Thread 的工作压力,减少阻塞。

5.3 如何保证事务

事务的特性,在前文就有提及,实际上这一节我们想研究MySQL中InnoDB引擎是如何保证事务的四大特性。

学新通

而对于这四大特性,实际上分为两个部分。 其中的原子性、一致性、持久化,实际上是由InnoDB中的两份日志来保证的,一份是redo log日志,一份是undo log日志。 而持久性是通过数据库的锁,加上MVCC来保证的。

5.3.1 redo log

重做日志,记录的是事务提交时数据页的物理修改,是用来实现事务的持久性。

该日志文件由两部分组成:重做日志缓冲(redo log buffer)以及重做日志文件(redo log file),前者是在内存中,后者在磁盘中。当事务提交之后会把所有修改信息都存到该日志文件中, 用于在刷新脏页到磁盘,发生错误时, 进行数据恢复使用。

如果没有redolog,可能会存在什么问题的? 我们一起来分析一下。

我们知道,在InnoDB引擎中的内存结构中,主要的内存区域就是缓冲池,在缓冲池中缓存了很多的数据页。 当我们在一个事务中,执行多个增删改的操作时,InnoDB引擎会先操作缓冲池中的数据,如果缓冲区没有对应的数据,会通过后台线程将磁盘中的数据加载出来,存放在缓冲区中,然后将缓冲池中的数据修改,修改后的数据页我们称为脏页

而脏页则会在一定的时机,通过后台线程刷新到磁盘中,从而保证缓冲区与磁盘的数据一致。 而缓冲区的脏页数据并不是实时刷新的,而是一段时间之后将缓冲区的数据刷新到磁盘中,假如刷新到磁盘的过程出错了,而提示给用户事务提交成功,而数据却没有持久化下来,这就出现问题了,没有保证事务的持久性。

学新通

那么,如何解决上述的问题呢? 在InnoDB中提供了一份日志 redo log,接下来我们再来分析一下,通过redolog如何解决这个问题。

学新通

有了redo log之后,当对缓冲区的数据进行增删改之后,会首先将操作的数据页的变化,记录在redo log buffer中。在事务提交时,会将redo log buffer中的数据刷新到redo log磁盘文件中。

过一段时间之后,如果刷新缓冲区的脏页到磁盘时,发生错误,此时就可以借助于redo log进行数据恢复,这样就保证了事务的持久性。 而如果脏页成功刷新到磁盘 或 或者涉及到的数据已经落盘,此时redo log就没有作用了,就可以删除了,所以存在的两个redo log文件是循环写的。

那为什么每一次提交事务,要刷新redo log 到磁盘中呢,而不是直接将buffer pool中的脏页刷新到磁盘呢 ?

因为在业务操作中,我们操作数据一般都是随机读写磁盘的,而不是顺序读写磁盘。 而redo log在往磁盘文件中写入数据,由于是日志文件,所以都是顺序写的。顺序写的效率,要远大于随机写。 这种先写日志的方式,称之为 WAL(Write-Ahead Logging)

5.3.2 undo log

回滚日志,用于记录数据被修改前的信息 , 作用包含两个 : 提供回滚(保证事务的原子性) 和 MVCC(多版本并发控制) 。

undo log和redo log记录物理日志不一样,它是逻辑日志。可以认为当delete一条记录时,undo log中会记录一条对应的insert记录,反之亦然,当update一条记录时,它记录一条对应相反的update记录。当执行rollback时,就可以从undo log中的逻辑记录读取到相应的内容并进行回滚。

Undo log销毁:undo log在事务执行时产生,事务提交时,并不会立即删除undo log,因为这些日志可能还用于MVCC。

Undo log存储:undo log采用段的方式进行管理和记录,存放在前面介绍的 rollback segment回滚段中,内部包含1024个undo log segment。

6. MVCC

在了解MVCC(Multi-Version Concurrency Control,多版本并发控制)之前,我们还要了解两个概念:快照读当前读

快照读(一致性非锁定读)就是单纯的 SELECT 语句,但不包括下面这两类 SELECT 语句:

SELECT ... FOR UPDATE
# 共享锁 可以在 MySQL 5.7 和 MySQL 8.0 中使用
SELECT ... LOCK IN SHARE MODE;
# 共享锁 可以在 MySQL 8.0 中使用
SELECT ... FOR SHARE;

快照即记录的历史版本,每行记录可能存在多个历史版本(多版本技术)。

快照读的情况下,如果读取的记录正在执行 UPDATE/DELETE 操作,读取操作不会因此去等待记录上 X 锁的释放,而是会去读取行的一个快照。

只有在事务隔离级别 RC(读取已提交) 和 RR(可重读)下,InnoDB 才会使用一致性非锁定读:

  • RC级别下,对于快照数据,一致性非锁定读总是读取被锁定行的最新一份快照数据
  • RR 级别下,对于快照数据,一致性非锁定读总是读取本事务开始时的行数据版本

快照读比较适合对于数据一致性要求不是特别高且追求极致性能的业务场景。

当前读 (一致性锁定读)就是给行记录加 X 锁或 S 锁。

当前读的一些常见 SQL 语句类型如下:

# 对读的记录加一个X锁
SELECT...FOR UPDATE
# 对读的记录加一个S锁
SELECT...LOCK IN SHARE MODE
# 对读的记录加一个S锁
SELECT...FOR SHARE
# 对修改的记录加一个X锁
INSERT...
UPDATE...
DELETE...

总结:

  • 当前读获取的是最新已提交的数据,而快照读获取的是事务开始时的数据快照。
  • 当前读使用排他锁,可能会阻塞其他事务的写操作;快照读使用共享锁或快照版本,不会阻塞写操作。
  • 当前读适用于事务隔离级别为“读已提交”和“串行化”;快照读适用于“可重复读”和“读未提交”。

MVCC

全称 Multi-Version Concurrency Control,多版本并发控制。指维护一个数据的多个版本,使得读写操作没有冲突,快照读为MySQL实现MVCC提供了一个非阻塞读功能。MVCC的具体实现,还需要依赖于数据库记录中的三个隐式字段、undo log日志、readView

接下来,我们再来介绍一下InnoDB引擎的表中涉及到的隐藏字段 、undolog 以及 readview,从而来介绍一下MVCC的原理。

6.1 隐藏字段

学新通

当我们创建了上面的这张表,我们在查看表结构的时候,就可以显式的看到这三个字段。 实际上除了这三个字段以外,InnoDB还会自动的给我们添加三个隐藏字段及其含义分别是:

  1. DB_TRX_ID(事务ID):

    • DB_TRX_ID 存储了最后一次修改行数据的事务的事务ID。
    • 用于判断一个事务是否可以读取一行数据。如果一个事务的事务ID早于行数据的 DB_TRX_ID,说明该事务修改了行数据,可能会导致不可重复读或幻读问题。
  2. DB_ROLL_PTR(回滚指针):

    • DB_ROLL_PTR 是一个指向回滚段中的回滚记录的指针。
    • 用于回滚行数据到之前的版本,以支持事务回滚操作。如果事务需要回滚,可以根据 DB_ROLL_PTR 找到相应的回滚记录,从而撤销该事务的修改。
  3. DB_ROW_ID(行ID):

    • DB_ROW_ID 是一个表示行的唯一标识的值。如果表没有主键,InnoDB 引擎会自动为每一行生成一个唯一的 DB_ROW_ID 值。
    • 用于区分不同的行,避免同一事务中的多个操作影响同一行的数据版本。

6.2 undo log

回滚日志,在insert、update、delete的时候产生的便于数据回滚的日志。当insert的时候,产生的undo log日志只在回滚时需要,在事务提交后,可被立即删除。而update、delete的时候,产生的undo log日志不仅在回滚时需要,在快照读时也需要,不会立即被删除。

版本链

有一张表原始数据为:

学新通

  • DB_TRX_ID : 代表最近修改事务ID,记录插入这条记录或最后一次修改该记录的事务ID,是自增的。
  • DB_ROLL_PTR : 由于这条数据是才插入的,没有被更新过,所以该字段值为null。

然后,有四个并发事务同时在访问这张表。

A. 第一步

学新通

事务2执行第一条修改语句时,会记录undo log日志,记录数据变更之前的样子; 然后更新记录,并且记录本次操作的事务ID,回滚指针,回滚指针用来指定如果发生回滚,回滚到哪一个版本。

学新通

B.第二步

学新通

当事务3执行第一条修改语句时,也会记录undo log日志,记录数据变更之前的样子; 然后更新记录,并且记录本次操作的事务ID,回滚指针,回滚指针用来指定如果发生回滚,回滚到哪一个版本。

学新通

C.第三步

学新通

当事务4执行第一条修改语句时,也会记录undo log日志,记录数据变更之前的样子; 然后更新记录,并且记录本次操作的事务ID,回滚指针,回滚指针用来指定如果发生回滚,回滚到哪一个版本。

学新通

最终我们发现,不同事务或相同事务对同一条记录进行修改,会导致该记录的undolog生成一条记录版本链表,链表的头部是最新的旧记录,链表尾部是最早的旧记录。

6.3 readview

ReadView(读视图)是 快照读 SQL执行时MVCC提取数据的依据,记录并维护系统当前活跃的事务(未提交的)id。

ReadView中包含了四个核心字段:

字段 含义
m_ids 当前活跃的事务ID集合
min_trx_id 最小活跃事务ID
max_trx_id 预分配事务ID,当前最大事务ID 1(因为事务ID是自增的)
creator_trx_id ReadView创建者的事务ID

而在readview中就规定了版本链数据的访问规则:

trx_id 代表当前undolog版本链对应事务ID。

条件 是否可以访问 说明
trx_id == creator_trx_id 可以访问该版本 成立,说明数据是当前这个事务更改的。
trx_id < min_trx_id 可以访问该版本 成立,说明数据已经提交了
trx_id > max_trx_id 不可以访问该版本 成立,说明该事务是在ReadView生成后才开启。
min_trx_id <= trx_id<= max_trx_id 如果trx_id不在m_ids中,是可以访问该版本的 成立,说明数据已经提交。

不同的隔离级别,生成ReadView的时机不同:

  • READ COMMITTED(RC) :在事务中每一次执行快照读时生成ReadView。
  • REPEATABLE READ(RR):仅在事务中第一次执行快照读时生成ReadView,后续复用该ReadView。

6.4 原理分析

1. RC隔离级别

RC隔离级别下,在事务中每一次执行快照读时生成ReadView。

我们就来分析事务5中,两次快照读读取数据,是如何获取数据的?

在事务5中,查询了两次id为30的记录,由于隔离级别为Read Committed,所以每一次进行快照读都会生成一个ReadView,那么两次生成的ReadView如下。

学新通

那么这两次快照读在获取数据时,就需要根据所生成的ReadView以及ReadView的版本链访问规则,到undolog版本链中匹配数据,最终决定此次快照读返回的数据。

A. 先来看第一次快照读具体的读取过程:

学新通

在进行匹配时,会从undo log的版本链,从上到下进行挨个匹配:

  1. 先匹配

学新通

这条记录,这条记录对应的trx_id为4,也就是将4带入右侧的匹配规则中。 ①不满足 ②不满足 ③不满足 ④也不满足 ,都不满足,则继续匹配undo log版本链的下一条。

  1. 再匹配第二条

学新通

这条记录对应的trx_id为3,也就是将3带入右侧的匹配规则中。①不满足 ②不满足 ③不满足 ④也不满足 ,都不满足,则继续匹配undo log版本链的下一条。

  1. 再匹配第三条

学新通

这条记录对应的trx_id为2,也就是将2带入右侧的匹配规则中。①不满足 ②满足 终止匹配,此次快照读,返回的数据就是版本链中记录的这条数据。

B. 再来看第二次快照读具体的读取过程:

学新通

在进行匹配时,会从undo log的版本链,从上到下进行挨个匹配:

  1. 先匹配

学新通

这条记录,这条记录对应的trx_id为4,也就是将4带入右侧的匹配规则中。 ①不满足 ②不满足 ③不满足 ④也不满足 ,都不满足,则继续匹配undo log版本链的下一条。

  1. 再匹配第二条

学新通

这条记录对应的trx_id为3,也就是将3带入右侧的匹配规则中。①不满足 ②满足 。终止匹配,此次快照读,返回的数据就是版本链中记录的这条数据。

2. RR隔离级别

RR隔离级别下,仅在事务中第一次执行快照读时生成ReadView,后续复用该ReadView。 而RR 是可重复读,在一个事务中,执行两次相同的select语句,查询到的结果是一样的。

那MySQL是如何做到可重复读的呢? 我们简单分析一下就知道了

学新通

我们看到,在RR隔离级别下,只是在事务中第一次快照读时生成ReadView,后续都是复用该ReadView,那么既然ReadView都一样, ReadView的版本链匹配规则也一样, 那么最终快照读返回的结果也是一样的。

所以呢,MVCC的实现原理就是通过 InnoDB表的隐藏字段、UndoLog 版本链、ReadView来实现的。而MVCC 锁,则实现了事务的隔离性。 而一致性则是由redolog 与 undolog保证。

学新通

参考

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhgbafkc
系列文章
更多 icon
同类精品
更多 icon
继续加载