本章概述

作为高性能MySQL一书的开篇章节,本章概要地描述了MySQL的服务器架构、各种存储引擎之间的主要区别,以及这些区别的重要性,同时介绍了MySQL的历史背景和基准测试(第二章会详细讲述)

1.1 Mysql逻辑架构

分为client客户端,server层和存储结构三部分

第一步,首先用户向server发送连接请求,server连接器提供连接服务,做一些权限认证,比如用户名和密码

第二步,分析器把sql语句进行切分,通过词法分析和语法分析最终转成抽象语法树(Abstract Syntax Tree,AST)

第三步,sql语句有N种执行方式,优化器对其进行优化,不同的执行方式对SQL语句的执行效率影响很大

– RBO:基于规则的优化

– CBO:基于成本的优化(用的多)

第四步,执行器,跟存储引擎挂钩,操作引擎,返回结果

最上层是通用服务,非MySQL独有

第二层包含了MySQL大多数的核心服务功能,包括查询解析、分析、优化、缓存以及日期、时间、数学和加密函数等内置函数,跨存储引擎的功能如存储过程、触发器、视图等也在这一层实现

第三层主要是存储引擎,存储引擎的API包含几十个而底层函数,但存储引擎不会解析事务(InnoDB是个例外,它会解析外键定义,因为MySQL服务器本身没有实现该功能),不同引擎之间不会通信,只会响应上层服务器的请求

1.1.1 连接管理与安全性

一个客户端连接对应一个服务器中的一个线程,该连接的查询只在线程中执行

付费版本mysql-Percona支持线程池插件,可以使用池中少量的线程来服务大量的连接,在前置环节,比如spring的声明式事务就可以通过ThreadLocal实现线程的复用,这两点本质上解决的都是同一个问题

1.1.2 优化与执行

优化器不关心表使用的是什么存储引擎,但存储引擎对于优化查询是有影响的

对于select语句,服务器在解析查询之前会先检查查询缓存

注意:查询缓存MySQL8中被弃用

1.2 并发控制

本章讨论MySQL两个层面的并发控制:服务器层与存储引擎层

1.2.1 读写锁

简单说下概念:读锁也叫共享锁,多个客户在同一时刻可以同时读取同一个资源;写锁也叫排他锁,一个写锁会阻塞其他的写锁和读锁,确保在给定的时间里,只有一个用户能执行写入,并防止其他用户读取正在写入的同一资源

1.2.2 锁粒度

表锁

表锁是MySQL中开销最小的策略,它会锁定整张表,这可以很好地避免死锁问题。进行写操作前需要先获得写锁,这会阻塞其他的读写操作,只有没有写锁时,其他读取的用户才能获得读锁

在特定场景中表锁也有良好的性能。例如,READ LOCAL表锁支持某些类型的并发写操作。另外,写锁的优先级高于读锁,一个写锁请求可能会被插入到读锁队列的前面,反之则不行

服务器会为诸如ALTER TABLE之类的语句使用表锁,而忽略存储引擎的锁机制

行级锁

行级锁可以最大程度地支持并发处理,同时也带来了最大的锁开销。行级锁只在存储引擎中实现,服务器层完全不了解存储引擎中的锁实现

意向锁

事务在添加共享锁(行级)之前,需要先添加意向共享锁(表级)。在添加排他锁(行级)之前,需要先获取意向排他锁(表级),在 InnoDB 中,我们对某条记录进行锁定时,为了提高并发度,通常都只是锁定这一行记录,而不是锁定整个表。而当我们需要为整个表加写锁的时候,我们就需要遍历整个表的记录,如果每条记录都没有被加锁,才可以给整个表加写锁。而这个遍历过程就很费时间,这时候就有了意向锁的诞生

意向锁其实就是标记这个表有没有被锁,如果有某条记录被锁住了,那么就必须获取该表的意向锁。所以当我们需要判断这个表的记录有没有被加锁时,直接判断意向锁就可以了,减少了遍历的时间,提高了效率,是典型的用空间换时间的做法

1.3 事务

事务就是一组原子性的SQL查询,或者说一个独立的工作单元,事务内的语句,要么全部执行成功,要么全部执行失败

ACID略

1.3.1 隔离级别

READ UNCOMMITTED(未提交读)

未提交读,事务中的修改,即使没有提交,对其他事务也都是可见的,事务可以读取未提交的数据,这也被称为脏读。该级别一般很少使用

READ COMMITTED(提交读)

一个事物从开始直到提交之前,所做的任务修改对其他事务都是不可见的,也叫不可重复读,因为两次执行同样的查询,可能会得到不一样的结果(比如b事务在a事务提交前读了一次数据,在a事务提交之后又读了一次事务,两次结果不一致)

REPEATABLE READ(可重复读)

解决脏读问题,该级别保证了在同一个事务中多次读取同样记录的结果是一致的,但无法解决幻读,即当某个事务在读取某个范围内的记录时,另外一个事务又在该范围内插入了新的记录,当之前的事务再次读取该范围的记录时,会产生幻行。InnoDB和XtraDB存储引擎通过多版本并发控制解决了幻读的问题

SERIALIZABLE(可串行化)

这是最高的隔离级别,它通过强制事务串行执行来避免幻读

缺点:在读取的每一行数据上都加锁,可鞥导致大量的超时和锁争用的问题

sql隔离级别


1.3.2 死锁

死锁是指两个或者多个事务在同一资源上相互占用,并请求锁定对方占用的资源从而导致恶性循环的现象

设想下面两个事务同时处理StockPrice表


InnoDB目前处理死锁的方法是:将持有最少行级排他锁的事务进行回滚

死锁发生以后,只有部分或者完全回滚其中一个事务才能打破死锁。应用程序的处理大多数情况下只需重新执行因死锁回滚的事务即可

1.3.3 事务日志

事务日志可以使存储引擎在修改数据时只修改其内存拷贝,再把该修改行为记录到硬盘中的事务日志中,而不用每次都持久化到磁盘。事务日志是追加方式,顺序io。事务日志持久以后,内存中被修改的数据在后台可以慢慢地刷回到磁盘

1.3.4 Mysql中的事务

自动提交(AUTOCOMMIT)

MySQL默认采用自动提交模式,也就是说,如果不是显式地开始一个事务,则每个查询都被当作一个事务执行提交操作。可以通过设置AUTOCOMMIT变量值启用或禁用,1/ON启用,0/OFF禁用

还有一些命令在执行之前会强制执行COMMIT提交当前的活动事务,比如ALTER TABLE,LOCK TABLES

在事务中混合使用存储引擎

是不可靠的,服务器层不管理事务,非事务型表上的变更无法撤销回滚

隐式和显式锁定

InnoDB采用两阶段锁定协议,即加锁阶段和解锁阶段。事务执行过程中随时都可以锁定,锁只有在执行COMMIT或者ROLLBACK的时候才会释放,并且所有锁是在同一时刻被释放。这些都是隐式锁定,InnDB会根据隔离级别在需要的时候自动加锁。另外InnoDB也支持通过特定的语句进行显式锁定,但应谨慎使用:

  • SELECT . . . LOCK IN SHARE MODE
  • SELECT . . . FOR UPDATE

服务器层也支持LOCK TABLES和UNLOCK TABLES,但并不能代替事务

1.4 多版本并发控制

MVCC是行级锁的一个变种,它在很多情况下避免了加锁操作,因此开销更低。实现了非阻塞的读操作,写操作也只锁定必要的行

MVCC的实现,是通过保存数据在某个时间点的快照来实现的。也就是说,不管需要执行多长时间,每个事务看到的数据都是一致的(不会被其他事务影响),根据事务开始的时间不同,每个事物对同一张表,同一时刻看到的数据可能是不一样的

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

MVCC的两个实现核心是undo log和一致性视图,通过undo log来保存多版本的数据,通过一致性视图来保存当前活跃的事务列表,将两者结合和制定一定的规则来判断当前可读数据

https://zhuanlan.zhihu.com/p/231947511

在REPEATABLE READ隔离级别下,MVCC具体操作如下:

SELECT

InnoDB会根据以下两个条件检查每行记录:

a. InnoDB只查找版本早于当前事务版本的数据行(也就是,行的系统版本号小于或等于事务的系统版本号),这样可以确保事务 读取的行,要么是在事务开始前已经存在的,要么是事务自身插入或者修改过的

b. 行的删除版本要么未定义,要么大于当前事务版本号。这可以确保事务读取到的行,在事务开始之前未被删除

只有符合上述两个条件的记录,才能返回作为查询结果

INSERT

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

DELETE

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

UPDATE

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

MVCC只在REPEATABLE READ和READ COMMITTED两个隔离级别下工作
为了实现事务的原子性,InnoDB存储引擎在实际进行增、删、改一条记录时,都需要先把对应的undo日志记下来。一般每对一条记录做一次改动,就对应着一条undo日志,但在某些更新记录的操作中,也可能会对应着2条undo日志。一个事务在执行过程中可能新增、删除、更新若干条记录,也就是说需要记录很多条对应的undo日志,这些undo日志会被从0开始编号,也就是说根据生成的顺序分别被称为第0号undo日志、第1号undo日志、…、第n号undo日志等,这个编号也被称之为undo no。

聚簇索引的记录除了会保存完整的用户数据以外,而且还会自动添加名为trx_id、roll_pointer的隐藏列,如果用户没有在表中定义主键以及UNIQUE键,还会自动添加一个名为row_id的隐藏列。

高性能mysql 第3版 高性能mysql第四版出版时间_数据库

其中的trx_id列就是某个对这个聚簇索引记录做改动的语句所在的事务对应的事务id而已。

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

每次对记录进行改动,都会记录一条undo日志,每条undo日志也都有一个roll_pointer属性(INSERT操作对应的undo日志没有该属性,因为该记录并没有更早的版本),可以将这些undo日志都连起来,串成一个链表,所以现在的情况就像下图一样:

高性能mysql 第3版 高性能mysql第四版出版时间_高性能mysql 第3版_02

对该记录每次更新后,都会将旧值放到一条undo日志中,就算是该记录的一个旧版本,随着更新次数的增多,所有的版本都会被roll_pointer属性连接成一个链表,我们把这个链表称之为版本链,版本链的头节点就是当前记录最新的值。另外,每个版本中还包含生成该版本时对应的事务id。于是可以利用这个记录的版本链来控制并发事务访问相同记录的行为,那么这种机制就被称之为多版本并发控制(Mulit-Version Concurrency Control MVCC)。

1.5 MySQL中的存储引擎

1.5.1 InnoDB存储引擎

InnoDB采用MVCC开支持高并发,默认存储引擎是REPEATABLE READ,并且通过间隙锁(next-key locking)策略防止幻读的出现。间隙锁使得InnoDB不仅仅锁定查询涉及的行,还会对索引中的间隙进行锁定,以防止幻影行的插入

InnoDB表是基于聚簇索引建立的,聚簇索引对主键查询有很高的性能。不过它的二级索引中必须包含主键列,如果主键列很大的话,其他的所有索引都会很大。因此,若表上的索引较多的话,主键应当尽可能地小

InnoDB内部做了很多优化,包括从磁盘读取文件时采用的可预测性预读,能够自动在内存中创建hash索引以加速读操作的自适应哈希索引,以及能够加速插入操作的插入缓冲区等

1.5.2 MyISAM存储引擎

在5.5之前,也就是5.1及之前的版本,MyISAM是默认的存储引擎,不支持事务和行级锁崩溃后无法安全恢复

MyISAM将表存储在两个文件中:数据文件.MYD和索引文件.MYI

MyISAM可以存储的行记录数,一般受限于可用的磁盘空间,或者操作系统中单个文件的最大尺寸

可以通过修改表的MAX_ROWS和AVG_ROW_LENGTH选项的值来改变MyISAM表指针的长度,两者相乘就是表可能达到的最大大小。但是修改会导致重建整个表和表的所有索引,耗时很长

MyISAM对整张表加锁,读操作时也可以插入,这被称为并发插入

可以通过CHECK TABLE mytable检查表的错误,通过REPAIR TABLE mytable进行修复。另外,如果MySQL服务器已经关闭,可以通过myisamchk命令行工具进行检查和修复操作

MyISAM可以对长字段基于其前500个字符创建索引,也支持全文索引

创建MyISAM表的时候,如果指定了DELAY_KEY_WRITE(延迟更新索引键)选项,在每次修改执行完成时,不会立刻将修改的索引数据写入磁盘,而是写到内存中的键缓冲区,只有在清理键缓冲区或关闭表的时候才会将对应的索引块写入到磁盘。这种方式可以极大地提升写入性能,但在数据库或主机崩溃时会造成索引损坏,需要执行修复操作。延迟更新索引键可以在全局设置也可以为单个表设置

使用myisampack对MyISAM表进行压缩,压缩表不能修改除非先解压。压缩表减少了磁盘的空间占用,因此也减少了磁盘I/O,从而提升查询性能。压缩表也支持索引,但索引也是只读的。表中的数据是独立压缩的,读取单行不需要解压整个表

MyISAM最调性的性能问题就是表锁的问题

1.5.3 Mysql内建的其他存储引擎

Archive、Blackhole、CSV、Federated、Merge、NDB集群引擎等引擎暂时略过

Memory引擎

适用于需要快速访问数据,且这些数据不会被修改,丢失无碍。比MyISAM快一个数量级,因为所有表都在内存中不需要进行磁盘I/O,底层是hash索引而非B+树。Memory表的结构重启之后还会保留但数据会丢失

应用场景:

  • 用于查找或者映射表,例如将邮编和州名映射的表
  • 用于缓存周期性聚合数据的结果
  • 用于保存数据分析中产生的中间数据

Memory也是表级锁,每行的长度是固定的,就算指定了varchar也会转成char

如果MySQL在执行查询的过程中需要使用临时表保存中间结果,内部使用的临时表就是Memory表,如果中间结果太大超出Memory限制,或者含有BLOB或TEXT字段,则会转成MyISAM表

1.5.4 第三方存储引擎(暂时略过)

1.5.5 选择合适的引擎

需要事务支持用InnoDB,如果不需要十五且主要是SELECT和INSERT操作可以使用MyISAM(如日志型应用)

需要在线热备份用InnoDB,如果可以定期关闭服务器来执行备份可以忽略这条

崩溃恢复用InnoDB

1.5.6 转换表的引擎

ALTER TABLE

ALTER TABLE mytable ENGINE = InnoDB;

适用任何引擎,但执行时间长,复制期间原表会加读锁

导出与导入

使用mysqldump工具将数据导出到文件,然后修改CREATE TABLE语句中的存储引擎选项并同时修改表名。同时要注意mysqldump默认会自动在CREATE TABLE语句前加上DROP TABLE语句,要注意这一点防止数据丢失

创建与查询(推荐)

高性能mysql 第3版 高性能mysql第四版出版时间_高性能mysql 第3版_03