数据库
一、基础知识
1.DB系统基本组成
- 数据库 : 数据库(DataBase,简称 DB)就是信息的集合或者说数据库是由数据库管理系统管理的数据的集合。
- 数据库管理系统 : 数据库管理系统(Database Management System,简称 DBMS)是一种操纵和管理数据库的大型软件,通常用于建立、使用和维护数据库。
- 数据库系统 : 数据库系统(Data Base System,简称 DBS)通常由软件、数据库和数据管理员(DBA)组成。
- 数据库管理员 : 数据库管理员(Database Administrator,简称 DBA)负责全面管理和控制数据库系统。
2.数据库模型分类
(1)关系型数据库
如Oracle、MySQL等。
如Oracle、MySQL等。
(2)非关系型数据库
NoSQL(Not Only SQL)。
NoSQL(Not Only SQL)。
3.关系模型基本组成
- 元组 :元组(tuple)是关系型数据库中的基本概念,关系是一张表,表中的每行(即数据库中的每条记录)就是一个元组,每列就是一个属性。
- 码 :码就是能唯一标识实体的属性,对应表中的列。
-
候选码 :若关系中的某一属性或属性组的值能唯一的标识一个元组,而其任何子集都不能再标识,则称该属性组为候选码。
例如:在学生实体中,“学号”是能唯一的区分学生实体的,同时又假设“姓名”、“班级”的属性组合足以区分学生实体,那么{学号}和{姓名,班级}都是候选码。 - 主键 : 主键也叫主码。主码是从候选码中选出来的。一个实体集中只能有一个主键,但可以有多个候选码。
- 外键 : 外键也叫外码。如果一个关系中的一个属性是另外一个关系中的主键则这个属性为外键。外键即“别人(别的关系)”的主键。
- 主属性:候选码中出现过的属性称为主属性。比如工人实体(工号,身份证号,姓名,性别,部门),显然工号和身份证号都能够唯一标识这个关系,所以都是候选码。工号、身份证号这两个属性就是主属性。如果主码是一个属性组,那么属性组中的属性都是主属性。
- 非主属性:不包含在任何一个候选码中的属性称为非主属性。比如在关系——学生(学号,姓名,年龄,性别,班级)中,主码是“学号”,那么其他的“姓名”、“年龄”、“性别”、“班级”就都可以称为非主属性。
3.主键和外键有什么区别?
- 主键:主键用于唯一标识一个元组,不允许重复,不允许为空,一个表只能有一个主键。
- 外键(外码) :外键用来和其他表建立联系,外键是另一表的主键,外键可以重复,可以为空,一个表可以有多个外键。
4.★外键与级联
(1)什么是级联?
假设表A有主键id,表B的id是外键(即表A的主键),也就是说这是有关联的两个表。
级联是指两个有关联的表A和表B,当对表A的主键id进行操作(更新/删除)时,表B的外键id也要跟着做相应操作。
因此,有级联更新和级联删除两种。
【举例】以学生表(表A)和成绩表(表B)的关系为例,学生表中的 student_id 是主键,那么成绩表中的 student_id 则为外键。如果更新学生表中的 student_id,同时触发成绩表中的 student_id 更新,即为级联更新。
(2)为什么不建议使用外键与级联?
在分布式、高并发集群开发中,很多公司都强制要求不能使用外键和级联。这要从外键的缺点来讲:
-
性能问题
每次对数据进行DELETE或UPDATE操作时,都必须考虑外键约束,数据库都会判断当前操作是否违反数据完整性,性能下降。
比如假设有一张表名user_tb,这张表里有两个外键字段,指向另外两张表。那么每次更新user_tb表里的数据时,就必须在两个外键对应的表里查询是否有对应数据。如果交由程序控制,这种查询过程就可以控制在我们手里,可以省略一些不必要的查询过程。但是如果由数据库控制,则是必须要去这两张表里判断。 -
并发下的死锁问题
在使用外键的情况下,每次修改数据都需要去另外一个表检查数据,需要获取额外的锁,因此如果是在高并发大流量事务场景,使用外键更容易造成死锁。 -
扩展性问题
像触发器、外键这种依赖于数据库本身的特性,可扩展性差。同时在分库分表时,外键也不会生效。
5.函数依赖
- 函数依赖:在一张表中,如果在属性(或属性组)X 的值确定的情况下,必定能确定属性 Y 的值,那么就可以说 X 确定 Y,Y 函数依赖于 X,写作 X → Y。
- 部分函数依赖:比如学生信息表 R 中(学号,身份证号,姓名),(学号,身份证号)→ (姓名),但是(学号)→ (姓名)、(身份证号)→ (姓名),所以说姓名部分函数依赖于(学号,身份证号);
-
完全函数依赖:在一个关系中,如果某个非主属性依赖于全部关键字,则称之为完全函数依赖。
比如学生基本信息表 R(学号,班级,姓名),假设不同的班级学号有相同的,班级内学号不能相同,所以学号+班级才能确定姓名,即(学号,班级)→ (姓名),但是(学号)→ (姓名)不成立,(班级)→ (姓名)也不成立,所以姓名完全函数依赖于(学号,班级); - 传递函数依赖 : 如果X→ Y,Y→ Z,则称 Z 传递函数依赖于 X。传递函数依赖会导致数据冗余和异常。
6.数据库范式
(1)1NF(第一范式)
1NF是指表中的字段属性不能再被分割。
1NF 是所有关系型数据库的最基本要求,关系型数据库中创建的表一定要满足第一范式。
(2)2NF(第二范式)
2NF 在 1NF 的基础之上(2NF也遵守1NF),消除了非主属性对码的部分函数依赖。
(3)3NF(第三范式)
3NF 在 2NF 的基础之上,消除了非主属性对码的传递函数依赖 。
符合 3NF 要求的数据库设计,基本上解决了数据冗余过大,插入异常,修改异常,删除异常的问题。
比如在关系 R(学号 , 姓名, 系名,系主任)中,学号 → 系名,系名 → 系主任,所以存在非主属性系主任对于学号的传递函数依赖,所以该表的设计,不符合 3NF 的要求。
7.存储过程
(1)什么是存储过程?
存储过程是存储在数据库中的一组 SQL 语句的集合,在创建存储过程时给出存储过程的名字,后续用户可以通过指定存储过程的名字来执行这一组 SQL 语句。
比如很多时候我们完成一个操作可能需要写一大串 SQL 语句,这时候我们就可以写有一个存储过程,这样也方便了我们下一次的调用。
(2)为什么不建议使用存储过程?
存储过程在互联网公司应用不多,因为存储过程难以调试和扩展,而且没有移植性,还会消耗数据库资源。
8.drop、delete 与 truncate 区别?
(1)功能不同
- drop(删除表):drop table 表名,直接删除表。
- truncate (清空数据):truncate table 表名,删除表中的所有数据,不会删除表。
- delete(删除数据):delete from 表名 where 列名=值,删除某一行的数据,如果不加 where 子句的话表示删除表中所有数据,和 truncate 的作用类似。
(2)属于不同的数据库语言
- truncate 和 drop 属于 DDL(数据定义语言),操作立即生效,原数据不放到 rollback segment 中,不能回滚,操作不触发 trigger。
- delete 语句是 DML (数据库操作语言),这个操作会放到 rollback segement 中,事务提交之后才生效。
(3)执行速度不同
一般来说:drop > truncate > delete
- delete命令执行的时候会产生数据库的binlog日志,而日志记录是需要消耗时间的,但是也有个好处方便数据回滚恢复。
- truncate命令执行的时候不会产生数据库日志,因此比delete要快。除此之外,还会把表的自增值重置和索引恢复到初始大小等。
- drop命令直接把表占用的空间全部释放掉。
二、MySQL的组成
1.MySQL的组成
从上图可以看出, MySQL 主要由下面几部分构成:
- 连接器: 身份认证和权限相关(登录 MySQL 的时候)。
- 查询缓存: 执行查询语句的时候,会先查询缓存(MySQL 8.0 版本后移除)。
- 分析器: 没有命中缓存的话,SQL 语句就会经过分析器,分析器说白了就是要先看你的 SQL 语句要干嘛,再检查你的 SQL 语句语法是否正确。
- 优化器: 按照 MySQL 认为最优的方案去执行。
- 执行器: 执行语句,然后从存储引擎返回数据。 执行语句之前会先判断是否有权限,如果没有权限的话,就会报错。
- 插件式存储引擎 : 主要负责数据的存储和读取,采用的是插件式架构,支持 InnoDB、MyISAM、Memory 等多种存储引擎。
2.MySQL的执行流程?——深入理解MySQL内部结构
MySQL 执行一条 select 查询语句期间发生了什么?
连接器→查询缓存→解析sql→执行sql
(1)连接器
如果在 Linux 操作系统里使用 MySQL,第一步肯定要先连接 MySQL 服务。连接的过程需要先经过 TCP 三次握手,因为 MySQL 是基于 TCP 协议进行传输的。如果 MySQL 服务正常运行,完成 TCP 连接的建立后,连接器就要开始验证用户名和密码。如果用户密码都没有问题,连接器就会获取该用户的权限,然后保存起来,后续该用户在此连接里的任何操作,都会基于连接开始时读到的权限进行权限逻辑的判断。所以,如果一个用户已经建立了连接,即使管理员中途修改了该用户的权限,也不会影响已经存在连接的权限。修改完成后,只有再新建的连接才会使用新的权限设置。
-
如何查看 MySQL 服务被多少个客户端连接了?
可以执行 show processlist 命令进行查看。
比如上图的显示结果,共有两个用户名为 root 的用户连接了 MySQL 服务,其中 id 为 6 的用户的 Command 列的状态为 Sleep ,这意味着该用户连接完 MySQL 服务就没有再执行过任何命令,也就是说这是一个空闲连接,并且空闲的时长是 736 秒( Time 列)。 -
空闲连接会一直占用着吗?
不会。MySQL 定义了空闲连接的最大空闲时长,由 wait_timeout 参数控制的,默认值是 8 小时(28880秒),如果空闲连接超过了这个时间,连接器就会自动将它断开。
当然,我们自己也可以手动断开空闲连接:kill connection + id:
-
MySQL 的连接数有限制吗?
MySQL 服务支持的最大连接数由 max_connections 参数控制,比如下面的 MySQL 服务默认最大连接数是 151 个,超过这个值,系统就会拒绝接下来的连接请求,并报错提示“Too many connections”。
-
怎么解决长连接占用内存的问题?
MySQL的连接有短连接和长连接两种,短连接一次连接只能执行一条sql,长连接可以执行多条sql(如下图)。但是使用长连接后可能会导致MySQL服务占用内存增多,因为 MySQL 在执行查询过程中临时使用内存管理连接对象,并且这些连接对象资源只有在连接断开时才会释放。如果长连接累计很多,将导致 MySQL 服务占用内存太大,有可能会被系统强制杀掉,这样会导致 MySQL 服务的异常重启。
有两种方法解决长连接占用内存的问题(本质是释放内存资源)。-
定期断开长连接。既然断开连接后就会释放连接占用的内存资源,那么我们可以定期断开长连接。
-
客户端主动重置连接。MySQL 5.7 以后的版本可以执行 mysql_reset_connection()来重置连接,达到释放内存资源的效果。这个过程不需要重连和重新做权限验证,但是会将连接恢复到刚刚创建完时的状态。
-
(2)查询缓存
成功建立连接后,客户端就可以向 MySQL 服务发送 sql语句了。如果 sql 是查询语句,MySQL 就会先去查询缓存中看看是否有结果:
- 如果缓存中有,就不必执行查询语句,直接取出缓存结果返回给客户端;
- 如果缓存未命中,就执行查询语句,将执行后的结果返回给客户端,并放入查询缓存中。
(3)解析sql
在正式执行 sql 查询语句之前, 解析器会先对 sql 语句进行解析:
-
词法分析。根据输入的字符串构建出 SQL 语法树,方便后面模块获取 SQL 类型、表名、字段名、 where 条件等信息;
-
语法分析。根据词法分析的结果,语法解析器会根据语法规则,判断 SQL 语句是否符合 MySQL 语法。
(4)执行sql
经过解析后接下来就要执行sql语句了。首先判断要查询的表或者字段是否存在,然后由优化器确定 sql语句 的执行方案,比如在表里面有多个索引的时候,优化器就会决定使用哪个索引;或者在⼀个语句有多表关联(join)的时候,决定各个表的连接顺序。在执行sql前还要判断当前用户是否有执行权限:
- 如果没有执行权限,会直接终止;
- 如果有执行权限,执行器就会根据表的引擎定义,从存储引擎读取记录,返回给客户端。
(5)★总结MySQL查询语句的执行流程
- 首先要与MySQL建立连接,由连接器验证用户名和密码;
- 成功建立连接后,客户端向 MySQL 服务发送查询语句;
-
MySQL 会先去查询缓存中是否有结果:
- 如果缓存中有,就不必执行查询语句,直接取出缓存结果返回给客户端;
- 如果缓存未命中,就继续往下执行。
- 然后会经过解析器,对查询语句进行词法分析构建出语法树并检查语法是否符合要求;
-
判断要查询的表或字段是否存在,然后由优化器确定sql的执行方案,最后判断当前用户是否有执行权限:
- 如果没有执行权限,会直接终止;
- 如果有执行权限,执行器就会根据表的引擎定义,从存储引擎读取记录,返回给客户端。
- 至此,一条查询语句的执行结束。
(6)★MySQL更新语句的执行流程?
具体更新一条记录 UPDATE t_user SET name = 'xiaolin' WHERE id = 1; 的流程如下:
- 首先要与MySQL建立连接,由连接器验证用户名和密码;
- 建立连接后,客户端向 MySQL 服务发送更新语句;
-
MySQL 会先去查询缓存中是否有要更新的这条记录:
- 如果缓存未命中,就继续往下执行。
- 然后会经过分析器,对语句进行词法分析并检查语法是否符合要求;
- 然后由优化器确定sql的执行方案;
- 执行器调用存储引擎的接口,先判断要更新的数据在不在缓冲池中,如果在就直接返回给执行器更新;如果记录不在就将数据页从磁盘读到缓冲池,并记录相应的undo log(更新前记录,因为记录的是更新前的数据)。
- 然后开启事务,InnoDB 会先更新内存(同时标记为脏页),然后将记录写到 redo log 里面,这个时候更新就算完成了。
- 更新语句执行完以后,开始记录该语句对应的 binlog(此时未刷盘,在事务提交时才会刷盘);
-
准备提交事务(两阶段提交):
- 将事务状态改为 prepare,然后将 redo log 刷新到硬盘;
- 然后将 binlog 刷新到磁盘,接着调用引擎的提交事务接口,将 redo log 状态设置为 commit;
- 至此,一条更新语句就执行完成。
3.MySQL的存储引擎
MySQL 的数据保存在磁盘中。MySQL 的存储是由存储引擎实现的,它支持多种存储引擎,不同的存储引擎保存的文件也不同。
MySQL默认的存储引擎是 InnoDB。所有存储引擎中只有 InnoDB 支持事务。
(1)MySQL数据库的数据保存在哪里?
我们每创建一个数据库都会在 /var/lib/mysql/ 目录里面创建一个以该数据库名命名的目录,保存表结构和表数据的文件都会存放在这个目录里。
比如创建了一个名为 my_test 的数据库,该数据库里有一张名为 t_order 表。在/var/lib/mysql/my_test目录下有三个文件:
- db.opt:用来存储当前数据库的默认字符集和字符校验规则;
- t_order.frm:保存 t_order 的表结构;
- t_order.ibd:保存 t_order 的表数据。这个文件也称为独占表空间文件。
(2).ibd表空间文件的结构
表空间由段(segment)、区(extent)、页(page)、行(row)组成,InnoDB存储引擎的逻辑存储结构是表空间划分为若干段,段分为若干区,区分为若干页,页分为若干行,而数据库表中的记录就是按行存放的。虽然记录是按行存储的,但是InnoDB 的数据是按「页」为单位来读写的。 InnoDB 存储引擎是用 B+ 树来组织数据的。
(3)InnoDB 的行的格式——Compact行格式
上面我们提到,每一条记录就是一行。InnoDB 提供了 4 种行格式,分别是 Redundant、Compact、Dynamic和 Compressed 行格式。
- Redundant:很古老,现在基本没人用了;
-
Compact:是一种紧凑的行格式,从 MySQL 5.1 版本之后,行格式默认设置成 Compact;
compact adj.紧凑的、紧密的。 - Dynamic 和 Compressed:都是紧凑的行格式,和 Compact 差不多,都是基于 Compact 改进一点东西。MySQL5.7 版本之后默认使用 Dynamic 行格式。
一条完整的 Compact 行记录分为「记录的额外信息」和「记录的真实数据」两个部分。
-
额外信息:记录的额外信息包含 3 个部分:变长字段长度列表、NULL 值列表、记录头信息。
- 变长字段长度列表:我们知道 char 是定长的,varchar 是变长的,变长字段存储的数据长度是不固定的。所以在存储数据的时候,要把数据大小存到变长字段长度列表里,这样读取数据时才能根据这个「变长字段长度列表」去读取对应长度的数据。当数据表没有变长字段的时候,比如全部都是 int 类型的字段,这时候表里的行格式就不会有「变长字段长度列表」了。TEXT、BLOB 等其他变长字段也是这么实现的。
- NULL值列表:Compact 行格式把值为 NULL 的数据存储到 NULL值列表中。当字段都定义成 NOT NULL 的时候,这时候表里的行格式就不会有 NULL 值列表了。
- 记录头信息:记录头信息中包含的内容很多,比如当前记录的类型、下一条记录的位置等。
-
真实数据:记录的真实数据除了我们定义的字段,还有三个隐藏字段:row_id、trx_id、roll_pointer。
- row_id:如果建表的时候指定了主键,那么就没有 row_id 隐藏字段了。如果既没有指定主键,又没有唯一约束,那么 InnoDB 就会为记录添加 row_id 隐藏字段。row_id不是必需的,占用 6 个字节。
- trx_id:事务id,表示这个数据是由哪个事务生成的。 trx_id是必需的,占用 6 个字节。
- roll_pointer:这条记录上一个版本的指针。roll_pointer 是必需的,占用 7 个字节。
(4)★varchar(n) 中 n 最大取值为多少?
首先要明确,varchar(n)中的 n 代表的是最多存储的字符数量,并不是字节大小。因此要算 varchar(n) 能允许存储的最大字节数,还要看数据库表的字符集,因为不同字符集1个字符占用的字节也不同,比如 ascii 字符集 1 个字符占用 1 字节,那么 varchar(100) 意味着最大能允许存储 100 字节的数据。
MySQL 规定除了 TEXT、BLOBs 这种大对象类型之外,一行的总长度不能超过 65535 个字节。
-
单字段
一行记录最大只能存储 65535 字节的数据。那假设数据库表只有一个 varchar(n) 类型的列且字符集是 ascii,在这种情况下, varchar(n) 中 n 最大取值是 65535 吗?
答案是不是的。我们说的一行数据的最大字节数 65535,其实是包含了变长字段长度列表和 NULL 值列表所占用的字节数。所以我们在算 varchar(n) 中 n 最大值时,需要减去变长字段长度列表和 NULL 值列表占用的字节数。 -
多字段
如果有多个字段的话,要保证所有字段的长度 + 变长字段字节数列表所占用的字节数 + NULL值列表所占用的字节数 <= 65535。
一行记录最大能存储 65535 字节的数据,但是一行记录中除了真实数据外,还包含「变长字段长度列表」和「NULL值列表」。所以, 我们在算 varchar(n) 中 n 最大值时,需要减去这两个列表所占用的字节数。
如果一张表只有一个 varchar(n) 字段,且允许为 NULL、字符集为 ascii的话,varchar(n) 中 n 最大取值为 65532(65535 - 变长字段字节数列表占用的2字节 - NULL值列表占用的1字节 = 65532)。
如果有多个字段的话,要保证所有字段的长度 + 变长字段字节数列表所占用的字节数 + NULL值列表所占用的字节数 <= 65535。
(5)行溢出后,MySQL 是怎么处理的?
在一般情况下,InnoDB 的数据都是存放在 「数据页」中。但是当发生行溢出时,溢出的数据会存放到「溢出页」中。这种现象也被称为页分裂。
-
Compact 行格式发生行溢出时,在记录真实数据只会保存的一部分数据,而把剩余的数据放在「溢出页」中,然后真实数据处用 20 字节存储指向溢出页的地址,从而可以找到剩余数据所在的页。
-
Compressed 和 Dynamic 采用完全的行溢出方式,记录真实数据不会存储该列的一部分数据,只用 20 字节存储指向溢出页的地址,实际的数据都存储在溢出页中。
(6)MySQL 的 NULL 值是怎么存放的?
MySQL 的 Compact 行格式中会用「NULL值列表」来存放 NULL 值,NULL 值并不会存储在行格式中的真实数据部分。NULL值列表会占用 1 个字节,但是当表中所有字段都定义成 NOT NULL时,行格式中就不会有 NULL值列表,这样可节省 1 字节的空间。
(7)存储引擎和server的关系?
MySQL的体系主要分为server层和存储引擎层。Server层主要包括连接器、查询缓存、分析器、优化器、执行器,主要负责与客户端建立连接、分析sql语句、确定执行方案、操作引擎返回结果等功能。存储引擎层主要负责数据的存储和提取。server层的执行器通过调用存储引擎的接口,可以对数据进行读写。
三、MySQL索引
索引是一种用于快速查询和检索数据的数据结构,其本质可以看成是一种排序好的数据结构。可以理解为数据库索引就是数据的目录,索引是以空间换时间的设计思想。
1.索引的分类
我们可以按照四个角度来分类索引:
- 按「数据结构」分类(底层实现):B+树索引、Hash索引、Full-text索引。
-
按「物理存储」分类:聚簇索引、二级索引(辅助索引、非聚簇索引)。
- 按「字段特性」分类:主键索引、唯一索引、普通索引、前缀索引。
- 按「字段个数」分类:单列索引、联合索引(组合索引、复合索引)。
(1)按数据结构分类
每一种存储引擎支持的索引类型不一定相同,InnoDB支持B+树索引和Full-Text索引,不支持hash索引,但是在内存结构中有一个自适应hash索引。
-
B+树索引
B+树索引是 MySQL 存储引擎采用最多的索引。创建的主键索引和二级索引默认使用的是 B+树索引。
B+树是 B 树的一种变体,B 树也称 B - 树,全称为多路平衡查找树。B 是 Balanced (平衡)的意思。B+树是一种多叉树,叶子节点存放索引和数据,非叶子节点只存放索引,而且每个节点里的数据是按主键顺序存放的。每一层父节点的索引值都会出现在下层子节点的索引值中,因此所有的叶子节点就包括了所有的索引值信息,并且每一个叶子节点都有两个指针,分别指向下一个叶子节点和上一个叶子节点,形成一个双向链表。 -
hash表索引
哈希表是键值对的集合,通过键(key)即可快速取出对应的值(value),因此哈希表可以快速检索数据(接近 O(1))。
既然哈希表这么快,为什么 MySQL 没有使用其作为索引的数据结构呢?
——①当有大量数据的Hash值相等时,Hash索引的性能就会大打折扣;
②Hash 索引不支持范围查询和排序操作;比如要查询<500的数据,使用Hash的话需要计算每一个数据的hash值来定位,就很麻烦了。
★为什么MySQL InnoDB用B+树作为索引结构?
常用的索引结构有B+树、B树、二叉树、Hash表。
-
B+树 vs B树:
- 存储相同数据量级别的情况下,B+树树高比B树低,磁盘I/O次数更少。
- B+树叶子节点用双向链表串起来,适合范围查询,B树无法做到这点。
-
B+树 vs二叉树:
- 随着数据量的增加,二叉树会越来越高,磁盘I/O次数也会更多,B+树在千万级别的数据量下,高度依然维持在3~4层左右,也就是说一次数据查询操作只需要做3~4次的磁盘l/0操作就能查询到目标数据。
-
B+树 vs Hash:
- 虽然Hash的等值查询效率很高,但是无法做范围查询。
(2)按物理存储分类
-
聚簇索引:聚簇索引是索引结构和数据一起存放的索引,并不是一种单独的索引类型。InnoDB 中的主键索引就属于聚簇索引,因为主键索引的B+树中叶子节点即存放主键索引又存放数据。
-
优点:
- 查询速度非常快 :因为整个 B+树本身就是一颗多叉平衡树,叶子节点也都是有序的,定位到索引的节点,就相当于定位到了数据。因此相比于非聚簇索引, 聚簇索引少了一次读取数据的 IO 操作。
- 对排序查找和范围查找优化 :聚簇索引对于主键的排序查找和范围查找速度非常快。
-
缺点:
- 依赖于有序的数据 :因为 B+树是多路平衡树,如果索引的数据不是有序的,那么就需要在插入时排序,如果数据是整型还好,否则类似于字符串或 UUID 这种又长又难比较的数据,插入或查找的速度肯定比较慢。
- 更新代价大 : 如果索引列的数据被修改时,那么对应的索引也将会被修改,而且聚簇索引的叶子节点还存放着数据,修改代价肯定是较大的,所以对于主键索引来说,主键一般都是不可被修改的。
-
在创建表时,InnoDB 存储引擎会根据不同的场景选择不同的列作为聚簇索引的索引键:
- 如果有主键,默认使用主键作为聚簇索引的索引键(key);
-
如果没有主键,就选择第一个不包含 NULL 值的唯一列作为聚簇索引的索引键(key);
- 在上面两个都没有的情况下,InnoDB 将自动生成一个隐式自增 id 列作为聚簇索引的索引键(key)。
-
优点:
-
非聚簇索引:是索引结构和数据分开存放的索引,并不是一种单独的索引类型。二级索引(辅助索引)就属于非聚簇索引。
- 优点:更新代价比聚簇索引要小 。非聚簇索引的更新代价就没有聚簇索引那么大了,因为非聚簇索引的叶子节点不存放数据;
-
缺点:
- 依赖于有序的数据 :跟聚簇索引一样,非聚簇索引也依赖于有序的数据;
- 可能会二次查询(回表) :这应该是非聚簇索引最大的缺点了。 当查到索引对应的指针或主键后,可能还需要根据指针或主键再到数据文件或表中查询。
(3)按字段特性分类
- 主键索引:建立在主键字段上的索引,通常在创建表的时候一起创建,一张表最多只有一个主键索引,索引列的值不允许有空值。
- 唯一索引:建立在 UNIQUE 字段上的索引,一张表可以有多个唯一索引,索引列的值必须唯一,但是允许有空值。
- 普通索引:建立在普通字段上的索引,既不要求字段为主键,也不要求字段为 UNIQUE。
-
前缀索引:前缀索引是指对字符类型字段的前几个字符建立的索引,而不是在整个字段上建立的索引,前缀索引可以建立在字段类型为 char、 varchar、binary、varbinary 的列上。
使用前缀索引的目的是为了减少索引占用的存储空间,提升查询效率。
(4)按字段个数分类
- 单列索引:建立在单列上的索引称为单列索引,比如主键索引;
- 联合索引:通过将多个字段组合成一个索引,该索引就被称为联合索引。
(5)覆盖索引
覆盖索引:覆盖索引包含了(或者说覆盖)所有要查询的字段的值。
在 InnoDB 存储引擎中,辅助索引叶子节点存储的是主键+列值,最终还是要“回表”,也就是要通过主键再查找一次,这样就会比较慢。而覆盖索引就是把要查询的列和索引对应起来,不用做回表操作!
由于覆盖索引可以减少树的搜索次数,显著提升查询性能,所以使用覆盖索引是⼀个常用的性能优化手段。
(6)最左前缀匹配原则
最左前缀匹配指的是在使用联合索引时,MySQL 会根据联合索引中的字段顺序,从左到右依次到查询条件中去匹配,如果查询条件中存在与联合索引中最左侧字段相匹配的字段,则就会使用该字段过滤一批数据,直至联合索引中全部字段匹配完成,或者在执行过程中遇到范围查询,如 >、< 等条件,才会停止匹配。
所以在使用联合索引时,可以将区分度高的字段放在最左边,这样可以过滤更多数据。
比如,将商品表中的 product_no 和 name 字段组合成联合索引(product_no, name),创建联合索引的方式如下:
CREATE INDEX index_product_no_name ON product(product_no, name);
从联合索引(product_no, name)的B+树可以看到,联合索引的非叶子节点用两个字段的值作为 key 值。
根据最左前缀匹配原则,当在联合索引(product_no, name)查询数据时,先按 product_no 字段比较,在 product_no 相同的情况下再按 name 字段比较。
【场景】 当单独为⼀个不频繁的查询请求创建索引时,会造成空间浪费;但是,如果直接让它走全表查询(⼀个⼀个找),效率也太低了。此时,应该怎么做?
——使用 B+ 树这种索引结构,可以利用索引的“最左前缀”,来定位记录。
——使用 B+ 树这种索引结构,可以利用索引的“最左前缀”,来定位记录。
(7)索引下推
对于联合索引(a, b),在执行 select * from table where a > 1 and b = 2 语句的时候,只有 a 字段能用到索引,在联合索引的 B+树中找到第一个满足 a>1 的主键值id=2后,还需要判断 b=2 是否满足,那么判断 b=2 是在联合索引里判断,还是回表去主键索引中判断呢?
- 在 MySQL 5.6 之前,只能从主键 id=2 开始一个个回表,到「主键索引」上找出数据行,再对比 b 字段值。
- 而 MySQL 5.6 引入的索引下推,可以在联合索引遍历过程中,对联合索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数。
【tips】当查询语句的执行计划里,出现了 Extra 为 Using index condition,那么说明使用了索引下推的优化。
2.索引的优缺点及适用场景
(1)优缺点
-
优点:
- 索引最大的有点就是可以提高查询速度;
-
缺点:
- 创建索引需要占用物理空间(每⼀个节点都是⼀个 16KB 大小的页);
- 创建和维护索引需要耗费时间,当对表中的数据进行增删改时,如果数据有索引,索引也需要进行动态修改,会降低sql的执行效率。
因此,不能无脑创建索引,需要根据场景来使用。
(2)索引的适用场景
- 字段有唯一性限制的,比如商品编码;
- 经常用于 WHERE 查询条件的字段,这样能够提高整个表的查询速度,如果查询条件不是一个字段,还可以建立联合索引;
- 经常用于 GROUP BY 和 ORDER BY 的字段,这样在查询的时候就不需要再去做一次排序了,因为建立索引后在 B+数中的记录都是排序好的。
3.索引优化
几种常见优化索引的方法:
- 前缀索引优化;
- 覆盖索引优化;
- 主键索引最好是自增的;
- 索引最好设为NOT NULL;
- 防止索引失效。
(1)前缀索引优化
前缀索引就是用某个字段中的前几个字符建立索引。
使用前缀索引是为了减小索引字段大小,可以增加一个索引页中存储的索引值,有效提高索引的查询速度。比如用一些大字符串的字段作为索引时,使用前缀索引可以减小索引项的大小。
但是前缀索引有一定的局限性,比如:
- order by 就无法使用前缀索引;
- 无法把前缀索引用作覆盖索引;
(2)覆盖索引优化
覆盖索引可以避免回表的操作。
【场景】假设我们只需要查询商品的名称、价格,有什么方式可以避免回表呢?
我们可以建立一个联合索引(商品id、名称、价格)。如果索引中存在这些数据,查询将不会再次检索主键索引,从而避免回表。所以,使用覆盖索引的好处就是,不需要查询出包含整行记录的所有信息,也就减少了大量的 I/O 操作。
(3)主键索引最好是自增的
InnoDB 创建主键索引默认为聚簇索引,数据被存放在了 B+树的叶子节点上。也就是说,同一个叶子节点内的各个数据是按主键顺序存放的,因此,每当有一条新的数据插入时,数据库会根据主键将其插入到对应的叶子节点中。
- 如果使用自增主键,那么每次插入一条新记录,都是追加操作,不需要重新移动数据,因此这样做后,插入操作的效率非常高。
- 如果使用非自增主键,每次插入新数据时,可能需要移动其它数据来满足新数据的插入,甚至需要从一个页面复制数据到另外一个页面,我们通常将这种情况称为页分裂。
(4)索引最好设为NOT NULL
- 索引列存在 NULL 就会导致优化器在做索引选择的时候更加复杂,更加难以优化,因为可以为 NULL 的列会使索引、索引统计和值比较都更复杂.比如进行索引统计时,count 会省略值为NULL 的行。
- NULL 值也会占用物理空间。(因为 InnoDB 存储记录的时候,如果表中存在允许为 NULL 的字段,那么行格式中至少会用 1 字节空间存储 NULL 值列表)
(5)防止索引失效
创建索引并不意味着查询的时候会使用到索引,要避免写出索引失效的查询语句,否则这样的查询效率是很低的。
发生索引失效的情况:
- 左或者左右模糊匹配(like %xx 或者 like %xx%)都会造成索引失效;
- 在查询条件中对索引列做了计算、函数、类型转换操作都会造成索引失效;
- 联合索引要遵循最左前缀匹配原则,否则就会导致索引失效;
- 查询条件中使用 or,但是 or 的前后条件中有一个列没有索引,会导致索引失效。
四、MySQL事务
1.事务基础
(1)概念
事务是逻辑上的一组操作,要么都执行,要么都不执行。
(2)作用
数据库事务可以保证多个对数据库的操作(也就是 SQL 语句)构成一个逻辑上的整体,这个整体要么全部执行成功,要么全部不执行 。
# 开启一个事务 START TRANSACTION; # 多条 SQL 语句 SQL1,SQL2... ## 提交事务 COMMIT;
2.事务的特性
事务是由 MySQL 的存储引擎来实现的,但并不是所有的引擎都能支持事务,比如 MySQL 原生的 MyISAM 引擎就不支持事务,也正是这样,所以大多数 MySQL 都用 InnoDB。
要实现事务必须要遵守 4 个特性——ACID。
(1)原子性(Atomicity)——同完成同失败
一个事务中的所有操作,要么全部成功,要么全部失败。事务如果在执行过程中发生错误,会被回滚到事务开始前的状态。
(2)一致性(Consistency)——数据一致性
是指事务执行前后,数据保持一致。
比如,用户 A 和用户 B 在银行分别有 800 元和 600 元,总共 1400 元,用户 A 给用户 B 转账 200 元,分为两个步骤,从 A 的账户扣除 200 元和对 B 的账户增加 200 元。一致性就是要求上述步骤操作后,最后的结果是用户 A 还有 600 元,用户 B 有 800 元,总共 1400 元,而不会出现用户 A 扣除了 200 元,但用户 B 未增加的情况(该情况,用户 A 和 B 均为 600 元,总共 1200 元)。
(3)★隔离性(Isolation)——互不干扰
并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的。
比如消费者A购买商品这个事务,不影响其他消费者的购买事务。
比如消费者A购买商品这个事务,不影响其他消费者的购买事务。
(4)持久性(Durability)——永久修改
事务处理结束后,对数据的修改就是永久的,即使系统故障也不会丢失。
(5)InnoDB 引擎如何保证事务的四个特性?
- 持久性是通过 redo log (重做日志)来保证的
- 原子性是通过 undo log(回滚日志) 来保证的;
- 隔离性是通过 MVCC(多版本并发控制) 或锁机制来保证的;
- 一致性则是通过持久性+原子性+隔离性来保证。(只有保证了事务的持久性、原子性、隔离性之后,才能保证一致性,也就是说 A、I、D 是手段,C 是目的!)
3.并发事务
MySQL 服务端是允许多个客户端连接的,这意味着会出现并发事务的情况。在处理并发事务时,就可能出现以下问题。(1)脏读(dirty read)——读到其他事务未提交的数据
脏读是指一个事务A读到了另一个未提交事务B修改过的数据。
修改完还没提交意味着随时可能发生回滚,那么事务A读到的数据就是过期的数据,这种现象就被称为脏读。
(2)不可重复读(non-repeatable read)——前后读取的数据不一致
不可重复读是指同一事务多次读取同一个数据,出现前后两次读到的数据不一致的现象。
(3)幻读(phantom read)——前后读取的记录数量不一致
幻读是不可重复读的一种特殊情况,也是两次读到的数据不一致,但是幻读侧重于第二次读到的记录数量不一致了。
-
不可重复读和幻读的区别?
- 不可重复读侧重于记录的内容修改了,比如多次读取同一条记录,发现前后读到的记录内容不一致;而幻读侧重于记录的数量不一致,比如多次执行同一条查询语句,查到的记录数量不一致。
- 幻读可以看做不可重复读的一种特殊情况,单独区分幻读主要是因为解决幻读和不可重复读的方案不一样。
4.事务四大隔离级别
SQL 标准提出了四种隔离级别来规避这些现象,这四个隔离级别如下:
- 读不提交 RU(read uncommitted):最低的隔离级别。允许读取未提交事务的数据修改,可能会导致脏读、不可重复读和幻读,一般不使用;
-
读提交 RC(read committed):读取的是并发事务已经提交的最新数据,可以阻止脏读,但是同一事务中前后两次相同的查询可能会读到不同的结果,所以可能导致不可重复读和幻读;
- 可重复读 RR(repeatable read):指同一个事务中读到的数据,一直跟事务开始时读到的数据是一致的,解决了脏读和不可重复读,还是可能会幻读。是MySQL InnoDB 引擎的默认隔离级别;
- 串行化(serializable ):最高的隔离级别。对记录加上读写锁,在并发事务对这条记录进行读写操作时,如果发生了读写冲突,后访问的事务必须等前一个事务执行完成,才能继续执行。可以防止脏读、不可重复读以及幻读。
综上所述,要解决脏读现象,就要升级到「读提交」以上的隔离级别;要解决不可重复读现象,就要升级到「可重复读」的隔离级别;但是!要解决幻读现象不建议将隔离级别升级到「串行化」。
(1)四种隔离级别是如何实现的?(基于 锁 和 MVCC机制 实现的)
- 读未提交:因为可以读到未提交事务修改的数据,所以是直接读取最新的数据;
- 串行化:通过加读写锁的方式来避免并行访问;
-
「读提交」和「可重复读」:基于MVCC(多版本并发控制),通过 Read View 来实现的。它们的区别在于创建 Read View 的时机不同:
- 「读提交」是在每个 select 都会生成一个新的 Read View,所以在事务期间多次读取同一条数据,前后两次读的数据可能会出现不一致,因为可能这期间另外一个事务修改了该记录,并提交了事务。
- 「可重复读」是在「启动事务时」生成一个 Read View,然后整个事务期间都用这个 Read View,这样就保证了在事务期间读到的数据都是事务启动前的记录。
(2)Read View 与 MVCC(★事务如何实现MVCC?)
1)什么是Read View?
ReadView(可读视图) 相当于一种快照,记录了当前活跃事务(已启动但未提交的事务)的id 列表及相关信息,主要用来判断当前事务是否有资格访问该行数据。
ReadView 有四个重要的字段:
- m_ids:是指当前事务在创建 Read View 时,所有活跃事务的事务 id 列表;
- min_trx_id :是指当前事务在创建 Read View 时,所有活跃事务中事务 id 最小的事务,即 m_ids 的最小值;
- max_trx_id :这个并不是 m_ids 的最大值,而是创建 Read View 时,下一个事务的 id 值,即 最大的事务 id + 1;
- creator_trx_id :是指创建该 Read View 的事务的事务 id。
2)版本链
先了解一下使用 InnoDB 的数据库表的聚簇索引记录中的两个隐藏列。假设在账户余额表插入一条小林余额为 100 万的记录,把两个隐藏列也画出来,该记录的整个示意图如下:
- trx_id:是指对该记录进行操作的事务的事务 id ;
- roll_pointer:用来指向旧版本记录。每次对某条聚簇索引记录进行改动时,都会把旧版本的记录写入到 undo 日志中,通过 roll_pointer 就能找到修改前的记录。
一个事务访问记录的时候,除了自己的更新记录总是可见之外,还有以下情况:
- 如果记录的 trx_id 值 < Read View 中的 min_trx_id 值,则表示当前记录是在创建 Read View 前已经提交的事务生成的,所以该版本的记录对当前事务可见。
- 如果记录的 trx_id 值 ≥ Read View 中的 max_trx_id 值,则表示这个版本的记录是在创建 Read View 后才启动的事务生成的,所以该版本的记录对当前事务不可见,此时要沿着undo log版本链找以前版本的记录,直到找到第一条trx_id小于Read View 的 min_trx_id 的旧版本记录,读取该版本的记录。
-
如果记录的 trx_id 值在 min_trx_id 和 max_trx_id 之间,需要判断 trx_id 是否在 m_ids 列表中:
- 如果记录的 trx_id 在 m_ids 列表中,表示生成该版本记录的活跃事务依然活跃着(还没提交事务),所以该版本的记录对当前事务不可见。
- 如果记录的 trx_id 不在 m_ids列表中,表示生成该版本记录的活跃事务已经被提交,所以该版本的记录对当前事务可见。
4)总结事务如何实现MVCC
InnoDB实现MVCC的核心主要有两个——undo log和Read View。
- 每个事务都有一个严格递增的事务id,当事务对记录进行修改时,会产生一条undo记录,这条undo记录里除了保存字段的数据外,还保存了修改记录的事务id(trx_id)和上一个版本的记录地址(roll_pointer),通过roll_pointer可以获得每一个版本的记录。
- 写操作会生成undo记录,形成版本链,而读操作只需要根据一定的规则来读取某个版本的数据。这个规则就是可见性判断规则(上面的3))。
(3)★如何解决幻读?
虽然串行化隔离级别可以解决幻读,但是会影响性能。MySQL 在「可重复读」隔离级别下,可以很大程度上避免幻读现象的发生(并不是彻底避免),因此 MySQL 不会使用串行化隔离级别来避免幻读现象的发生,所以InnoDB默认是RR。
★★★InnoDB 可重复读隔离级别解决幻读的方法★★★:
-
针对快照读(普通 select 语句),是通过 MVCC 的方式解决了幻读,并不会对记录进行加锁。
——实现方式是开始事务后(执行 begin 语句后),在执行第一个查询语句后,会创建一个 Read View,后续的查询语句都用这个 Read View,通过这个 Read View 就可以在 undo log 版本链找到事务开始时的数据,所以事务过程中每次查询的数据都是一样的,即使中途有其他事务插入了新纪录,也查不到新记录,所以就很好了避免幻读问题。 -
针对当前读(select ... for update 等语句),是通过临键锁(记录锁+间隙锁)的方式解决了幻读。
——因为当执行 select ... for update 语句的时候,会加上临键锁,如果有其他事务在锁范围内插入了一条记录,那么这个插入语句就会被阻塞,无法成功插入,所以就避免了幻读问题。
(4)可重复读能完全解决幻读吗?
可重复读虽然能很大程度上避免幻读,但是没有完全解决幻读。
有一些特殊情况也会导致幻读:
- 情况一:当事务 A 更新了一条事务 B 插入的记录,事务 A 再查这条记录就能查到了,那么事务 A 前后两次查询的记录条目就不一样了,此时就会发生幻读。
- 情况二:如果事务 A 开启后并没有执行当前读,而是先快照读,然后这期间事务 B 插入了一条记录,那么事务 A 再使用当前读进行查询的时候,就会发现两次查询的记录条目就不一样了,此时就会发生幻读。
五、MySQL锁
锁是一种常见的并发事务的控制方式。
1.分类
在 MySQL 里,根据加锁的范围,可以分为全局锁、表级锁和行锁三类。
(1)全局锁
顾名思义,全局锁就是对整个数据库实例加锁。
1)全局锁的使用
-
加全局锁:
flush tables with read lock
执行后,整个数据库就处于只读状态了,这时其他线程执行以下操作,都会被阻塞:- 对数据的增删改操作,比如 insert、delete、update等语句;
- 对表结构的更改操作,比如 alter table、drop table 等语句;
- 更新类事务的提交操作。
-
释放全局锁:
unlock tables
当会话断开全局锁会被自动释放。
2)应用场景
全局锁主要应用于做全库逻辑备份,这样在备份数据库期间,不会因为数据或表结构的更新,而出现备份文件的数据与预期的不一样。
3)缺点
加上全局锁,意味着整个数据库都是只读状态,所以在备份期间就不能更新数据,这样可能会造成业务停滞。
4)在备份数据库时,如何避免使用全局锁而影响业务?——可重复读+MVCC
如果数据库的引擎支持事务的可重复读的隔离级别,可以在备份数据库前先开启事务,会先创建一个 Read View,整个事务执行期间用的都是这个 Read View,而且由于 MVCC 的支持,备份期间业务依然可以对数据进行更新操作。因为在可重复读的隔离级别下,即使其他事务更新了表数据,也不会影响备份数据库时的 Read View,这样备份期间备份的数据一直是在开启事务时的数据。
备份数据库的工具是 mysqldump,在使用 mysqldump 时加上 --single-transaction 参数的时候,就会在备份数据库之前先开启事务。
mysqldump -u root -p 数据库 [表名1 表名2…] > 文件备份路径
但是,对于 MyISAM 这种不支持事务的引擎,在备份数据库时就要使用全局锁的方法。
(2)表级锁(table-level locking)
表级锁可以对表进行加锁。
1)分类
MySQL 表级锁主要有以下几种:
- 表锁;
- 元数据锁(meta data lock,MDL);
- 意向锁;
- AUTO-INC 锁(自增锁,了解即可)。
-
如果想对表 t_student 加表锁,可以使用下面的命令:
//表级别的共享锁,也就是读锁; lock tables t_student read; //表级别的独占锁,也就是写锁; lock tables t_stuent write;
表锁除了会限制其他线程的读写操作外,也会限制本线程接下来的读写操作。
-
解锁:
unlock tables
会话退出会自动释放所有表锁。
【tips】InnoDB 还实现了颗粒度更细的行级锁,所以尽量避免对使用 InnoDB 引擎的表使用表锁,因为表锁的颗粒度太大,会影响并发性能。
3)元数据锁 MDL
不需要显示地使用 MDL,因为当对数据库表进行操作时会自动给表加上 MDL:
- 对一张表进行 CRUD 操作时,加的是 MDL 读锁;
- 对一张表做结构变更操作的时候,加的是 MDL 写锁。
- 当有线程在执行 select 语句( 加 MDL 读锁)的期间,如果有其他线程要更改该表的结构( 申请 MDL 写锁),那么将会被阻塞,直到执行完 select 语句( 释放 MDL 读锁)。
- 反之,当有线程对表结构进行变更( 加 MDL 写锁)的期间,如果有其他线程执行了 CRUD 操作( 申请 MDL 读锁),那么就会被阻塞,直到表结构变更完成( 释放 MDL 写锁)。
MDL 不需要显示调用,那它是在什么时候释放的?
——MDL 是在事务提交后才会释放,这意味着事务执行期间,MDL 是一直持有的。所以为了能安全的对表结构进行变更,在对表结构变更前,先要看看数据库中的长事务(会长时间持有MDL导致大量阻塞),是否有事务已经对表加上了 MDL 读锁,如果可以考虑 kill 掉长事务,然后再做表结构的变更。
4)意向锁
意向锁的目的是为了快速判断表里是否有记录被加锁。
- 如果要对数据表的某行记录加「共享锁」,需要先在数据表上加一个「意向共享锁」;同理要给某行记录加「独占锁」之前,需要先在数据表上加一个「意向独占锁」。
- 这样在对某行记录加独占锁时,直接看该表有没有意向独占锁,如果有意向独占锁,说明该表已经有记录被加了独占锁了,就不用逐行遍历去看加没加独占锁了。
(3)行级锁(row-level locking)
InnoDB 是支持行级锁的,而 MyISAM 并不支持行级锁。
普通的 select 语句是不会对记录加锁的,因为它属于快照读。如果要在查询时对记录加行锁,可以使用下面这两个方式,这种查询会加锁的语句称为锁定读。
//对读取的记录加共享锁 select ... lock in share mode; //对读取的记录加独占锁 select ... for update;1)分类
- 记录锁(Record Lock):给一条行记录上锁,所以也称为行锁;
- 间隙锁(Gap Lock):锁定一个范围,不包括记录本身;
- 临键锁(Next-Key Lock):行锁+间隙锁,锁定一个范围,包含记录本身。主要目的是为了解决幻读问题。记录锁只能锁住已经存在的记录,为了避免插入新记录,所以还需要间隙锁。
2)记录锁(行锁)
行锁也分共享锁(读锁)和独占锁(写锁)。只有读读可以,读写、写写不可以。
如何给一条记录加行锁?——for update
当一个事务执行了下面这条语句,就是对 t_test 表中主键 id 为 1 的这条记录加上独占记录锁,这样其他事务就无法对这条记录进行修改了。
mysql > begin; mysql > select * from t_test where id = 1 for update;3)间隙锁
间隙锁只存在于可重复读隔离级别,目的是为了解决可重复读隔离级别下幻读的现象。
间隙锁如何解决幻读问题?
假设,表中有一个范围 id 为(3,5)的间隙锁,那么其他事务就无法插入 id = 4 这条记录了,这样就有效的防止幻读现象的发生。
4)临键锁
临键锁是行锁+间隙锁,作用范围是( 左开右闭 ]的。
假设,表中有一个范围 id 为(3,5] 的临键锁,那么其他事务即不能插入 id = 4 记录,也不能修改 id = 5 这条记录。
所以临键锁即能保护该记录,又能阻止其他事务将新纪录插到该记录前面的间隙中。
2.什么sql语句会加行级锁?(InnoDB是如何加行级锁的?)
普通的 select 语句不会加行级锁,因为它属于快照读,是通过 MVCC 实现的。
如果要在查询时加行级锁,可以使用下面这两个方式,这两种查询会加锁的语句称为锁定读。
//对读取的记录加共享锁(S型锁) select ... lock in share mode; //对读取的记录加独占锁(X型锁) select ... for update;在执行insert、delete和update时,也会加行级锁,且都是独占锁。
3.MySQL加行级锁的原则?
行级锁是加在索引上,如果索引不存在自然不会加行级锁;加锁的基本单位是临键锁。
但是临键锁可能会退化成行锁或间隙锁。在使用行锁或间隙锁就能避免幻读的情况下,临键锁就会退化成行锁或间隙锁。
(1)唯一索引等值查询
当我们用唯一索引进行等值查询时,查询的记录存不存在,加锁的规则也会不同:
-
当查询的记录是「存在」的,即在索引树上定位到这一条记录后,将该记录的索引中的 next-key lock 会退化成「行锁」。
因为记录存在,要避免幻读,只需要加行锁来阻塞对这条记录的更新和删除操作即可。 -
当查询的记录是「不存在」的,在索引树找到第一条大于该记录的记录后,将该记录的索引中的 next-key lock 会退化成「间隙锁」。
因为记录不存在,就没有索引,而行锁是加在索引上的,所以只需要间隙锁即可。
(2)非唯一索引等值查询
非唯一索引意味着可能有相同的索引。
(3)范围查询
唯一索引和非唯一索引的范围查询的加锁规则不同之处在于:
- 唯一索引在满足一些条件的时候,索引的 next-key lock 退化为间隙锁或者记录锁。
- 非唯一索引范围查询,索引的 next-key lock 不会退化。
4.MySQL中的死锁问题
(1)为什么会死锁?(举一个发生死锁的场景)
如果两个事务分别向对方持有的间隙锁范围内插入一条记录,就会产生死锁。
因为间隙锁是互不冲突的,所以一个事务获取了间隙锁并不会阻止其他事务获取同一范围的间隙锁。在执行插入语句时,如果要插入的记录在其他事务持有间隙锁范围内,插入语句就会被阻塞(因为插入语句在碰到间隙锁时,会生成一个插入意向锁,而插入意向锁和间隙锁之间是互斥的关系)。此时两个事务都在等待对方释放间隙锁,造成了循环等待,所以发生了死锁。
(2)如何避免/解决死锁?
在数据库中有两种策略通过「打破循环等待条件」来解除死锁:
-
设置事务等待锁的超时时间:
- 当一个事务的等待时间超过设定的时间后,就对这个事务进行回滚,于是锁就释放了,另一个事务就可以继续执行了。
- 在 InnoDB 中,参数 innodb_lock_wait_timeout 是用来设置超时时间的,默认值是 50 秒。
-
当发生超时时会出现以下提示(1205):
-
开启主动死锁检测:
-
开启主动死锁检测后,如果发生了死锁,会主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。
-
将参数 innodb_deadlock_detect 设置为 on,表示开启主动死锁检测,默认就是开启的。
-
当检测到死锁后,就会出现下面这个提示:
-
以上两种策略都是发生死锁后的处理方式。
六、MySQL日志
MySQL中比较重要有二进制日志 binlog(归档日志)、事务日志 redo log(重做日志)和 undo log(回滚日志)。
MySQL InnoDB 引擎使用 redo log(重做日志) 保证事务的持久性,使用 undo log(回滚日志) 来保证事务的原子性。
MySQL数据库的数据备份、主备、主主、主从都离不开binlog,需要依靠binlog来同步数据,保证数据一致性。
1.undo log 回滚日志
undo log 的两大用途:
-
实现事务回滚,保障事务的原子性。
事务处理过程中,如果出现了错误或者用户执行了 ROLLBACK 语句,MySQL 可以利用 undo log 中的历史数据将数据恢复到事务开始之前的状态。 -
是实现 MVCC关键因素之一。
MVCC 是通过 ReadView + undo log 实现的。undo log 为每条记录保存多份历史数据,MySQL 在执行快照读的时候,会根据事务的 Read View ,顺着 undo log 的版本链找到满足其可见性的记录。
2.buffer pool 缓冲池
在介绍redo log之前,先学习一下什么是buffer pool。
我们知道 MySQL 的数据都是存在磁盘中的,当要更新一条记录的时候,需要先从磁盘读取该记录,再在内存中修改这条记录(磁盘读取、内存修改)。修改完的记录会缓存起来,这样下次有查询语句命中了这条记录,直接读取缓存中的记录,就不需要从磁盘获取数据了。因此 Innodb 存储引擎设计了一个缓冲池(Buffer Pool),来提高数据库的读写性能。
(1)★有buffer pool的数据读写——如何提高读写性能?
- 读取数据时,先从 Buffer Pool 中的读取数据,如果缓存池中没有,再去磁盘中读取;
-
修改数据时,如果数据在 Buffer Pool 中,那么直接修改 Buffer Pool 中的数据页,然后将其设置为脏页,为了减少磁盘I/O,不会立即将脏页写入磁盘,而是由后台线程在合适时机将脏页写入磁盘。
(2)buffer pool 的大小
Buffer Pool 是在 MySQL 启动的时候,向操作系统申请的一片连续的内存空间,默认配置下 Buffer Pool 只有 128MB 。
可以通过调整 innodb_buffer_pool_size 参数来设置 Buffer Pool 的大小,一般建议设置成可用物理内存的 60%~80%。
(3)★buffer pool 中的缓存内容
记录是按行存储的,但是数据的读取是以数据页为单位的,一页包括多条记录行。InnoDB 会把存储的数据划分为数据页,以页为单位在磁盘和内存中交换,页默认大小为 16KB。因此 Buffer Pool 中的数据同样需要按页来划分,buffer pool中的页叫缓存页。
Buffer Pool 中缓存了索引页、数据页、 undo 页、插入缓存、自适应哈希索引、锁信息等。
【undo 页】缓冲池中的 undo 页缓存的是 undo log 中的一条条记录。
Buffer Pool 是一片连续的内存空间,当我们从磁盘读取数据的时候,为了能够快速找到空闲的缓存页,可以使用链表结构,将空闲缓存页的「控制块」作为链表的节点,这个链表称为 Free 链表(空闲链表)。Free 链表节点是一个一个的控制块,而每个控制块包含着对应缓存页的地址,所以相当于 Free 链表节点都对应一个空闲的缓存页。有了 Free 链表后,每当需要从磁盘中加载一个页到 Buffer Pool 中时,就从 Free链表中取一个空闲的缓存页,并且把该缓存页对应的控制块的信息填上,然后把该缓存页对应的控制块从 Free 链表中移除。
【控制块】InnoDB 为每一个缓存页都创建了一个控制块,控制块信息包括「缓存页的表空间、页号、缓存页地址、链表节点」等等,所有的控制块都放在 Buffer Pool 的最前面,接着才是缓存页。
(4)查询一条记录,就只需要缓冲一条记录吗?
不是的。
查询一条记录时,InnoDB 会把该记录所在的整个页加载到 Buffer Pool 中。因为通过索引只能定位到磁盘中的页,而不能定位到页中具体哪一条记录。将页加载到 Buffer Pool 后,再通过页目录定位到某条具体的记录。
(5)如何提高缓存命中率?
对于一些频繁访问的数据我们希望可以一直留在 Buffer Pool 中,而一些很少访问的数据,希望可以在某些时机可以淘汰掉。要实现这个功能最先想到的就是LRU算法(最近最少使用)。
该算法的思路是,链表头部的节点是最近使用的,而链表末尾的节点是最久没使用的:
- 当访问的页在 Buffer Pool 里,就直接把该页对应的 LRU 链表节点移动到头部。(在缓冲池,移到头部)
- 当访问的页不在 Buffer Pool 里,除了要把页放入到 LRU 链表的头部,还要淘汰 LRU 链表末尾的节点。(不在缓冲池,移到头部,淘汰尾部)
- 预读失效
- buffer pool 污染
(6)★预读失效
为了减少磁盘 IO,MySQL 在加载数据页时,会把相邻的数据页一起加载进来,这就是MySQL 的预读机制。这些被提前加载进来的数据页,可能没有被访问,相当于这个预读是白做了,这个就是预读失效。
如果使用简单的 LRU 算法,就会把预读页放到 LRU 链表头部,当 Buffer Pool空间不够的时候,还需要把末尾的页淘汰掉。如果这些预读页一直不会被访问的话,就会导致不会被访问的预读页占用了 LRU 链表前排的位置,而末尾淘汰的页,可能是频繁访问的页,这样就大大降低了缓存命中率。
如何解决预读失效而导致缓存命中率降低的问题?
如果使用简单的 LRU 算法,就会把预读页放到 LRU 链表头部,当 Buffer Pool空间不够的时候,还需要把末尾的页淘汰掉。如果这些预读页一直不会被访问的话,就会导致不会被访问的预读页占用了 LRU 链表前排的位置,而末尾淘汰的页,可能是频繁访问的页,这样就大大降低了缓存命中率。
如何解决预读失效而导致缓存命中率降低的问题?
——要避免预读失效导致的命中率降低的问题,可以让预读页停留在 Buffer Pool 里的时间尽可能短,让真正被访问的页才移动到 LRU 链表的头部。所以 MySQL 改进了 LRU 算法,将 LRU 划分成 2 个区域:old 区域 和 young 区域。young 区域在 LRU 链表的前半部分,old 区域则是在后半部分。这时,预读页只需要加到 old 区域的头部,当真正访问的时候,才将页插入 young 区域的头部。这样一来如果预读页一直没有被访问,就会从 old 区域移除,这样就不会影响 young 区域中的热点数据了。
(7)★buffer pool污染
当一个 SQL 语句扫描了大量的数据或做了全表扫描时,可能会将 Buffer Pool 里的所有页都替换出去,导致大量热点数据被淘汰了,后续再次访问这些热点数据时,缓存就会未命中,产生大量的磁盘 IO,MySQL 性能就会急剧下降,这个过程被称为 Buffer Pool 污染。
如何解决出现 Buffer Pool 污染而导致缓存命中率下降的问题?
——造成缓存污染的原因是只要数据被访问一次,就会将数据加入到 young 区域,也就是说从old区域进入young区域的门槛太低了。那么就可以提高进入young区域的门槛:当数据被访问第二次的时候,不让它立刻从 old 区域升级到 young 区域,还要对停留在 old 区域的时间进行判断:
- 如果第二次和第一次访问的时间间隔在 1 秒内(默认值),那么该页就不会被从 old 区域升级到 young 区域;
- 如果第二次和第一次访问的时间间隔超过 1 秒,该页才会从 old 区域升级到 young 区域。
3.redo log 重做日志
redo log 记录了对某个数据页做了什么修改,当系统崩溃(如断电重启)时,可以通过 redo log 实现数据恢复。
(1)为什么有了buffer pool还需要redo log来恢复数据?
前面我们学了buffer pool可以缓存数据,那么为什么不能通过buffer pool缓存来恢复数据?
——因为buffer pool是基于内存的,内存数据在突然断电重启时会丢失。所以当更新记录时,会先更新内存(同时标记为脏页),然后将对数据页的修改(所以说redo log是物理日志)以 redo log 的形式记录下来,此时更新就算完成了。然后由后台线程在合适的时机将脏页刷新到磁盘(即WAL技术)。
【WAL技术】:指的是MySQL 的写操作并不是立刻写到磁盘上,而是先写日志,然后在合适的时间再写到磁盘上。
(2)redo log 为什么能实现数据恢复?
在事务提交时,可以先将 redo log 持久化到磁盘,不用等脏页数据持久化到磁盘。这样当系统崩溃时,即使脏页数据没有持久化,但是 redo log 已经持久化了,MySQL 重启后,可以根据 redo log 的内容,将所有数据恢复到最新的状态。
(3)redo log 和 undo log 区别在哪?
二者都是属于 InnoDB 存储引擎的日志,区别在于:
- redo log 记录了事务「完成后」的数据状态,记录的是更新之后的数据;
- undo log 记录了事务「开始前」的数据状态,记录的是更新之前的数据。
(4)redo log 要写到磁盘,数据也要写磁盘,为什么要多此一举?(WAL如何将随机写变成顺序写?)
写入 redo log 使用了追加操作, 是顺序写,而写入数据需要先找到写入位置,再写入磁盘,所以写入数据是随机写。磁盘的「顺序写 」比「随机写」 更高效,因此 redo log 写入磁盘的开销更小。
这也是 WAL 技术的一个优点:将MySQL 的写操作从磁盘的「随机写」变成了「顺序写」。
这也是 WAL 技术的一个优点:将MySQL 的写操作从磁盘的「随机写」变成了「顺序写」。
(5)redo log 的刷盘时机
上面我们提到把对记录的修改记在redo log里,在合适的时机再刷新到磁盘,那么redo log到底什么时间刷新到磁盘呢?
主要有下面几个时机:
- MySQL 正常关闭时,会对redo log进行刷盘;
- 当 redo log buffer 中记录的写入量大于 redo log buffer 内存空间的一半时,会进行刷盘;
- InnoDB 的后台线程每隔 1 秒,会进行刷盘;
-
还有一个由 innodb_flush_log_at_trx_commit 参数控制的事务提交时的三种刷盘策略:
- 0 :innodb_flush_log_at_trx_commit 设置为 0 的时候,表示每次事务提交时不进行刷盘操作,还是将 redo log 留在 redo log buffer 中;
- 1 :设置为 1 的时候,表示每次事务提交时(prepare阶段结束)都将进行刷盘操作(默认操作);
-
2 :设置为 2 的时候,表示每次事务提交时都只把 redo log buffer 内容写入 page cache。
Page Cache 是专门用来缓存文件数据的,所以将 redo log文件写入page cache意味着写入到了操作系统的文件缓存,而不是写入到磁盘了。
(6)innodb_flush_log_at_trx_commit 为 0 和 2 的时候,什么时候将 redo log 写入磁盘?
由 InnoDB 的后台线程每隔 1 秒进行刷盘:
- 针对参数 0 :会把缓存在 redo log buffer 中的 redo log ,通过调用 write() 写到 Page Cache,然后调用 fsync() 持久化到磁盘。所以参数为 0 的策略,MySQL 的崩溃会导致上一秒钟所有事务数据的丢失;
-
针对参数 2 :调用 fsync(),将缓存在Page Cache 里的 redo log 持久化到磁盘。所以参数为 2 的策略,比 0 更安全,因为 MySQL 的崩溃并不会丢失数据,只有在操作系统崩溃或者系统断电的情况下,上一秒钟所有事务数据才可能丢失。
(7)0、1、2三个参数的应用场景?
要根据三个参数的安全性和写入性能分析应用场景:
- 数据安全性:参数 1 > 参数 2 > 参数 0
- 写入性能:参数 0 > 参数 2> 参数 1
(8)redo log满了怎么办?
InnoDB 中的redo log文件不止一个,而是以重做日志文件组(redo log group)的形式存在,假设由有 2 个 redo log 文件组成,分别叫 ib_logfile0 和 ib_logfile1 。
重做日志文件组是以循环写的方式工作的,相当于一个环形。先写 ib_logfile0 ,当 ib_logfile0被写满了,会切换至 ib_logfile1,当 ib_logfile1也被写满时,再切回 ib_logfile0。
在循环写的过程中有两个参数:
- 用 write pos 表示当前记录写到的位置;
- 用 checkpoint 表示当前要擦除的位置
- write pos ~ checkpoint 之间的部分(图中的红色部分),用来记录新的更新操作;
- check point ~ write pos 之间的部分(图中蓝色部分),是等待刷盘的脏页数据。
4.binlog 归档日志
undo log 和 redo log 都是在 Innodb 存储引擎生成的。
MySQL 在完成一条更新操作后,Server 层还会生成一条 binlog,事务提交后,会将该事物执行过程中产生的所有 binlog 统一写入 binlog 文件。
binlog 文件记录了数据库表结构变更和表数据修改的日志,记录的是sql语句(逻辑日志),不会记录查询类的操作,比如 SELECT 和 SHOW 操作。
【tips】只有事务提交之后才会将修改记录到 binlog ,此时binlog还是在内存,然后才会被写入磁盘,所以从库只有在主库提交之后才能读到主库写入的binlog日志。
(1)redo log 和 binlog 有什么区别?
主要有以下四个区别:
-
1)适用对象不同(实现不同):
- binlog 是 MySQL 的 Server 层实现的日志,所有存储引擎都可以使用;
- redo log 是 Innodb 存储引擎实现的日志,只有InnoDB才有redo log。
-
2)文件格式不同:
-
binlog 有 3 种格式类型,分别是 STATEMENT(默认格式)、ROW、 MIXED,可以通过binlog_format参数指定,区别如下:
- STATEMENT:每一条更新类的 SQL 语句原文都会被记录到 binlog 中,相当于记录了逻辑操作(所以针对这种格式, binlog 可以称为逻辑日志),主从复制中 slave 端可以根据 SQL 语句重现。但 STATEMENT 有动态函数的问题,比如你用了 uuid 或者 now 这些函数,你在主库上执行的结果并不是你在从库执行的结果,这种随时在变的函数会导致复制的数据不一致;
- ROW:记录行数据最终被修改成什么样了。不会出现动态函数的问题,但缺点是每行数据的变化结果都会被记录,比如执行批量 update 语句,更新多少行数据就会产生多少条记录,会使 binlog 文件过大,而在 STATEMENT 格式下只会记录一个 update 语句而已;
- MIXED:包含了 STATEMENT 和 ROW 模式,它会根据不同的情况自动使用 ROW 模式和 STATEMENT 模式;
- redo log 是物理日志,记录的是在某个数据页做了什么修改。
-
binlog 有 3 种格式类型,分别是 STATEMENT(默认格式)、ROW、 MIXED,可以通过binlog_format参数指定,区别如下:
-
3)写入方式不同:
- binlog 是追加写,写满一个文件,就创建一个新的文件继续写,不会覆盖以前的日志,保存的是全量日志。
-
redo log 是循环写,日志空间大小是固定,全部写满就从头开始。
所以如果数据库数据全部删除了的话,要用binlog而不是redo log来恢复所有数据。因为 redo log 会边写边擦除日志的,它只记录了未刷盘的数据;而 binlog 保存了所有数据变更的情况(全量日志)。
-
4)用途不同:
- binlog 用于备份恢复、主从复制;
- redo log 用于断电等故障恢复。
-
5)持久化时机不同:
- redo log 可以在事务没提交之前持久化到磁盘;
- binlog 必须在事务提交之后,才可以持久化到磁盘。
(2)MySQL如何实现主从复制?
MySQL 的主从复制依赖于 binlog ,binlog 记录了 MySQL 上的所有变化(所以是全量日志)并以二进制形式保存在磁盘上(说明持久化了)。复制的过程就是将 binlog 中的数据从主库传输到从库上。
主从复制的过程一般是异步的,也就是说主库上执行事务操作的线程不会等待复制 binlog 的线程完成才执行事务操作。
MySQL 集群的主从复制过程可以成为 3 个阶段:
-
写入 Binlog:主库写 binlog 日志,提交事务,并更新本地存储数据。
- 主库在收到客户端提交事务的请求之后,会先写入 binlog,再提交事务,更新存储引擎中的数据,事务提交完成后,返回给客户端“操作成功”的响应。
-
同步 Binlog:把 binlog 复制到所有从库上,每个从库把 binlog 写到暂存日志中。
- 从库会创建一个专门的 I/O 线程,连接主库的 log dump 线程,来接收主库的 binlog 日志,再把 binlog 信息写入 relay log(中继日志)里;
- 回放 Binlog:回放 binlog,并更新存储引擎中的数据。
- 从库会创建一个用于回放 binlog 的线程,去读 relay log 中继日志,然后回放 binlog ,更新存储引擎中的数据,最终实现主从的数据一致性。
【为什么要对MySQL进行主从复制?】
对于同一时刻有大量并发读和较少写操作的数据库来说,将数据库拆分为主库和从库,让主库负责写操作,从库负责读操作,可以缓解只有一个数据库时的压力,避免由数据更新导致的行锁,提高查询性能(更新时上了行锁就不能读了,主从复制以后,即使主库正在更新上了行锁或表锁,还是可以从从库读数据的)。
【从库是不是越多越好?】
不是的。
因为从库数量增加,从库连接上来的 I/O 线程也比较多,主库也要创建同样多的 log dump 线程来处理复制的请求,这样对主库资源消耗比较高,同时还受限于主库的网络带宽。
所以一个主库一般跟 2~3 个从库(1 套数据库,1 主 2 从 1 备主),这就是一主多从的 MySQL 集群结构。
主要有三种:
【上面提到的 MySQL 主从复制是异步进行的,还有其他主从复制的模型吗?】
-
异步复制(默认模型):主库提交事务的线程并不会等待从库复制完成,就会给客户端返回结果。
- 这种模式下一旦主库宕机,数据就会发生丢失。
-
同步复制:主库提交事务的线程要等待所有从库的复制完成后,才会给客户端返回结果。
- 这种模式下缺点比较多,一是性能很差,因为要等待复制到所有从库(节点)才能给客户端返回响应;二是可用性也很差,主库和所有从库只要任何一个数据库出了问题,都会影响业务。
-
半同步复制:这是MySQL 5.7 才有的,介于同步和异步复制之间,事务线程不用等待所有的从库复制完成,只要一部分复制完成就可以给客户端返回响应结果,比如一主二从的集群,只要数据成功复制到任意一个从库上,主库的事务线程就可以返回给客户端。
- 这种方式兼顾了异步复制和同步复制的优点,即使出现主库宕机,至少还有一个从库有最新的数据,不存在数据丢失的风险。
(3)binlog 什么时候刷盘?
事务在执行过程中,会先把对记录的修改写到 binlog cache 中,等到事务提交时,再把 binlog cache 写入(write) binlog 文件(位于page cache)中,并清空 cache,然后再在合适时机(后面会讲)写入(fsync)磁盘。
【binlog cache】MySQL 给每个线程分配了一块内存用于缓冲 binlog ,这块内存就叫 binlog cache。参数 binlog_cache_size 用于控制单个线程内 binlog cache 所占内存的大小。如果超过了这个参数规定的大小,就要暂存到磁盘。
虽然每个线程有自己 binlog cache,但是最终都写到同一个 binlog 文件:
- 图中的 write指的是把日志写入到 binlog 文件,但是并没有把数据持久化到磁盘,因为数据还缓存在文件系统的 page cache 里。write 的写入速度比较快的,因为不涉及磁盘 I/O。
- 图中的 fsync才是将数据持久化到磁盘的操作,这里就会涉及磁盘 I/O,所以频繁的 fsync 会导致磁盘的 I/O 升高。
MySQL 通过 sync_binlog 参数来控制 binlog 的刷盘时机:
-
sync_binlog = 0(默认):表示每次提交事务都只 write,不 fsync,由操作系统决定何时刷盘;
- MySQL默认的是 sync_binlog = 0,这时候的性能是最好的,但是风险也是最大的:因为一旦主机发生异常重启,还没持久化的数据就会丢失。
-
sync_binlog = 1 :表示每次提交事务 write 后,马上 fsync;
- 当 sync_binlog = 1 的时候,是最安全但是性能最差的。因为即使主机发生异常重启,最多只会丢失一个事务的 binlog,但是频繁的磁盘 I/O 对性能影响太大。
-
sync_binlog =N(N>1):表示每次提交事务都 write,但累积 N 个事务后才 fsync。
- 如果能接受少量事务的 binlog 丢失,为了提高写入的性能,一般会将 sync_binlog 设置为 100~1000 中的某个数值。
5.两阶段提交
(1)什么是两阶段提交?
两阶段提交就是把单个事务的提交拆成了 2 个阶段——准备(Prepare)阶段和提交(Commit)阶段,每个阶段都由协调者(Coordinator)和参与者(Participant)共同完成。
【tips】不要把提交(Commit)阶段和 commit 语句混淆了,commit 语句执行的时候,会包含提交(Commit)阶段。
(2)两阶段提交的过程
当客户端执行 commit 语句或者在自动提交的情况下,MySQL 内部会开启一个 XA 事务,将 redo log 的写入拆成了两个阶段:prepare 和 commit,中间再穿插写入binlog。
- prepare 阶段:将 XID(内部 XA 事务的 ID) 和记录的修改写入 redo log,同时将 redo log 对应的事务状态设置为 prepare,然后将 redo log 持久化到磁盘(innodb_flush_log_at_trx_commit = 1 的作用);
- 写入binlog:把 XID 和更新操作写入到 binlog,然后将 binlog 持久化到磁盘(sync_binlog = 1 的作用);
- commit 阶段:调用引擎的提交事务接口,将 redo log 状态设置为 commit(此时该状态并不需要持久化到磁盘,只需要 write 到文件系统的 page cache 中就够了,因为只要 binlog 写磁盘成功,就算 redo log 的状态还是 prepare 也没有关系,一样会被认为事务已经执行成功)。
(3)为什么需要两阶段提交?
事务提交后,redo log 和 binlog 都要持久化到磁盘,但这两个过程是相互独立的,可能出现半成功的状态(一个成功另一个未成功),这样会导致两份日志的逻辑不一致。
假设 id = 1 这行数据的 name 字段原来是"zhangsan",然后执行 UPDATE t_user SET name = 'lisi' WHERE id = 1; 如果在持久化 redo log 和 binlog 的过程中,出现了半成功状态,那么就有两种情况:
-
redo log 成功持久化,binlog 未持久化:
- 如果将 redo log 持久化到磁盘之后, MySQL 突然宕机了,而 binlog 还没来得及写入磁盘,这时 MySQL 重启后,通过 redo log 能将 name 字段恢复到更新后的值"lisi",但是 binlog 里面没有记录这条更新语句,所以从库的这一行记录的 name 字段仍是原来的"zhangsan",导致与主库数据不一致。
-
binlog 成功持久化,redo log 未持久化:
- 如果将 binlog 持久化到磁盘之后, MySQL 突然宕机了,而 redo log 还没来得及写入磁盘。由于redo log没有持久化,所以此时 MySQL 重启后这个事务相当于无效了,所以这行数据的 name 字段还是原来的"zhangsan",而 binlog 里面记录了这条更新语句,在从库中这一行记录的 name 字段就是更新后的"lisi",也会导致与主库数据不一致。
综上所述,因为 redo log 会影响主库的数据,binlog 会影响从库的数据,所以在持久化 redo log 和 binlog 的时候,如果出现半成功的状态,都会造成主从数据的不一致。因此 redo log 和 binlog 必须保持一致才能保证主从数据的一致。MySQL 使用了「两阶段提交」来解决redo log 和 binlog 日志逻辑不一致的问题。
(4)两阶段提交时发生的异常重启(两阶段提交如何保证redo log和binlog的数据一致性?)
不管是redo log 已经写入磁盘, 而 binlog 还没写入磁盘(时刻A),还是redo log 和 binlog 都已经写入磁盘,但是还没写入 commit 标识(时刻B),此时的 redo log 都处于 prepare 状态。
在 MySQL 重启后会按顺序扫描 redo log 文件,碰到处于 prepare 状态的 redo log,会判断 redo log 中的 XID 在 binlog 中是否存在:
- 如果 binlog 中没有 XID,说明 redo log 完成刷盘,但是 binlog 还没有刷盘,则回滚事务。(时刻A)
- 如果 binlog 中有 XID,说明 redolog 和 binlog 都已经完成了刷盘,则提交事务。(时刻B)
所以说对于 prepare 阶段的 redo log,即可以提交事务,也可以回滚事务,这取决于是否能在 binlog 中找到与 redo log 相同的 XID,如果有就提交事务,如果没有就回滚事务。这样就能保证 redo log 和 binlog 的一致性了。
换句话说,两阶段提交是以写入 binlog 成功为事务提交成功的标识,因为只要 binlog 写成功了,就意味着能在 binlog 中找到与 redo log 相同的 XID。
(我理解的两阶段提交的原理就是,用xid来标识redo log和binlog是否都写入成功了,都写入成功了二者就会有相同的xid,此时可以提交事务;如果binlog没写入成功,就没有跟redo log相同的xid,此时就回滚。)(5)两阶段提交带来了什么问题?
两阶段提交虽然保证了redo log 和binlog的数据一致性,但是性能很差,主要体现在两个方面:
-
磁盘 I/O 次数高:
binlog 和 redo log 在内存中都有对应的缓存空间,binlog 会缓存在 binlog cache,redo log 会缓存在 redo log buffer,它们持久化到磁盘的时机分别由 sync_binlog 和 innodb_flush_log_at_trx_commit 这两个参数控制。一般我们为了避免日志丢失,会将这两个参数设置为 1。这种 “双1” 配置,导致每个事务提交都会进行两次 fsync(刷盘),一次是 redo log 刷盘,另一次是 binlog 刷盘。 - 锁竞争激烈:两阶段提交在单事务中一定能保证两个日志的数据一致,但在多事务情况下,由于不能保证提交的顺序一致,所以无法保证二者的数据一致性。因此,在两阶段提交的基础上,还需要加一个锁来保证提交的原子性,从而保证多事务情况下,两个日志的提交顺序一致。
(6)如何解决两阶段提交带来的频繁I/O和锁竞争激烈问题?
MySQL 引入了 binlog 组提交(group commit)机制,当有多个事务提交的时候,会将多个 binlog 的刷盘操作合并成一个,从而减少磁盘 I/O 的次数。
引入了binlog组提交机制后,prepare 阶段不变,将 commit 阶段再拆分为三个阶段,每个阶段都有一个队列,有锁进行保护,保证了事务写入的顺序,第一个进入队列的事务会成为 leader,leader 领导所在队列的所有事务,负责整队的操作,完成后通知队内其他事务操作结束:
- flush 阶段:多个事务按进入的顺序将 binlog 从 cache 写入文件(不刷盘);
- sync 阶段:对 binlog 文件做 fsync 操作(多个事务的 binlog 合并一次刷盘);
- commit 阶段:各个事务按顺序进行提交。
(7)有 binlog 组提交,那有 redo log 组提交吗?
MySQL 5.7 才有的 redo log 组提交。
- 在 MySQL 5.7 之前,每个事务各自执行 prepare 阶段,也就是各自将 redo log 刷盘,这样就没办法对 redo log 进行组提交。
- 在 MySQL 5.7 中, prepare 阶段不再让事务各自执行 redo log 刷盘操作,而是推迟到组提交的 flush 阶段,也就是说 prepare 阶段融合在了 flush 阶段。通过延迟写 redo log 的方式,为 redolog 做了一次组写入。
(8)MySQL 磁盘 I/O 很高,有什么优化的方法?
事务在提交时需要将 binlog 和 redo log 持久化到磁盘(涉及到磁盘I/O),如果出现磁盘 I/O 很高的现象,我们可以通过控制以下参数,来降低磁盘 I/O 的频率:
-
设置组提交的两个参数: binlog_group_commit_sync_delay 和 binlog_group_commit_sync_no_delay_count 参数,延迟 binlog 刷盘的时机,从而减少 binlog 的刷盘次数。
【缺点】这个方法可能会增加语句的响应时间,但即使 MySQL 进程中途挂了,也没有丢失数据的风险,因为 binlog 早被写入到 page cache 了,只要系统没有宕机,缓存在 page cache(系统文件缓存) 里的 binlog 就会被持久化到磁盘。 -
将 sync_binlog 设置为大于 1 的值(比较常见是 100~1000):表示每次提交事务都 write,但累积 N 个事务后才 fsync,相当于延迟了 binlog 刷盘的时机。
【缺点】主机掉电时会丢 N 个事务的 binlog 日志。 -
将 innodb_flush_log_at_trx_commit 设置为 :表示每次事务提交时,都只是把缓存在 redo log buffer 里的 redo log 写到 Page Cache,然后由操作系统控制持久化到磁盘的时机。
【缺点】主机掉电的时候会丢数据。
七、count(*) 和 count(1) 有什么区别?哪个性能最好?
先说结论:
1.count() 是什么?
count() 的参数不仅可以是字段名,也可以是其他任意表达式(比如count(1)),该函数作用是统计符合查询条件的记录中,函数指定参数不为 NULL 的记录有多少个,也就是说如果一条记录的指定字段为null,就不会被统计。
2.count(主键字段) 执行过程是怎样的?
MySQL在用count函数统计时会维护一个count变量。如果表里只有主键索引没有二级索引,innoDB会循环遍历主键索引,对读到的记录的指定字段进行判断,如果不为null就给count+1;如果表里有二级索引,innoDB会遍历二级索引,同样对读到的记录进行null的判断。最后将count变量的值返回给客户端。
【为什么有二级索引就不走主键索引了?】
因为对于相同的记录,二级索引比聚簇索引占用更少的存储空间,所以二级索引树比聚簇索引树小,这样遍历二级索引的成本就比遍历聚簇索引的成本低,所以「优化器」会优先选择二级索引。
3.count(1) 执行过程是怎样的?
如果说表中只有主键索引没有二级索引,innoDB会遍历主键索引,读到一条记录就会将count+1;如果表里有二级索引,InnoDB 就会循环遍历就二级索引。
注意!count(1)在执行时不会读取记录中的任何字段(因为 count 函数的参数是 1,不是字段,所以不需要读取记录中的字段值),因此说count(1) 比 count(主键字段) 少一个步骤,所以通常会说 count(1) 执行效率会比 count(主键字段) 高一点。而且1不是NULL,所以即使记录中某个字段的值为NULL也会被统计。
4.count(*) 执行过程是怎样的?
count(*) 其实等于 count(0),也就是说,当用 count(*) 时,MySQL 会将参数 * 当作 0 来处理。所以,count(*) 执行过程跟 count(1) 执行过程基本一样,性能没有什么差异。
5.count(字段) 执行过程是怎样的?
count()一个普通的非主键字段时,会采用全表扫描来统计,所以说它的效率是最差的。
6.如何优化count(*)?
对于一张记录数很多的表即使创建了二级索引,也不建议经常用count(*)来统计,它的耗时还是会很慢。比如一个有1200+万条的表,执行 select count(*) from t_order 要花费差不多 5 秒!
(1)第一种,近似值
如果业务对于统计结果不需要很精确,比如搜索引擎在搜索关键词的时候,给出的搜索结果条数是一个大概值。可以用 show table status 或者 explain 命令来进行估算。
(2)第二种,额外表保存计数值
可以将表的总记录数保存到单独的一张计数表中。当在数据表插入一条记录的同时,将计数表中的计数字段 + 1。也就是说,在新增和删除操作时,需要额外维护这个计数表。
八、补充
1.MySQL支持哪些存储引擎?有什么区别?
(1)MySQL支持的存储引擎
我了解的MySQL支持的存储引擎有 InnoDB 和 MyISAM。
(2)MyISAM 和 InnoDB 有什么区别?
- 是否支持行级锁:MyISAM 最小的锁粒度是表级锁,而 InnoDB 还支持行级锁。也就是说 MyISAM 一锁就锁住了整张表,所以 InnoDB 在并发写情况下的性能比 MyISAM 好很多;
- 是否支持事务:MyISAM 不支持事务,而InnoDB 支持事务;
- 数据恢复:InnoDB 支持故障恢复,当数据库崩了,可以依靠redo log实现数据恢复;而 MyISAM 不支持数据恢复;
- 索引的实现:虽然 MyISAM 和 InnoDB 都用 B+树作为索引结构,但是实现方式不太一样:在 InnoDB 中,索引和数据都放在叶子节点上;而在 MyISAM 中,索引和数据是分离的。
2.