图解MySQL笔记

文章目录

    • 2.1 执行一条 select 语句,期间发生了什么?
      • MySQL 执行流程是怎样的?
        • 第一步:连接器
        • 第二步:查询缓存
        • 第三步:解析器 解析SQL
        • 第四步:执行 SQL
    • 2.2 MySQL 一行记录是怎么存储的?
      • MySQL 的数据存放在哪个文件?
      • 表空间文件的结构是怎么样的?
      • InnoDB行格式有哪些?
      • varchar(n)中n最大取值为多少?
      • 行溢出后,MySQL 是怎么处理的?
    • 3.1 索引常见面试题
      • 索引的分类
      • 什么时候需要 / 不需要创建索引?
      • 有什么优化索引的方法?
    • 3.2 从数据页的角度看B + 树
      • 数据页的组成
      • B + 树是如何进行查询的?
    • 3.3 为什么MySQL采用B + 树作为索引?
      • 怎样的索引的数据结构是好的?
      • 为什么MySQL使用B + 树作为索引?
    • 3.4 MySQL单表不要超过2000W行,靠谱吗?
    • 3.5 索引失效有哪些?
    • 3.6 MySQL使用like "%x",索引一定会失效吗?
    • 3.7 count(*)和count(1)有什么区别?哪个性能好?
    • 4.1 事务隔离级别是怎么实现的?
      • 事务的隔离级别
      • Read View 在MVCC 中是如何工作的?
      • 可重复读是如何工作的?
      • 读提交是如何工作的?
    • 4.2 MySQL可重复读隔离级别,完全解决幻读了吗?
    • 5. 1MySQL有哪些锁?
      • 全局锁
      • 表级锁
      • 行级锁
    • 5.2 MySQL是怎么加锁的?
    • 5.3 update没加索引会锁全表?
    • 5.4 MySQL死锁了,怎么办?
    • 5.5 字节面试: 加了什么锁,导致死锁的?
    • 6.1 MySQL日志:undo log、redo log、binlog 有什么用?
      • 为什么需要undo log?
      • 为什么需要redo log?
      • redo log什么时候刷新到磁盘?
      • redo log文件写满了怎么办?
      • 为什么需要binlog?
      • redo log 和binlog 有什么区别?
      • 主从复制是怎么实现的?
      • binlog 什么时候刷新到磁盘?
      • 为什么需要两阶段提交?
        • 异常重启出现什么问题?
        • 两阶段提交的问题?
        • 组提交
      • MySQL磁盘I/O很高的优化方法
      • 总结
    • 7.1 解开Buffer Pool 的面纱
      • 为什么要有Buffer Pool?
      • 如何管理Buffer Pool?

2.1 执行一条 select 语句,期间发生了什么?

MySQL 执行流程是怎样的?

图解MySQL笔记_第1张图片

  • MySQL 的架构共分为两层:Server 层和存储引擎层
    • Server 层负责建立连接、分析和执行 SQL。MySQL 大多数的核心功能模块都在这实现,主要包括连接器,查询缓存、解析器、预处理器、优化器、执行器等。
    • 存储引擎层负责数据的存储和提取。支持 InnoDB、MyISAM、Memory 等多个存储引擎,不同的存储引擎共用一个 Server 层。现在最常用的存储引擎是 InnoDB,从 MySQL 5.5 版本开始, InnoDB 成为了 MySQL 的默认存储引擎。

第一步:连接器

  • 连接器的工作主要如下:
    • MySQL是基于TCP协议进行传输的,需要与客户端进行 TCP 三次握手建立连接
    • 校验客户端的用户名和密码,如果用户名或密码不对,则会报错;
    • 如果用户名和密码都对了,会读取该用户的权限,然后后面的权限逻辑判断都基于此时读取到的权限;

  • 查看MySQL服务被多少客户端连接了?
    • 执行 show processlist 命令进行查看。
  • 空闲连接会一直占用着吗?
    • MySQL 定义了空闲连接的最大空闲时长,由 wait_timeout 参数控制的,默认值是 8 小时(28880秒)。
  • MySQL 的连接数有限制吗?
    • MySQL的连接和HTTP一样,也有长连接和短链接的概念。
// 短连接
连接 mysql 服务(TCP 三次握手)
执行sql
断开 mysql 服务(TCP 四次挥手)

// 长连接
连接 mysql 服务(TCP 三次握手)
执行sql
执行sql
执行sql
....
断开 mysql 服务(TCP 四次挥手)
  • MySQL 服务支持的最大连接数由 max_connections 参数控制,比如我的 MySQL 服务默认是 151 个,超过这个值,系统就会拒绝接下来的连接请求,并报错提示“Too many connections”。
  • 怎么解决长连接占用内存的问题?
    • 第一种,定期断开长连接。既然断开连接后就会释放连接占用的内存资源,那么我们可以定期断开长连接。
    • 第二种,客户端主动重置连接。MySQL 5.7 版本实现了 mysql_reset_connection() 函数的接口,注意这是接口函数不是命令,那么当客户端执行了一个很大的操作后,在代码里调用 mysql_reset_connection 函数来重置连接,达到释放内存的效果。这个过程不需要重连和重新做权限验证,但是会将连接恢复到刚刚创建完时的状态。

第二步:查询缓存

  • 连接器的工作完成后,客户端就可以向 MySQL 服务发送 SQL 语句了,MySQL 服务收到 SQL 语句后,就会解析出 SQL 语句的第一个字段,看看是什么类型的语句。
    如果 SQL 是查询语句(select 语句),MySQL 就会先去查询缓存( Query Cache )里查找缓存数据,看看之前有没有执行过这一条命令,这个查询缓存是以 key-value 形式保存在内存中的,key 为 SQL 查询语句,value 为 SQL 语句查询的结果。如果查询的语句命中查询缓存,那么就会直接返回 value 给客户端。如果查询的语句没有命中查询缓存中,那么就要往下继续执行,等执行完后,查询的结果就会被存入查询缓存中。
    • 对于更新比较频繁的表,查询缓存的命中率很低的,因为只要一个表有更新操作,那么这个表的查询缓存就会被清空。所以,MySQL 8.0 版本直接将查询缓存删掉了。

注意,这里的查询缓存是server层的,并不是InnoDB存储引擎中的Buffer Pool。

第三步:解析器 解析SQL

  • 第一件事情,词法分析。MySQL 会根据你输入的字符串识别出关键字出来,构建出 SQL 语法树,这样方便后面模块获取 SQL 类型、表名、字段名、 where 条件等等。
  • 第二件事情,语法分析。根据词法分析的结果,语法解析器会根据语法规则,判断你输入的这个 SQL 语句是否满足 MySQL 语法。

第四步:执行 SQL

  • 经过解析器后,接着就要进入执行 SQL 查询语句的流程了,每条SELECT 查询语句流程主要可以分为下面这三个阶段:
    • prepare 阶段,也就是预处理阶段,使用预处理器
      • 检查 SQL 查询语句中的表或者字段是否存在;
      • select * 中的 * 符号,扩展为表上的所有列;(SELECT * 意为查询所有列)
    • optimize 阶段,也就是优化阶段,使用优化器
      • 负责将 SQL 查询语句的执行方案确定下来,选择查询成本最小的执行计划,比如在表里面有多个索引的时候,优化器会基于查询成本的考虑,来决定选择使用哪个索引。如果想要知道优化器选择了哪个索引,我们可以在查询语句最前面加个 explain 命令,这样就会输出这条 SQL 语句的执行计划。
    • execute 阶段,也就是执行阶段,使用执行器
      • 根据执行计划执行 SQL 查询语句,从存储引擎读取记录,返回给客户端;在执行的过程中,执行器就会和存储引擎交互了,交互是以记录为单位的。
  • 用三种方式执行过程,体现执行器和存储引擎的交互过程:
    • 主键索引查询
select * from product where id = 1;
  • 执行器第一次查询,会调用 read_first_record 函数指针指向的函数,因为优化器选择的访问类型为 const(表示按主键读取,,只读一次),这个函数指针被指向为 InnoDB 引擎索引查询的接口,把条件 id = 1 交给存储引擎,让存储引擎定位符合条件的第一条记录
  • 存储引擎通过主键索引的 B+ 树结构定位到 id = 1的第一条记录,如果记录是不存在的,就会向执行器上报记录找不到的错误,然后查询结束。如果记录是存在的,就会将记录返回给执行器;
  • 执行器从存储引擎读到记录后,接着判断记录是否符合查询条件,如果符合则发送给客户端,如果不符合则跳过该记录。
  • 执行器查询的过程是一个 while 循环,所以还会再查一次,但是这次因为不是第一次查询了,所以会调用 read_record 函数指针指向的函数,因为优化器选择的访问类型为 const,这个函数指针被指向为一个永远返回 - 1 的函数,所以当调用该函数的时候,执行器就退出循环,也就是结束查询了。
  • 全表扫描
select * from product where name = 'iphone';
  • 执行器第一次查询,会调用 read_first_record 函数指针指向的函数,因为优化器选择的访问类型为 all,这个函数指针被指向为 InnoDB 引擎全扫描的接口,让存储引擎读取表中的第一条记录
    • 执行器判断 name == iphone ?如果不是则跳过;如果是则将记录发给客户端(是的没错,Server 层每从存储引擎读到一条记录就会发送给客户端,之所以客户端显示的时候是直接显示所有记录的,是因为客户端是等查询语句查询完成后,才会显示出所有的记录)。
    • 执行器查询的过程是一个 while 循环,所以还会再查一次,会调用 read_record 函数指针指向的函数,因为优化器选择的访问类型为 all,read_record 函数指针指向的还是 InnoDB 引擎全扫描的接口,所以接着向存储引擎层要求继续读刚才那条记录的下一条记录,存储引擎把下一条记录取出后就将其返回给执行器(Server层),执行器继续判断条件,不符合查询条件即跳过该记录,否则发送到客户端;
    • 一直重复上述过程,直到存储引擎把表中的所有记录读完,向执行器(Server层) 返回了读取完毕的信息; 执行器收到存储引擎报告的查询完毕的信息,退出循环,停止查询。
  • 索引下推
    索引下推能够减少二级索引在查询时的回表操作,提高查询的效率,因为它将 Server 层负责的事情,交给存储引擎层去处理了。
select * from t_user  where age > 20 and reward = 100000;


联合索引当遇到范围查询 (>、<) 就会停止匹配,也就是 age 字段能用到联合索引,但是 reward 字段则无法利用到索引。注意,联合索引也是二级索引的一种。
不使用索引下推优化:

  • Server 层首先调用存储引擎的接口定位到满足查询条件的第一条二级索引记录,也就是定位到 age > 20 的第一条记录;
  • 存储引擎根据二级索引的 B+ 树快速定位到这条记录后,获取主键值,然后进行回表操作,将完整的记录返回给 Server 层;
  • Server 层在判断该记录的 reward 是否等于 100000,如果成立则将其发送给客户端;否则跳过该记录;
  • 接着,继续向存储引擎索要下一条记录,存储引擎在二级索引定位到记录后,获取主键值,然后回表操作,将完整的记录返回给 Server 层;
  • 如此往复,直到存储引擎把表中的所有记录读完。

使用索引下推优化:

  • Server 层首先调用存储引擎的接口定位到满足查询条件的第一条二级索引记录,也就是定位到 age > 20 的第一条记录;
  • 存储引擎定位到二级索引后,先不执行回表操作,而是先判断一下该索引中包含的列(reward列)的条件(reward 是否等于 100000)是否成立。如果条件不成立,则直接跳过该二级索引。如果成立,则执行回表操作,将完成记录返回给 Server 层。
  • 之后Server 层再判断其他的查询条件(本次查询没有其他条件)是否成立,如果成立则将其发送给客户端;否则跳过该记录,然后向存储引擎索要下一条记录。
  • 如此往复,直到存储引擎把表中的所有记录读完。

2.2 MySQL 一行记录是怎么存储的?

MySQL 的数据存放在哪个文件?

mysql> SHOW VARIABLES LIKE 'datadir';
+---------------+-----------------+
| Variable_name | Value           |
+---------------+-----------------+
| datadir       | /var/lib/mysql/ |
+---------------+-----------------+
1 row in set (0.00 sec)
  • 进入/var/lib/mysql/my_test目录,创建一个t_order表,会随之创建三个文件:
    • db.opt,字符相关。用来存储当前数据库的默认字符集和字符校验规则。
    • t_order.frm ,t_order 的表结构会保存在这个文件。在 MySQL 中建立一张表都会生成一个.frm 文件,该文件是用来保存每个表的元数据信息的,主要包含表结构定义。
    • t_order.ibd,t_order 的表数据会保存在这个文件。
      • 参数 innodb_file_per_table 控制表数据存在哪里,从 MySQL 5.6.6 版本开始,它的默认值就是 1 了,因此从这个版本之后, MySQL 中每一张表的数据都存放在一个独立的 .ibd 文件。
      • MySQL8.0之后,不再使用.frm文件。只使用.idb文件。

表空间文件的结构是怎么样的?

表空间由段(segment)、区(extent)、页(page)、行(row)组成

图解MySQL笔记_第2张图片

  • 行:数据库表中的记录都是按行(row)进行存放的,每行记录根据不同的行格式,有不同的存储结构。
  • 页:InnoDB 的数据是按「页」为单位来读写的,也就是说,当需要读一条记录的时候,并不是将这个行记录从磁盘读出来,而是以页为单位,将其整体读入内存。默认每个页的大小为 16KB,也就是最多能保证 16KB 的连续存储空间。
  • 区:B + 树每一层都是通过双向链表连接的,如果只使用页来存储空间,可能链表中相邻的两个页之间物理位置并不是连续的。在表中数据量大的时候,为某个索引分配空间的时候就不再按照页为单位分配了,而是按照区(extent)为单位分配。每个区的大小为 1MB,对于 16KB 的页来说,连续的 64 个页会被划为一个区,这样就使得链表中相邻的页的物理位置也相邻,就能使用顺序 I/O 了
  • 段:表空间是由各个段(segment)组成的,段是由多个区(extent)组成的。段一般分为数据段、索引段和回滚段等。
    • 索引段:存放 B + 树的非叶子节点的区的集合;
    • 数据段:存放 B + 树的叶子节点的区的集合;
    • 回滚段:存放的是回滚数据的区的集合。

InnoDB行格式有哪些?

InnoDB 提供了 4 种行格式,分别是 Redundant(冗余)、Compact(紧凑)、Dynamic(动态)和 Compressed (压缩)行格式。

  • Redundant 是很古老的行格式了, MySQL 5.0 版本之前用的行格式。
  • Dynamic 和 Compressed 两个都是紧凑的行格式,它们的行格式都和 Compact 差不多,因为都是基于 Compact 改进一点东西。从 MySQL5.7 版本之后,默认使用 Dynamic 行格式。
  • 由于 Redundant 不是一种紧凑的行格式,所以 MySQL 5.0 之后引入了 Compact 行记录存储方式,Compact 是一种紧凑的行格式,设计的初衷就是为了让一个数据页中可以存放更多的行记录,从 MySQL 5.1 版本之后,行格式默认设置成 Compact。 图解MySQL笔记_第3张图片
    • 记录的额外信息
      • 其中包含 3 个部分:变长字段长度列表、NULL 值列表、记录头信息。
      • 变长字段长度列表:存放变长字段的数据的长度大小,读取数据时根据这个「变长字段长度列表」去读取对应长度的数据。比如varchar就是变长的,存储的实际大小是不确定的。
        • **变长字段长度列表和NULL值列表的信息要按照逆序存放。**逆序存放主要是因为记录头信息中指向下一个记录的指针,指向的是下一条记录的「记录头信息」和「真实数据」之间的位置,这样的好处是向左读就是记录头信息,向右读就是真实数据,比较方便。这样使得位置靠前的记录的真实数据和数据对应的字段长度信息可以同时在一个 CPU Cache Line 中,这样就可以提高 CPU Cache 的命中率
        • 变长字段列表不是必须的。当数据表没有变长字段的时候,比如全部都是 int 类型的字段,就会去掉节省空间。
      • NULL值列表:表中的某些列可能会存储 NULL 值,如果把这些 NULL 值都放到记录的真实数据中会比较浪费空间,所以 Compact 行格式把这些值为 NULL 的列存储到 NULL值列表中。
        • 如果存在允许 NULL 值的列,则每个列对应一个二进制位(bit),二进制位按照列的顺序逆序排列。0表示不为NULL,1表示为NULL。
        • NULL 值列表也不是必须的。当数据表的字段都定义成 NOT NULL 的时候,这时候表里的行格式就不会有 NULL 值列表了
        • NULL 值列表的空间不是固定 1 字节的。当一条记录有 9 个字段值都是 NULL,那么就会创建 2 字节空间的NULL 值列表,以此类推。
      • 记录头信息:有几个比较重要的记录头信息。
        • delete_mask :标识此条数据是否被删除。从这里可以知道,我们执行 detele 删除记录的时候,并不会真正的删除记录,只是将这个记录的 delete_mask 标记为 1。
        • next_record:下一条记录的位置。从这里可以知道,**记录与记录之间是通过链表组织的。**指向的是下一条记录的「记录头信息」和「真实数据」之间的位置,这样的好处是向左读就是记录头信息,向右读就是真实数据,比较方便。
        • record_type:表示当前记录的类型,0表示普通记录,1表示B+树非叶子节点记录,2表示最小记录,3表示最大记录。
    • 记录的真实数据
      trx_id 和 roll_pointer主要为MVCC服务。
      • row_id:如果我们建表的时候指定了主键或者唯一约束列,那么就没有 row_id 隐藏字段了。如果既没有指定主键,又没有唯一约束,那么 InnoDB 就会为记录添加 row_id 隐藏字段。row_id不是必需的,占用 6 个字节。
      • trx_id:事务id,表示这个数据是由哪个事务生成的。 trx_id是必需的,占用 6 个字节。
      • roll_pointer:这条记录上一个版本的指针。roll_pointer 是必需的,占用 7 个字节。

varchar(n)中n最大取值为多少?

MySQL 规定除了 TEXT、BLOBs 这种大对象类型之外,其他所有的列(不包括隐藏列和记录头信息)占用的字节长度加起来不能超过 65535 个字节(16位)。

varchar(n) 字段类型的 n 代表的是最多存储的字符数量,并不是字节大小。所以要看数据库表的字符集,因为字符集代表着1个字符要占用多少字节,比如 ascii 字符集, 1 个字符占用 1 字节,那么 varchar(100) 意味着最大能允许存储 100 字节的数据。

  • 单字段的情况只有一个 varchar(n) 类型的列且字符集是 ascii。varchar(n) 中 n 最大值 = 65535 - 2(「变长字段长度列表」所占字节数) - 1(「NULL值列表」所占字节数) = 65532
    • 计算变长字段长度列表:「变长字段长度列表」所占用的字节数 = 所有「变长字段长度」占用的字节数之和。
      • 条件一:如果变长字段允许存储的最大字节数小于等于 255 字节,就会用 1 字节表示「变长字段长度」;
        条件二:如果变长字段允许存储的最大字节数大于 255 字节,就会用 2 字节表示「变长字段长度」;
        • 字段类型是 varchar(65535) ,字符集是 ascii,所以代表着变长字段允许存储的最大字节数是 65535,符合条件二,所以会用 2 字节来表示「变长字段长度」。
    • 计算NULL值列表:创建表时,如果字段是允许为 NULL 的,用 1 字节来表示「NULL 值列表」。
  • 多字段的情况:如果有多个字段的话,要保证所有字段的长度 + 变长字段长度列表所占用的字节数 + NULL值列表所占用的字节数 <= 65535。

行溢出后,MySQL 是怎么处理的?

如果一个数据页存不了一条记录,InnoDB 存储引擎会自动将溢出的数据存放到「溢出页」中。在一般情况下,InnoDB 的数据都是存放在 「数据页」中。但是当发生行溢出时,溢出的数据会存放到「溢出页」中。

当发生行溢出时,在记录的真实数据处只会保存该列的一部分数据,而把剩余的数据放在「溢出页」中,然后真实数据处用 20 字节存储指向溢出页的地址,从而可以找到剩余数据所在的页。

图解MySQL笔记_第4张图片

Compressed 和 Dynamic 这两个行格式采用完全的行溢出方式,记录的真实数据处不会存储该列的一部分数据,只存储 20 个字节的指针来指向溢出页。实际的数据都存储在溢出页中。

3.1 索引常见面试题

索引的分类

  • 什么是索引?
    索引是一种数据结构,可以帮助MySQL快速定位到表中的数据。使用索引,可以大大提高查询的性能。
  • 按「数据结构」分类:B+tree索引、Hash索引、Full-text索引
    InnoDB 存储引擎创建的聚簇索引或者二级索引默认使用的都是 B+Tree 这种数据结构。
    Full-text是全文索引,在搜索一些长文本的情况下使用最好。
  • 按「存储类型」分类:聚簇索引(主键索引)、二级索引(辅助索引)。术语“聚簇”表示数据行和相邻的键值聚簇地存储在一起。这也形象地说明了聚簇索引叶子节点的特点,保存表的完整数据。
    对于B + 树来说,每个节点都是一个数据页。B + 树只有叶子节点才会存放数据,非叶子节点仅用来存放目录项作为索引。所有节点按照索引键大小排序,构成双向链表,便于顺序查找和范围查找。
    而B + Tree索引又可以分成聚簇索引和二级索引(非聚簇索引),它们区别就在于叶子节点存放的是什么数据。
    • 聚簇索引的叶子节点存放的是实际数据,所有完整的用户记录都存放在叶子节点
      • InnoDB存储引擎一定会为表创建一个聚簇索引,一般情况下会使用主键作为聚簇索引,且一张表只允许存在一个聚簇索引。
        • 如果没有主键,就选择第一个不包含 NULL 值的唯一列作为聚簇索引的索引键;
        • 在上面两个都没有的情况下,InnoDB 将自动生成一个隐式自增 id 列作为聚簇索引的索引键;
    • 由于一张表只能有一个聚簇索引,为了实现非主键字段的快速搜索,就引出了二级索引(非聚簇索引/辅助索引),它也是利用了 B+ 树的数据结构,但是二级索引的叶子节点存放的是主键值,不是实际数据。
      • 回表,就是先检索二级索引,找到叶子节点并获取主键值,通过聚簇索引查询到对应的叶子节点,也就是要查两个 B+Tree 才能查到数据
      • 索引覆盖,就是当查询的数据是主键值时,那么在二级索引就能查询到,不用再去聚簇索引查,就表示发生了索引覆盖,也就是只需要查一个 B+Tree就能找到数据
  • 按「字段特性」分类:主键索引、唯一索引、普通索引、前缀索引。
    • 主键索引也叫聚簇索引,就是建立在主键字段上的索引,在创建表的时候一起创建,一张表最多只有一个主键索引,索引列的值不允许有空值。
    • 唯一索引建立在 UNIQUE 字段上的索引,表示索引列的值必须唯一,允许有空值。一张表可以有多个唯一索引。
    • 普通索引就是建立在普通字段上的索引,既不要求字段为主键,也不要求字段为 UNIQUE。
    • 前缀索引是指对字符型字段的前几个字符建立的索引。使用前缀索引的目的是为了减少索引占用的存储空间,提升查询效率。
  • 按「字段个数」分类:单列索引、联合索引。
    • 单列索引:一个字段的索引。
    • 联合索引:将多个字段组合成一个索引。
      • 联合索引按照最左匹配原则,如果创建了一个 (a, b, c) 联合索引,查询条件存在a就可以匹配上联合索引,比如where a=1;
        • 比如联合索引(a,b),a全局有序(1,2,2,3,4,5),b是全局无序的(12,7,8,2,3,1)。因此,直接执行where = 2这种查询没办法利用联合索引。利用索引的前提是,索引里的key是有序的。
          只有在 a 相同的情况才,b 才是有序的,比如 a 等于 2 的时候,b 的值为(7,8),这时就是有序的,这个有序状态是局部的,因此,执行where a = 2 and b = 7是 a 和 b 字段能用到联合索引的,也就是联合索引生效了。

联合索引的最左匹配原则会一直向右匹配直到遇到范围查询(>、<)就会停止往下使用联合索引。也就是范围查询的字段可以用到联合索引,但是在范围查询字段之后的字段无法用到联合索引。注意,对于 >=、<=、BETWEEN、like 前缀匹配,这类范围查询,并不会停止使用索引,两个字段都会用到联合索引查询,但是只是 = 的部分用到了。

  • 索引下推优化**(index condition pushdown,ICP), 是针对联合索引的一种优化。可以在联合索引遍历过程中,对联合索引中包含的字段先做判断**,直接过滤掉不满足条件的记录,从而减少回表次数。
    • 因为二级索引存储字段和主键值,联合索引在二级索引中的形态是存储联合索引中的所有字段和主键值。所以可以优先对联合索引中包含的字段先做判断
  • 实际开发工作中在建立联合索引时,建议把区分度大的字段排在前面,区分度越大,搜索的记录越少。 UUID 这类字段就比较适合做索引或排在联合索引列的靠前的位置。
    比如,性别的区分度就很小,字段的值分布均匀,那么无论搜索哪个值都可能得到一半的数据。

什么时候需要 / 不需要创建索引?

  • 索引的缺点
    • 索引是一种数据结构,也需要占用磁盘空间
    • 创建、维护索引要耗费时间,这种时间随着数据量的增加而增大;
    • 索引有可能失效。
  • 什么时候适用索引?
    • 字段有唯一性限制,比如商品编码。
    • 经常用于 WHERE 查询条件、GROUP BY 和 ORDER BY 的字段
      • 在查询的时候就不需要再去做一次排序了,因为 B+Tree 的记录都是有序的。
  • 什么时候不需要创建索引?
    • WHERE 条件,GROUP BY,ORDER BY 里用不到的字段
      • 因为索引的价值是快速定位,如果起不到定位作用的字段不需要创建索引。
    • 字段中存在大量重复数据,不需要创建索引
      • MySQL 有一个查询优化器,查询优化器发现某个值出现在表的数据行中的百分比很高的时候,它一般会忽略索引,进行全表扫描。
    • 表数据太少的时候,不需要创建索引。
    • 如果字段经常更新,不需要创建索引。
      • 维护B + 树的结构,就需要频繁重建索引,影响数据库性能。

有什么优化索引的方法?

  • 使用前缀索引:使用字段中字符串的前几个字符建立索引,可以减小索引字段大小,节省空间。可以增加一个索引页存储前缀索引值,从而提高索引查询速度。
    • 前缀索引的局限性
      order by 就无法使用前缀索引,因为前缀字符无法排序。
      无法把前缀索引用作覆盖索引,因为一般只有查询主键值时会用到覆盖索引。
  • 使用覆盖索引:争取 SQL 中查询的所有字段,都能从二级索引中查询得到记录,避免回表的操作。
  • 主键索引最好是自增的:如果我们使用主键自增,就不需要频繁改变B + 树的结构,直接按顺序插入。
    • 如果我们使用非自增主键,每次主键值都是随机的,插入新的数据时,有可能产生数据页分裂,导致索引结构不紧凑,从而影响查询效率。
  • 索引列最好设置为NOT NULL(非空)约束:索引列存在 NULL 就会导致优化器在做索引选择的时候更加复杂,更加难以优化。因为可为 NULL 的列会使索引、索引统计、值的比较,都更复杂。比如进行索引统计时,count 会省略值为NULL 的行。
    并且NULL 值是一个没意义的值,行格式会至少使用1字节空间存储NULL 值列表,会占用物理空间。

3.2 从数据页的角度看B + 树

数据页的组成

InnoDB的数据是按照数据页为单位来读写的,默认的大小是16KB。

数据页由七个部分组成:文件头File Header)、页头(Page Header)、用户空间UserRecords) 、最大、最小记录(Infimum + Supermum)、空闲空间(Free + Space)、页目录PageDirectory)、文件尾(File Trailer)。

  • 文件头:文件头有两个指针,分别指向上一个和下一个数据页,连接起来的数据页相当于一个双向链表。实现逻辑上的连续存储。
  • 页目录:
    页目录创建的过程如下:
    1. 将所有的记录划分成几个组,这些记录包括最小记录和最大记录,但不包括标记为“已删除”的记录;
    2. 每个记录组的最后一条记录就是组内最大的那条记录,并且最后一条记录的头信息中会存储该组一共有多少条记录,作为 n_owned 字段
    3. 页目录用来存储每组最后一条记录的地址偏移量,这些地址偏移量会按照先后顺序存储起来,每组的地址偏移量也被称之为槽(slot),每个槽相当于指针指向了不同组的最后一个记录

页目录起到数据的索引作用。数据页中的数据**按照主键的顺序组成单向链表。**页目录由多个槽组成,**槽相当于分组数据的索引。**我们通过槽查找记录时,可以使用二分法快速定位要查询的记录在哪个槽(哪个记录分组),随后遍历槽内的所有记录,找到对应的记录。每个槽对应的值都是这个组的主键最大的记录。 图解MySQL笔记_第5张图片

  • 用户空间:在页的 7 个组成部分中,我们自己存储的记录会按照我们指定的行格式存储到 用户空间(User Records) 部分。一开始生成页的时候,并没有这个部分,每当我们插入一条记录,都会从 Free Space 部分申请一个记录大小的空间划分到User Records部分。当 Free Space 部分的空间全部被 User Records 部分替代掉之后,也就意味着这个页使用完了,如果还有新的记录插入的话,就需要去申请新的页了。

B + 树是如何进行查询的?

在进行查询时,B + 树通过二分法快速定位到包含该记录的查询范围,继续通过二分法定位到页。定位到该页后,又会在该页内进行二分法快速定位记录所在的分组(槽号),最后在分组内进行遍历查找。

3.3 为什么MySQL采用B + 树作为索引?

怎样的索引的数据结构是好的?

  • 读取数据时,先读取索引到内存,再通过索引找到磁盘中的某行数据,然后读到内存中,磁盘I/O操作次数越多,所消耗的时间也越大。所以MySQL的索引应该要尽可能减少磁盘I/O次数,并且能够高效地查找某一个记录,也要能高效的范围查找。
  • 为什么不用二叉查找树?
    • 二叉查找树的特点是一个节点的左子树的所有节点都小于这个节点,右子树的所有节点都大于这个节点。
    • 二叉查找树的搜索速度快,解决了插入新节点的问题,但是如果每次插入的元素都是二叉查找树中最大的元素,二叉查找树就会退化成了一条链表,查找数据的时间复杂度变成了 O(n)。随着元素插入越多,树的高度越高,磁盘IO操作也就越多,查询性能严重下降。
  • 为什么不用自平衡二叉树(AVL树)?
    • 自平衡二叉树在二叉查找树的基础上增加了一些条件约束:每个节点的左子树和右子树的高度差不能超过 1
    • 但是和二叉查找树一样,随着插入的元素变多,会导致树的高度变高,磁盘IO操作次数就会变多,影响整体数据查询效率。
  • 为什么不用B树?
    • B树解决了树的高度问题,但是B树的每个节点都包含数据(索引+记录),而用户记录的数据大小有可能远远超过索引数据,就要花费更多的IO来读取到有用的索引数据。
    • 一些用不到的数据也会从磁盘加载到内存中,会增加IO操作次数,浪费内存资源。
  • B + 树对B树进行了升级,与B树的区别主要是以下几点
    • 叶子节点才会存放实际数据(索引+记录),非叶子节点只会存放索引;
    • 叶子节点之间构成一个有序链表叶子结点本身依关键字的大小自小而大顺序链接。对于范围查找可以提高效率;
    • 非叶子节点的索引也会同时存在在子节点中,并且是在子节点中所有索引的最大(或最小)。
    • 非叶子节点中有多少个子节点,就有多少个索引;
  • Innodb 使用的 B+ 树有一些特别的点,比如:
    • B+ 树的叶子节点之间是用「双向链表」进行连接,这样的好处是既能向右遍历,也能向左遍历。
    • B+ 树节点的内容是数据页,数据页里存放了用户的记录以及各种信息,每个数据页默认大小是 16 KB。

为什么MySQL使用B + 树作为索引?

MySQL 默认的存储引擎 InnoDB 采用的是 B+ 树作为索引的数据结构,原因有如下几点:

  • B+ 树的非叶子节点仅存放索引
    • 在数据量相同的情况下,相比既存索引又存记录的 B 树,B+树的非叶子节点可以存放更多的索引,因此 B+ 树可以比 B 树更「矮胖」,查询底层节点的磁盘 I/O次数会更少。
  • B+ 树有大量的冗余节点
    • 这些冗余索引让 B+ 树在插入、删除的效率都更高,比如删除根节点的时候,不会像 B 树那样会发生复杂的树的变化;
  • B+ 树将数据按照顺序存储在叶子节点中,叶子节点之间用双向链表连接,既能向右遍历,也能向左遍历,在排序操作和范围查询中有很高的效率。
    • B+Tree 存储千万级数据只需要 3-4 层高度就可以满足,从千万级的表查询目标数据最多需要 3-4 次磁盘 I/O。
    • 而 B 树要实现范围查询,因此只能通过树的遍历来完成范围查询,这会涉及多个节点的磁盘 I/O 操作,范围查询效率不如 B+ 树。

综上所述,B+树是一种非常高效的数据结构,可以在大规模数据的情况下提供高效的索引支持,因此MySQL选择使用B+树作为索引。

3.4 MySQL单表不要超过2000W行,靠谱吗?

InnoDB存储引擎的表数据存放在一个.idb(innodb data)的文件中,也叫做表空间。

索引结构不会影响单表最大行数,2000W 也只是推荐值,超过了这个值可能会导致 B + 树层级更高,影响查询性能。

但是,当单表数据库到达某个量级的上限时,导致内存无法存储其索引,使得之后的 SQL 查询会产生磁盘 IO,从而导致性能下降,所以增加硬件配置,可能会带来立竿见影的性能提升。

3.5 索引失效有哪些?

  • 当我们使用左或者左右模糊匹配的时候,也就是 like %xx 或者 like %xx% 这两种方式都会造成索引失效。
    • 因为索引 B+ 树是按照「索引值」有序排列存储的,只能根据前缀进行比较。
  • 对索引列进行表达式计算、使用函数,这些情况下都会造成索引失效。
    • 因为索引保存的是索引字段的原始值,而不是经过计算后的值。
  • 索引列发生隐式类型转换。MySQL 在遇到字符串和数字比较的时候,会自动把字符串转为数字,然后再进行比较。如果字符串是索引列,而输入的参数是数字的话,那么索引列会发生隐式类型转换。
    • 由于隐式类型转换是通过 CAST 函数实现的,等同于对索引列使用了函数,所以就会导致索引失效。
  • 联合索引没有遵循最左匹配原则,也就是按照最左边的索引优先的方式进行索引的匹配,就会导致索引失效。
  • 在 WHERE 子句中,如果在 OR 前的条件列是索引列,而在 OR 后的条件列不是索引列,那么索引会失效。
    • 因为 OR 的含义就是两个只要满足一个即可,只要有条件列不是索引列,就会进行全表扫描。

3.6 MySQL使用like “%x”,索引一定会失效吗?

  • 当数据库表中的字段只有主键+二级索引时,使用左模糊匹配(like “%x”),不会走全表扫描(索引不会生效),而是走全扫描二级索引树。
    • 因为查二级索引的B + 树就可以查到全部结果,MySQL 优化器认为直接遍历二级索引树要比遍历聚簇索引树的成本要小的多,因此 MySQL 选择了「全扫描二级索引树」的方式查询数据。
  • 附加题:不遵循联合索引的最左匹配原则,索引一定会失效吗?
    • 如果数据库表中的字段都是索引的话,即使查询过程中,没有遵循联合索引的最左匹配原则,也是走全扫描二级索引树(type=index)。

3.7 count(*)和count(1)有什么区别?哪个性能好?

  • 性能:**count() = count(1) > count(主键字段) > count(字段)
  • count()是什么?
    • count()是一个聚合函数,函数的参数不仅可以是字段名,也可以是其他任意的表达式,作用是统计函数指定的参数不为 NULL 的记录有多少个
      • 比如count(name):统计name不为NULL的字段有多少。
        count(1):统计1不为NULL的字段有多少。1永远不可能是NULL,所以其实是在统计一共有多少条记录。
  • count(主键字段)执行过程是怎样的?
    • 如果表中只有主键索引,没有二级索引,InnoDB在遍历时就会遍历聚簇索引,将读取到的记录返回给server层(server层维护了一个count的变量),然后读取记录中的主键值,如果不为NULL,就将count变量 + 1。
      如果表中有二级索引,InnoDB就会遍历二级索引,通过二级索引获取主键值,进一步统计个数。
  • count(1)执行过程是怎样的?
    • 如果表中只有主键索引,没有二级索引,InnoDB遍历时会遍历聚簇索引,将读取到的记录返回给server层,但是不会读取记录中的任何字段的值。因为 count 函数的参数是 1,不是字段,所以不需要读取记录中的字段值。如果表中有二级索引,InnoDB就会遍历二级索引。
      • 因为 count 函数的参数是 1,不是字段,所以不需要读取记录中的字段值。
  • count(*)执行过程是怎样的?
    • count(*****) 其实等于 count(**0**),也就是说,当你使用 count(*) 时,MySQL 会将 * 参数转化为参数 0 来处理。
  • count(字段)执行过程是怎样的?
    • 会采用全表扫描的方式来计数,所以它的执行效率是比较差的。
  • count(1)、 count(*)、 count(主键字段)在执行的时候,如果表里存在二级索引,优化器就会选择二级索引进行扫描。所以,如果要**执行 count(1)、 count()、 count(主键字段) 时,尽量在数据表上建立二级索引,这样优化器会自动采用 key_len 最小的二级索引进行扫描,相比于扫描主键索引效率会高一些。
  • 为什么InnoDB存储引擎要通过遍历索引的方式计数?
    • InnoDB 存储引擎是支持事务的,同一个时刻的多个查询,由于多版本并发控制(MVCC)的原因,InnoDB 表“应该返回多少行”也是不确定的。
  • 如何优化count(*) ?
    • 使用近似值。如果业务对于统计个数不需要很精确,可以使用explain 命令来进行估算。
    • 使用额外表保存计数值。将这个计数值保存到单独的一张计数表中,在数据表插入一条记录的同时,将计数表中的计数字段 + 1。但是在新增和删除操作时,我们需要额外维护这个计数表。

4.1 事务隔离级别是怎么实现的?

在提到隔离级别之前,注意并不是所有的引擎都能支持事务,比如 MySQL 原生的 MyISAM 引擎就不支持事务,也正是这样,所以大多数 MySQL 的引擎都是用 InnoDB。
想要理解事务,就不得不提到事务的ACID特性:

  • 原子性(Atomicity)同一个事务中的所有操作,要么全部完成,要么全部回滚
    • 就好比买一件商品,购买成功时,则给商家付了钱,商品到手;购买失败时,则商品在商家手中,消费者的钱也没花出去。
  • 一致性(Consistency)事务的执行不会破坏数据的完整性约束
    • 什么叫完整性约束?其实就是要符合业务逻辑。比如完整性约束要求A + B的和为100,那么在事务执行后,应该依然满足完整性约束。
  • 隔离性(Isolation):隔离性针对多个事务,多个事务同时使用相同的数据时,不会相互干扰
    • 因为每个事务都有一个完整的数据空间,对其他并发事务是隔离的。举个例子就是消费者购买商品这个事务,是不影响其他消费者购买的。
  • 持久性(Durability):事务一旦提交,对数据的修改就是永久的。即使系统故障,也不应该丢失数据。那么MySQL主要通过redo log 来实现持久性的。

InnoDB 引擎通过什么技术来保证事务的这四个特性的呢?

  • 持久性是通过 redo log (重做日志)来保证的;
  • 原子性是通过 undo log(回滚日志) 来保证的;
  • 隔离性是通过 MVCC(多版本并发控制) 或锁机制来保证的;
  • 一致性则是通过持久性+原子性+隔离性来保证;

MySQL 有两种开启事务的命令,分别是:

  • 第一种:begin/start transaction 命令;当执行了增删查改操作的 SQL 语句,才是事务真正启动的时机。
  • 第二种:start transaction with consistent snapshot 命令;马上启动事务。

事务的隔离级别

MySQL 服务端是允许多个客户端连接的,在同时处理多个事务的时候,事务并发就有可能出现**脏读(dirty read)、不可重复读(non-repeatable read)、幻读(phantom read)**的问题。

  • 脏读:一个事务读取到了另一个事务修改但未提交的数据,就发生了脏读。如果另一个事务发生回滚,那么刚才得到的数据就是过期的数据。
  • 不可重复读:一个事务内多次读取同一个数据,前后出现数据不一样的情况,就意为着发生了不可重复读现象。
  • 幻读:一个事务多次查询某个符合查询条件的记录数量,前后查询到的记录数量不一样,就意味着发生了幻读现象。

针对这些事务并发产生的问题,SQL 标准提出了四种隔离级别,隔离级别越高,性能效率就越低,这四个隔离级别如下:

  • (可以)读未提交(read uncommitted,指一个事务还没提交时,它做的变更就能被其他事务看到;
    • 可能发生脏读、不可重复读和幻读现象。
  • 读已提交(read committed,RC,指一个事务提交之后,它做的变更才能被其他事务看到;
    • 避免了脏读。
  • 可重复读(repeatable read,RR,指一个事务执行中,读取到的数据前后一致。是MySQL InnoDB 引擎的默认隔离级别
    • 可能发生幻读现象,但是不可能脏读和不可重复读现象。
  • 串行化(serializable,多个事务如果发生读写冲突,后访问的事务必须等前一个事务执行完成,才能继续执行;
    • 脏读、不可重复读和幻读现象都不可能会发生

四种隔离级别具体的实现方式如下:

  • 对于「读未提交」:直接读取最新的数据就好。
  • 对于「串行化」:通过加读写锁的方式来避免并行访问。
  • 对于「读提交」和「可重复读」:通过 **Read View **来实现,主要区别在于创建 Read View 的时机不同。
    • 「读提交」隔离级别是在「每个语句执行前」都会重新生成一个 Read View,而「可重复读」隔离级别是「启动事务时」生成一个 Read View,然后整个事务期间都在用这个 Read View。

Read View 在MVCC 中是如何工作的?

可以把 Read View 理解成一个数据快照,可重复读隔离级别在启动事务时会生成一个 Read View,然后整个事务期间都在用这个 Read View。读提交隔离级别是在每次读取数据时,都会生成一个新的 Read View。
Read View 有四个重要的字段:

  • creator_trx_id :创建该 Read View 的事务 id
  • m_ids :创建 Read View 时,当前数据库中「活跃事务」的事务 id 列表
    • " 活跃事务 "指的就是,启动了但还没提交的事务
  • min_trx_id:创建 Read View 时,当前数据库中「活跃事务」中事务 id 最小的事务,也就是 m_ids 的最小值。
  • max_trx_id :创建 Read View 时,当前数据库中应该给下一个事务的 id 值,也就是全局事务中最大的事务 id 值 + 1。

MVCC全称是多版本并发控制 (Multi-Version Concurrency Control),只有在InnoDB存储引擎下存在。MVCC的机制可以避免同一个数据在不同事务之间的竞争。它只在读已提交和可重复读的事务隔离级别下工作。
在早期的数据库中,只有读读之间的操作才可以并发执行,读写,写读,写写操作都要阻塞,这样就会导致MySQL的并发性能极差。
采用了MVCC机制后,只有写写之间相互阻塞,其他三种操作都可以并行,这样就可以提高了MySQL的并发性能。
聚簇索引的记录中有两个隐藏列,作为实现MVCC的基础:

  • trx_id,保存事务id,表示这个数据是哪个事务生成的。
  • 在创建 Read View 后,我们可以将记录中的 trx_id 划分这三种情况:
  • 如果记录的 trx_id 值小于Read View中的 min_trx_id 值,表示这个版本的记录是在创建 Read View 已经提交的事务生成的,所以该版本的记录对当前事务可见
  • 如果记录的 trx_id 值大于等于Read View中的 max_trx_id 值,表示这个版本的记录是在创建 Read View 才启动的事务生成的,所以该版本的记录对当前事务不可见
  • 如果记录的 trx_id 值在 Read View 的min_trx_id和max_trx_id之间,需要判断 trx_id 是否在 m_ids 列表中:
    • 如果记录的 trx_id m_ids 列表中,表示生成该版本记录的活跃事务还未提交,所以该版本的记录对当前事务不可见
    • 如果记录的 trx_id 不在 m_ids列表中,表示生成该版本记录的活跃事务已经被提交,所以该版本的记录对当前事务可见
  • roll_pointer,是一个指针,指向上一个版本的记录。每次对某条聚簇索引记录进行改动时,都会把旧版本的记录写入到 undo log 中,于是就可以通过这个指针找到修改前的记录。

可重复读是如何工作的?

可重复读隔离级别是启动事务时生成一个 Read View,然后整个事务期间都在用这个 Read View

  • 假设事务 A (事务 id 为51)启动后,紧接着事务 B (事务 id 为52)也启动了,那这两个事务创建的 Read View 如下:
    • 此时事务 B 读取小林的账户余额记录,读到余额是 100 万;
      • 因为此时记录的trx_id 为 50,比事务 B 的 Read View 中的 min_trx_id 值(51,最小活跃事务id)还小,这意味着修改这条记录的事务早就在事务 B 启动前提交过了,所以该版本的记录对事务 B 可见的,也就是事务 B 可以获取到这条记录。
  • 随后事务 A 将小林的账户余额记录修改成 200 万,并没有提交事务;
    • 事务 A 通过 update 语句将这条记录修改了(还未提交事务),将小林的余额改成 200 万,这时 MySQL 会记录相应的 undo log,并以链表的方式串联起来,形成版本链。
  • 事务 B 读取小林的账户余额记录,读到余额还是 100 万。
    • 事务 B 第二次去读取该记录,发现这条记录的 trx_id 值为 51,在事务 B 的 Read View 的 min_trx_id 和 max_trx_id 之间,判断 trx_id 值在 m_ids 范围内**,判断的结果是在的,那么说明这条记录是被还未提交的事务修改的,这时事务 B 并不会读取这个版本的记录。而是沿着 undo log 链条往下找旧版本的记录,**直到找到 trx_id 小于事务 B 的 Read View 中的 min_trx_id 值的第一条记录,所以事务 B 能读取到的是 trx_id 为 50 的记录,也就是小林余额是 100 万的这条记录。
  • 事务 A 提交事务;
  • 事务 B 读取小林的账户余额记录,读到余额依然还是 100 万。

读提交是如何工作的?

读提交隔离级别是在每次读取数据时,都会生成一个新的 Read View

  • 假设事务 A (事务 id 为51)启动后,紧接着事务 B (事务 id 为52)也启动了,接着按顺序执行了以下操作:
    • 事务 B 读取数据(创建 Read View),小林的账户余额为 100 万;
      • 找到记录后,会先看这条记录的 trx_id,此时发现 trx_id 为 50,比事务 B 的 Read View 中的 min_trx_id 值(51,最小活跃事务id)还小,这意味着修改这条记录的事务早就在事务 B 启动前提交过了,所以该版本的记录对事务 B 可见的,也就是事务 B 可以获取到这条记录。
  • 事务 A 修改数据(还没提交事务),将小林的账户余额从 100 万修改成了 200 万;
  • 事务 B 读取数据(创建 Read View),小林的账户余额为 100 万;
    • 事务 B 在找到小林这条记录时,会看这条记录的 trx_id 是 51,在事务 B 的 Read View 的 min_trx_id 和 max_trx_id 之间,接下来需要判断 trx_id 值是否在 m_ids 范围内,判断的结果是在的,那么说明这条记录是被还未提交的事务修改的,这时事务 B 并不会读取这个版本的记录。而是,沿着 undo log 链条往下找旧版本的记录,直到找到 trx_id 「小于」事务 B 的 Read View 中的 min_trx_id 值的第一条记录,所以事务 B 能读取到的是 trx_id 为 50 的记录,也就是小林余额是 100 万的这条记录。
  • 事务 A 提交事务;
  • 事务 B 读取数据(创建 Read View),小林的账户余额为 200 万;
    • 第三次创建的Read View(m_ids变为只有52,因为A事务id为51的已经提交了,min_trx_id变为52。):事务 B 在找到小林这条记录时,会发现这条记录的 trx_id 是 51,比事务 B 的 Read View 中的 min_trx_id 值(52,最小活跃事务id)还小,这意味着修改这条记录的事务早就在创建 Read View 前提交过了,所以该版本的记录对事务 B 是可见的。

正是因为在读提交隔离级别下,事务每次读数据时都重新创建 Read View,那么在事务期间的多次读取同一条数据,前后两次读的数据可能会出现不一致,因为可能这期间另外一个事务修改了该记录,并提交了事务。

4.2 MySQL可重复读隔离级别,完全解决幻读了吗?

当同一个查询在不同的时间产生不同的结果集时,事务中就会出现所谓的幻象问题。例如,如果 SELECT 执行了两次,但第二次返回了第一次没有返回的行,则该行是“幻像”行。
MySQL InnoDB 引擎的默认隔离级别虽然是「可重复读」,但是它很大程度上避免幻读现象

  • 在可重复读隔离级别中,普通的 select 语句就是基于 MVCC 实现的快照读,也就是不会加锁的。而MySQL 里除了普通查询是快照读,其他都是当前读,比如 update、insert、delete,这些语句执行前都会查询最新版本的数据,然后再做进一步的操作。
    • 快照读(普通 select 语句),是通过 **MVCC(多版本控制)方式解决了幻读。**因为可重复读隔离级别下,开始事务并执行第一个查询语句后,会创建Read View,通过这个 Read View 就可以在 undo log 版本链找到事务开始时的数据,所以事务过程中每次查询的数据都是一样的,即使中途有其他事务插入了新纪录,是查询不出来这条数据的,所以就很好了避免幻读问题。
    • 当前读(select … for update 等语句),是**通过 next-key lock(记录锁+间隙锁)方式解决了幻读。**因为当执行 select … for update 语句的时候,会加上 next-key lock,如果有其他事务在 next-key lock 锁范围内插入了一条记录,那么这个插入语句就会被阻塞,无法成功插入,所以就很好了避免幻读问题。
      • InnoDB 引擎为了解决可重复读隔离级别使用「当前读」而造成的幻读问题,就引出了间隙锁。事务 A 执行了下面这条锁定读语句后,就在对表中的记录加上 id 范围为 (2, +∞] 的 next-key lock(next-key lock 是间隙锁+记录锁的组合)。图解MySQL笔记_第6张图片然后,事务 B 在执行插入语句的时候,判断到插入的位置被事务 A 加了 next-key lock,于是事物 B 会生成一个插入意向锁,同时进入等待状态,直到事务 A 提交,这就避免了幻读。
  • 幻读被完全解决了吗?由此可见,**MySQL 可重复读隔离级别并没有彻底解决幻读,只是很大程度上避免了幻读现象的发生。**要避免这类特殊场景下发生幻读的现象的话,就是尽量在开启事务之后,马上执行 select … for update 这类当前读的语句,因为它会对记录加 next-key lock,从而避免其他事务插入一条新记录。
    • 第一个例子:因为当事务 A 更新了一条事务 B 插入的记录(此时事务A看不见该记录),那么事务 A 前后两次查询的记录条目就不一样了,所以就发生幻读。
      • 因为进行更新操作时,新记录的 trx_id 隐藏列的值就分配为事务 A 的事务 id,此时A再使用普通SELECT语句去查询该记录时就可以看到这条记录了。
    • 第二个例子:如果事务开启后,并没有执行当前读,而是先快照读(此时没有加锁),然后这期间如果其他事务插入了一条记录,那么事务后续使用当前读进行查询的时候,就会发现两次查询的记录条目就不一样了,所以就发生幻读。

5. 1MySQL有哪些锁?

为保证数据的一致性,需要对并发操作进行控制,因此产生了锁。同时,锁机制也为实现MySQL的各个隔离级别提供了保证。 锁冲突 也是影响数据库并发访问性能的一个重要因素。所以锁对数据库而言显得尤其重要,也更加复杂。
按照数据操作的类型,可以分为读锁、写锁。

  • 读锁 :也称为 共享锁 、英文用 S 表示。针对同一份数据,多个事务的读操作可以同时进行而不会互相影响,相互不阻塞的。
  • 写锁 :也称为 排他锁 、英文用 X 表示。当前写操作没有完成前,它会阻断其他写锁和读锁。这样就能确保在给定的时间里,只有一个事务能执行写入,并防止其他用户读取正在写入的同一资源。

在 MySQL 里,根据加锁的范围,可以分为全局锁、表级锁和行锁三类。

全局锁

使用全局锁,整个数据库就处于只读状态了。

  • 应用场景:主要应用于做全库逻辑备份,这样在备份数据库期间,不会因为数据或表结构的更新,而出现备份文件的数据与预期的不一样。
  • 缺点:业务只能读数据,而不能更新数据,这样会造成业务停滞。
  • 避免方法:使用可重复读的隔离级别,在备份数据库之前先开启事务,整个数据库的数据就都是可重复读的,而且由于 MVCC 的支持,备份期间业务依然可以对数据进行更新操作。
# 加全局锁 FTWRL
flush tables with read lock

表级锁

MySQL 里面表级别的锁有:表锁、元数据锁(MDL)、意向锁、AUTO-INC 锁。

  • 表锁表锁也有表级别的共享锁,表级别的独占锁。表锁除了会限制别的线程的读写外,也会限制本线程接下来的读写操作。表锁的颗粒度太大,会影响并发性能,应该尽量避免使用表锁。
  • 元数据锁(Meta Database Lock,MDL)对一张表进行 CRUD 操作时,加的是 MDL 读锁;对一张表做结构变更操作的时候,加的是 MDL 写锁;MDL 不需要显示调用,在事务提交后才会释放,这意味着事务执行期间,MDL 是一直持有的。申请 MDL 锁的操作会形成一个队列,队列中写锁获取优先级高于读锁,一旦出现 MDL 写锁等待(线程申请不到 MDL 写锁),会阻塞后续该表的所有 CRUD 操作(MDL读锁)。
    • 只允许读,不能做结构的修改,比如修改表字段等。
    • 只允许写,修改表结构时不能通过CRUD读取数据。
    • 所以为了能安全的对表结构进行变更,在对表结构变更前,先要看看数据库中的长事务,是否有事务已经对表加上了 MDL 读锁,如果可以考虑 kill 掉这个长事务,然后再做表结构的变更。
  • 意向锁意向锁的目的是快速判断表里是否有记录被加锁意向共享锁(intention shared lock, IS):事务有意向对表中的某些行加共享锁(S锁)意向排他锁(intention exclusive lock, IX):事务有意向对表中的某些行加排他锁(X锁)普通的SELECT语句利用MVCC实现一致性读,是无锁的,但是可以使用以下方式加锁:
    • 如果没有「意向锁」,那么加「独占表锁」时,就需要遍历表里所有记录,查看是否有记录存在独占锁,这样效率会很慢。那么有了「意向锁」,由于在对记录加独占锁前,先会加上表级别的意向独占锁,那么在加「独占表锁」时,直接查该表是否有意向独占锁,如果有就意味着表里已经有记录被加了独占锁,这样就不用去遍历表里的记录。
//先在表上加上意向共享锁,然后对读取的记录加共享锁
select ... lock in share mode;

//先表上加上意向独占锁,然后对读取的记录加独占锁
select ... for update;

意向锁之间不冲突,也不会和行级的共享锁和独占锁发生冲突,只会和共享表锁(lock tables … read)或独占表锁(lock tables … write)发生冲突。

  • AUTO-INC 锁(自动增长)表里的主键通常都会设置成自增的,这是通过对主键字段声明 AUTO_INCREMENT 属性实现的。在插入数据时,会加一个表级别的 AUTO-INC 锁,然后为被 AUTO_INCREMENT 修饰的字段赋值递增的值,等插入语句执行完成后,才会把 AUTO-INC 锁释放掉。
    • InnoDB 存储引擎提供了一种轻量级的锁来实现自增。只是在赋值完成后,就把该锁释放。

行级锁

对于表锁和行锁,满足读读共享、读写互斥、写写互斥的。
不同隔离级别下,行级锁的种类不同。

  • 在读已提交隔离级别下,行级锁的种类只有记录锁,也就是仅仅把一条记录锁上。
  • 在可重复读隔离级别下,行级锁的种类除了有记录锁,还有间隙锁(目的是为了避免幻读)。

行级锁的类型主要有三类:

  • 记录锁(Record Lock),也就是仅仅把一条记录锁上;记录锁是有 S 锁和 X 锁之分的。共享锁(S锁)满足读读共享,读写互斥;独占锁(X锁)满足写写互斥、读写互斥。
    • 共享锁(S锁)指的就是对于多个不同的事务,对同一个资源共享同一个锁。 相当于对于同一把门,它拥有多个钥匙一样。
    • 独占锁(X锁)也叫排他锁,是指该锁一次只能被一个线程所持有。
  • 间隙锁(Gap Lock),锁定一个范围,但是不包含记录本身;只存在于可重复读隔离级别,目的是为了解决可重复读隔离级别下幻读的现象。
    • 间隙锁之间是兼容的,即两个事务可以同时持有包含共同间隙范围的间隙锁,并不存在互斥关系,因为间隙锁仅仅是为了防止插入幻影记录而提出的。
  • 临键锁(Next-Key Lock),Record Lock + Gap Lock 的组合,锁定一个范围,并且锁定记录本身。next-key lock 即能保护记录,又能阻止其他事务将新纪录插入到被保护记录前面的间隙中。
    • next-key lock 是包含间隙锁+记录锁的,如果一个事务获取了 X 型的 next-key lock,那么另外一个事务在获取相同范围的 X 型的 next-key lock 时,是会被阻塞的
  • 插入意向锁:插入意向锁名字虽然有意向锁,但是它并不是意向锁,它是一种特殊的间隙锁,属于行级别锁。存在间隙锁时,执行Insert语句时会用到。
    • 插入意向锁是一种特殊的间隙锁,但不同于间隙锁的是,该锁只用于并发插入操作。如果说间隙锁锁住的是一个区间,那么「插入意向锁」锁住的就是一个点。因而从这个角度来说,插入意向锁确实是一种特殊的间隙锁。

5.2 MySQL是怎么加锁的?

  1. 从语句角度来看

普通的SELECT默认是不加锁的,属于快照读,是使用MVCC的方式实现的。但是可以在查询时对记录加行级锁,查询会加锁的语句称为锁定读。锁定读的语句必须在事务中,因为当事务提交了,锁就会被释放。而update 和 delete 操作都会加行级锁,且锁的类型都是独占锁(X型锁)。

//对读取的记录加共享锁(S型锁)
select ... lock in share mode;

//对读取的记录加独占锁(X型锁)
select ... for update;

Insert 语句在正常执行时是不会生成锁结构的,它是靠聚簇索引记录自带的 trx_id 隐藏列来作为隐式锁来保护记录的。但此时记录之间加有间隙锁,隐式锁会转换为显示锁。
如果已加间隙锁,此时会生成一个插入意向锁,然后锁的状态设置为等待状态,现象就是Insert语句被阻塞。

  • 因为插入意向锁与间隙锁是冲突的,所以当其它事务持有该间隙的间隙锁时,需要等待其它事务释放间隙锁之后,才能获取到插入意向锁。

如果记录之间加有间隙锁,为了避免幻读,此时是不能插入记录的,因为插入意向锁会被设置为等待状态
如果 Insert 的记录和已有记录存在唯一键冲突,此时也不能插入记录,会对这条记录加上S型的锁

  • 至于是记录锁,还是 next-key 锁,跟是「主键冲突」还是「唯一二级索引冲突」有关系。
    • 如果主键冲突:给已存在的主键索引记录添加S型记录锁
    • 如果唯一二级索引冲突:给已存在的二级索引记录添加S型next-key锁

  1. 从MySQL角度来看

MySQL加锁的对象是索引,加锁的基本单位是 next-key lock

  • next-key lock 是前开后闭区间,而间隙锁是前开后开区间。在能使用记录锁或者间隙锁就能避免幻读现象的场景下, next-key lock 就会退化成退化成记录锁或间隙锁。

唯一索引等值查询,加锁情况分析:(主键索引为例)

  • 当查询的记录是存在的,在索引树上定位到这一条记录后,该记录的索引中的 next-key lock 会退化成「记录锁」
    • 等值查询唯一索引,只需要加锁一条记录,并且加记录锁就可以避免幻读。
  • 当查询的记录是不存在的,在索引树找到第一条大于该查询记录的记录后,将该记录的索引中的 next-key lock 会退化成「间隙锁」,因为仅靠间隙锁就可以避免幻读。

唯一索引范围查询,加锁情况分析:

  • 首先会对每一个扫描到的索引加 next-key 锁,如果遇到下面这些情况,会退化成记录锁或者间隙锁:
    • 大于的范围查询,next-key锁不会退化。select * from user where id > 15 for update;图解MySQL笔记_第7张图片
    • 大于等于的范围查询,如果“等于”的等值查询的记录是存在于表中,那么该记录的索引中的 next-key 锁会退化成记录锁。select * from user where id >= 15 for update;图解MySQL笔记_第8张图片
    • 小于/小于等于的范围查询,扫描到终止范围查询的记录时,next-key锁就会退化为间隙锁。小于等于时,等值查询的记录在表中,next-key不会退化,因为next-key本就是左开右闭,避免幻读。select * from user where id < 6 for update;图解MySQL笔记_第9张图片
  • 非唯一索引等值查询
    • 当查询的记录存在时,扫描到的二级索引记录加的是next-key lock,扫描到的第一个不符合条件的二级索引记录,next-key 锁会退化成间隙锁。同时,在符合查询条件的记录的主键索引上加记录锁。select * from user where age = 22 for update;图解MySQL笔记_第10张图片
    • 当查询的记录不存在时,扫描到第一条不符合条件的二级索引记录, next-key 锁会退化成间隙锁。**因为不存在满足查询条件的记录,所以不会对主键索引加锁。**select * from user where age = 25 for update;图解MySQL笔记_第11张图片
  • 非唯一索引范围查询
    • 非唯一索引范围查询,索引的 next-key lock 不会有退化为间隙锁和记录锁的情况
    • 对扫描到的二级索引记录加锁都是加 next-key 锁,主键索引加记录锁。
  • 没有加索引的查询
    • 在线上在执行 update、delete、select … for update 等具有加锁性质的语句,一定要检查语句是否走了索引,如果是全表扫描的话,会对每一个索引加 next-key 锁,相当于把整个表锁住了

5.3 update没加索引会锁全表?

当我们执行 update 语句时,实际上是会对记录加独占锁(X 锁)的,此时其他事务对持有独占锁的记录进行修改时是会被阻塞的。另外,这个锁并不是执行完 update 语句就会释放的,而是会等事务结束时才会释放。

  • 在 update 语句的 where 条件没有使用索引,就会全表扫描,于是就会对所有记录加上 next-key 锁(记录锁 + 间隙锁),相当于把整个表锁住了
  • 即使where条件使用了索引,还得看这条语句在执行过程中,优化器最终选择的是索引扫描,还是全表扫描,如果走了全表扫描,就会对全表的记录加锁了。

5.4 MySQL死锁了,怎么办?

RR隔离级别下,会存在幻读的问题,InnoDB为了解决可重复读隔离级别下的幻读问题,就引出了next-key 锁,是记录锁和间隙锁的组合。
我们可以执行 select * from performance_schema.data_locks\G; 语句 ,确定事务加了什么类型的锁。

  • 为什么会出现死锁?
    1. 建了一张订单表,其中 id 字段为主键索引,order_no 字段普通索引,也就是非唯一索引(二级索引)# 插入六条记录,id 1-6 、order_on 1001-1006
    2. 事务A要插入1007订单记录:在插入之前,给订单做幂等性校验,目的是为了保证不会出现重复的订单。
    3. 事务B也做幂等性校验:SELECT id FROM t_order WHERE order_no = 1008 for UPDATE;事务B在二级索引加了X型next-key锁,范围也是(1006 , +∞)。
    4. 事务A、B执行insert语句,插入1007、1008。
SELECT id FROM t_order WHERE `order_no` = 1007 for UPDATE;
# 需要对订单做幂等性校验,所以两个事务先要查询该订单是否存在,不存在才插入记录

执行该语句,事务A在二级索引加了X型next-key锁,范围是(1006 , +∞)。

Insert into t_order (order_no, create_date) values (1007, now());
Insert into t_order (order_no, create_date) values (1008, now());

此时两个事务都陷入了等待状态,也就是发生了死锁,因为都在相互等待对方释放锁。

  • 因为当我们执行insert语句时,会在插入间隙上获取插入意向锁,**而插入意向锁与间隙锁是冲突的,所以当其它事务持有间隙锁时,需要等待其它事务释放间隙锁之后,才能获取到插入意向锁。**而间隙锁与间隙锁之间是兼容的,并且两个事务中 select … for update 语句并不会相互影响。
    • 因为间隙锁的意义只在于阻止区间被插入一个事务获取的间隙锁不会阻止另一个事务获取同一个间隙范围的间隙锁,共享和排他的间隙锁是没有区别的,他们相互不冲突,且功能相同,即两个事务可以同时持有包含共同间隙的间隙锁。
    • next-key lock 是包含间隙锁+记录锁的,如果一个事务获取了 X 型的 next-key lock,那么另外一个事务再获取相同范围的 X 型的 next-key lock 时,是会被阻塞的。但是,对于这种范围为 (1006, +∞] 的 next-key lock,两个事务是可以同时持有的,不会冲突。因为 +∞ 并不是一个真实的记录,自然就不需要考虑 X 型与 S 型关系。
  • 如何避免死锁?死锁的四个必要条件:互斥、占有且等待、不可强占用、循环等待。只要系统发生死锁,这些条件必然成立,但是只要破坏任意一个条件就死锁就不会成立。在数据库层面,有两种策略通过「打破循环等待条件」来解除死锁状态:
    • 设置事务等待锁的超时时间。当一个事务的等待时间超过该值后,就对这个事务进行回滚,于是锁就释放了,另一个事务就可以继续执行了。在 InnoDB 中,参数 innodb_lock_wait_timeout 是用来设置超时时间的,默认值时 50 秒。
    • 开启主动死锁检测。主动死锁检测在发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。将参数 innodb_deadlock_detect 设置为 on,表示开启这个逻辑,默认就开启。

5.5 字节面试: 加了什么锁,导致死锁的?

图解MySQL笔记_第12张图片
创建一张学生表:其中id为主键索引,其他都是普通字段。
事务 A 和 事务 B 都在执行 insert 语句后,都陷入了等待状态,也就是发生了死锁,因为都在相互等待对方释放锁。
图解MySQL笔记_第13张图片

  • Time1阶段:此时事务 A 在主键索引(INDEX_NAME : PRIMARY)上加的是间隙锁,锁范围是(20, 30)。(唯一索引等值查询,查询id不在索引中,退化成间隙锁)
  • Time2阶段:此时事务B在主键索引上加的是也是间隙锁,和事务A相同。
  • Time3阶段:事务 A 的状态为等待状态(LOCK_STATUS: WAITING),因为向事务 B 生成的间隙锁(范围 (20, 30))中插入了一条记录,所以事务 A 的插入操作生成了一个插入意向锁(LOCK_MODE:INSERT_INTENTION)。
  • Time4阶段:与Time3阶段相同。

本次案例中,事务 A 和事务 B 在执行完后 update 语句后都持有范围为(20, 30)的间隙锁,而接下来的插入操作为了获取到插入意向锁,都在等待对方事务的间隙锁释放,于是就造成了循环等待,满足了死锁的四个条件:互斥、占有且等待、不可强占用、循环等待,因此发生了死锁。

6.1 MySQL日志:undo log、redo log、binlog 有什么用?

在执行update语句时,会涉及到MySQL的三个日志:

  • undo log(回滚日志):是 Innodb 存储引擎层生成的日志,实现了事务中的原子性,主要用于事务回滚和 MVCC
  • redo log(重做日志):是 Innodb 存储引擎层生成的日志,实现了事务中的持久性,主要用于掉电等故障恢复
  • binlog (归档日志):是 Server 层生成的日志,主要用于数据备份和主从复制

为什么需要undo log?

  • 实现事务回滚,保障事务的原子性。事务处理过程中,如果出现了错误或者用户执 行了 ROLLBACK 语句,MySQL 可以利用 undo log 中的历史数据将数据恢复到事务开始之前的状态。
    • 在事务还没提交之前,MSQL会先记录更新前的数据到undo log日志中,当事务回滚时,可以利用undo log 日志文件进行回滚。
      • 比如:在插入一条记录时,要把这条记录的主键值记下来,这样之后回滚时只需要把这个主键值对应的记录删掉就好了;在删除一条记录时,要把这条记录中的内容都记下来,这样之后回滚时再把由这些内容组成的记录插入到表中就好了;
  • 实现 MVCC(多版本并发控制)关键因素之一。MVCC 是通过 ReadView + undo log 实现的。undo log 为每条记录保存多份历史数据,MySQL 在执行快照读(普通 select 语句)的时候,会根据事务的 Read View 里的信息,顺着 undo log 的版本链找到满足其可见性的记录。
    • 一条记录的每一次更新操作产生的 undo log 格式都有一个 roll_pointer 指针和一个 trx_id 事务id:
      • 通过 trx_id 可以知道该记录是被哪个事务修改的;
      • 通过 roll_pointer 指针可以将这些 undo log 串成一个链表,这个链表就被称为版本链;

为什么需要redo log?

  • 为什么需要redo log?
    • 实现事务的持久性,让 MySQL 有 crash-safe 的能力,能够保证 MySQL 在任何时间段突然崩溃,重启后之前已提交的记录都不会丢失;
    • 因为使用了WAL(预先日志持久化)技术,将写操作从「随机写」变成了「顺序写」,提升 MySQL 写入磁盘的性能。(redo log是顺序写的,数据是随机写的)
      • WAL技术:当有一条记录需要更新的时候,InnoDB 引擎就会先更新内存(同时标记为脏页),然后将本次对这个页的修改以 redo log 的形式记录下来,这个时候更新就算完成了。然后InnoDB 引擎会在适当的时候,由后台线程将缓存在 Buffer Pool 的脏页刷新到磁盘里,这就是 WAL (Write-Ahead Logging)技术,MySQL 的写操作并不是立刻写到磁盘上,而是先写日志,然后在合适的时间再写到磁盘上。
  • 什么是redo log?**redo log是物理日志,记录了某个数据页做了什么修改。**在事务提交时,只要先将redo log 持久化到磁盘即可,可以不需要等待Buffer Pool 中的脏页数据持久化到磁盘,这样,即使脏页数据没有持久化,但是redo log已经持久化,MySQL重启后,可以根据redo log的内容,将所有数据恢复到最新状态。
  • undo页面被修改,需要记录redo log吗?
    • 开启事务后,InnoDB更新记录前,要先记录undo log,如果是更新操作,需要把更新的列的旧值记录,也就是要生成undo log,undo log会写入Buffer Pool 的undo页面。在内存修改过undo页面后,也需要记录对应的redo log,所以只要redo log写入磁盘了,undo就不怕丢失。
  • undo log 和redo log
    • undo log日志记录此次事务开始之前的状态,记录的是更新数据之前的值。
    • redo log日志记录此次事务完成后的数据状态,记录的是更新之后的值。
    • 事务执行时发生崩溃,重启后使用undo log恢复事务,事务提交后发生崩溃,重启后通过redo log恢复事务。有了redo log 和WAL技术,使得数据库具有crash-safe(崩溃恢复)的能力。
  • redo log 是直接写入磁盘吗?
    • redo log有自己的缓存——redo log buffer,每产生一条redo log 时,会先写入到redo log buffer ,后续在持久化到磁盘。
    • redo log buffer 默认大小 16 MB,可以通过 innodb_log_Buffer_size 参数动态的调整大小,增大它的大小可以让 MySQL 处理「大事务」是不必写入磁盘,进而提升写 IO 性能。

redo log什么时候刷新到磁盘?

  • 缓存在redo log buffer中的redo log还是在内存中,它什么时候刷新到磁盘?
    • MySQL 正常关闭时;
    • 当 redo log buffer 中记录的写入量大于 redo log buffer 内存空间的一半时,会触发落盘;
    • InnoDB 的后台线程每隔 1 秒,将 redo log buffer 持久化到磁盘。
    • 每次事务提交时都将缓存在 redo log buffer 里的 redo log 直接持久化到磁盘(这个策略可由 innodb_flush_log_at_trx_commit 参数控制,下面会说)。
  • innodb_flush_log_at_trx_commit 参数InnoDB 还提供了另外两种策略,由参数 innodb_flush_log_at_trx_commit 参数控制,可取的值有:0、1、2,默认值为 1,这三个值分别代表的策略如下:
    • 当设置该参数为 0 时,表示每次事务提交时 ,还是将 redo log 留在 redo log buffer 中 ,该模式下在事务提交时不会主动触发写入磁盘的操作。
      • 参数0时:InnoDB后台进程每隔一秒,会把缓存在 redo log buffer 中的 redo log ,通过调用 write() 写到操作系统的 Page Cache,然后调用 fsync() 持久化到磁盘。所以参数为 0 的策略,MySQL 进程的崩溃会导致上一秒钟所有事务数据的丢失;
    • 当设置该参数为 1 时(默认),表示每次事务提交时,都将缓存在 redo log buffer 里的 redo log 直接持久化到磁盘,这样可以保证 MySQL 异常重启之后数据不会丢失。
    • 当设置该参数为 2 时,表示每次事务提交时,都只是缓存在 redo log buffer 里的 redo log 写到 redo log 文件,注意写入到「 redo log 文件」并不意味着写入到了磁盘,因为操作系统的文件系统中有个 Page Cache,Page Cache 是专门用来缓存文件数据的,所以写入「 redo log文件」意味着写入到了操作系统的文件缓存。
      • 参数 2 :InnoDB后台进程每隔一秒,调用 fsync( ),将缓存在操作系统中 Page Cache 里的 redo log 持久化到磁盘。所以参数为 2 的策略,较取值为 0 情况下更安全,因为 MySQL 进程的崩溃并不会丢失数据,只有在操作系统崩溃或者系统断电的情况下,上一秒钟所有事务数据才可能丢失
  • 三个参数的应用场景
    • 数据安全性:参数 1 > 参数 2 > 参数 0
    • 写入性能:参数 0 > 参数 2> 参数 1
    • 在一些对数据安全性要求比较高的场景中,显然 innodb_flush_log_at_trx_commit 参数需要设置为 1。
    • 在一些可以容忍数据库崩溃时丢失 1s 数据的场景中,我们可以将该值设置为 0,这样可以明显地减少日志同步到磁盘的 I/O 操作。
    • 安全性和性能折中的方案就是参数 2,虽然参数 2 没有参数 0 的性能高,但是数据安全性方面比参数 0 强,因为参数 2 只要操作系统不宕机,即使数据库崩溃了,也不会丢失数据,同时性能方便比参数 1 高。

redo log文件写满了怎么办?

  • 默认情况下, InnoDB 存储引擎有 1 个重做日志文件组( redo log Group),「重做日志文件组」由有 2 个 redo log 文件组成,这两个 redo 日志的文件名叫 :ib_logfile0 和 ib_logfile1 。
    • 在重做日志组中,每个 redo log File 的大小是固定且一致的,假设每个 redo log File 设置的上限是 1 GB,那么总共就可以记录 2GB 的操作。
    • 重做日志文件组是以循环写的方式工作的,从头开始写,写到末尾就又回到开头,相当于一个环形。
    • 所以 InnoDB 存储引擎会先写 ib_logfile0 文件,当 ib_logfile0 文件被写满的时候,会切换至 ib_logfile1 文件,当 ib_logfile1 文件也被写满时,会切换回 ib_logfile0 文件。
  • 我们知道 redo log 是为了防止 Buffer Pool 中的脏页丢失而设计的,那么如果随着系统运行,Buffer Pool 的脏页刷新到了磁盘中,那么 redo log 对应的记录也就没用了,这时候我们擦除这些旧记录,以腾出空间记录新的更新操作。
    • redo log 是循环写的方式,相当于一个环形,InnoDB 用 write pos 表示 redo log 当前记录写到的位置,用 checkpoint 表示当前要擦除的位置,如下图:图解MySQL笔记_第14张图片
    • 如果 write pos 追上了 checkpoint,就意味着 redo log 文件满了,这时 MySQL 不能再执行新的更新操作,也就是说 MySQL 会被阻塞因此所以针对并发量大的系统,适当设置 redo log 的文件大小非常重要),此时会停下来将 Buffer Pool 中的脏页刷新到磁盘中,然后标记 redo log 哪些记录可以被擦除,接着对旧的 redo log 记录进行擦除,等擦除完旧记录腾出了空间,checkpoint 就会往后移动(图中顺时针),然后 MySQL 恢复正常运行,继续执行新的更新操作。
    • 一次 checkpoint 的过程就是脏页刷新到磁盘中变成干净页,然后标记 redo log 哪些记录可以被覆盖的过程

为什么需要binlog?

MySQL 在完成一条更新操作后,Server 层还会生成一条 binlog,等之后事务提交的时候,会将该事物执行过程中产生的所有 binlog 统一写 入 binlog 文件。
binlog 文件是记录了所有数据库表结构变更和表数据修改的日志,不会记录查询类的操作,比如 SELECT 和 SHOW 操作。

  • redo log 和bin logbinlog没有crash-safe的功能,只能用于归档。而InnoDB 是另一个公司以插件形式引入 MySQL 的,既然 binlog 没有 crash-safe 能力,所以 InnoDB 使用 redo log 来实现 crash-safe 能力 。

redo log 和binlog 有什么区别?

  1. 适用对象不同:
  • binlog 是 MySQL 的 Server 层实现的日志,所有存储引擎都可以使用;
  • redo log 是 Innodb 存储引擎实现的日志;

2、文件格式不同:

  • binlog 有 3 种格式类型,分别是 STATEMENT(默认格式)、ROW、 MIXED,区别如下:
    • STATEMENT:每一条修改数据的 SQL 都会被记录到 binlog 中(相当于记录了逻辑操作,所以针对这种格式, binlog 可以称为逻辑日志),主从复制中 slave 端再根据 SQL 语句重现。但 STATEMENT 有动态函数的问题,比如你用了 uuid 或者 now 这些函数,你在主库上执行的结果并不是你在从库执行的结果,这种随时在变的函数会导致复制的数据不一致;
    • ROW:记录行数据最终被修改成什么样了(这种格式的日志,就不能称为逻辑日志了),不会出现 STATEMENT 下动态函数的问题。但 ROW 的缺点是每行数据的变化结果都会被记录,比如执行批量 update 语句,更新多少行数据就会产生多少条记录,使 binlog 文件过大,而在 STATEMENT 格式下只会记录一个 update 语句而已;
    • MIXED:包含了 STATEMENT 和 ROW 模式,它会根据不同的情况自动使用 ROW 模式和 STATEMENT 模式;
  • redo log 是物理日志,记录的是在某个数据页做了什么修改,比如对 XXX 表空间中的 YYY 数据页 ZZZ 偏移量的地方做了AAA 更新;

3、写入方式不同:

  • binlog 是追加写,写满一个文件,就创建一个新的文件继续写,不会覆盖以前的日志,保存的是全量的日志。
  • redo log 是循环写,日志空间大小是固定,全部写满就从头开始,保存未被刷入磁盘的脏页日志。

4、用途不同:

  • binlog 用于备份恢复、主从复制;
  • redo log 用于掉电等故障恢复。
  • 如果整个数据库都被删除了,就不能使用redo log文件恢复。因为redo log有check point机制,而binlog保存的是全量的日志,可以使用binlog恢复数据。

主从复制是怎么实现的?

  • 主从复制是怎么实现的?
    • MySQL 的主从复制依赖于 binlog 实现,也就是记录 MySQL 上的所有变化并以二进制形式保存在磁盘上。复制的过程就是将 binlog 中的数据从主库传输到从库上。
    • 这个过程一般是异步的,也就是主库上执行事务操作的线程不会等待复制 binlog 的线程同步完成。
  • 主从复制的好处
    • 在完成主从复制之后,你就可以在写数据时只写主库,在读数据时只读从库,这样可以降低主库的压力。
    • 如果主库存在问题,可以快速切换到从库提供服务。
    • 可以在从库进行备份,以免备份期间影响主库的服务。
  • MySQL 集群的主从复制过程的 3 个阶段:
    • 主库写入 Binlog:MySQL 主库在收到客户端提交事务的请求之后,主库写 binlog 日志,提交事务,并更新本地存储数据,事务提交完成后,返回给客户端“操作成功”的响应。
    • 从库同步 Binlog:把 binlog 复制到所有从库上,每个从库把 binlog 写到暂存日志中。
      • 从库会创建一个专门的 I/O 线程,连接主库的 log dump 线程,来接收主库的 binlog 日志,再把 binlog 信息写入 relay log 的中继日志里,再返回给主库“复制成功”的响应。
    • 从库回放 Binlog:回放 binlog,并更新存储引擎中的数据。
      • 从库会创建一个用于回放 binlog 的线程,去读 relay log 中继日志,然后回放 binlog ,更新存储引擎中的数据,最终实现主从的数据一致性。
  • 从库是不是越多越好?
    • 从库数量增加,从库连接上来的 I/O 线程也比较多,主库也要创建同样多的 log dump 线程来处理复制的请求,对主库资源消耗比较高,同时还受限于主库的网络带宽。所以在实际使用中,一个主库一般跟 2~3 个从库(1 套数据库,1 主 2 从 1 备主),这就是一主多从的 MySQL 集群结构。
  • MySQL 主从复制还有哪些模型?
    • 同步复制:MySQL 主库提交事务的线程要等待所有从库的复制成功响应,才返回客户端结果。这种方式在实际项目中,基本上没法用,原因有两个:一是性能很差,因为要复制到所有节点才返回响应;二是可用性也很差,主库和所有从库任何一个数据库出问题,都会影响业务。
    • 异步复制(默认模型):MySQL 主库提交事务的线程并不会等待 binlog 同步到各从库,就返回客户端结果。这种模式一旦主库宕机,数据就会发生丢失。
    • 半同步复制:MySQL 5.7 版本之后增加的一种复制方式,介于两者之间,事务线程不用等待所有的从库复制成功响应,只要一部分复制成功响应回来就行,比如一主二从的集群,只要数据成功复制到任意一个从库上,主库的事务线程就可以返回给客户端。这种半同步复制的方式,兼顾了异步复制和同步复制的优点,即使出现主库宕机,至少还有一个从库有最新的数据,不存在数据丢失的风险

binlog 什么时候刷新到磁盘?

  • binlog cache
    • MySQL 给每个线程分配了一片内存用于缓冲 binlog ,该内存叫 binlog cache,参数 binlog_cache_size 用于控制单个线程内 binlog cache 所占内存的大小。如果超过了这个参数规定的大小,就要暂存到磁盘。
    • 事务执行过程中,先把日志写到 binlog cache(Server 层的 cache),事务提交的时候,再把 binlog cache 写到 binlog 文件中。
    • 一个事务的binlog不能被拆开,要保证一次性写入,确保事务的原子性。
  • 什么时候binlog cache 会写到binlog文件
    • 在事务提交的时候,执行器使用write()把 binlog cache 里的完整事务写入到 binlog 文件中,并清空 binlog cache。虽然每个线程有自己 binlog cache,但是最终都写到同一个 binlog 文件。但是并没有把数据持久化到磁盘,因为数据还缓存在文件系统的 page cache 里,write 的写入速度还是比较快的,因为不涉及磁盘 I/O。
    • fsync()函数,才是将数据持久化到磁盘的操作,这里就会涉及磁盘 I/O,所以频繁的 fsync 会导致磁盘的 I/O 升高。MySQL提供一个 sync_binlog 参数来控制数据库的 binlog 刷到磁盘上的频率。
  • sync_binlog参数
    • sync_binlog = 0 的时候,表示每次提交事务都只 write,不 fsync,后续交由操作系统决定何时将数据持久化到磁盘;
      • 这时候的性能是最好的,但是风险也是最大的。因为一旦主机发生异常重启,还没持久化到磁盘的数据就会丢失。
    • sync_binlog = 1 的时候,表示每次提交事务都会 write,然后马上执行 fsync;
      • 是最安全但是性能损耗最大的设置。因为当设置为 1 的时候,即使主机发生异常重启,最多丢失一个事务的 binlog,而已经持久化到磁盘的数据就不会有影响,不过就是对写入性能影响太大。
    • sync_binlog =N(N>1) 的时候,表示每次提交事务都 write,但累积 N 个事务后才 fsync。
      • 如果能容少量事务的 binlog 日志丢失的风险,为了提高写入的性能,一般会 sync_binlog 设置为 100~1000 中的某个数值。

为什么需要两阶段提交?

  • 为什么需要两阶段提交:
    • 在持久化 redo log 和 binlog 这两份日志的时候,由于两个日志的写入时机不同,(redo log在事务执行过程中就可以写入,而binlog在事务提交时才可以写入),可能会出现半成功的状态,出现主从数据不一致的情况。这是因为 redo log 影响主库的数据,binlog 影响从库的数据,所以 redo log 和 binlog 必须保持一致才能保证主从数据一致。
    • MySQL 为了避免出现两份日志之间的逻辑不一致的问题,使用了「两阶段提交」来解决,两阶段提交其实是分布式事务一致性协议,它可以保证多个逻辑操作要不全部成功,要不全部失败,不会出现半成功的状态。
  • 两阶段提交的过程?
    • 两阶段提交把单个事务的提交拆分成了 2 个阶段,分别是「准备(Prepare)阶段」和「提交(Commit)阶段」,每个阶段都由协调者(Coordinator)和参与者(Participant)共同完成。
    • 为了保证这两个日志的一致性,MySQL 使用了内部 XA 事务,内部 XA 事务由 binlog 作为协调者,存储引擎是参与者。当客户端执行 commit 语句或者在自动提交的情况下,MySQL 内部开启一个 XA 事务,分两阶段来完成 XA 事务的提交。
      • prepare 阶段:将 XID(内部 XA 事务的 ID) 写入到 redo log,同时将 redo log 对应的事务状态设置为 prepare,然后将 redo log 持久化到磁盘(innodb_flush_log_at_trx_commit = 1 的作用);
      • commit 阶段:把 XID 写入到 binlog,然后将 binlog 持久化到磁盘(sync_binlog = 1 的作用),之后将 redo log 状态设置为 commit。

异常重启出现什么问题?

  • 不管是时刻 A(redo log 已经写入磁盘, binlog 还没写入磁盘),还是时刻 B (redo log 和 binlog 都已经写入磁盘,还没写入 commit 标识)崩溃,此时的 redo log 都处于 prepare 状态图解MySQL笔记_第15张图片
  • 在 MySQL 重启后会按顺序扫描 redo log 文件,碰到处于 prepare 状态的 redo log,就拿着 redo log 中的 XID 去 binlog 查看是否存在此 XID:
    • 如果 binlog 中没有当前内部 XA 事务的 XID,说明 redolog 完成刷盘,但是 binlog 还没有刷盘,则回滚事务。对应时刻 A 崩溃恢复的情况。
    • 如果 binlog 中有当前内部 XA 事务的 XID,说明 redolog 和 binlog 都已经完成了刷盘,则提交事务。对应时刻 B 崩溃恢复的情况。
    • 所以,两阶段提交是以 binlog 写成功为事务提交成功的标识。因为 binlog 写成功了,就意味着能在 binlog 中查找到与 redo log 相同的 XID。即使redo log 处于prepare状态,MySQL在时刻B崩溃,此时重启服务器,就提交事务。
  • 为什么要这么设计?
    • 因为binlog 已经写入了,之后就会被从库(或者用这个 binlog 恢复出来的库)使用。所以,在主库上也要提交这个事务。采用这个策略,主库和备库的数据就保证了一致性。
  • 事务没提交时,redo log 会被持久化到磁盘吗?
    • 事务执行中间过程的 redo log 也是直接写在 redo log buffer 中的,这些缓存在 redo log buffer 里的 redo log 也会被「后台线程」每隔一秒一起持久化到磁盘。也就是说,事务没提交的时候,redo log 也是可能被持久化到磁盘的
    • 如果Mysql崩溃了,但是没提交事务的redo log却被持久化到磁盘中,重启后,会不会造成数据不一致?
      • 这种情况下MySQL会进行回滚操作,因为事务没有提交,binlog没有持久化到磁盘。
      • 所以, redo log 可以在事务没提交之前持久化到磁盘,但是 binlog 必须在事务提交之后,才可以持久化到磁盘

两阶段提交的问题?

两阶段提交虽然保证了两个日志文件的数据一致性,但是性能很差,主要有两个方面的影响:

  • 磁盘 I/O 次数高:对于“双1”配置,每个事务提交都会进行两次 fsync(刷盘)操作,会影响性能。
    • 当 sync_binlog = 1 的时候,表示每次提交事务都会将 binlog cache 里的 binlog 直接持久化到磁盘;
    • 当 innodb_flush_log_at_trx_commit = 1 时,表示每次事务提交时,都将缓存在 redo log buffer 里的 redo log 直接持久化到磁盘;
  • 锁竞争激烈:在两阶段提交的流程基础上,还需要加一个锁来保证提交的原子性,从而保证多事务的情况下,两个日志的提交顺序一致。在并发量较大的时候,就会导致对锁的争用,性能不佳。

组提交

MySQL 引入了 binlog 组提交(group commit)机制,当有多个事务提交的时候,会将多个 binlog 刷盘操作合并成一个,从而减少磁盘 I/O 的次数
引入了组提交机制后,prepare 阶段不变,只针对 commit 阶段,将 commit 阶段拆分为三个过程:

  • flush 阶段:多个事务按进入的顺序将 binlog 从 cache 写入文件(不刷盘);
    • 每个阶段都有一个队列,每个阶段有锁进行保护,因此保证了事务写入的顺序,第一个进入队列的事务会成为 leader,leader领导所在队列的所有事务,全权负责整队的操作,完成后通知队内其他事务操作结束。
    • 对每个阶段引入了队列后,锁就只针对每个队列进行保护,不再锁住提交事务的整个过程,可以看的出来,锁粒度减小了,这样就使得多个阶段可以并发执行,从而提升效率
  • sync 阶段:对 binlog 文件做 fsync 操作(多个事务的 binlog 合并一次刷盘);
  • commit 阶段:各个事务按顺序做 InnoDB commit 操作;

MySQL磁盘I/O很高的优化方法

  • 设置组提交的两个参数: binlog_group_commit_sync_delay 和 binlog_group_commit_sync_no_delay_count 参数,延迟 binlog 刷盘的时机,从而减少 binlog 的刷盘次数。这个方法是基于“额外的故意等待”来实现的,因此可能会增加语句的响应时间。
  • 将 sync_binlog 设置为大于 1 的值(比较常见是 100~1000),表示每次提交事务都 write,但累积 N 个事务后才 fsync,相当于延迟了 binlog 刷盘的时机。
  • 将 innodb_flush_log_at_trx_commit 设置为 2。表示每次事务提交时,都只是缓存在 redo log buffer 里的 redo log 写到 redo log 文件(Page Cache中)。

总结

具体更新一条记录 UPDATE t_user SET name = ‘xiaolin’ WHERE id = 1; 的流程如下:

  1. 执行器通过主键索引获取记录。执行器负责具体执行,会调用存储引擎的接口,通过主键索引树搜索获取 id = 1 这一行记录:
    • 如果 id=1 这一行所在的数据页本来就在 buffer pool 中,就直接返回给执行器更新;
    • 如果记录不在 buffer pool,将数据页从磁盘读入到 buffer pool,返回记录给执行器。
  2. 执行器得到聚簇索引记录后,会看一下更新前的记录和更新后的记录是否一样:
    • 如果一样的话就不进行后续更新流程;
    • 如果不一样的话就把更新前的记录和更新后的记录都当作参数传给 InnoDB 层,让 InnoDB 真正的执行更新记录的操作;
  3. 更新前,记录undo log ,redo log 记录BufferPool中的Undo页面。开启事务, InnoDB 层更新记录前,首先要记录相应的 undo log,因为这是更新操作,需要把被更新的列的旧值记下来,也就是要生成一条 undo log,undo log 会写入 Buffer Pool 中的 Undo 页面,不过在内存修改该 Undo 页面后,需要记录对应的 redo log。
  4. 使用WAL技术,语句更新完成,记录redo log。 InnoDB 层开始更新记录,会先更新内存(同时标记为脏页),然后将记录写到 redo log 里面,这个时候更新就算完成了。为了减少磁盘I/O,不会立即将脏页写入磁盘,后续由后台线程选择一个合适的时机将脏页写入到磁盘。这就是 WAL 技术,MySQL 的写操作并不是立刻写到磁盘上,而是先写 redo 日志,然后在合适的时间再将修改的行数据写到磁盘上。
  5. 至此,一条记录更新完了。
  6. 记录binglog,保存到binlog cache。在一条更新语句执行完成后,然后开始记录该语句对应的 binlog,此时记录的 binlog 会被保存到 binlog cache,并没有刷新到硬盘上的 binlog 文件,在事务提交时才会统一将该事务运行过程中的所有 binlog 刷新到硬盘。
  7. 事务提交(redo log 刷盘,binlog刷盘):
    • prepare 阶段:将 redo log 对应的事务状态设置为 prepare,然后将 redo log 刷新到硬盘;
    • commit 阶段:将 binlog 刷新到磁盘,接着调用引擎的提交事务接口,将 redo log 状态设置为 commit(将事务设置为 commit 状态后,刷入到磁盘 redo log 文件);
  8. 至此,一条更新语句执行完成。

7.1 解开Buffer Pool 的面纱

为什么要有Buffer Pool?

  • Innodb 存储引擎设计了一个缓冲池(Buffer Pool,来提高数据库的读写性能。
  • Buffer Pool 以页为单位缓冲数据,可以通过 innodb_buffer_pool_size 参数调整缓冲池的大小,默认是 128 M。
  • 有了 Buffer Pool 后
    • 当读取数据时,如果数据存在于 Buffer Pool 中,客户端就会直接读取 Buffer Pool 中的数据,否则去磁盘中读取到buffer pool中,返回给执行器。
    • 当修改数据时,如果数据存在于 Buffer Pool 中,那直接修改 Buffer Pool 中数据所在的页,然后将其页设置为脏页(该页的内存数据和磁盘上的数据已经不一致),为了减少磁盘I/O,不会立即将脏页写入磁盘,后续由后台线程选择一个合适的时机将脏页写入到磁盘。
  • Buffer Poll 缓存了什么?
    • 在 MySQL 启动的时候,InnoDB 会为 Buffer Pool 申请一片连续的内存空间,然后按照默认的16KB的大小划分出一个个的页, Buffer Pool 中的页就叫做缓存页。此时这些缓存页都是空闲的,之后随着程序的运行,才会有磁盘上的页被缓存到 Buffer Pool 中。
      • 为了更好的管理这些在 Buffer Pool 中的缓存页,InnoDB 为每一个缓存页都创建了一个控制块,控制块信息包括「缓存页的表空间、页号、缓存页地址、链表节点」等等。
  • Buffer Pool 除了缓存「索引页」和「数据页」,还包括了 Undo 页,插入缓存、自适应哈希索引、锁信息等等。
    • undo log 会写入到 Buffer Pool的Undo页面中。
  • Buffer Pool 是提高了读写效率没错,但是问题来了,Buffer Pool 是基于内存的,而内存总是不可靠,万一断电重启,还没来得及落盘的脏页数据就会丢失。

如何管理Buffer Pool?

  • Innodb 通过三种链表来管理缓页:
    • Free List (空闲页链表),管理空闲页;
    • Flush List (脏页链表),管理脏页;
    • LRU List,管理脏页+干净页,将最近且经常查询的数据缓存在其中,而不常查询的数据就淘汰出去。;
  • 如何提高缓存命中率?
    • 使用LRU算法,链表头部的节点是最近使用的,而链表末尾的节点是最久没被使用的。那么,当空间不够了,就淘汰最久没被使用的节点,从而腾出空间。
  • 简单的 LRU 算法的实现思路
    • 当访问的页在 Buffer Pool 里,就直接把该页对应的 LRU 链表节点移动到链表的头部。
    • 当访问的页不在 Buffer Pool 里,除了要把页放入到 LRU 链表的头部,还要淘汰 LRU 链表末尾的节点。
  • 简单的LRU算法的局限性
    • 预读失效:MySQL 在加载数据页时,会提前把它相邻的数据页一并加载进来,目的是为了减少磁盘 IO。但是可能这些被提前加载进来的数据页,并没有被访问,相当于这个预读是白做了,这个就是预读失效
    • 当某一个 SQL 语句扫描了大量的数据时,在 Buffer Pool 空间比较有限的情况下,可能会将 Buffer Pool 里的所有页都替换出去,导致大量热数据被淘汰了,等这些热数据又被再次访问的时候,由于缓存未命中,就会产生大量的磁盘 IO,MySQL 性能就会急剧下降,这个过程被称为 Buffer Pool 污染
  • 解决预读失效:要避免预读失效带来影响,最好就是让预读的页停留在 Buffer Pool 里的时间要尽可能的短,让真正被访问的页才移动到 LRU 链表的头部,从而保证真正被读取的热数据留在 Buffer Pool 里的时间尽可能长
    • 将 LRU 划分了 2 个区域:old 区域 和 young 区域划分这两个区域后,预读的页就只需要加入到 old 区域的头部,当页被真正访问的时候,才将页插入 young 区域的头部。如果预读的页一直没有被访问,就会从 old 区域移除,这样就不会影响 young 区域中的热点数据。
  • 解决Buffer Pool污染:LRU 链表中 young 区域就是热点数据,只要我们提高进入到 young 区域的门槛,就能有效地保证 young 区域里的热点数据不会被替换掉。进入到 young 区域条件增加了一个停留在 old 区域的时间判断
    • 对某个处在 old 区域的缓存页进行第一次访问时,就在它对应的控制块中记录下来这个访问时间:
      • 如果后续的访问时间与第一次访问的时间在某个时间间隔内,那么该缓存页就不会被从 old 区域移动到 young 区域的头部
      • 如果后续的访问时间与第一次访问的时间不在某个时间间隔内,那么该缓存页移动到 young 区域的头部
      • 这个间隔时间是由 innodb_old_blocks_time 控制的,默认是 1000 ms。也就说,只有同时满足「被访问」与「在 old 区域停留时间超过 1 秒」两个条件,才会被插入到 young 区域头部,这样就解决了 Buffer Pool 污染的问题 。
    • 另外,MySQL 针对 young 区域其实做了一个优化,为了防止 young 区域节点频繁移动到头部。young 区域前面 1/4 被访问不会移动到链表头部,只有后面的 3/4被访问了才会。
  • 脏页什么时候会被刷入磁盘?脏页需要被刷入磁盘,保证缓存和磁盘数据一致,但是若每次修改数据都刷入磁盘,则性能会很差,因此一般都会在一定时机进行批量刷盘。
    • 当 redo log 日志满了的情况下,会主动触发脏页刷新到磁盘;
    • Buffer Pool 空间不足时,需要将一部分数据页淘汰掉,如果淘汰的是脏页,需要先将脏页同步到磁盘;
    • MySQL 认为空闲时,后台线程会定期将适量的脏页刷入到磁盘;
    • MySQL 正常关闭之前,会把所有的脏页刷入到磁盘;

完。

你可能感兴趣的:(个人笔记,MySQL,java)